This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,332 @@
<audio id="audio" title="27 | 文件系统:项目成果要归档,我们就需要档案库" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/92/e4/92b69c9fc81719fa6666a7cfd5fb01e4.mp3"></audio>
咱们花了这么长的时间,规划了会议室管理系统,这样多个项目执行的时候,隔离性可以得到保证。但是,会议室里面保存的资料还是暂时的,一旦项目结束,会议室会被回收,会议室里面的资料就丢失了。有一些资料我们希望项目结束也能继续保存,这就需要一个和项目运行生命周期无关的地方,可以永久保存,并且空间也要比会议室大得多。
## 文件系统的功能规划
要知道,这些资料才是咱们公司的财富,是执行多个项目积累下来的,是公司竞争力的保证,需要有一个地方归档。这就需要我们有一个存放资料的档案库,在操作系统中就是**文件系统**。那我们应该如何组织规划文件系统这个档案库呢?
对于运行的进程来说,内存就像一个纸箱子,仅仅是一个暂存数据的地方,而且空间有限。如果我们想要进程结束之后,数据依然能够保存下来,就不能只保存在内存里,而是应该保存在外部存储中。就像图书馆这种地方,不仅空间大,而且能够永久保存。
我们最常用的外部存储就是硬盘,数据是以文件的形式保存在硬盘上的。为了管理这些文件,我们在规划文件系统的时候,需要考虑到以下几点。
**第一点,文件系统要有严格的组织形式,使得文件能够以块为单位进行存储**。这就像图书馆里,我们会设置一排排书架,然后再把书架分成一个个小格子,有的项目存放的资料非常多,一个格子放不下,就需要多个格子来存放。我们把这个区域称为存放原始资料的仓库区。
**第二点,文件系统中也要有索引区,用来方便查找一个文件分成的多个块都存放在了什么位置**。这就好比,图书馆的书太多了,为了方便查找,我们需要专门设置一排书架,这里面会写清楚整个档案库有哪些资料,资料在哪个架子的哪个格子上。这样找资料的时候就不用跑遍整个档案库,在这个书架上找到后,直奔目标书架就可以了。
<img src="https://static001.geekbang.org/resource/image/93/07/93bf5e8e940752b32531ed6752b5f607.png" alt="">
**第三点,如果文件系统中有的文件是热点文件,近期经常被读取和写入,文件系统应该有缓存层**。这就相当于图书馆里面的热门图书区,这里面的书都是畅销书或者是常常被借还的图书。因为借还的次数比较多,那就没必要每次有人还了之后,还放回遥远的货架,我们可以专门开辟一个区域,放置这些借还频次高的图书。这样借还的效率就会提高。
**第四点,文件应该用文件夹的形式组织起来,方便管理和查询**。这就像在图书馆里面,你可以给这些资料分门别类,比如分成计算机类、文学类、历史类等等。这样你也容易管理,项目组借阅的时候只要在某个类别中去找就可以了。
在文件系统中,每个文件都有一个名字,这样我们访问一个文件,希望通过它的名字就可以找到。文件名就是一个普通的文本。当然文件名会经常冲突,不同用户取相同的名字的情况还是会经常出现的。
要想把很多的文件有序地组织起来,我们就需要把它们成为目录或者文件夹。这样,一个文件夹里可以包含文件夹,也可以包含文件,这样就形成了一种树形结构。而我们可以将不同的用户放在不同的用户目录下,就可以一定程度上避免了命名的冲突问题。
<img src="https://static001.geekbang.org/resource/image/e7/4f/e71da53d6e2e4458bcc0af1e23f08e4f.png" alt="">
如图所示不同的用户的文件放在不同的目录下虽然很多文件都叫“文件1”只要在不同的目录下就不会有问题。
有了目录结构,定位一个文件的时候,我们还会分**绝对路径**Absolute Path和**相对路径**Relative Path。所谓绝对路径就是从根目录开始一直到当前的文件例如“/根目录/用户A目录/目录1/文件2”就是一个绝对路径。而通过cd命令可以改变当前路径例如“cd /根目录/用户A目录”就是将用户A目录设置为当前目录而刚才那个文件的相对路径就变成了“./目录1/文件2”。
**第五点Linux内核要在自己的内存里面维护一套数据结构来保存哪些文件被哪些进程打开和使用**。这就好比,图书馆里会有个图书管理系统,记录哪些书被借阅了,被谁借阅了,借阅了多久,什么时候归还。
好了,这样下来,这文件系统的几个部分,是不是就很好理解、记忆了?你不用死记硬背,只要按照一个正常的逻辑去理解,自然而然就能记住了。接下来的整个章节,我们都要围绕这五点展开解析。
## 文件系统相关命令行
在Linux命令的那一节我们学了一些简单的文件操作的命令这里我们再来学几个常用的。
首先是**格式化**也即将一块盘使用命令组织成一定格式的文件系统的过程。咱们买个硬盘或者U盘经常说要先格式化才能放文件说的就是这个。
使用Windows的时候咱们常格式化的格式为**NTFS**New Technology File System。在Linux下面常用的是ext3或者ext4。
当一个Linux系统插入了一块没有格式化的硬盘的时候我们可以通过命令**fdisk -l**,查看格式化和没有格式化的分区。
```
# fdisk -l
Disk /dev/vda: 21.5 GB, 21474836480 bytes, 41943040 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disk label type: dos
Disk identifier: 0x000a4c75
Device Boot Start End Blocks Id System
/dev/vda1 * 2048 41943006 20970479+ 83 Linux
Disk /dev/vdc: 107.4 GB, 107374182400 bytes, 209715200 sectors
Units = sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
```
例如从上面的命令的输出结果可以看出vda这块盘大小21.5G,是格式化了的,有一个分区/dev/vda1。vdc这块盘大小107.4G,是没有格式化的。
我们可以通过命令**mkfs.ext3**或者**mkfs.ext4**进行格式化。
```
mkfs.ext4 /dev/vdc
```
执行完这个命令后vdc会建立一个分区格式化为ext4文件系统的格式。至于这个格式是如何组织的我们下一节仔细讲。
当然,你也可以选择不将整块盘格式化为一个分区,而是格式化为多个分区。下面的这个命令行可以启动一个交互式程序。
```
fdisk /dev/vdc
```
在这个交互式程序中,你可以输入**p**来打印当前分了几个区。如果没有分过,那这个列表应该是空的。
接下来,你可以输入**n**新建一个分区。它会让你选择创建主分区primary还是扩展分区extended。我们一般都会选择主分区p。
接下来它会让你输入分区号。如果原来没有分过区应该从1开始。或者你直接回车使用默认值也行。
接下来,你可以一路选择默认值,直到让你指定这个分区的大小,通过+sizeM或者+sizeK的方式默认值是整块盘都用上。你可以 输入+5620M分配一个5G的分区。这个时候再输入p就能看到新创建的分区了最后输入w将对分区的修改写入硬盘。
分区结束之后可能会出现vdc1, vdc2等多个分区这个时候你可以mkfs.ext3 /dev/vdc1将第一个分区格式化为ext3通过mkfs.ext4 /dev/vdc2将第二个分区格式化为ext4.
格式化后的硬盘,需要挂在到某个目录下面,才能作为普通的文件系统进行访问。
```
mount /dev/vdc1 /根目录/用户A目录/目录1
```
例如,上面这个命令就是将这个文件系统挂载到“/根目录/用户A目录/目录1”这个目录下面。一旦挂在过去“/根目录/用户A目录/目录1”这个目录下面原来的文件1和文件2就都看不到了换成了vdc1这个硬盘里面的文件系统的根目录。
有挂载就有卸载,卸载使用**umount**命令。
```
umount /根目录/用户A目录/目录1
```
前面我们讲过Linux里面一切都是文件那从哪里看出是什么文件呢要从ls -l的结果的第一位标识位看出来。
<li>
-表示普通文件;
</li>
<li>
d表示文件夹
</li>
<li>
c表示字符设备文件这在设备那一节讲解
</li>
<li>
b表示块设备文件这也在设备那一节讲解
</li>
<li>
s表示套接字socket文件这在网络那一节讲解
</li>
<li>
l表示符号链接也即软链接就是通过名字指向另外一个文件例如下面的代码instance这个文件就是指向了/var/lib/cloud/instances这个文件。软链接的机制我们这一章会讲解。
</li>
```
# ls -l
lrwxrwxrwx 1 root root 61 Dec 14 19:53 instance -&gt; /var/lib/cloud/instances
```
## 文件系统相关系统调用
看完了命令行,我们来看一下,如何使用系统调用操作文件?我们先来看一个完整的例子。
```
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;fcntl.h&gt;
int main(int argc, char *argv[])
{
int fd = -1;
int ret = 1;
int buffer = 1024;
int num = 0;
if((fd=open(&quot;./test&quot;, O_RDWR|O_CREAT|O_TRUNC))==-1)
{
printf(&quot;Open Error\n&quot;);
exit(1);
}
ret = write(fd, &amp;buffer, sizeof(int));
if( ret &lt; 0)
{
printf(&quot;write Error\n&quot;);
exit(1);
}
printf(&quot;write %d byte(s)\n&quot;,ret);
lseek(fd, 0L, SEEK_SET);
ret= read(fd, &amp;num, sizeof(int));
if(ret==-1)
{
printf(&quot;read Error\n&quot;);
exit(1);
}
printf(&quot;read %d byte(s)the number is %d\n&quot;, ret, num);
close(fd);
return 0;
}
```
当使用系统调用open打开一个文件时操作系统会创建一些数据结构来表示这个被打开的文件。下一节我们就会看到这些。为了能够找到这些数据结构在进程中我们会为这个打开的文件分配一个文件描述符fdFile Descriptor
文件描述符就是用来区分一个进程打开的多个文件的。它的作用域就是当前进程出了当前进程这个文件描述符就没有意义了。open返回的fd必须记录好我们对这个文件的所有操作都要靠这个fd包括最后关闭文件。
在Open函数中有一些参数
<li>
O_CREAT表示当文件不存在创建一个新文件
</li>
<li>
O_RDWR表示以读写方式打开
</li>
<li>
O_TRUNC表示打开文件后将文件的长度截断为0。
</li>
接下来write要用于写入数据。第一个参数就是文件描述符第二个参数表示要写入的数据存放位置第三个参数表示希望写入的字节数返回值表示成功写入到文件的字节数。
lseek用于重新定位读写的位置第一个参数是文件描述符第二个参数是重新定位的位置第三个参数是SEEK_SET表示起始位置为文件头第二个参数和第三个参数合起来表示将读写位置设置为从文件头开始0的位置也即从头开始读写。
read用于读取数据第一个参数是文件描述符第二个参数是读取来的数据存到指向的空间第三个参数是希望读取的字节数返回值表示成功读取的字节数。
最终close将关闭一个文件。
对于命令行来讲通过ls可以得到文件的属性使用代码怎么办呢
我们有下面三个函数可以返回与打开的文件描述符相关的文件状态信息。这个信息将会写到类型为struct stat的buf结构中。
```
int stat(const char *pathname, struct stat *statbuf);
int fstat(int fd, struct stat *statbuf);
int lstat(const char *pathname, struct stat *statbuf);
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* Inode number */
mode_t st_mode; /* File type and mode */
nlink_t st_nlink; /* Number of hard links */
uid_t st_uid; /* User ID of owner */
gid_t st_gid; /* Group ID of owner */
dev_t st_rdev; /* Device ID (if special file) */
off_t st_size; /* Total size, in bytes */
blksize_t st_blksize; /* Block size for filesystem I/O */
blkcnt_t st_blocks; /* Number of 512B blocks allocated */
struct timespec st_atim; /* Time of last access */
struct timespec st_mtim; /* Time of last modification */
struct timespec st_ctim; /* Time of last status change */
};
```
函数stat和lstat返回的是通过文件名查到的状态信息。这两个方法区别在于stat没有处理符号链接软链接的能力。如果一个文件是符号链接stat会直接返回它所指向的文件的属性而lstat返回的就是这个符号链接的内容fstat则是通过文件描述符获取文件对应的属性。
接下来我们来看,如何使用系统调用列出一个文件夹下面的文件以及文件的属性。
```
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#include &lt;fcntl.h&gt;
#include &lt;sys/types.h&gt;
#include &lt;sys/stat.h&gt;
#include &lt;dirent.h&gt;
int main(int argc, char *argv[])
{
struct stat sb;
DIR *dirp;
struct dirent *direntp;
char filename[128];
if ((dirp = opendir(&quot;/root&quot;)) == NULL) {
printf(&quot;Open Directory Error%s\n&quot;);
exit(1);
}
while ((direntp = readdir(dirp)) != NULL){
sprintf(filename, &quot;/root/%s&quot;, direntp-&gt;d_name);
if (lstat(filename, &amp;sb) == -1)
{
printf(&quot;lstat Error%s\n&quot;);
exit(1);
}
printf(&quot;name : %s, mode : %d, size : %d, user id : %d\n&quot;, direntp-&gt;d_name, sb.st_mode, sb.st_size, sb.st_uid);
}
closedir(dirp);
return 0
}
```
opendir函数打开一个目录名所对应的DIR目录流。并返回指向DIR目录流的指针。流定位在DIR 目录流的第一个条目。
readdir函数从DIR目录流中读取一个项目返回的是一个指针指向dirent结构体且流的自动指向下一个目录条目。如果已经到流的最后一个条目则返回NULL。
closedir()关闭参数dir所指的目录流。
到这里,你应该既会使用系统调用操作文件,也会使用系统调用操作目录了。下一节,我们开始来看内核如何实现的。
## 总结时刻
这一节,我们对于文件系统的主要功能有了一个总体的印象,我们通过下面这张图梳理一下。
<li>
在文件系统上需要维护文件的严格的格式要通过mkfs.ext4命令来格式化为严格的格式。
</li>
<li>
每一个硬盘上保存的文件都要有一个索引,来维护这个文件上的数据块都保存在哪里。
</li>
<li>
文件通过文件夹组织起来,可以方便用户使用。
</li>
<li>
为了能够更快读取文件,内存里会分配一块空间作为缓存,让一些数据块放在缓存里面。
</li>
<li>
在内核中,要有一整套的数据结构来表示打开的文件。
</li>
<li>
在用户态,每个打开的文件都有一个文件描述符,可以通过各种文件相关的系统调用,操作这个文件描述符。
</li>
<img src="https://static001.geekbang.org/resource/image/27/50/2788a6267f8361c9b6c338b06a1afc50.png" alt="">
## 课堂练习
你可以试着将一块空闲的硬盘分区成为两块并安装不同的文件系统进行挂载。这是Linux运维人员经常做的一件事情。
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,384 @@
<audio id="audio" title="28 | 硬盘文件系统:如何最合理地组织档案库的文档?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6a/e6/6a0da7ae53e1ca6bfc93896d619a66e6.mp3"></audio>
上一节,我们按照图书馆的模式,规划了档案库,也即文件系统应该有的样子。这一节,我们将这个模式搬到硬盘上来看一看。
<img src="https://static001.geekbang.org/resource/image/2e/d2/2ea68b40d928e6469233fcb4948c7cd2.jpg" alt="">
我们常见的硬盘是上面这幅图左边的样子中间圆的部分是磁盘的盘片右边的图是抽象出来的图。每一层里分多个磁道每个磁道分多个扇区每个扇区是512个字节。
文件系统就是安装在这样的硬盘之上。这一节我们重点目前Linux下最主流的文件系统格式——**ext系列**的文件系统的格式。
## inode与块的存储
就像图书馆的书架都要分成大小相同的格子,硬盘也是一样的。硬盘分成相同大小的单元,我们称为**块**Block。一块的大小是扇区大小的整数倍默认是4K。在格式化的时候这个值是可以设定的。
一大块硬盘被分成了一个个小的块,用来存放文件的数据部分。这样一来,如果我们像存放一个文件,就不用给他分配一块连续的空间了。我们可以分散成一个个小块进行存放。这样就灵活得多,也比较容易添加、删除和插入数据。
但是这也带来一个新的问题,那就是文件的数据存放得太散,找起来就比较困难。有什么办法解决呢?我们是不是可以像图书馆那样,也设立一个索引区域,用来维护“某个文件分成几块、每一块在哪里”等等这些**基本信息**?
另外,文件还有**元数据**部分,例如名字、权限等,这就需要一个结构**inode**来存放。
什么是inode呢inode的“i”是index的意思其实就是“索引”类似图书馆的索引区域。既然如此我们每个文件都会对应一个inode一个文件夹就是一个文件也对应一个inode。
至于inode里面有哪些信息其实我们在内核中就有定义。你可以看下面这个数据结构。
```
struct ext4_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size_lo; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Inode Change time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks_lo; /* Blocks count */
__le32 i_flags; /* File flags */
......
__le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl_lo; /* File ACL */
__le32 i_size_high;
......
};
```
从这个数据结构中我们可以看出inode里面有文件的读写权限i_mode属于哪个用户i_uid哪个组i_gid大小是多少i_size_io占用多少个块i_blocks_io。咱们讲ls命令行的时候列出来的权限、用户、大小这些信息就是从这里面取出来的。
另外这里面还有几个与文件相关的时间。i_atime是access time是最近一次访问文件的时间i_ctime是change time是最近一次更改inode的时间i_mtime是modify time是最近一次更改文件的时间。
这里你需要注意区分几个地方。首先访问了不代表修改了也可能只是打开看看就会改变access time。其次修改inode有可能修改的是用户和权限没有修改数据部分就会改变change time。只有数据也修改了才改变modify time。
我们刚才说的“某个文件分成几块、每一块在哪里”这些在inode里面应该保存在i_block里面。
具体如何保存的呢EXT4_N_BLOCKS有如下的定义计算下来一共有15项。
```
#define EXT4_NDIR_BLOCKS 12
#define EXT4_IND_BLOCK EXT4_NDIR_BLOCKS
#define EXT4_DIND_BLOCK (EXT4_IND_BLOCK + 1)
#define EXT4_TIND_BLOCK (EXT4_DIND_BLOCK + 1)
#define EXT4_N_BLOCKS (EXT4_TIND_BLOCK + 1)
```
在ext2和ext3中其中前12项直接保存了块的位置也就是说我们可以通过i_block[0-11],直接得到保存文件内容的块。
<img src="https://static001.geekbang.org/resource/image/73/e2/73349c0fab1a92d4e1ae0c684cfe06e2.jpeg" alt="">
但是如果一个文件比较大12块放不下。当我们用到i_block[12]的时候就不能直接放数据块的位置了要不然i_block很快就会用完了。这该怎么办呢我们需要想个办法。我们可以让i_block[12]指向一个块,这个块里面不放数据块,而是放数据块的位置,这个块我们称为**间接块**。也就是说我们在i_block[12]里面放间接块的位置通过i_block[12]找到间接块后,间接块里面放数据块的位置,通过间接块可以找到数据块。
如果文件再大一些i_block[13]会指向一个块我们可以用二次间接块。二次间接块里面存放了间接块的位置间接块里面存放了数据块的位置数据块里面存放的是真正的数据。如果文件再大一些i_block[14]会指向三次间接块。原理和上面都是一样的,就像一层套一层的俄罗斯套娃,一层一层打开,才能拿到最中心的数据块。
如果你稍微有点经验,现在你应该能够意识到,这里面有一个非常显著的问题,对于大文件来讲,我们要多次读取硬盘才能找到相应的块,这样访问速度就会比较慢。
为了解决这个问题ext4做了一定的改变。它引入了一个新的概念叫做**Extents**。
我们来解释一下Extents。比方说一个文件大小为128M如果使用4k大小的块进行存储需要32k个块。如果按照ext2或者ext3那样散着放数量太大了。但是Extents可以用于存放连续的块也就是说我们可以把128M放在一个Extents里面。这样的话对大文件的读写性能提高了文件碎片也减少了。
Exents如何来存储呢它其实会保存成一棵树。
<img src="https://static001.geekbang.org/resource/image/b8/2a/b8f184696be8d37ad6f2e2a4f12d002a.jpeg" alt="">
树有一个个的节点有叶子节点也有分支节点。每个节点都有一个头ext4_extent_header可以用来描述某个节点。
```
struct ext4_extent_header {
__le16 eh_magic; /* probably will support different formats */
__le16 eh_entries; /* number of valid entries */
__le16 eh_max; /* capacity of store in entries */
__le16 eh_depth; /* has tree real underlying blocks? */
__le32 eh_generation; /* generation of the tree */
};
```
我们仔细来看里面的内容。eh_entries表示这个节点里面有多少项。这里的项分两种如果是叶子节点这一项会直接指向硬盘上的连续块的地址我们称为数据节点ext4_extent如果是分支节点这一项会指向下一层的分支节点或者叶子节点我们称为索引节点ext4_extent_idx。这两种类型的项的大小都是12个byte。
```
/*
* This is the extent on-disk structure.
* It's used at the bottom of the tree.
*/
struct ext4_extent {
__le32 ee_block; /* first logical block extent covers */
__le16 ee_len; /* number of blocks covered by extent */
__le16 ee_start_hi; /* high 16 bits of physical block */
__le32 ee_start_lo; /* low 32 bits of physical block */
};
/*
* This is index on-disk structure.
* It's used at all the levels except the bottom.
*/
struct ext4_extent_idx {
__le32 ei_block; /* index covers logical blocks from 'block' */
__le32 ei_leaf_lo; /* pointer to the physical block of the next *
* level. leaf or next index could be there */
__le16 ei_leaf_hi; /* high 16 bits of physical block */
__u16 ei_unused;
};
```
如果文件不大inode里面的i_block中可以放得下一个ext4_extent_header和4项ext4_extent。所以这个时候eh_depth为0也即inode里面的就是叶子节点树高度为0。
如果文件比较大4个extent放不下就要分裂成为一棵树eh_depth&gt;0的节点就是索引节点其中根节点深度最大在inode中。最底层eh_depth=0的是叶子节点。
除了根节点其他的节点都保存在一个块4k里面4k扣除ext4_extent_header的12个byte剩下的能够放340项每个extent最大能表示128MB的数据340个extent会使你表示的文件达到42.5GB。这已经非常大了,如果再大,我们可以增加树的深度。
## inode位图和块位图
到这里我们知道了硬盘上肯定有一系列的inode和一系列的块排列起来。
接下来的问题是如果我要保存一个数据块或者要保存一个inode我应该放在硬盘上的哪个位置呢难道需要将所有的inode列表和块列表扫描一遍找个空的地方随便放吗
当然这样效率太低了。所以在文件系统里面我们专门弄了一个块来保存inode的位图。在这4k里面每一位对应一个inode。如果是1表示这个inode已经被用了如果是0则表示没被用。同样我们也弄了一个块保存block的位图。
上海虹桥火车站的厕位智能引导系统,不知道你有没有见过?这个系统很厉害,我们要想知道哪个位置有没有被占用,不用挨个拉门,从这样一个电子版上就能看到了。
<img src="https://static001.geekbang.org/resource/image/d7/25/d790fb19b76d7504985639aceac43c25.jpeg" alt="">
接下来我们来看位图究竟是如何在Linux操作系统里面起作用的。前一节我们讲过如果创建一个新文件会调用open函数并且参数会有O_CREAT。这表示当文件找不到的时候我们就需要创建一个。open是一个系统调用在内核里面会调用sys_open定义如下
```
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
```
这里我们还是重点看对于inode的操作。其实open一个文件很复杂下一节我们会详细分析整个过程。
我们来看接下来的调用链do_sys_open-&gt; do_filp_open-&gt;path_openat-&gt;do_last-&gt;lookup_open。这个调用链的逻辑是要打开一个文件先要根据路径找到文件夹。如果发现文件夹下面没有这个文件同时又设置了O_CREAT就说明我们要在这个文件夹下面创建一个文件那我们就需要一个新的inode。
```
static int lookup_open(struct nameidata *nd, struct path *path,
struct file *file,
const struct open_flags *op,
bool got_write, int *opened)
{
......
if (!dentry-&gt;d_inode &amp;&amp; (open_flag &amp; O_CREAT)) {
......
error = dir_inode-&gt;i_op-&gt;create(dir_inode, dentry, mode,
open_flag &amp; O_EXCL);
......
}
......
}
```
想要创建新的inode我们就要调用dir_inode也就是文件夹的inode的create函数。它的具体定义是这样的
```
const struct inode_operations ext4_dir_inode_operations = {
.create = ext4_create,
.lookup = ext4_lookup,
.link = ext4_link,
.unlink = ext4_unlink,
.symlink = ext4_symlink,
.mkdir = ext4_mkdir,
.rmdir = ext4_rmdir,
.mknod = ext4_mknod,
.tmpfile = ext4_tmpfile,
.rename = ext4_rename2,
.setattr = ext4_setattr,
.getattr = ext4_getattr,
.listxattr = ext4_listxattr,
.get_acl = ext4_get_acl,
.set_acl = ext4_set_acl,
.fiemap = ext4_fiemap,
};
```
这里面定义了如果文件夹inode要做一些操作每个操作对应应该调用哪些函数。这里create操作调用的是ext4_create。
接下来的调用链是这样的ext4_create-&gt;ext4_new_inode_start_handle-&gt;__ext4_new_inode。在__ext4_new_inode函数中我们会创建新的inode。
```
struct inode *__ext4_new_inode(handle_t *handle, struct inode *dir,
umode_t mode, const struct qstr *qstr,
__u32 goal, uid_t *owner, __u32 i_flags,
int handle_type, unsigned int line_no,
int nblocks)
{
......
inode_bitmap_bh = ext4_read_inode_bitmap(sb, group);
......
ino = ext4_find_next_zero_bit((unsigned long *)
inode_bitmap_bh-&gt;b_data,
EXT4_INODES_PER_GROUP(sb), ino);
......
}
```
这里面一个重要的逻辑就是从文件系统里面读取inode位图然后找到下一个为0的inode就是空闲的inode。
对于block位图在写入文件的时候也会有这个过程我就不展开说了。感兴趣的话你可以自己去找代码看。
## 文件系统的格式
看起来我们现在应该能够很顺利地通过inode位图和block位图创建文件了。如果仔细计算一下其实还是有问题的。
数据块的位图是放在一个块里面的共4k。每位表示一个数据块共可以表示$4 * 1024 * 8 = 2^{15}$个数据块。如果每个数据块也是按默认的4K最大可以表示空间为$2^{15} * 4 * 1024 = 2^{27}$个byte也就是128M。
也就是说按照上面的格式,如果采用“**一个块的位图+一系列的块**”,外加“**一个块的inode的位图+一系列的inode的结构**”最多能够表示128M。是不是太小了现在很多文件都比这个大。我们先把这个结构称为一个**块组**。有N多的块组就能够表示N大的文件。
对于块组我们也需要一个数据结构来表示为ext4_group_desc。这里面对于一个块组里的inode位图bg_inode_bitmap_lo、块位图bg_block_bitmap_lo、inode列表bg_inode_table_lo都有相应的成员变量。
这样一个个块组,就基本构成了我们整个文件系统的结构。因为块组有多个,块组描述符也同样组成一个列表,我们把这些称为**块组描述符表**。
当然,我们还需要有一个数据结构,对整个文件系统的情况进行描述,这个就是**超级块**ext4_super_block。这里面有整个文件系统一共有多少inodes_inodes_count一共有多少块s_blocks_count_lo每个块组有多少inodes_inodes_per_group每个块组有多少块s_blocks_per_group等。这些都是这类的全局信息。
对于整个文件系统别忘了咱们讲系统启动的时候说的。如果是一个启动盘我们需要预留一块区域作为引导区所以第一个块组的前面要留1K用于启动引导区。
最终,整个文件系统格式就是下面这个样子。
<img src="https://static001.geekbang.org/resource/image/e3/1b/e3718f0af6a2523a43606a0c4003631b.jpeg" alt="">
这里面我还需要重点说一下,超级块和块组描述符表都是全局信息,而且这些数据很重要。如果这些数据丢失了,整个文件系统都打不开了,这比一个文件的一个块损坏更严重。所以,这两部分我们都需要备份,但是采取不同的策略。
默认情况下,超级块和块组描述符表都有副本保存在每一个块组里面。
如果开启了sparse_super特性超级块和块组描述符表的副本只会保存在块组索引为0、3、5、7的整数幂里。除了块组0中存在一个超级块外在块组1$3^0=1$的第一个块中存在一个副本在块组3$3^1=3$、块组5$5^1=5$、块组7$7^1=7$、块组9$3^2=9$、块组25$5^2=25$、块组27$3^3=27$的第一个block处也存在一个副本。
对于超级块来讲由于超级块不是很大所以就算我们备份多了也没有太多问题。但是对于块组描述符表来讲如果每个块组里面都保存一份完整的块组描述符表一方面很浪费空间另一个方面由于一个块组最大128M而块组描述符表里面有多少项这就限制了有多少个块组128M * 块组的总数目是整个文件系统的大小,就被限制住了。
我们的改进的思路就是引入**Meta Block Groups特性**。
首先块组描述符表不会保存所有块组的描述符了而是将块组分成多个组我们称为元块组Meta Block Group。每个元块组里面的块组描述符表仅仅包括自己的一个元块组包含64个块组这样一个元块组中的块组描述符表最多64项。我们假设一共有256个块组原来是一个整的块组描述符表里面有256项要备份就全备份现在分成4个元块组每个元块组里面的块组描述符表就只有64项了这就小多了而且四个元块组自己备份自己的。
<img src="https://static001.geekbang.org/resource/image/b0/b9/b0bf4690882253a70705acc7368983b9.jpeg" alt="">
根据图中每一个元块组包含64个块组块组描述符表也是64项备份三份在元块组的第一个第二个和最后一个块组的开始处。
这样化整为零我们就可以发挥出ext4的48位块寻址的优势了在超级块ext4_super_block的定义中我们可以看到块寻址分为高位和低位均为32位其中有用的是48位2^48个块是1EB足够用了。
```
struct ext4_super_block {
......
__le32 s_blocks_count_lo; /* Blocks count */
__le32 s_r_blocks_count_lo; /* Reserved blocks count */
__le32 s_free_blocks_count_lo; /* Free blocks count */
......
__le32 s_blocks_count_hi; /* Blocks count */
__le32 s_r_blocks_count_hi; /* Reserved blocks count */
__le32 s_free_blocks_count_hi; /* Free blocks count */
......
}
```
## 目录的存储格式
通过前面的描述,我们现在知道了一个普通的文件是如何存储的。有一类特殊的文件,我们会经常用到,就是目录,它是如何保存的呢?
其实目录本身也是个文件也有inode。inode里面也是指向一些块。和普通文件不同的是普通文件的块里面保存的是文件数据而目录文件的块里面保存的是目录里面一项一项的文件信息。这些信息我们称为ext4_dir_entry。从代码来看有两个版本在成员来讲几乎没有差别只不过第二个版本ext4_dir_entry_2是将一个16位的name_len变成了一个8位的name_len和8位的file_type。
```
struct ext4_dir_entry {
__le32 inode; /* Inode number */
__le16 rec_len; /* Directory entry length */
__le16 name_len; /* Name length */
char name[EXT4_NAME_LEN]; /* File name */
};
struct ext4_dir_entry_2 {
__le32 inode; /* Inode number */
__le16 rec_len; /* Directory entry length */
__u8 name_len; /* Name length */
__u8 file_type;
char name[EXT4_NAME_LEN]; /* File name */
};
```
在目录文件的块中最简单的保存格式是列表就是一项一项地将ext4_dir_entry_2列在哪里。
每一项都会保存这个目录的下一级的文件的文件名和对应的inode通过这个inode就能找到真正的文件。第一项是“.”表示当前目录第二项是“…”表示上一级目录接下来就是一项一项的文件名和inode。
有时候,如果一个目录下面的文件太多的时候,我们想在这个目录下找一个文件,按照列表一个个去找,太慢了,于是我们就添加了索引的模式。
如果在inode中设置EXT4_INDEX_FL标志则目录文件的块的组织形式将发生变化变成了下面定义的这个样子
```
struct dx_root
{
struct fake_dirent dot;
char dot_name[4];
struct fake_dirent dotdot;
char dotdot_name[4];
struct dx_root_info
{
__le32 reserved_zero;
u8 hash_version;
u8 info_length; /* 8 */
u8 indirect_levels;
u8 unused_flags;
}
info;
struct dx_entry entries[0];
};
```
当然,首先出现的还是差不多的,第一项是“.”表示当前目录第二项是“…”表示上一级目录这两个不变。接下来就开始发生改变了。是一个dx_root_info的结构其中最重要的成员变量是indirect_levels表示间接索引的层数。
接下来我们来看索引项dx_entry。这个也很简单其实就是文件名的哈希值和数据块的一个映射关系。
```
struct dx_entry
{
__le32 hash;
__le32 block;
};
```
如果我们要查找一个目录下面的文件名可以通过名称取哈希。如果哈希能够匹配上就说明这个文件的信息在相应的块里面。然后打开这个块如果里面不再是索引而是索引树的叶子节点的话那里面还是ext4_dir_entry_2的列表我们只要一项一项找文件名就行。通过索引树我们可以将一个目录下面的N多的文件分散到很多的块里面可以很快地进行查找。
<img src="https://static001.geekbang.org/resource/image/3e/6d/3ea2ad5704f20538d9c911b02f42086d.jpeg" alt="">
## 软链接和硬链接的存储格式
还有一种特殊的文件格式硬链接Hard Link和软链接Symbolic Link。在讲操作文件的命令的时候我们讲过软链接的概念。所谓的链接Link我们可以认为是文件的别名而链接又可分为两种硬链接与软链接。通过下面的命令可以创建。
```
ln [参数][源文件或目录][目标文件或目录]
```
ln -s创建的是软链接不带-s创建的是硬链接。它们有什么区别呢在文件系统里面是怎么保存的呢
<img src="https://static001.geekbang.org/resource/image/45/7b/45a6cfdd9d45e30dc2f38f0d2572be7b.jpeg" alt="">
如图所示硬链接与原始文件共用一个inode的但是inode是不跨文件系统的每个文件系统都有自己的inode列表因而硬链接是没有办法跨文件系统的。
而软链接不同软链接相当于重新创建了一个文件。这个文件也有独立的inode只不过打开这个文件看里面内容的时候内容指向另外的一个文件。这就很灵活了。我们可以跨文件系统甚至目标文件被删除了链接文件还是在的只不过指向的文件找不到了而已。
## 总结时刻
这一节我们描述了复杂的硬盘上的文件系统但是对于咱们平时的应用来讲用的最多的是两个概念一个是inode一个是数据块。
这里我画了一张图来总结一下inode和数据块在文件系统上的关联关系。
为了表示图中上半部分的那个简单的树形结构在文件系统上的布局就像图的下半部分一样。无论是文件夹还是文件都有一个inode。inode里面会指向数据块对于文件夹的数据块里面是一个表是下一层的文件名和inode的对应关系文件的数据块里面存放的才是真正的数据。
<img src="https://static001.geekbang.org/resource/image/f8/38/f81bf3e5a6cd060c3225a8ae1803a138.png" alt="">
## 课堂练习
你知道如何查看inode的内容和文件夹的内容吗
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,438 @@
<audio id="audio" title="29 | 虚拟文件系统:文件多了就需要档案管理系统" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/4e/50/4e86134e9bdcf5fcdffeac276c05e550.mp3"></audio>
上一节,咱们的图书馆书架,也就是硬盘上的文件系统格式都搭建好了,现在我们还需要一个图书管理与借阅系统,也就是文件管理模块,不然我们怎么知道书都借给谁了呢?
进程要想往文件系统里面读写数据,需要很多层的组件一起合作。具体是怎么合作的呢?我们一起来看一看。
- 在应用层进程在进行文件读写操作时可通过系统调用如sys_open、sys_read、sys_write等。
- 在内核,每个进程都需要为打开的文件,维护一定的数据结构。
- 在内核,整个系统打开的文件,也需要维护一定的数据结构。
- Linux可以支持多达数十种不同的文件系统。它们的实现各不相同因此Linux内核向用户空间提供了虚拟文件系统这个统一的接口来对文件系统进行操作。它提供了常见的文件系统对象模型例如inode、directory entry、mount等以及操作这些对象的方法例如inode operations、directory operations、file operations等。
- 然后就是对接的是真正的文件系统例如我们上节讲的ext4文件系统。
- 为了读写ext4文件系统要通过块设备I/O层也即BIO层。这是文件系统层和块设备驱动的接口。
- 为了加快块设备的读写效率,我们还有一个缓存层。
- 最下层是块设备驱动程序。
<img src="https://static001.geekbang.org/resource/image/3c/73/3c506edf93b15341da3db658e9970773.jpg" alt="">
接下来我们逐层解析。
在这之前,有一点你需要注意。解析系统调用是了解内核架构最有力的一把钥匙,这里我们只要重点关注这几个最重要的系统调用就可以了:
- mount系统调用用于挂载文件系统
- open系统调用用于打开或者创建文件创建要在flags中设置O_CREAT对于读写要设置flags为O_RDWR
- read系统调用用于读取文件内容
- write系统调用用于写入文件内容。
## 挂载文件系统
想要操作文件系统,第一件事情就是挂载文件系统。
内核是不是支持某种类型的文件系统需要我们进行注册才能知道。例如咱们上一节解析的ext4文件系统就需要通过register_filesystem进行注册传入的参数是ext4_fs_type表示注册的是ext4类型的文件系统。这里面最重要的一个成员变量就是ext4_mount。记住它这个我们后面还会用。
```
register_filesystem(&amp;ext4_fs_type);
static struct file_system_type ext4_fs_type = {
.owner = THIS_MODULE,
.name = &quot;ext4&quot;,
.mount = ext4_mount,
.kill_sb = kill_block_super,
.fs_flags = FS_REQUIRES_DEV,
};
```
如果一种文件系统的类型曾经在内核注册过,这就说明允许你挂载并且使用这个文件系统。
刚才我说了几个需要重点关注的系统调用那我们就从第一个mount系统调用开始解析。mount系统调用的定义如下
```
SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name, char __user *, type, unsigned long, flags, void __user *, data)
{
......
ret = do_mount(kernel_dev, dir_name, kernel_type, flags, options);
......
}
```
接下来的调用链为do_mount-&gt;do_new_mount-&gt;vfs_kern_mount。
```
struct vfsmount *
vfs_kern_mount(struct file_system_type *type, int flags, const char *name, void *data)
{
......
mnt = alloc_vfsmnt(name);
......
root = mount_fs(type, flags, name, data);
......
mnt-&gt;mnt.mnt_root = root;
mnt-&gt;mnt.mnt_sb = root-&gt;d_sb;
mnt-&gt;mnt_mountpoint = mnt-&gt;mnt.mnt_root;
mnt-&gt;mnt_parent = mnt;
list_add_tail(&amp;mnt-&gt;mnt_instance, &amp;root-&gt;d_sb-&gt;s_mounts);
return &amp;mnt-&gt;mnt;
}
```
vfs_kern_mount先是创建struct mount结构每个挂载的文件系统都对应于这样一个结构。
```
struct mount {
struct hlist_node mnt_hash;
struct mount *mnt_parent;
struct dentry *mnt_mountpoint;
struct vfsmount mnt;
union {
struct rcu_head mnt_rcu;
struct llist_node mnt_llist;
};
struct list_head mnt_mounts; /* list of children, anchored here */
struct list_head mnt_child; /* and going through their mnt_child */
struct list_head mnt_instance; /* mount instance on sb-&gt;s_mounts */
const char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */
struct list_head mnt_list;
......
} __randomize_layout;
struct vfsmount {
struct dentry *mnt_root; /* root of the mounted tree */
struct super_block *mnt_sb; /* pointer to superblock */
int mnt_flags;
} __randomize_layout;
```
其中mnt_parent是装载点所在的父文件系统mnt_mountpoint是装载点在父文件系统中的dentrystruct dentry表示目录并和目录的inode关联mnt_root是当前文件系统根目录的dentrymnt_sb是指向超级块的指针。
接下来我们来看调用mount_fs挂载文件系统。
```
struct dentry *
mount_fs(struct file_system_type *type, int flags, const char *name, void *data)
{
struct dentry *root;
struct super_block *sb;
......
root = type-&gt;mount(type, flags, name, data);
......
sb = root-&gt;d_sb;
......
}
```
这里调用的是ext4_fs_type的mount函数也就是咱们上面提到的ext4_mount从文件系统里面读取超级块。在文件系统的实现中每个在硬盘上的结构在内存中也对应相同格式的结构。当所有的数据结构都读到内存里面内核就可以通过操作这些数据结构来操作文件系统了。
可以看出来理解各个数据结构在这里的关系非常重要。我这里举一个例子来解析经过mount之后刚刚那些数据结构之间的关系。
我们假设根文件系统下面有一个目录home有另外一个文件系统A挂载在这个目录home下面。在文件系统A的根目录下面有另外一个文件夹hello。由于文件系统A已经挂载到了目录home下面所以我们就有了目录/home/hello然后有另外一个文件系统B挂载在/home/hello下面。在文件系统B的根目录下面有另外一个文件夹world在world下面有个文件夹data。由于文件系统B已经挂载到了/home/hello下面所以我们就有了目录/home/hello/world/data。
为了维护这些关系,操作系统创建了这一系列数据结构。具体你可以看下面的图。
<img src="https://static001.geekbang.org/resource/image/66/27/663b3c5903d15fd9ba52f6d049e0dc27.jpeg" alt="">
文件系统是树形关系。如果所有的文件夹都是几代单传,那就变成了一条线。你注意看图中的三条斜线。
第一条线是最左边的向左斜的**dentry斜线**。每一个文件和文件夹都有dentry用于和inode关联。第二条线是最右面的向右斜的**mount斜线**因为这个例子涉及两次文件系统的挂载再加上启动的时候挂载的根文件系统一共三个mount。第三条线是中间的向右斜的**file斜线**每个打开的文件都有一个file结构它里面有两个变量一个指向相应的mount一个指向相应的dentry。
我们从最上面往下看。根目录/对应一个dentry根目录是在根文件系统上的根文件系统是系统启动的时候挂载的因而有一个mount结构。这个mount结构的mount point指针和mount root指针都是指向根目录的dentry。根目录对应的file的两个指针一个指向根目录的dentry一个指向根目录的挂载结构mount。
我们再来看第二层。下一层目录home对应了两个dentry而且它们的parent都指向第一层的dentry。这是为什么呢这是因为文件系统A挂载到了这个目录下。这使得这个目录有两个用处。一方面home是根文件系统的一个挂载点另一方面home是文件系统A的根目录。
因为还有一次挂载因而又有了一个mount结构。这个mount结构的mount point指针指向作为挂载点的那个dentry。mount root指针指向作为根目录的那个dentry同时parent指针指向第一层的mount结构。home对应的file的两个指针一个指向文件系统A根目录的dentry一个指向文件系统A的挂载结构mount。
我们再来看第三层。目录hello又挂载了一个文件系统B所以第三层的结构和第二层几乎一样。
接下来是第四层。目录world就是一个普通的目录。只要它的dentry的parent指针指向上一层就可以了。我们来看world对应的file结构。由于挂载点不变还是指向第三层的mount结构。
接下来是第五层。对于文件data是一个普通的文件它的dentry的parent指向第四层的dentry。对于data对应的file结构由于挂载点不变还是指向第三层的mount结构。
## 打开文件
接下来我们从分析Open系统调用说起。
在[系统调用](https://time.geekbang.org/column/article/89251)的那一节我们知道在进程里面通过open系统调用打开文件最终对调用到内核的系统调用实现sys_open。当时我们仅仅解析了系统调用的原理没有接着分析下去现在我们接着分析这个过程。
```
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
......
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
......
fd = get_unused_fd_flags(flags);
if (fd &gt;= 0) {
struct file *f = do_filp_open(dfd, tmp, &amp;op);
if (IS_ERR(f)) {
put_unused_fd(fd);
fd = PTR_ERR(f);
} else {
fsnotify_open(f);
fd_install(fd, f);
}
}
putname(tmp);
return fd;
}
```
要打开一个文件首先要通过get_unused_fd_flags得到一个没有用的文件描述符。如何获取这个文件描述符呢
在每一个进程的task_struct中有一个指针files类型是files_struct。
```
struct files_struct *files;
```
files_struct里面最重要的是一个文件描述符列表每打开一个文件就会在这个列表中分配一项下标就是文件描述符。
```
struct files_struct {
......
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};
```
对于任何一个进程默认情况下文件描述符0表示stdin标准输入文件描述符1表示stdout标准输出文件描述符2表示stderr标准错误输出。另外再打开的文件都会从这个列表中找一个空闲位置分配给它。
文件描述符列表的每一项都是一个指向struct file的指针也就是说每打开一个文件都会有一个struct file对应。
do_sys_open中调用do_filp_open就是创建这个struct file结构然后fd_install(fd, f)是将文件描述符和这个结构关联起来。
```
struct file *do_filp_open(int dfd, struct filename *pathname,
const struct open_flags *op)
{
......
set_nameidata(&amp;nd, dfd, pathname);
filp = path_openat(&amp;nd, op, flags | LOOKUP_RCU);
......
restore_nameidata();
return filp;
}
```
do_filp_open里面首先初始化了struct nameidata这个结构。我们知道文件都是一串的路径名称需要逐个解析。这个结构在解析和查找路径的时候提供辅助作用。
在struct nameidata里面有一个关键的成员变量struct path。
```
struct path {
struct vfsmount *mnt;
struct dentry *dentry;
} __randomize_layout;
```
其中struct vfsmount和文件系统的挂载有关。另一个struct dentry除了上面说的用于标识目录之外还可以表示文件名还会建立文件名及其inode之间的关联。
接下来就调用path_openat主要做了以下几件事情
- get_empty_filp生成一个struct file结构
- path_init初始化nameidata准备开始节点路径查找
- link_path_walk对于路径名逐层进行节点路径查找这里面有一个大的循环用“/”分隔逐层处理;
- do_last获取文件对应的inode对象并且初始化file对象。
```
static struct file *path_openat(struct nameidata *nd,
const struct open_flags *op, unsigned flags)
{
......
file = get_empty_filp();
......
s = path_init(nd, flags);
......
while (!(error = link_path_walk(s, nd)) &amp;&amp;
(error = do_last(nd, file, op, &amp;opened)) &gt; 0) {
......
}
terminate_walk(nd);
......
return file;
}
```
例如,文件“/root/hello/world/data”link_path_walk会解析前面的路径部分“/root/hello/world”解析完毕的时候nameidata的dentry为路径名的最后一部分的父目录“/root/hello/world”而nameidata的filename为路径名的最后一部分“data”。
最后一部分的解析和处理我们交给do_last。
```
static int do_last(struct nameidata *nd,
struct file *file, const struct open_flags *op,
int *opened)
{
......
error = lookup_fast(nd, &amp;path, &amp;inode, &amp;seq);
......
error = lookup_open(nd, &amp;path, file, op, got_write, opened);
......
error = vfs_open(&amp;nd-&gt;path, file, current_cred());
......
}
```
在这里面我们需要先查找文件路径最后一部分对应的dentry。如何查找呢
Linux为了提高目录项对象的处理效率设计与实现了目录项高速缓存dentry cache简称dcache。它主要由两个数据结构组成
- 哈希表dentry_hashtabledcache中的所有dentry对象都通过d_hash指针链到相应的dentry哈希链表中
- 未使用的dentry对象链表s_dentry_lrudentry对象通过其d_lru指针链入LRU链表中。LRU的意思是最近最少使用我们已经好几次看到它了。只要有它就说明长时间不使用就应该释放了。
<img src="https://static001.geekbang.org/resource/image/82/59/82dd76e1e84915206eefb8fc88385859.jpeg" alt="">
这两个列表之间会产生复杂的关系:
- 引用为0一个在散列表中的dentry变成没有人引用了就会被加到LRU表中去
- 再次被引用一个在LRU表中的dentry再次被引用了则从LRU表中移除
- 分配当dentry在散列表中没有找到则从Slub分配器中分配一个
- 过期归还当LRU表中最长时间没有使用的dentry应该释放回Slub分配器
- 文件删除文件被删除了相应的dentry应该释放回Slub分配器
- 结构复用当需要分配一个dentry但是无法分配新的就从LRU表中取出一个来复用。
所以do_last()在查找dentry的时候当然先从缓存中查找调用的是lookup_fast。
如果缓存中没有找到就需要真的到文件系统里面去找了lookup_open会创建一个新的dentry并且调用上一级目录的Inode的inode_operations的lookup函数对于ext4来讲调用的是ext4_lookup会到咱们上一节讲的文件系统里面去找inode。最终找到后将新生成的dentry赋给path变量。
```
static int lookup_open(struct nameidata *nd, struct path *path,
struct file *file,
const struct open_flags *op,
bool got_write, int *opened)
{
......
dentry = d_alloc_parallel(dir, &amp;nd-&gt;last, &amp;wq);
......
struct dentry *res = dir_inode-&gt;i_op-&gt;lookup(dir_inode, dentry,
nd-&gt;flags);
......
path-&gt;dentry = dentry;
path-&gt;mnt = nd-&gt;path.mnt;
}
const struct inode_operations ext4_dir_inode_operations = {
.create = ext4_create,
.lookup = ext4_lookup,
...
```
do_last()的最后一步是调用vfs_open真正打开文件。
```
int vfs_open(const struct path *path, struct file *file,
const struct cred *cred)
{
struct dentry *dentry = d_real(path-&gt;dentry, NULL, file-&gt;f_flags, 0);
......
file-&gt;f_path = *path;
return do_dentry_open(file, d_backing_inode(dentry), NULL, cred);
}
static int do_dentry_open(struct file *f,
struct inode *inode,
int (*open)(struct inode *, struct file *),
const struct cred *cred)
{
......
f-&gt;f_mode = OPEN_FMODE(f-&gt;f_flags) | FMODE_LSEEK |
FMODE_PREAD | FMODE_PWRITE;
path_get(&amp;f-&gt;f_path);
f-&gt;f_inode = inode;
f-&gt;f_mapping = inode-&gt;i_mapping;
......
f-&gt;f_op = fops_get(inode-&gt;i_fop);
......
open = f-&gt;f_op-&gt;open;
......
error = open(inode, f);
......
f-&gt;f_flags &amp;= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
file_ra_state_init(&amp;f-&gt;f_ra, f-&gt;f_mapping-&gt;host-&gt;i_mapping);
return 0;
......
}
const struct file_operations ext4_file_operations = {
......
.open = ext4_file_open,
......
};
```
vfs_open里面最终要做的一件事情是调用f_op-&gt;open也就是调用ext4_file_open。另外一件重要的事情是将打开文件的所有信息填写到struct file这个结构里面。
```
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
......
struct address_space *f_mapping;
errseq_t f_wb_err;
}
```
## 总结时刻
对于虚拟文件系统的解析就到这里了,我们可以看出,有关文件的数据结构层次多,而且很复杂,就得到了下面这张图,这张图在这个专栏最开始的时候,已经展示过一遍,到这里,你应该能明白它们之间的关系了。
<img src="https://static001.geekbang.org/resource/image/80/b9/8070294bacd74e0ac5ccc5ac88be1bb9.png" alt="">
这张图十分重要,一定要掌握。因为我们后面的字符设备、块设备、管道、进程间通信、网络等等,全部都要用到这里面的知识。希望当你再次遇到它的时候,能够马上说出各个数据结构之间的关系。
这里我带你简单做一个梳理,帮助你理解记忆它。
对于每一个进程打开的文件都有一个文件描述符在files_struct里面会有文件描述符数组。每个一个文件描述符是这个数组的下标里面的内容指向一个file结构表示打开的文件。这个结构里面有这个文件对应的inode最重要的是这个文件对应的操作file_operation。如果操作这个文件就看这个file_operation里面的定义了。
对于每一个打开的文件都有一个dentry对应虽然叫作directory entry但是不仅仅表示文件夹也表示文件。它最重要的作用就是指向这个文件对应的inode。
如果说file结构是一个文件打开以后才创建的dentry是放在一个dentry cache里面的文件关闭了他依然存在因而他可以更长期地维护内存中的文件的表示和硬盘上文件的表示之间的关系。
inode结构就表示硬盘上的inode包括块设备号等。
几乎每一种结构都有自己对应的operation结构里面都是一些方法因而当后面遇到对于某种结构进行处理的时候如果不容易找到相应的处理函数就先找这个operation结构就清楚了。
## 课堂练习
上一节的总结中,我们说,同一个文件系统中,文件夹和文件的对应关系。如果跨的是文件系统,你知道如何维护这种映射关系吗?
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,453 @@
<audio id="audio" title="30 | 文件缓存:常用文档应该放在触手可得的地方" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fb/8e/fb5daec4baa92b1c2cf0850886a3e18e.mp3"></audio>
上一节,我们讲了文件系统的挂载和文件的打开,并通过打开文件的过程,构建了一个文件管理的整套数据结构体系。其实到这里,我们还没有对文件进行读写,还属于对于元数据的操作。那这一节,我们就重点关注读写。
## 系统调用层和虚拟文件系统层
文件系统的读写其实就是调用系统函数read和write。由于读和写的很多逻辑是相似的这里我们一起来看一下这个过程。
下面的代码就是read和write的系统调用在内核里面的定义。
```
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct fd f = fdget_pos(fd);
......
loff_t pos = file_pos_read(f.file);
ret = vfs_read(f.file, buf, count, &amp;pos);
......
}
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
struct fd f = fdget_pos(fd);
......
loff_t pos = file_pos_read(f.file);
ret = vfs_write(f.file, buf, count, &amp;pos);
......
}
```
对于read来讲里面调用vfs_read-&gt;__vfs_read。对于write来讲里面调用vfs_write-&gt;__vfs_write。
下面是__vfs_read和__vfs_write的代码。
```
ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
loff_t *pos)
{
if (file-&gt;f_op-&gt;read)
return file-&gt;f_op-&gt;read(file, buf, count, pos);
else if (file-&gt;f_op-&gt;read_iter)
return new_sync_read(file, buf, count, pos);
else
return -EINVAL;
}
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
loff_t *pos)
{
if (file-&gt;f_op-&gt;write)
return file-&gt;f_op-&gt;write(file, p, count, pos);
else if (file-&gt;f_op-&gt;write_iter)
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
}
```
上一节我们讲了每一个打开的文件都有一个struct file结构。这里面有一个struct file_operations f_op用于定义对这个文件做的操作。__vfs_read会调用相应文件系统的file_operations里面的read操作__vfs_write会调用相应文件系统file_operations里的write操作。
## ext4文件系统层
对于ext4文件系统来讲内核定义了一个ext4_file_operations。
```
const struct file_operations ext4_file_operations = {
......
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
......
}
```
由于ext4没有定义read和write函数于是会调用ext4_file_read_iter和ext4_file_write_iter。
ext4_file_read_iter会调用generic_file_read_iterext4_file_write_iter会调用__generic_file_write_iter。
```
ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
......
if (iocb-&gt;ki_flags &amp; IOCB_DIRECT) {
......
struct address_space *mapping = file-&gt;f_mapping;
......
retval = mapping-&gt;a_ops-&gt;direct_IO(iocb, iter);
}
......
retval = generic_file_buffered_read(iocb, iter, retval);
}
ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
......
if (iocb-&gt;ki_flags &amp; IOCB_DIRECT) {
......
written = generic_file_direct_write(iocb, from);
......
} else {
......
written = generic_perform_write(file, from, iocb-&gt;ki_pos);
......
}
}
```
generic_file_read_iter和__generic_file_write_iter有相似的逻辑就是要区分是否用缓存。
缓存其实就是内存中的一块空间。因为内存比硬盘快得多Linux为了改进性能有时候会选择不直接操作硬盘而是读写都在内存中然后批量读取或者写入硬盘。一旦能够命中内存读写效率就会大幅度提高。
因此根据是否使用内存做缓存我们可以把文件的I/O操作分为两种类型。
第一种类型是**缓存I/O**。大多数文件系统的默认I/O操作都是缓存I/O。对于读操作来讲操作系统会先检查内核的缓冲区有没有需要的数据。如果已经缓存了那就直接从缓存中返回否则从磁盘中读取然后缓存在操作系统的缓存中。对于写操作来讲操作系统会先将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成。至于什么时候再写到磁盘中由操作系统决定除非显式地调用了sync同步命令。
第二种类型是**直接IO**,就是应用程序直接访问磁盘数据,而不经过内核缓冲区,从而减少了在内核缓存和用户程序之间数据复制。
如果在读的逻辑generic_file_read_iter里面发现设置了IOCB_DIRECT则会调用address_space的direct_IO的函数将数据直接读取硬盘。我们在mmap映射文件到内存的时候讲过address_space它主要用于在内存映射的时候将文件和内存页产生关联。
同样对于缓存来讲也需要文件和内存页进行关联这就要用到address_space。address_space的相关操作定义在struct address_space_operations结构中。对于ext4文件系统来讲 address_space的操作定义在ext4_aopsdirect_IO对应的函数是ext4_direct_IO。
```
static const struct address_space_operations ext4_aops = {
......
.direct_IO = ext4_direct_IO,
......
};
```
如果在写的逻辑__generic_file_write_iter里面发现设置了IOCB_DIRECT则调用generic_file_direct_write里面同样会调用address_space的direct_IO的函数将数据直接写入硬盘。
ext4_direct_IO最终会调用到__blockdev_direct_IO-&gt;do_blockdev_direct_IO这就跨过了缓存层到了通用块层最终到了文件系统的设备驱动层。由于文件系统是块设备所以这个调用的是blockdev相关的函数有关块设备驱动程序的原理我们下一章详细讲这一节我们就讲到文件系统到块设备的分界线部分。
```
/*
* This is a library function for use by filesystem drivers.
*/
static inline ssize_t
do_blockdev_direct_IO(struct kiocb *iocb, struct inode *inode,
struct block_device *bdev, struct iov_iter *iter,
get_block_t get_block, dio_iodone_t end_io,
dio_submit_t submit_io, int flags)
{......}
```
接下来,我们重点看带缓存的部分如果进行读写。
## 带缓存的写入操作
我们先来看带缓存写入的函数generic_perform_write。
```
ssize_t generic_perform_write(struct file *file,
struct iov_iter *i, loff_t pos)
{
struct address_space *mapping = file-&gt;f_mapping;
const struct address_space_operations *a_ops = mapping-&gt;a_ops;
do {
struct page *page;
unsigned long offset; /* Offset into pagecache page */
unsigned long bytes; /* Bytes to write to page */
status = a_ops-&gt;write_begin(file, mapping, pos, bytes, flags,
&amp;page, &amp;fsdata);
copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
flush_dcache_page(page);
status = a_ops-&gt;write_end(file, mapping, pos, bytes, copied,
page, fsdata);
pos += copied;
written += copied;
balance_dirty_pages_ratelimited(mapping);
} while (iov_iter_count(i));
}
```
这个函数里是一个while循环。我们需要找出这次写入影响的所有的页然后依次写入。对于每一个循环主要做四件事情
- 对于每一页先调用address_space的write_begin做一些准备
- 调用iov_iter_copy_from_user_atomic将写入的内容从用户态拷贝到内核态的页中
- 调用address_space的write_end完成写操作
- 调用balance_dirty_pages_ratelimited看脏页是否太多需要写回硬盘。所谓脏页就是写入到缓存但是还没有写入到硬盘的页面。
我们依次来看这四个步骤。
```
static const struct address_space_operations ext4_aops = {
......
.write_begin = ext4_write_begin,
.write_end = ext4_write_end,
......
}
```
第一步对于ext4来讲调用的是ext4_write_begin。
ext4是一种日志文件系统是为了防止突然断电的时候的数据丢失引入了**日志******<strong>Journal**</strong>****模式**。日志文件系统比非日志文件系统多了一个Journal区域。文件在ext4中分两部分存储一部分是文件的元数据另一部分是数据。元数据和数据的操作日志Journal也是分开管理的。你可以在挂载ext4的时候选择Journal模式。这种模式在将数据写入文件系统前必须等待元数据和数据的日志已经落盘才能发挥作用。这样性能比较差但是最安全。
另一种模式是**order模式**。这个模式不记录数据的日志,只记录元数据的日志,但是在写元数据的日志前,必须先确保数据已经落盘。这个折中,是默认模式。
还有一种模式是**writeback**,不记录数据的日志,仅记录元数据的日志,并且不保证数据比元数据先落盘。这个性能最好,但是最不安全。
在ext4_write_begin我们能看到对于ext4_journal_start的调用就是在做日志相关的工作。
在ext4_write_begin中还做了另外一件重要的事情就是调用grab_cache_page_write_begin来得到应该写入的缓存页。
```
struct page *grab_cache_page_write_begin(struct address_space *mapping,
pgoff_t index, unsigned flags)
{
struct page *page;
int fgp_flags = FGP_LOCK|FGP_WRITE|FGP_CREAT;
page = pagecache_get_page(mapping, index, fgp_flags,
mapping_gfp_mask(mapping));
if (page)
wait_for_stable_page(page);
return page;
}
```
在内核中缓存以页为单位放在内存里面那我们如何知道一个文件的哪些数据已经被放到缓存中了呢每一个打开的文件都有一个struct file结构每个struct file结构都有一个struct address_space用于关联文件和内存就是在这个结构里面有一棵树用于保存所有与这个文件相关的的缓存页。
我们查找的时候往往需要根据文件中的偏移量找出相应的页面而基数树radix tree这种数据结构能够快速根据一个长整型查找到其相应的对象因而这里缓存页就放在radix基数树里面。
```
struct address_space {
struct inode *host; /* owner: inode, block_device */
struct radix_tree_root page_tree; /* radix tree of all pages */
spinlock_t tree_lock; /* and lock protecting it */
......
}
```
pagecache_get_page就是根据pgoff_t index这个长整型在这棵树里面查找缓存页如果找不到就会创建一个缓存页。
第二步调用iov_iter_copy_from_user_atomic。先将分配好的页面调用kmap_atomic映射到内核里面的一个虚拟地址然后将用户态的数据拷贝到内核态的页面的虚拟地址中调用kunmap_atomic把内核里面的映射删除。
```
size_t iov_iter_copy_from_user_atomic(struct page *page,
struct iov_iter *i, unsigned long offset, size_t bytes)
{
char *kaddr = kmap_atomic(page), *p = kaddr + offset;
iterate_all_kinds(i, bytes, v,
copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
v.bv_offset, v.bv_len),
memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
)
kunmap_atomic(kaddr);
return bytes;
}
```
第三步调用ext4_write_end完成写入。这里面会调用ext4_journal_stop完成日志的写入会调用block_write_end-&gt;__block_commit_write-&gt;mark_buffer_dirty将修改过的缓存标记为脏页。可以看出其实所谓的完成写入并没有真正写入硬盘仅仅是写入缓存后标记为脏页。
但是这里有一个问题数据很危险一旦宕机就没有了所以需要一种机制将写入的页面真正写到硬盘中我们称为回写Write Back
第四步,调用 balance_dirty_pages_ratelimited是回写脏页的一个很好的时机。
```
/**
* balance_dirty_pages_ratelimited - balance dirty memory state
* @mapping: address_space which was dirtied
*
* Processes which are dirtying memory should call in here once for each page
* which was newly dirtied. The function will periodically check the system's
* dirty state and will initiate writeback if needed.
*/
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
struct inode *inode = mapping-&gt;host;
struct backing_dev_info *bdi = inode_to_bdi(inode);
struct bdi_writeback *wb = NULL;
int ratelimit;
......
if (unlikely(current-&gt;nr_dirtied &gt;= ratelimit))
balance_dirty_pages(mapping, wb, current-&gt;nr_dirtied);
......
}
```
在balance_dirty_pages_ratelimited里面发现脏页的数目超过了规定的数目就调用balance_dirty_pages-&gt;wb_start_background_writeback启动一个背后线程开始回写。
```
void wb_start_background_writeback(struct bdi_writeback *wb)
{
/*
* We just wake up the flusher thread. It will perform background
* writeback as soon as there is no other work to do.
*/
wb_wakeup(wb);
}
static void wb_wakeup(struct bdi_writeback *wb)
{
spin_lock_bh(&amp;wb-&gt;work_lock);
if (test_bit(WB_registered, &amp;wb-&gt;state))
mod_delayed_work(bdi_wq, &amp;wb-&gt;dwork, 0);
spin_unlock_bh(&amp;wb-&gt;work_lock);
}
(_tflags) | TIMER_IRQSAFE); \
} while (0)
/* bdi_wq serves all asynchronous writeback tasks */
struct workqueue_struct *bdi_wq;
/**
* mod_delayed_work - modify delay of or queue a delayed work
* @wq: workqueue to use
* @dwork: work to queue
* @delay: number of jiffies to wait before queueing
*
* mod_delayed_work_on() on local CPU.
*/
static inline bool mod_delayed_work(struct workqueue_struct *wq,
struct delayed_work *dwork,
unsigned long delay)
{....
```
通过上面的代码我们可以看出bdi_wq是一个全局变量所有回写的任务都挂在这个队列上。mod_delayed_work函数负责将一个回写任务bdi_writeback挂在这个队列上。bdi_writeback有个成员变量struct delayed_work dworkbdi_writeback就是以delayed_work的身份挂到队列上的并且把delay设置为0意思就是一刻不等马上执行。
那具体这个任务由谁来执行呢这里的bdi的意思是backing device info用于描述后端存储相关的信息。每个块设备都会有这样一个结构并且在初始化块设备的时候调用bdi_init初始化这个结构在初始化bdi的时候也会调用wb_init初始化bdi_writeback。
```
static int wb_init(struct bdi_writeback *wb, struct backing_dev_info *bdi,
int blkcg_id, gfp_t gfp)
{
wb-&gt;bdi = bdi;
wb-&gt;last_old_flush = jiffies;
INIT_LIST_HEAD(&amp;wb-&gt;b_dirty);
INIT_LIST_HEAD(&amp;wb-&gt;b_io);
INIT_LIST_HEAD(&amp;wb-&gt;b_more_io);
INIT_LIST_HEAD(&amp;wb-&gt;b_dirty_time);
wb-&gt;bw_time_stamp = jiffies;
wb-&gt;balanced_dirty_ratelimit = INIT_BW;
wb-&gt;dirty_ratelimit = INIT_BW;
wb-&gt;write_bandwidth = INIT_BW;
wb-&gt;avg_write_bandwidth = INIT_BW;
spin_lock_init(&amp;wb-&gt;work_lock);
INIT_LIST_HEAD(&amp;wb-&gt;work_list);
INIT_DELAYED_WORK(&amp;wb-&gt;dwork, wb_workfn);
wb-&gt;dirty_sleep = jiffies;
......
}
#define __INIT_DELAYED_WORK(_work, _func, _tflags) \
do { \
INIT_WORK(&amp;(_work)-&gt;work, (_func)); \
__setup_timer(&amp;(_work)-&gt;timer, delayed_work_timer_fn, \
(unsigned long)(_work), \
```
这里面最重要的是INIT_DELAYED_WORK。其实就是初始化一个timer也即定时器到时候我们就执行wb_workfn这个函数。
接下来的调用链为wb_workfn-&gt;wb_do_writeback-&gt;wb_writeback-&gt;writeback_sb_inodes-&gt;__writeback_single_inode-&gt;do_writepages写入页面到硬盘。
在调用write的最后当发现缓存的数据太多的时候会触发回写这仅仅是回写的一种场景。另外还有几种场景也会触发回写
- 用户主动调用sync将缓存刷到硬盘上去最终会调用wakeup_flusher_threads同步脏页
- 当内存十分紧张以至于无法分配页面的时候会调用free_more_memory最终会调用wakeup_flusher_threads释放脏页
- 脏页已经更新了较长时间时间上超过了timer需要及时回写保持内存和磁盘上数据一致性。
## 带缓存的读操作
带缓存的写分析完了接下来我们看带缓存的读对应的是函数generic_file_buffered_read。
```
static ssize_t generic_file_buffered_read(struct kiocb *iocb,
struct iov_iter *iter, ssize_t written)
{
struct file *filp = iocb-&gt;ki_filp;
struct address_space *mapping = filp-&gt;f_mapping;
struct inode *inode = mapping-&gt;host;
for (;;) {
struct page *page;
pgoff_t end_index;
loff_t isize;
page = find_get_page(mapping, index);
if (!page) {
if (iocb-&gt;ki_flags &amp; IOCB_NOWAIT)
goto would_block;
page_cache_sync_readahead(mapping,
ra, filp,
index, last_index - index);
page = find_get_page(mapping, index);
if (unlikely(page == NULL))
goto no_cached_page;
}
if (PageReadahead(page)) {
page_cache_async_readahead(mapping,
ra, filp, page,
index, last_index - index);
}
/*
* Ok, we have the page, and it's up-to-date, so
* now we can copy it to user space...
*/
ret = copy_page_to_iter(page, offset, nr, iter);
}
}
```
读取比写入总体而言简单一些,主要涉及预读的问题。
在generic_file_buffered_read函数中我们需要先找到page cache里面是否有缓存页。如果没有找到不但读取这一页还要进行预读这需要在page_cache_sync_readahead函数中实现。预读完了以后再试一把查找缓存页应该能找到了。
如果第一次找缓存页就找到了我们还是要判断是不是应该继续预读如果需要就调用page_cache_async_readahead发起一个异步预读。
最后copy_page_to_iter会将内容从内核缓存页拷贝到用户内存空间。
## 总结时刻
这一节对于读取和写入的分析就到这里了。我们发现这个过程还是很复杂的,我这里画了一张调用图,你可以看到调用过程。
在系统调用层我们需要仔细学习read和write。在VFS层调用的是vfs_read和vfs_write并且调用file_operation。在ext4层调用的是ext4_file_read_iter和ext4_file_write_iter。
接下来就是分叉。你需要知道缓存I/O和直接I/O。直接I/O读写的流程是一样的调用ext4_direct_IO再往下就调用块设备层了。缓存I/O读写的流程不一样。对于读从块设备读取到缓存中然后从缓存中拷贝到用户态。对于写从用户态拷贝到缓存设置缓存页为脏然后启动一个线程写入块设备。
<img src="https://static001.geekbang.org/resource/image/0c/65/0c49a870b9e6441381fec8d9bf3dee65.png" alt="">
## 课堂练习
你知道如何查询和清除文件系统缓存吗?
欢迎留言和我分享你的疑惑和见解 ,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">