本文记录Linux 0.11的文件操作源码的学习过程,该文章参考了《Linux内核设计的艺术》中的内容,并在原文的基础上加以理解,并总结于此以便学习回顾。
基础篇
文件系统用来存储文件内容、文件属性、和目录。这些类型的数据如何存储在磁盘块上的呢?unix/linux使用了一个简单的方法。
它将磁盘块分为三个部分:
- 超级块,文件系统中第一个块被称为超级块。这个块存放文件系统本身的结构信息。比如,超级块记录了每个区域的大小,超级块也存放未被使用的磁盘块的信息。
- inode表。超级块的下一个部分就是i-节点表,每个文件都有一些属性,如文件的大小、文件所有者、和创建时间等,这些性质被记录在一个称为i-节点的结构中。所有i-节点都有相同的大小,并且i-节点表是这些结构的一个列表,文件系统中每个文件在该表中都有一个i-节点。
- 数据区。文件系统的第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]中记录。
第一步,首先需要将 *filp[20]
与内核中的file_table[64]进行挂接以便
挂载的目的是方便用户进程操作文件。书中以“/mnt/user/user1/user2/hello.txt”为例进行讲解:
sys_open函数是系统
即寻找到两个数据结构中空余的部分,并进行对应挂接。当超过使用的极限后,内核报错,所以需要文件系统先检查后使用。
上图为查找hello.txt的流程图
在底层是调用“open_namei()
函数实现的”。
之后依次调用-open_namei()
- dir_namei()
- get_dir()
函数,用于分析用户给出的文件路径名,遍历路径所有目录文件i节点,目的是获取最后一个目录文件i节点。
在get_dir()
函数中,确定目录项对应的函数是find_entry()
;通过目录项获取i节点对应的函数是iget()
。
get_dir()
核心代码如下:
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节点已在其中,就直接使用;如果找不到,再加载。
同样使用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。
到此为止,file_table[64]中的挂接点,一端与当前进程的*filp[20]指针绑定,另一端与inode_table[32]中hello.txt文件的i节点绑定。绑定关系建立后,操作系统把fd返给用户进程。这个fd是挂接点在file_table[64]中的偏移量,即“文件句柄”。进程此后只要把这个fd传递给操作系统,操作系统就可以判断出进程需要操作哪个文件。
这就是我们常见的c中的读操作代码,此时fd已经被赋予了open后的句柄,变可以进行读操作了。
读文件操作
read()函数最终映射到sys_read()系统调用函数去执行,而该函数最终调用file_read()。在执行主体内容之前,先对此次操作的可行性进行检查,包括用户进程传递的文件句柄、读取字节数是否在合理范围内,用户进程数据所在的页面能否被写入数据,等等。
file_read()
函数如下:
其中最重要的是bmap函数,该函数将1KB的数据复制到缓冲区中。
inode在管理文件时使用的是i_zone结构,如下图:
当数据总量小于等于7 KB时,i_zone[9]的前7个成员已经足够用了,它们就直接记录该文件的这7个数据块在数据区的“块号(剩余两个存储其他系统信息)。
由于一个i_zone元素代表1kb数据,所以当数据>7时,就需要使用二级索引或者三级索引。即最多存储:(7+512+512×512)KB数据。
那我们如何将数据从数据块读入缓冲区呢?
即调用bread()函数。
当数据从设备被读入到缓冲区后,系统需要再将其从缓冲区中复制到用户的进程中(*buf)。
从hello.txt文件的起始位置读出了一个数据块(1 KB)的数据。通过while不断地循环,将指定数量的数据全部载入用户进程的*buf区域。
新建文件操作
新建文件就是根据用户进程要求,创建一个文件系统中不存在的文件。新建文件由creat()函数实现。
新建文件首先需要查找该文件是否存在,即调用sys_creat(),之后调用sys_open()函数,此时仍然调用open_namei()函数来获取inode节点,但是不同的地方是这次并不能获取到。
于是将bh赋值为null。
之后进行新建操作,但是此处注意,没有找到对应文件的目录项并不意味就必须要新建,有可能是用户输入错了。所以需要检查O_CREAT标志位。
在创建新文件时,需要创建文件的目录项:
hello.txt的目录项要载入user2目录文件中。add_entry()
函数的任务是:只要在目录文件中寻找到空闲项,就在此位置处加载新目录项;如果确实找不到空闲项,就在外设上创建新的数据块来加载。
之后调用new_block()新建数据块。
下一期我们讲一下如何进行写文件操作。
本文许多资料来自书中,大家如果想了解更多可以去书中寻找,我只是把自己的一些看法抽象出来便于回顾。