阅读前注意事项:
1、我的博客从lab2之后,如果没有特殊说明,所有标注的代码行数位置,以labcodes_answer(答案包)里的文件为准!!!因为你以后会发现做实验用meld软件比较费时费力,对于咱们学校的验收不如直接对着答案来;
2、感谢网上的各路前辈大佬们,本人在这学期初次完成实验的过程中,各位前辈们的博客给了我很多有用的指导;本人的博客内容在现有的内容上,做了不少细节的增补内容,有些地方属个人理解,如果有错在所难免,还请各位大佬们批评指正;
3、所有实验的思考题,我把它规整到了文章最后;
4、所有实验均默认不做challenge,对实验评分无影响;
5、lab8可能是八个实验中最难的一个实验,临近期末,写得较为简略难以避免。
一、实验内容
本次实验涉及的是文件系统,通过分析了解ucore文件系统的总体架构设计,完善读写文件操作,从新实现基于文件系统的执行程序机制(即改写do_execve),从而可以完成执行存储在磁盘上的文件和实现文件读写等功能。
二、目的
了解基本的文件系统系统调用的实现方法;
了解一个基于索引节点组织方式的Simple FS文件系统的设计与实现;
了解文件系统抽象层-VFS的设计与实现;
三、实验设计思想和流程
练习0:填写已有实验
本实验依赖实验1/2/3/4/5/6/7。请把你做的实验1/2/3/4/5/6/7的代码填入本实验中代码中
有“LAB1”/“LAB2”/“LAB3”/“LAB4”/“LAB5”/“LAB6” /“LAB7”的注释相应部分。并确保编译通过。注意:为了能够正确执行lab8的测试应用程序,可能需对已完成的实验1/2/3/4/5/6/7的代码进行进一步改进。
使用meld软件进行对比,发现需要更改的代码如下:
proc.c
default_pmm.c
pmm.c
swap_fifo.c
vmm.c
trap.c
sche.c
monitor.
check_sync.c
进一步比对发现,无需改进代码实现,直接使用即可。
练习1: 完成读文件操作的实现(需要编码)
首先了解打开文件的处理流程,然后参考本实验后续的文件读写操作的过程分析,编写在sfs_inode.c中sfs_io_nolock读文件中数据的实现代码。
ucore的文件系统模型源于Havard的OS161的文件系统和Linux文件系统。但其实这二者都是源于传统的UNIX文件系统设计。UNIX提出了四个文件系统抽象概念:文件(file)、目录项(dentry)、索引节点(inode)和安装点(mount point)。
文件:UNIX文件中的内容可理解为是一有序字节buffer,文件都有一个方便应用程序识别的文件名称(也称文件路径名)。典型的文件操作有读、写、创建和删除等。
目录项:目录项不是目录,而是目录的组成部分。在UNIX中目录被看作一种特定的文件,而目录项是文件路径中的一部分。如一个文件路径名是“/test/testfile”,则包含的目录项为:根目录“/”,目录“test”和文件“testfile”,这三个都是目录项。一般而言,目录项包含目录项的名字(文件名或目录名)和目录项的索引节点(见下面的描述)位置。
索引节点:UNIX将文件的相关元数据信息(如访问控制权限、大小、拥有者、创建时间、数据内容等等信息)存储在一个单独的数据结构中,该结构被称为索引节点。
安装点:在UNIX中,文件系统被安装在一个特定的文件路径位置,这个位置就是安装点。所有的已安装文件系统都作为根文件系统树中的叶子出现在系统中。安装点是一个起点,从安装点开始可以访问文件系统中的所有文件。
其中,文件和目录是给应用程序看到的一个抽象。
从ucore操作系统不同的角度来看,ucore中的文件系统架构包含四类主要的数据结构, 它们分别是:
1、超级块(SuperBlock),它主要从文件系统的全局角度描述特定文件系统的全局信息。它的作用范围是整个OS空间。
2、索引节点(inode):它主要从文件系统的单个文件的角度它描述了文件的各种属性和数据所在位置。它的作用范围是整个OS空间。
3、目录项(dentry):它主要从文件系统的文件路径的角度描述了文件路径中的特定目录。它的作用范围是整个OS空间。
4、文件(file),它主要从进程的角度描述了一个进程在访问文件时需要了解的文件标识,文件读写的位置,文件引用情况等信息。它的作用范围是某一具体进程。
文件系统,会将磁盘上的文件(程序)读取到内存里面来,在用户空间里面变成进程去进一步执行或其他操作。通过一系列系统调用完成这个过程。
如上图,ucore文件系统中,是这样处理读写硬盘操作的:
(1)首先是应用程序发出请求,请求硬盘中写数据或读数据,应用程序通过FS syscall接口执行系统调用,获得ucore操作系统关于文件的一些服务;
(2)之后,一旦操作系统内系统调用得到了请求,就会到达VFS层面(虚拟文件系统),包含很多部分比如文件接口、目录接口等,是一个抽象层面,它屏蔽底层具体的文件系统;
(3)VFS如果得到了处理,那么VFS会将这个iNode传递给SimpleFS,注意,此时,VFS中的iNode还是一个抽象的结构,在SimpleFS中会转化为一个具体的iNode;
(4)通过该iNode经过IO接口对于磁盘进行读写。
那么,硬盘中的文件布局又是怎样的呢?硬盘中的布局信息存在SFS中,如下图所示:
上图所示的是一个SFS的文件系统,其定义在(kern/fs/sfs/sfs.h,83——94行):
struct sfs_fs {
struct sfs_super super; /* on-disk superblock */
struct device *dev; /* device mounted on */
struct bitmap *freemap; /* blocks in use are mared 0 */
bool super_dirty; /* true if super/freemap modified */
void *sfs_buffer; /* buffer for non-block aligned io */
semaphore_t fs_sem; /* semaphore for fs */
semaphore_t io_sem; /* semaphore for io */
semaphore_t mutex_sem; /* semaphore for link/unlink and rename */
list_entry_t inode_list; /* inode linked-list */
list_entry_t *hash_list; /* inode hash linked-list */
};
其中,SFS的前3项对应的就是硬盘文件布局的全局信息。
那么,接下来分析这些文件布局的数据结构:
(1)超级块super_block(kern/fs/sfs/sfs.h,40——45行)
struct sfs_super {
uint32_t magic; /* magic number, should be SFS_MAGIC */
uint32_t blocks; /* # of blocks in fs */
uint32_t unused_blocks; /* # of unused blocks in fs */
char info[SFS_MAX_INFO_LEN + 1]; /* infomation for sfs */
};
超级块,刚刚说过是一个文件系统的全局角度描述特定文件系统的全局信息。这里面定义了标识符magic、总块数blocks、空闲块数unused_blocks和一些关于SFS的信息,通常是字符串。
(2)根目录结构root_dir(kern/fs/sfs/sfs/h,48——57行)
struct sfs_disk_inode {
uint32_t size; /* size of the file (in bytes) */
uint16_t type; /* one of SYS_TYPE_* above */
uint16_t nlinks; /* # of hard links to this file */
uint32_t blocks; /* # of blocks */
uint32_t direct[SFS_NDIRECT]; /* direct blocks */
uint32_t indirect; /* indirect blocks */
};
我们刚刚讲过,iNode是从文件系统的单个文件的角度它描述了文件的各种属性和数据所在位置,相当于一个索引,而root_dir是一个根目录索引,根目录表示,我们一开始访问这个文件系统可以看到的目录信息。主要关注direct和indirect,代表根目录下的直接索引和间接索引。
(3)目录项entry(kern/fs/sfs/sfs.h,60——63行)
struct sfs_disk_entry {
uint32_t ino; /* inode number */
char name[SFS_MAX_FNAME_LEN + 1]; /* file name */
};
数组中存放的是文件的名字,ino是该文件的iNode值。
仅有硬盘文件布局还不够,SFS毕竟是一个在硬盘之上的抽象,它还需要传递上一层过来的索引值INODE。这个INODE是SFS层面的,我们刚刚讨论的iNode是硬盘上实际的索引。
sfs_inode(kern/fs/sfs/sfs.h,69——77行)
struct sfs_inode {
struct sfs_disk_inode *din; /* on-disk inode */
uint32_t ino; /* inode number */
bool dirty; /* true if inode modified */
int reclaim_count; /* kill inode if it hits zero */
semaphore_t sem; /* semaphore for din */
list_entry_t inode_link; /* entry for linked-list in sfs_fs */
list_entry_t hash_link; /* entry for hash linked-list in sfs_fs */
};
我们看到,sfs_disk_inode是SFS层面上的iNode的一个成员,代表了这两个结构之间的上下级关系。
接下来,我们来分析更高层的数据结构VFS(虚拟文件系统)。
在VFS层中,我们需要对于虚拟的iNode,和下一层的SFS的iNode进行对接。
文件系统抽象层是把不同文件系统的对外共性接口提取出来,形成一个函数指针数组,这样,通用文件系统访问接口层只需访问文件系统抽象层,而不需关心具体文件系统的实现细节和接口。
(1)VFS的抽象定义(kern/fs/vfs/vfs.h,35——46行)
struct fs {
union {
struct sfs_fs __sfs_info;
} fs_info; // filesystem-specific data
enum {
fs_type_sfs_info,
} fs_type; // filesystem type
int (*fs_sync)(struct fs *fs); // Flush all dirty buffers to disk
struct inode *(*fs_get_root)(struct fs *fs); // Return root inode of filesystem.
int (*fs_unmount)(struct fs *fs); // Attempt unmount of filesystem.
void (*fs_cleanup)(struct fs *fs); // Cleanup of filesystem.???
};
主要是一些函数指针用于处理VFS的操作。
(2)文件结构(kern/fs/file.c,14——24行)
struct file {
enum {
FD_NONE, FD_INIT, FD_OPENED, FD_CLOSED,
} status; //访问文件的执行状态
bool readable; //文件是否可读
bool writable; //文件是否可写
int fd; //文件在filemap中的索引值
off_t pos; //访问文件的当前位置
struct inode *node; //该文件对应的内存inode指针
atomic_t open_count; //打开此文件的次数
};
在file基础之上还有一个管理所有file的数据结构file_struct(kern/fs/fs.h,25——30行)
struct files_struct {
struct inode *pwd; //当前工作目录
struct file *fd_array; //已经打开的文件对应的数组
int files_count; //打开的文件个数
};
(3)VFS的索引iNode(kern/fs/vfs/inode.h,29——42行)
struct inode {
union { //不同文件系统特定inode信息的union域
struct device __device_info; //设备文件系统内存inode信息
struct sfs_inode __sfs_inode_info; //SFS文件系统内存inode信息
} in_info;
enum {
inode_type_device_info = 0x1234,
inode_type_sfs_inode_info,
} in_type; //此inode所属文件系统类型
atomic_t ref_count; //此inode的引用计数
atomic_t open_count; //打开此inode对应文件的个数
struct fs *in_fs; //抽象的文件系统,包含访问文件系统的函数指针
const struct inode_ops *in_ops; //抽象的inode操作,包含访问inode的函数指针
};
我们看到在VFS层面的iNode值,包含了SFS和硬件设备device的情况。
(4)inode的操作函数指针列表(kern/fs/vfs/inode.c,169——186行)
inode_ops是对常规文件、目录、设备文件所有操作的一个抽象函数表示。对于某一具体的文件系统中的文件或目录,只需实现相关的函数,就可以被用户进程访问具体的文件了,且用户进程无需了解具体文件系统的实现细节。
有了上述分析后,我们可以看看如果一个用户进程打开文件会做哪些事情?
首先假定用户进程需要打开的文件已经存在在硬盘上。以user/sfs_filetest1.c为例,首先用户进程会调用在main函数中的如下语句:
int fd1 = safe_open("/test/testfile", O_RDWR | O_TRUNC);
如果ucore能够正常查找到这个文件,就会返回一个代表文件的文件描述符fd1,这样在接下来的读写文件过程中,就直接用这样fd1来代表就可以了。
接下来实现需要编码的函数:
通用文件访问接口层的处理流程:
首先进入通用文件访问接口层的处理流程,即进一步调用如下用户态函数:open->sys_open->syscall,从而引起系统调用进入到内核态。到了内核态后,通过中断处理例程,会调用到sys_open内核函数,并进一步调用sysfile_open内核函数。到了这里,需要把位于用户空间的字符串”/test/testfile”拷贝到内核空间中的字符串path中,并进入到文件系统抽象层的处理流程完成进一步的打开文件操作中。
文件系统抽象层(VFS)的处理流程:
1、分配一个空闲的file数据结构变量file在文件系统抽象层的处理中,首先调用的是file_open函数,它要给这个即将打开的文件分配一个file数据结构的变量,这个变量其实是当前进程的打开文件数组current->fs_struct->filemap[]中的一个空闲元素(即还没用于一个打开的文件),而这个元素的索引值就是最终要返回到用户进程并赋值给变量fd1。到了这一步还仅仅是给当前用户进程分配了一个file数据结构的变量,还没有找到对应的文件索引节点。
为此需要进一步调用vfs_open函数来找到path指出的文件所对应的基于inode数据结构的VFS索引节点node。vfs_open函数需要完成两件事情:通过vfs_lookup找到path对应文件的inode;调用vop_open函数打开文件。
2、找到文件设备的根目录/的索引节点需要注意,这里的vfs_lookup函数是一个针对目录的操作函数,它会调用vop_lookup函数来找到SFS文件系统中的/test目录下的testfile文件。为此,vfs_lookup函数首先调用get_device函数,并进一步调用vfs_get_bootfs函数(其实调用了)来找到根目录/对应的inode。这个inode就是位于vfs.c中的inode变量bootfs_node。这个变量在init_main函数(位于kern/process/proc.c)执行时获得了赋值。
找到根目录/下的test子目录对应的索引节点,在找到根目录对应的inode后,通过调用vop_lookup函数来查找/和test这两层目录下的文件testfile所对应的索引节点,如果找到就返回此索引节点。
3、把file和node建立联系。完成第3步后,将返回到file_open函数中,通过执行语句file->node=node,就把当前进程的current->fs_struct->filemap[fd](即file所指变量)的成员变量node指针指向了代表/test/testfile文件的索引节点node。这时返回fd。经过重重回退,通过系统调用返回,用户态的syscall->sys_open->open->safe_open等用户函数的层层函数返回,最终把把fd赋值给fd1。自此完成了打开文件操作。但这里我们还没有分析第2和第3步是如何进一步调用SFS文件系统提供的函数找位于SFS文件系统上的/test/testfile所对应的sfs磁盘inode的过程。下面需要进一步对此进行分析。
sfs_lookup(kern/fs/sfs/sfs_inode.c,975——993行)
static int sfs_lookup(struct inode *node, char *path, struct inode **node_store) {
struct sfs_fs *sfs = fsop_info(vop_fs(node), sfs);
assert(*path != '\0' && *path != '/');
//以“/”为分割符,从左至右分解path获得各子目录和最终文件对应的inode节点。
vop_ref_inc(node);
struct sfs_inode *sin = vop_info(node, sfs_inode);
if (sin->din->type != SFS_TYPE_DIR) {
vop_ref_dec(node);
return -E_NOTDIR;
}
struct inode *subnode;
int ret = sfs_lookup_once(sfs, sin, path, &subnode, NULL); //循环进一步调用 sfs_lookup_once查找以“test”子目录下的文件“testfile1”所对应的inode节点。
vop_ref_dec(node);
if (ret != 0) {
return ret;
}
*node_store = subnode;
//当无法分解path后,就意味着找到了需要对应的inode节点,就可顺利返回了。
return 0;
}
看到函数传入的三个参数,其中node是根目录“/”所对应的inode节点;path是文件的绝对路径(例如“/test/file”),而node_store是经过查找获得的file所对应的inode节点。
函数以“/”为分割符,从左至右逐一分解path获得各个子目录和最终文件对应的inode节点。在本例中是分解出“test”子目录,并调用sfs_lookup_once函数获得“test”子目录对应的inode节点subnode,然后循环进一步调用sfs_lookup_once查找以“test”子目录下的文件“testfile1”所对应的inode节点。当无法分解path后,就意味着找到了testfile1对应的inode节点,就可顺利返回了。
而我们再进一步观察sfs_lookup_once函数,它调用sfs_dirent_search_nolock函数来查找与路径名匹配的目录项,如果找到目录项,则根据目录项中记录的inode所处的数据块索引值找到路径名对应的SFS磁盘inode,并读入SFS磁盘inode对的内容,创建SFS内存inode。
sfs_lookup_once(kern/fs/sfs/sfs_inode.c,498——512行)
static int
sfs_lookup_once(struct sfs_fs *sfs, struct sfs_inode *sin, const char *name, struct inode **node_store, int *slot) {
int ret;
uint32_t ino;
lock_sin(sin);
{ // find the NO. of disk block and logical index of file entry
ret = sfs_dirent_search_nolock(sfs, sin, name, &ino, slot, NULL);
}
unlock_sin(sin);
if (ret == 0) {
// load the content of inode with the the NO. of disk block
ret = sfs_load_inode(sfs, node_store, ino);
}
return ret;
}
最后是需要实现的函数,这里只注释了读文件的部分:
sfs_io_nolock(kern/fs/sfs/sfs_inode.c,552——644行)
static int
sfs_io_nolock(struct sfs_fs *sfs, struct sfs_inode *sin, void *buf, off_t offset, size_t *alenp, bool write) {
......
......
if ((blkoff = offset % SFS_BLKSIZE) != 0) { //读取第一部分的数据
size = (nblks != 0) ? (SFS_BLKSIZE - blkoff) : (endpos - offset);//计算第一个数据块的大小
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
//找到内存文件索引对应的block的编号ino
goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, blkoff)) != 0) {
goto out;
}
//完成实际的读写操作
alen += size;
if (nblks == 0) {
goto out;
}
buf += size, blkno ++, nblks --;
}
//读取中间部分的数据,将其分为size大小的块,然后一次读一块直至读完
size = SFS_BLKSIZE;
while (nblks != 0) {
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
goto out;
}
if ((ret = sfs_block_op(sfs, buf, ino, 1)) != 0) {
goto out;
}
alen += size, buf += size, blkno ++, nblks --;
}
//读取第三部分的数据
if ((size = endpos % SFS_BLKSIZE) != 0) {
if ((ret = sfs_bmap_load_nolock(sfs, sin, blkno, &ino)) != 0) {
goto out;
}
if ((ret = sfs_buf_op(sfs, buf, size, ino, 0)) != 0) {
goto out;
}
alen += size;
}
......
每次通过sfs_bmap_load_nolock函数获取文件索引编号,然后调用sfs_buf_op完成实际的文件读写操作。
其中参数的解释:
uint32_t blkno = offset / SFS_BLKSIZE; // The NO. of Rd/Wr begin block
uint32_t nblks = endpos / SFS_BLKSIZE - blkno; // The size of Rd/Wr blocks
blkno就是文件开始块的位置,nblks是文件的大小。
练习2:完成基于文件系统的执行程序机制的实现(需要编码)
改写proc.c中的load_icode函数和其他相关函数,实现基于文件系统的执行程序机制。执行:make qemu。如果能看看到sh用户程序的执行界面,则基本成功了。如果在sh用户界面上可以执行”ls”,”hello”等其他放置在sfs文件系统中的其他执行程序,则可以认为本实验基本成功。
在proc.c中,根据注释我们需要先初始化fs中的进程控制结构,即在alloc_proc函数中我们需要做一下修改,加上一句proc->filesp = NULL;从而完成初始化。
为什么要这样做的呢,因为我们之前讲过,一个文件需要在VFS中变为一个进程才能被执行。
修改之后alloc_proc函数如下:(增加一行,kern/process/proc.c,136行)
proc->filesp = NULL; //初始化fs中的进程控制结构
修改load_icode函数(lab5中的加载进程函数),如下,主要是第三步,读取文件的更改:
(kern/process/proc.c,632——648行):
if ((ret = load_icode_read(fd, elf, sizeof(struct elfhdr), 0)) != 0) {
goto bad_elf_cleanup_pgdir;
}
if (elf->e_magic != ELF_MAGIC) {
ret = -E_INVAL_ELF;
goto bad_elf_cleanup_pgdir;
}
struct proghdr __ph, *ph = &__ph;
uint32_t vm_flags, perm, phnum;
for (phnum = 0; phnum < elf->e_phnum; phnum ++) {
off_t phoff = elf->e_phoff + sizeof(struct proghdr) * phnum;
if ((ret = load_icode_read(fd, ph, sizeof(struct proghdr), phoff)) != 0) {
goto bad_cleanup_mmap;
}
与之前的对比一下:
为什么这次调用load_icode_read呢?因为这次有了文件系统,在lab5没有文件系统的时候,所有的文件都是一起被加载到内存的,内存中间有elf格式文件的起始地址,但是这次实验需要自己找。其实现方式即为调用load_icode_read。
四、运行结果
直接运行答案文件夹可能会出错,需要在Makefile最后加上:
tags:
@echo TAGS ALL
$(V)rm -f cscope.files cscope.in.out cscope.out cscope.po.out tags
$(V)find . -type f -name "*.[chS]" >cscope.files
$(V)cscope -bq
$(V)ctags -L cscope.files
或者将lab8中的Makefile,复制到lab8_answer中,覆盖掉原有的answer文件夹中的Makefile,
待make qemu信息输出完毕后,点进qemu界面,输入ls,回车,可以看到文件信息: