ucore文件系统详解

最近一直在mooc上学习清华大学的操作系统课程,也算是复习下基本概念和原理,为接下来的找工作做准备。
每次深入底层源码都让我深感操作系统实现的琐碎,即使像ucore这样简单的kernel也让我烦躁不已,文件系统相比于中断子系统、调度子系统、进程管理子系统等等,要复杂很多,因此被称为文件系统而不是文件子系统。参看网络上的资料有时会增加我的困惑,很多人只是简单转载,很多细节描述的很模糊,实验环境也各不相同,最终很难深入理解文件系统的本质,参考源码我觉得有点像从三维世界进入到二维世界,一切变得清晰但是却需要消耗更多精力,我觉得这样做是值得的,因为如果不能深入而只是泛泛的理解,对于操作系统这样偏向于工程学的东西来说意义不大,本文的研究内容主要是根据清华大学陈渝副教授、向勇副教授在mooc上所讲,以及实验参考书的内容和我自己在系统上打的log验证过的,如果有读者发现错误还请批评指正。
本篇博客希望尽可能照顾到初学者,但是有些简单原理默认读者已经掌握,很多细节不会展开叙述,读者请自行Google或者参看Intel Development Manual,实验用的源码来自于清华大学教学操作系统,读者可在github上搜索ucore_os_lab。
附上ucore的实验参考书Ucore Lab Document.

综述

最初实现文件系统是为了实现对磁盘数据的高效管理,使得用户可以高效、快速的读取磁盘上的数据内容。其实我个人觉得文件系统就是操作系统内核和外设之间的一套输入输出协议,我们所采用的算法和索引结点的建立方式都是为了根据实际应用情况所设计的一套高效的协议。在实现的过程中,我们还要将文件系统进行分层抽象,也就是实现我们的虚拟文件系统,虚拟文件系统有对上和对下两个方面,对上是为了向上层应用提供一套通用的访问接口,可以用相同的方式去访问磁盘上的文件(包括目录,后面会介绍)和外设;对下兼容不同的文件系统和外设。虚拟文件系统的层次和依赖关系如下图所示:

对照上面的层次我们再大致介绍一下文件系统的访问处理过程,加深对文件系统的总体理解。假如应用程序操作文件(打开/创建/删除/读写),首先需要通过文件系统的通用文件系统访问接口层给用户空间提供的访问接口进入文件系统内部,接着由文件系统抽象层把访问请求转发给某一具体文件系统(比如SFS文件系统),具体文件系统(Simple FS文件系统层)把应用程序的访问请求转化为对磁盘上的block的处理请求,并通过外设接口层交给磁盘驱动例程来完成具体的磁盘操作。结合用户态写文件函数write的整个执行过程,我们可以比较清楚地看出ucore文件系统架构的层次和依赖关系。

总体设计

ucore的文件系统模型源于Havard的OS161文件系统和Linux文件系统。但其实这两者都源于UNIX文件系统设计。UNIX提出了四个文件系统抽象概念:文件(file)、目录项(descriptor entry,简写为dentry)、索引节点(inode)和安装点(mount point)。

  • 文件:UNIX文件中的内容可理解为是一有序字节buffer,文件都有一个方便应用程序识别的文件名称(也称文件路径名)。典型的文件操作有读、写、创建和删除等。
  • 目录项:目录项不是目录,而是目录的组成部分。在UNIX中目录被看作一种特定的文件,而目录项是文件路径中的一部分。如一个文件路径名是“/test/testfile”,则包含的目录项为:根目录“/”,目录“test”和文件“testfile”,这三个都是目录项。一般而言,目录项包含目录项的名字(文件名或目录名)和目录项的索引节点(见下面的描述)位置。
  • 索引节点:UNIX将文件的相关元数据信息(如访问控制权限、大小、拥有者、创建时间、数据内容等等信息)存储在一个单独的数据结构中,该结构被称为索引节点。
  • 安装点:在UNIX中,文件系统被安装在一个特定的文件路径位置,这个位置就是安装点。所有的已安装文件系统都作为根文件系统树中的叶子出现在系统中。
    上述抽象概念形成了UNIX文件系统的逻辑数据结构,并需要通过一个具体文件系统的架构设计与实现把上述信息映射并储存到磁盘介质上。一个具体的文件系统需要在磁盘布局也实现上述抽象概念。比如文件元数据信息存储在磁盘块中的索引节点上。当文件被载如内存时,内核需要使用磁盘块中的索引点来构造内存中的索引节点。
    ucore模仿了UNIX的文件系统设计,ucore的文件系统架构主要由四部分组成:
  • 通用文件系统访问接口层:该层提供了一个从用户空间到文件系统的标准访问接口。这一层访问接口让应用程序能够通过一个简单的接口获得ucore内核的文件系统服务。
  • 文件系统抽象层:向上提供一个一致的接口给内核其他部分(文件系统相关的系统调用实现模块和其他内核功能模块)访问。向下提供一个同样的抽象函数指针列表和数据结构屏蔽不同文件系统的实现细节。
  • Simple FS文件系统层:一个基于索引方式的简单文件系统实例。向上通过各种具体函数实现以对应文件系统抽象层提出的抽象函数。向下访问外设接口
  • 外设接口层:向上提供device访问接口屏蔽不同硬件细节。向下实现访问各种具体设备驱动的接口,比如disk设备接口/串口设备接口/键盘设备接口等。

相关数据结构

在文件系统初始化时,主要完成三个函数:

    void fs_init(void) {
    vfs_init();
    dev_init();
    sfs_init();
}

它们的具体实现如下:

    void vfs_init(void) {
    sem_init(&bootfs_sem, 1);
    vfs_devlist_init();
}
    void dev_init(void) {
    init_device(stdin);
    init_device(stdout);
    init_device(disk0);
}
    void sfs_init(void) {
    int ret;
    if ((ret = sfs_mount("disk0")) != 0) {
        panic("failed: sfs: sfs_mount: %e.\n", ret);
    }
}

对于vfs_init,它只是完成了对vfs访问的信号量和devlist的初始化。dev_init则完成了对设备的初始化,这里的stdin代表输入设备,即键盘,stdout代表输出设备,包括UART串口和显示器,disk0代表磁盘,我们以外设stdin作为例子进行讲述,stdout和stdin类似,disk0和stdin有一些不同,会在下文进行对比。首先,这里调用了init_device(stdin),它是一个宏,如下所示:

#define init_device(x)                                  \
    do {                                                \
        extern void dev_init_##x(void);                 \
        dev_init_##x();                                 \
    } while (0)

最后会调用到dev_init_stdin()

void dev_init_stdin(void) {
    struct inode *node;
    if ((node = dev_create_inode()) == NULL) {
        panic("stdin: dev_create_node.\n");
    }
    cprintf("dev_init_stdin is called\n");
    stdin_device_init(vop_info(node, device));

    int ret;
    if ((ret = vfs_add_dev("stdin", node, 0)) != 0) {
        panic("stdin: vfs_add_dev: %e.\n", ret);
    }
}

在dev_init_stdin中主要涉及两个数据结构,分别是struct inode和device,它们的数据结构如下所示:

struct inode {
    union {
        struct device __device_info;
        struct sfs_inode __sfs_inode_info;
    } in_info;
    enum {
        inode_type_device_info = 0x1234,
        inode_type_sfs_inode_info,
    } in_type;
    int ref_count;
    int open_count;
    struct fs *in_fs;
    const struct inode_ops *in_ops;
};
struct device {
    size_t d_blocks;
    size_t d_blocksize;
    int (*d_open)(struct device *dev, uint32_t open_flags);
    int (*d_close)(struct device *dev);
    int (*d_io)(struct device *dev, struct iobuf *iob, bool write);
    int (*d_ioctl)(struct device *dev, int op, void *data);
};

这里的关键函数是stdin_device_init(vop_info(node, device)),它完成设置inode为设备文件,初始化设备文件。其中vop_info是一个宏,实现如下:

#define vop_info(node, type)                                        __vop_info(node, type)
#define __vop_info(node, type)                                      \
    ({                                                              \
        struct inode *__node = (node);                              \
        assert(__node != NULL && check_inode_type(__node, type));   \
        &(__node->in_info.__##type##_info);                         \
     })

它完成返回in_info这个联合体里device的地址,然后交个stdin_device_init处理,实现如下:

static void
stdin_device_init(struct device *dev) {
    dev->d_blocks = 0;
    dev->d_blocksize = 1;
    dev->d_open = stdin_open;
    dev->d_close = stdin_close;
    dev->d_io = stdin_io;
    dev->d_ioctl = stdin_ioctl;

    p_rpos = p_wpos = 0;
    wait_queue_init(wait_queue);
}

inode代表的是一个抽象意义的文件,根据in_info和in_type的值的不同,它既可以表示文件也可以表示外设,open_count表示一个文件被进程打开的次数,当open_count=0时我们可以在kernel移除这个inode结点。这个inode是系统管理文件用的,因此用户层的程序不需要关心这个数据结构。device这个数据结构只有当inode表示设备时才会有用,其中d_blocks表示设备占据的数据块个数,d_blocksize表示数据占据的数据块大小,相应的四个操作也很简单,直接翻译过来就能理解,这里不再详述。需要注意的是,stdin相对于stdout多了一个输入缓冲区,需要额外的两个指针p_rpos,p_wpos分别记录当前读的位置和写的位置,当p_rpos < p_wpos时,说明当前有从键盘输入到缓冲区的数据但是还没有读到进程里,需要唤醒进程从缓冲区进行读操作,当p_rpos=p_wpos而进程发起读的系统调用时(如调用c库的scanf),这时需要阻塞进程,等待键盘输入时产生中断唤醒对应进程。
disk0的初始化过程其实和stdin和stdout几乎一样,只是在第三步sfs_init的过程中需要执行mount操作。sys_mount的调用过程如下:

int sfs_mount(const char *devname) {
    return vfs_mount(devname, sfs_do_mount);
}
int
vfs_mount(const char *devname, int (*mountfunc)(struct device *dev, struct fs **fs_store)) {
    int ret;
    lock_vdev_list();
    vfs_dev_t *vdev;
    if ((ret = find_mount(devname, &vdev)) != 0) {
        goto out;
    }
    if (vdev->fs != NULL) {
        ret = -E_BUSY;
        goto out;
    }
    assert(vdev->devname != NULL && vdev->mountable);

    struct device *dev = vop_info(vdev->devnode, device);
    if ((ret = mountfunc(dev, &(vdev->fs))) == 0) {
        assert(vdev->fs != NULL);
        cprintf("vfs: mount %s.\n", vdev->devname);
    }
out:
    unlock_vdev_list();
    return ret;
}

其中这里面最重要的就是对回调函数sfs_do_mount的调用,sfs_do_mount主要完成对struct sfs数据结构的初始化,这里的sfs是simple file system的缩写,本文讨论的ucore目前只支持这一种文件系统,该数据结构的实现如下:

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 */
};

其中最主要的是从磁盘中读取该文件系统的superblock(上文提到过,每个文件系统一个,记录该文件系统的信息),至此整个初始化过程讨论完毕。

Simple FS文件系统

ucore内核把所有文件都看作是字节流,任何内部逻辑结构都是专用的,由应用程序负责解释。ucore区分文件的物理结构,目前ucore支持如下几种类型的文件:

  • 常规文件:文件中包括的内容信息是由应用程序输入。SFS文件系统在普通文件上不强加任何内部结构,把其文件内容信息看作为字节。
  • 目录:包含一系列的entry,每个entry包含文件名和指向与之相关联的索引节点(index node)的指针。目录是按层次结构组织的。
  • 链接文件:实际上一个链接文件是一个已经存在的文件的另一个可选择的文件名。
  • 设备文件:不包含数据,但是提供了一个映射物理设备(如串口、键盘等)到一个文件名的机制。可通过设备文件访问外围设备。
  • 管道:管道是进程间通讯的一个基础设施。管道缓存了其输入端所接受的数据,以便在管道输出端读的进程能一个先进先出的方式来接受数据。
    在github上的ucore教学操作系统中,主要关注的是常规文件、目录和链接中的hardlink的设计实现。SFS文件系统中目录和常规文件具有共同的属性,而这些属性保存在索引节点中。SFS通过索引节点来管理目录和常规文件,索引节点包含操作系统所需要的关于某个文件的关键信息,比如文件的属性、访问许可权以及其他控制信息都保存在索引节点中。可以有多个文件名指向一个索引节点。

文件系统的布局

文件系统通常保存在磁盘上,disk0代表磁盘,用来存放一个SFS文件系统。磁盘的使用是以扇区为单位的,但是在文件系统中,一般按数据块来使用磁盘,在sfs中,我们以4k(8个sector,和page大小相等)为一个数据块。sfs文件系统的布局如下图所示。

第0个块(4K)是超级块(superblock),它包含了关于文件系统的所有关键参数,当计算机被启动或文件系统被首次接触时,超级块的内容就会被装入内存。其定义如下:

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代表一个魔数,其值为0x2f8dbe2a,内核用它来检查磁盘镜像是否合法,blocks记录了sfs中block的数量,unused_block记录了sfs中还没有被使用的block数量,其中关于物理磁盘的管理与虚拟内存的管理十分类似,每次使用物理磁盘也会有一个类似于物理内存管理的分配算法。最后info是记录一个字符串"simple file system"。
第1个块放了一个root-dir的inode,用来记录根目录的相关信息。有关inode还将在后续部分介绍。这里只要理解root-dir是SFS文件系统的根结点,通过这个root-dir的inode信息就可以定位并查找到根目录下的所有文件信息。
从第2个块开始,根据SFS中所有块的数量,用1个bit来表示一个块的占用和未被占用的情况。这个区域称为SFS的freemap区域,这将占用若干个块空间。为了更好地记录和管理freemap区域,专门提供了两个文件kern/fs/sfs/bitmap.[ch]来完成根据一个块号查找或设置对应的bit位的值。
最后在剩余的磁盘空间中,存放了所有其他目录和文件的inode信息和内容数据信息。需要注意的是虽然inode的大小小于一个块的大小(4096B),但为了实现简单,每个 inode 都占用一个完整的 block。
在sfs_fs.c文件中的sfs_do_mount函数中,完成了加载位于硬盘上的SFS文件系统的超级块superblock和freemap的工作。这样,在内存中就有了SFS文件系统的全局信息。

索引节点

磁盘索引节点

之前在初始化过程中讨论过vfs对应的索引节点,其实索引节点主要是指存在磁盘中的索引节点,当把磁盘中的索引节点load到内存中之后,在内存中也会存在一个索引节点,下面先讨论磁盘中的索引节点,数据结构如下所示。

struct sfs_disk_inode {
    uint32_t size;                              如果inode表示常规文件,则size是文件大小
    uint16_t type;                                  inode的文件类型
    uint16_t nlinks;                               此inode的硬链接数
    uint32_t blocks;                              此inode的数据块数的个数
    uint32_t direct[SFS_NDIRECT];                此inode的直接数据块索引值(有SFS_NDIRECT个)
    uint32_t indirect;                            此inode的一级间接数据块索引值
};

对于磁盘索引节点,这里只解释最后的两个成员变量,direct指的是这个inode的直接索引块的索引值,它的大小是12,所以最多能够通过direct的方式支持最大12*4096B的文件大小。之所以这样设计是因为我们实际的文件系统中,绝大多数文件都是小文件,因此直接索引的方式能够提高小文件的存取速度,而且通过间接索引的方式还能支持大文件。当使用一级间接数据块索引时,ucore 支持最大的文件大小为 12 * 4k + 1024 * 4k = 48k + 4m。
对于普通文件,索引值指向的 block 中保存的是文件中的数据。而对于目录,索引值指向的数据保存的是目录下所有的文件名以及对应的索引节点所在的索引块(磁盘块)所形成的数组。数据结构如下:

/* file entry (on disk) */
struct sfs_disk_entry {
    uint32_t ino;                                   索引节点所占数据块索引值
    char name[SFS_MAX_FNAME_LEN + 1];               文件名
};

操作系统中,每个文件系统下的 inode 都应该分配唯一的 inode 编号。SFS 下,为了实现的简便(偷懒),每个 inode 直接用他所在的磁盘 block 的编号作为 inode 编号。比如,root block 的 inode 编号为 1;每个 sfs_disk_entry 数据结构中,name 表示目录下文件或文件夹的名称,ino 表示磁盘 block 编号,通过读取该 block 的数据,能够得到相应的文件或文件夹的 inode。ino 为0时,表示一个无效的 entry。(因为 block 0 用来保存 super block,它不可能被其他任何文件或目录使用,所以这么设计也是合理的)。
此外,和 inode 相似,每个 sfs_dirent_entry 也占用一个 block。

内存索引节点

内存索引节点的数据结构如下图所示。

/* inode for sfs */
struct sfs_inode {
    struct sfs_disk_inode *din;                     /* on-disk inode */
    uint32_t ino;                                   /* inode number */
    uint32_t flags;                                 /* inode flags */
    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 */
};

内存inode只有在打开一个文件后才会创建,如果关机则相关信息都会消失。可以看到,内存inode包含了硬盘inode的信息,而且还增加了其他一些信息,这是为了实现判断是否改写(dirty),互斥操作(sem),回收(reclaim——count)和快速定位(hash_link)等作用。为了便于实现上面提到的多级数据访问以及目录中entry的操作,对inode SFS实现了一些辅助函数:

  1. sfs_bmap_load_nolock:将对应 sfs_inode 的第 index 个索引指向的 block 的索引值取出存到相应的指针指向的单元(ino_store)。该函数只接受 index <= inode-<blocks 的参数。当 index == inode-&ltblocks 时,该函数理解为需要为 inode 增长一个 block。并标记 inode 为 dirty(所有对 inode 数据的修改都要做这样的操作,这样,当 inode 不再使用的时候,sfs 能够保证 inode 数据能够被写回到磁盘)。sfs_bmap_load_nolock 调用的 sfs_bmap_get_nolock 来完成相应的操作。(sfs_bmap_get_nolock 只由 sfs_bmap_load_nolock 调用)
  2. sfs_bmap_truncate_nolock:将多级数据索引表的最后一个 entry 释放掉。他可以认为是 sfs_bmap_load_nolock 中,index == inode->blocks 的逆操作。当一个文件或目录被删除时,sfs 会循环调用该函数直到 inode->blocks 减为 0,释放所有的数据页。函数通过 sfs_bmap_free_nolock 来实现,他应该是 sfs_bmap_get_nolock 的逆操作。和 sfs_bmap_get_nolock 一样,调用 sfs_bmap_free_nolock 也要格外小心。
  3. sfs_dirent_read_nolock:将目录的第 slot 个 entry 读取到指定的内存空间。他通过上面提到的函数来完成。
  4. sfs_dirent_write_nolock:用指定的 entry 来替换某个目录下的第 slot 个entry。他通过调用 sfs_bmap_load_nolock保证,当第 slot 个entry 不存在时(slot == inode->blocks),SFS 会分配一个新的entry,即在目录尾添加了一个 entry。
  5. sfs_dirent_search_nolock:是常用的查找函数。他在目录下查找 name,并且返回相应的搜索结果(文件或文件夹)的 inode 的编号(也是磁盘编号),和相应的 entry 在该目录的 index 编号以及目录下的数据页是否有空闲的 entry。(SFS 实现里文件的数据页是连续的,不存在任何空洞;而对于目录,数据页不是连续的,当某个 entry 删除的时候,SFS 通过设置 entry->ino 为0将该 entry 所在的 block 标记为 free,在需要添加新 entry 的时候,SFS 优先使用这些 free 的 entry,其次才会去在数据页尾追加新的 entry。
    这部分比较复杂,关于这部分以后会单独开一篇博客来叙述。

文件系统抽象层-VFS

文件系统抽象层是把不同文件系统的对外共性借口提取出来,形成一个数据结构(包含多个函数指针),这样,通用文件系统访问接口层只需要访问文件系统抽象层,而不需要关心具体文件系统的实现细节和接口。

file&dir接口

file&dir接口层定义了进程在内核中直接访问的文件相关信息,这定义在file数据结构中,具体描述如下:

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;              //打开此文件的次数
};

而在kern/process/proc.h中的proc_struct结构中描述了进程访问文件的数据接口fs_struct,其数据结构定义如下:

struct fs_struct {
    struct inode *pwd;                //进程当前执行目录的内存inode指针
    struct file *filemap;             //进程打开文件的数组
    atomic_t fs_count;                //访问此文件的线程个数??
    semaphore_t fs_sem;                //确保对进程控制块中fs_struct的互斥访问
};

当创建一个进程后,该进程的fs_struct将会被初始化或复制父进程的fs_struct。当用户进程打开一个文件时,将从filemap数组中取得一个空闲file项,然后会把此file的成员变量node指针指向一个代表此文件的inode的起始地址。

inode接口

index node是位于内存的索引节点,它是VFS结构中的重要数据结构,因为它实际负责把不同文件系统的特定索引节点信息(甚至不能算是一个索引节点)统一封装起来,避免了进程直接访问具体文件系统。它的数据结构在上文已经介绍过,在inode中,有一个成员变量in_ops,这是对此inode的操作函数指针列表,其数据结构定义如下:

struct inode_ops {
    unsigned long vop_magic;
    int (*vop_open)(struct inode *node, uint32_t open_flags);
    int (*vop_close)(struct inode *node);
    int (*vop_read)(struct inode *node, struct iobuf *iob);
    int (*vop_write)(struct inode *node, struct iobuf *iob);
    int (*vop_getdirentry)(struct inode *node, struct iobuf *iob);
    int (*vop_create)(struct inode *node, const char *name, bool excl, struct inode **node_store);
int (*vop_lookup)(struct inode *node, char *path, struct inode **node_store);
……
 };

参照上面对SFS中的索引节点操作函数的说明,可以看出inode_ops是对常规文件、目录、设备文件所有操作的一个抽象函数表示。对于某一具体的文件系统中的文件或目录,只需实现相关的函数,就可以被用户进程访问具体的文件了,且用户进程无需了解具体文件系统的实现细节。

一个例子(打开文件,读文件)

有了上述实现后,ucore就可以支持文件系统了,那么我们来看一看当用户进程打开文件时会做哪些事情?
首先假定用户进程需要打开的文件已经存在在硬盘上。以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中,并进入到文件系统抽象层的处理流程完成进一步的打开文件操作中。

文件接口抽象层的处理流程

分配一个空闲的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函数打开文件。

  1. 找到文件设备的根目录“/”的索引节点需要注意,这里的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)执行时获得了赋值。

  2. 找到根目录“/”下的“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文件系统层的处理流程

这里需要分析文件系统抽象层中没有彻底分析的vop_lookup函数到底做了啥。下面我们来看看。在sfs_inode.c中的sfs_node_dirops变量定义了“.vop_lookup = sfs_lookup”,所以我们重点分析sfs_lookup的实现。
sfs_lookup有三个参数:node,path,node_store。其中node是根目录“/”所对应的inode节点;path是文件“testfile”的绝对路径“/test/testfile”,而node_store是经过查找获得的“testfile”所对应的inode节点。
Sfs_lookup函数以“/”为分割符,从左至右逐一分解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。

posted @ 2017-05-02 10:47  miao_zheng  阅读(3847)  评论(0编辑  收藏  举报