Linux文件操作源码分析记录(打开与读文件)

本文记录Linux 0.11的文件操作源码的学习过程,该文章参考了《Linux内核设计的艺术》中的内容,并在原文的基础上加以理解,并总结于此以便学习回顾。

基础篇

image.png

文件系统用来存储文件内容、文件属性、和目录。这些类型的数据如何存储在磁盘块上的呢?unix/linux使用了一个简单的方法。

它将磁盘块分为三个部分:

  1. 超级块,文件系统中第一个块被称为超级块。这个块存放文件系统本身的结构信息。比如,超级块记录了每个区域的大小,超级块也存放未被使用的磁盘块的信息。
  2. inode表。超级块的下一个部分就是i-节点表,每个文件都有一些属性,如文件的大小、文件所有者、和创建时间等,这些性质被记录在一个称为i-节点的结构中。所有i-节点都有相同的大小,并且i-节点表是这些结构的一个列表,文件系统中每个文件在该表中都有一个i-节点。
  3. 数据区。文件系统的第3个部分是数据区。文件的内容保存在这个区域。磁盘上所有块的大小都一样。如果文件包含了超过一个块的内容,则文件内容会存放在多个磁盘块中。一个较大的文件很容易分布上千个独立的磁盘块中.

打开文件操作

获取目标文件的inode

Linux 0.11 版本的文件系统中存在*filp[20]、file_table[64]、inode_table[32]

文件系统需要确定进程操作哪个文件:

“1)将用户进程task_struct中的*filp[20]与内核中的file_table[64]进行挂接。

2)将用户进程需要打开的文件对应的inode在file_table[64]中进行登记。”

其中“内核通过 *filp[20] 掌控一个进程可以打开的文件,既可以打开多个不同的文件,也可以同一个文件多次打开,每打开一次文件(不论是否是同一个文件),就要在*filp[20]中占用一个项(比如hello.txt文件被一个用户进程打开两次,就要在*filp[20]中占用两项)记录指针。

操作系统中file_table[64]是管理所有进程打开文件的数据结构,与*filp[20]类似,只要打开一次文件,就要在file_table[64]中记录。

image.png

第一步,首先需要将 *filp[20]与内核中的file_table[64]进行挂接以便

挂载的目的是方便用户进程操作文件。书中以“/mnt/user/user1/user2/hello.txt”为例进行讲解:

sys_open函数是系统

image.png

即寻找到两个数据结构中空余的部分,并进行对应挂接。当超过使用的极限后,内核报错,所以需要文件系统先检查后使用。

image.png

上图为查找hello.txt的流程图

image.png

在底层是调用“open_namei()函数实现的”。

image.png

之后依次调用-open_namei() - dir_namei() - get_dir()函数,用于分析用户给出的文件路径名,遍历路径所有目录文件i节点,目的是获取最后一个目录文件i节点。

get_dir()函数中,确定目录项对应的函数是find_entry();通过目录项获取i节点对应的函数是iget()

get_dir()核心代码如下:

image.png

dentry,即directory entry,目录项,就是多个文件或者目录的链接,通过这个链接可以找寻到目录之下的文件或者是目录项。

struct dentry {
    atomic_t d_count;
    unsigned int d_flags;       /* protected by d_lock */
    spinlock_t d_lock;      /* per dentry lock */
    struct inode *d_inode;      /* Where the name belongs to - NULL is
                     * negative */
    /*
     * The next three fields are touched by __d_lookup.  Place them here
     * so they all fit in a cache line.
     */
    struct hlist_node d_hash;   /* lookup hash list */
    struct dentry *d_parent;    /* parent directory */
    struct qstr d_name;
 
 
    struct list_head d_lru;     /* LRU list */
    /*
     * d_child and d_rcu can share memory
     */
    union {
        struct list_head d_child;   /* child of parent list */
        struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs; /* our children */
    struct list_head d_alias;   /* inode alias list */
    unsigned long d_time;       /* used by d_revalidate */
    struct dentry_operations *d_op;
    struct super_block *d_sb;   /* The root of the dentry tree */
    void *d_fsdata;         /* fs-specific data */
#ifdef CONFIG_PROFILING
    struct dcookie_struct *d_cookie; /* cookie, if any */
#endif
    int d_mounted;
    unsigned char d_iname[DNAME_INLINE_LEN_MIN];    /* small names */
};

根据目录项我们要找到对应目录的inode(Linux中每一个目录均是一个文件),之后循环寻找(/mnt/user/user1/user2/hello.txt为例依次寻找/mnt、 /user、 /user1、 /user2、 /hello.txt)

其中find_entry()函数的任务是:先通过目录文件i节点,确定目录文件中有多少目录项,之后从目录文件对应的第一个逻辑块开始,不断将该文件的逻辑块从外设读入缓冲区,并从中查找指定目录项,直到找到指定的目录项为止。

iget()函数的任务是:根据目录项中提供的i节点号、设备号获取i节点。具体的获取方式是:先在inode_table[32]中搜索,如果指定的i节点已在其中,就直接使用;如果找不到,再加载。

image.png

同样使用find_entry()iget()函数获取hello.txt的inode。

到此,我们就找到了hello.txt的inode号,并且将inode放入到空闲的inode_table[32]中。

将inode与file_table[64]挂载

此时我们已经获得了目标文件对应的inode_table[32]。

现在要将该inode与file_table[64]进行挂接,目的是使file_table[64]通过inode_table[32]中hello.txt文件inode所在表项的指针,找到该inode。

image.png

到此为止,file_table[64]中的挂接点,一端与当前进程的*filp[20]指针绑定,另一端与inode_table[32]中hello.txt文件的i节点绑定。绑定关系建立后,操作系统把fd返给用户进程。这个fd是挂接点在file_table[64]中的偏移量,即“文件句柄”。进程此后只要把这个fd传递给操作系统,操作系统就可以判断出进程需要操作哪个文件。

image.png

这就是我们常见的c中的读操作代码,此时fd已经被赋予了open后的句柄,变可以进行读操作了。

读文件操作

read()函数最终映射到sys_read()系统调用函数去执行,而该函数最终调用file_read()。在执行主体内容之前,先对此次操作的可行性进行检查,包括用户进程传递的文件句柄、读取字节数是否在合理范围内,用户进程数据所在的页面能否被写入数据,等等。

file_read()函数如下:

image.png

其中最重要的是bmap函数,该函数将1KB的数据复制到缓冲区中。

inode在管理文件时使用的是i_zone结构,如下图:

image.png

image.png

当数据总量小于等于7 KB时,i_zone[9]的前7个成员已经足够用了,它们就直接记录该文件的这7个数据块在数据区的“块号(剩余两个存储其他系统信息)。

由于一个i_zone元素代表1kb数据,所以当数据>7时,就需要使用二级索引或者三级索引。即最多存储:(7+512+512×512)KB数据。

那我们如何将数据从数据块读入缓冲区呢?

即调用bread()函数。

image.png

当数据从设备被读入到缓冲区后,系统需要再将其从缓冲区中复制到用户的进程中(*buf)。

image.png

从hello.txt文件的起始位置读出了一个数据块(1 KB)的数据。通过while不断地循环,将指定数量的数据全部载入用户进程的*buf区域。

新建文件操作

新建文件就是根据用户进程要求,创建一个文件系统中不存在的文件。新建文件由creat()函数实现。

新建文件首先需要查找该文件是否存在,即调用sys_creat(),之后调用sys_open()函数,此时仍然调用open_namei()函数来获取inode节点,但是不同的地方是这次并不能获取到。

image.png

于是将bh赋值为null。

之后进行新建操作,但是此处注意,没有找到对应文件的目录项并不意味就必须要新建,有可能是用户输入错了。所以需要检查O_CREAT标志位。

image.png

在创建新文件时,需要创建文件的目录项:

hello.txt的目录项要载入user2目录文件中。add_entry()函数的任务是:只要在目录文件中寻找到空闲项,就在此位置处加载新目录项;如果确实找不到空闲项,就在外设上创建新的数据块来加载。

image.png

之后调用new_block()新建数据块。

下一期我们讲一下如何进行写文件操作。

本文许多资料来自书中,大家如果想了解更多可以去书中寻找,我只是把自己的一些看法抽象出来便于回顾。

你可能感兴趣的:(Linux文件操作源码分析记录(打开与读文件))