mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-20 08:03:43 +08:00
del
This commit is contained in:
@@ -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 -> /var/lib/cloud/instances
|
||||
|
||||
```
|
||||
|
||||
## 文件系统相关系统调用
|
||||
|
||||
看完了命令行,我们来看一下,如何使用系统调用操作文件?我们先来看一个完整的例子。
|
||||
|
||||
```
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
|
||||
|
||||
int fd = -1;
|
||||
int ret = 1;
|
||||
int buffer = 1024;
|
||||
int num = 0;
|
||||
|
||||
|
||||
if((fd=open("./test", O_RDWR|O_CREAT|O_TRUNC))==-1)
|
||||
{
|
||||
printf("Open Error\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
ret = write(fd, &buffer, sizeof(int));
|
||||
if( ret < 0)
|
||||
{
|
||||
printf("write Error\n");
|
||||
exit(1);
|
||||
}
|
||||
printf("write %d byte(s)\n",ret);
|
||||
|
||||
|
||||
lseek(fd, 0L, SEEK_SET);
|
||||
ret= read(fd, &num, sizeof(int));
|
||||
if(ret==-1)
|
||||
{
|
||||
printf("read Error\n");
|
||||
exit(1);
|
||||
}
|
||||
printf("read %d byte(s),the number is %d\n", ret, num);
|
||||
|
||||
|
||||
close(fd);
|
||||
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
当使用系统调用open打开一个文件时,操作系统会创建一些数据结构来表示这个被打开的文件。下一节,我们就会看到这些。为了能够找到这些数据结构,在进程中,我们会为这个打开的文件分配一个文件描述符fd(File 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 <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <fcntl.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
#include <dirent.h>
|
||||
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
struct stat sb;
|
||||
DIR *dirp;
|
||||
struct dirent *direntp;
|
||||
char filename[128];
|
||||
if ((dirp = opendir("/root")) == NULL) {
|
||||
printf("Open Directory Error%s\n");
|
||||
exit(1);
|
||||
}
|
||||
while ((direntp = readdir(dirp)) != NULL){
|
||||
sprintf(filename, "/root/%s", direntp->d_name);
|
||||
if (lstat(filename, &sb) == -1)
|
||||
{
|
||||
printf("lstat Error%s\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
|
||||
printf("name : %s, mode : %d, size : %d, user id : %d\n", direntp->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="">
|
||||
@@ -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>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-> do_filp_open->path_openat->do_last->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->d_inode && (open_flag & O_CREAT)) {
|
||||
......
|
||||
error = dir_inode->i_op->create(dir_inode, dentry, mode,
|
||||
open_flag & 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->ext4_new_inode_start_handle->__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->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。这里面有整个文件系统一共有多少inode,s_inodes_count;一共有多少块,s_blocks_count_lo,每个块组有多少inode,s_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="">
|
||||
@@ -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(&ext4_fs_type);
|
||||
|
||||
|
||||
static struct file_system_type ext4_fs_type = {
|
||||
.owner = THIS_MODULE,
|
||||
.name = "ext4",
|
||||
.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->do_new_mount->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->mnt.mnt_root = root;
|
||||
mnt->mnt.mnt_sb = root->d_sb;
|
||||
mnt->mnt_mountpoint = mnt->mnt.mnt_root;
|
||||
mnt->mnt_parent = mnt;
|
||||
list_add_tail(&mnt->mnt_instance, &root->d_sb->s_mounts);
|
||||
return &mnt->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->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是装载点在父文件系统中的dentry;struct dentry表示目录,并和目录的inode关联;mnt_root是当前文件系统根目录的dentry,mnt_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->mount(type, flags, name, data);
|
||||
......
|
||||
sb = root->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 >= 0) {
|
||||
struct file *f = do_filp_open(dfd, tmp, &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(&nd, dfd, pathname);
|
||||
filp = path_openat(&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)) &&
|
||||
(error = do_last(nd, file, op, &opened)) > 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, &path, &inode, &seq);
|
||||
......
|
||||
error = lookup_open(nd, &path, file, op, got_write, opened);
|
||||
......
|
||||
error = vfs_open(&nd->path, file, current_cred());
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这里面,我们需要先查找文件路径最后一部分对应的dentry。如何查找呢?
|
||||
|
||||
Linux为了提高目录项对象的处理效率,设计与实现了目录项高速缓存dentry cache,简称dcache。它主要由两个数据结构组成:
|
||||
|
||||
- 哈希表dentry_hashtable:dcache中的所有dentry对象都通过d_hash指针链到相应的dentry哈希链表中;
|
||||
- 未使用的dentry对象链表s_dentry_lru:dentry对象通过其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, &nd->last, &wq);
|
||||
......
|
||||
struct dentry *res = dir_inode->i_op->lookup(dir_inode, dentry,
|
||||
nd->flags);
|
||||
......
|
||||
path->dentry = dentry;
|
||||
path->mnt = nd->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->dentry, NULL, file->f_flags, 0);
|
||||
......
|
||||
file->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->f_mode = OPEN_FMODE(f->f_flags) | FMODE_LSEEK |
|
||||
FMODE_PREAD | FMODE_PWRITE;
|
||||
path_get(&f->f_path);
|
||||
f->f_inode = inode;
|
||||
f->f_mapping = inode->i_mapping;
|
||||
......
|
||||
f->f_op = fops_get(inode->i_fop);
|
||||
......
|
||||
open = f->f_op->open;
|
||||
......
|
||||
error = open(inode, f);
|
||||
......
|
||||
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
|
||||
file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);
|
||||
return 0;
|
||||
......
|
||||
}
|
||||
|
||||
|
||||
const struct file_operations ext4_file_operations = {
|
||||
......
|
||||
.open = ext4_file_open,
|
||||
......
|
||||
};
|
||||
|
||||
|
||||
```
|
||||
|
||||
vfs_open里面最终要做的一件事情是,调用f_op->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="">
|
||||
@@ -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, &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, &pos);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
对于read来讲,里面调用vfs_read->__vfs_read。对于write来讲,里面调用vfs_write->__vfs_write。
|
||||
|
||||
下面是__vfs_read和__vfs_write的代码。
|
||||
|
||||
```
|
||||
ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
|
||||
loff_t *pos)
|
||||
{
|
||||
if (file->f_op->read)
|
||||
return file->f_op->read(file, buf, count, pos);
|
||||
else if (file->f_op->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->f_op->write)
|
||||
return file->f_op->write(file, p, count, pos);
|
||||
else if (file->f_op->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_iter,ext4_file_write_iter会调用__generic_file_write_iter。
|
||||
|
||||
```
|
||||
ssize_t
|
||||
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
|
||||
{
|
||||
......
|
||||
if (iocb->ki_flags & IOCB_DIRECT) {
|
||||
......
|
||||
struct address_space *mapping = file->f_mapping;
|
||||
......
|
||||
retval = mapping->a_ops->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->ki_flags & IOCB_DIRECT) {
|
||||
......
|
||||
written = generic_file_direct_write(iocb, from);
|
||||
......
|
||||
} else {
|
||||
......
|
||||
written = generic_perform_write(file, from, iocb->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_aops,direct_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->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->f_mapping;
|
||||
const struct address_space_operations *a_ops = mapping->a_ops;
|
||||
do {
|
||||
struct page *page;
|
||||
unsigned long offset; /* Offset into pagecache page */
|
||||
unsigned long bytes; /* Bytes to write to page */
|
||||
status = a_ops->write_begin(file, mapping, pos, bytes, flags,
|
||||
&page, &fsdata);
|
||||
copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
|
||||
flush_dcache_page(page);
|
||||
status = a_ops->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->__block_commit_write->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->host;
|
||||
struct backing_dev_info *bdi = inode_to_bdi(inode);
|
||||
struct bdi_writeback *wb = NULL;
|
||||
int ratelimit;
|
||||
......
|
||||
if (unlikely(current->nr_dirtied >= ratelimit))
|
||||
balance_dirty_pages(mapping, wb, current->nr_dirtied);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在balance_dirty_pages_ratelimited里面,发现脏页的数目超过了规定的数目,就调用balance_dirty_pages->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(&wb->work_lock);
|
||||
if (test_bit(WB_registered, &wb->state))
|
||||
mod_delayed_work(bdi_wq, &wb->dwork, 0);
|
||||
spin_unlock_bh(&wb->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 dwork,bdi_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->bdi = bdi;
|
||||
wb->last_old_flush = jiffies;
|
||||
INIT_LIST_HEAD(&wb->b_dirty);
|
||||
INIT_LIST_HEAD(&wb->b_io);
|
||||
INIT_LIST_HEAD(&wb->b_more_io);
|
||||
INIT_LIST_HEAD(&wb->b_dirty_time);
|
||||
wb->bw_time_stamp = jiffies;
|
||||
wb->balanced_dirty_ratelimit = INIT_BW;
|
||||
wb->dirty_ratelimit = INIT_BW;
|
||||
wb->write_bandwidth = INIT_BW;
|
||||
wb->avg_write_bandwidth = INIT_BW;
|
||||
spin_lock_init(&wb->work_lock);
|
||||
INIT_LIST_HEAD(&wb->work_list);
|
||||
INIT_DELAYED_WORK(&wb->dwork, wb_workfn);
|
||||
wb->dirty_sleep = jiffies;
|
||||
......
|
||||
}
|
||||
|
||||
|
||||
#define __INIT_DELAYED_WORK(_work, _func, _tflags) \
|
||||
do { \
|
||||
INIT_WORK(&(_work)->work, (_func)); \
|
||||
__setup_timer(&(_work)->timer, delayed_work_timer_fn, \
|
||||
(unsigned long)(_work), \
|
||||
|
||||
```
|
||||
|
||||
这里面最重要的是INIT_DELAYED_WORK。其实就是初始化一个timer,也即定时器,到时候我们就执行wb_workfn这个函数。
|
||||
|
||||
接下来的调用链为:wb_workfn->wb_do_writeback->wb_writeback->writeback_sb_inodes->__writeback_single_inode->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->ki_filp;
|
||||
struct address_space *mapping = filp->f_mapping;
|
||||
struct inode *inode = mapping->host;
|
||||
for (;;) {
|
||||
struct page *page;
|
||||
pgoff_t end_index;
|
||||
loff_t isize;
|
||||
page = find_get_page(mapping, index);
|
||||
if (!page) {
|
||||
if (iocb->ki_flags & 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="">
|
||||
Reference in New Issue
Block a user