linux 文件系统工作原理简介
文件系统时对存储设备上的文件进行组织管理的机制,组织方式不同就形成了不同的文件系统类型。
linux 中一切皆文件:不仅时普通的文件和目录,连块设备,套接字,管道等都是通过统一的文件系统来进行管理的。
索引节点和目录项
linux 系统为每个文件都分配了两个数据结构(索引节点 index node 和目录项 dictionary entry),来对文件进行管理。其中索引节点(index node)记录文件的元数据信息,目录项(dictionary entry)记录文件的目录结构信息。
-
索引节点:简称为
inode
,用来记录文件的元数据信息:如inode编号,文件大小,访问权限,修改日期,数据位置等。索引节点和文件一一对应,跟文件内容一样都会被持久化存储到磁盘中,所以索引节点也同样占据存储空间。 -
目录项:简称为
dentry
,用来记录文件的名字,索引节点指针以及其他目录项的关联关系。 多个关联的目录项构成了文件系统的目录结构,与索引节点不同,目录项是内核维护的一个内存数据结构,所以也通常称为目录项缓存。
即索引节点是每个文件的唯一标志,而目录项维护的正是文件系统的树状结构。目录项和索引节点的关系是多对一,可以简单理解为,一个文件可以有多个别名。如,通过硬链接为文件创建的别名,就会对应不同的目录项,不过这些目录项本质上还是链接同一个文件,所以,它们的索引节点是相同的。
文件数据的存储方式
磁盘读写的最小单位是扇区,然而扇区只有 512B 大小,如果每次都读写这么小的单位,效率一定很低。所以,文件系统又把连续的扇区组成了逻辑块,然后每次都以逻辑块为最小单元来管理数据。常见的逻辑块大小为 4KB,由连续的 8 个扇区组成。
目录项、索引节点以及文件数据的关系:
inode.png-
目录项本身就是一个内存缓存,而索引节点则是存储在磁盘中的数据。在前面的 Buffer 和 Cache 原理中,为了协调慢速磁盘与快速 CPU 的性能差异,文件内容会缓存到页缓存 Cache 中。所以这些索引节点自然也会缓存到内存中,加速文件的访问。
-
磁盘在执行文件系统格式化时,会被分成三个存储区域,超级块、索引节点区和数据块区。其中,
- 超级块,存储整个文件系统的状态。
- 索引节点区,用来存储索引节点。
- 数据块区,则用来存储文件数据。
虚拟文件系统
目录项,索引节点、逻辑块以及超级块构成了linux文件系统的四大基本要素。另外,为了支持各种不同类型的文件系统,linux内核在用户进程和文件系统之间又加入了一个抽象层-- 虚拟文件系统VFS(virtual file system).
VFS 定义了一组所有文件系统都支持的数据结构和标准接口。这样,用户进程和内核中的其他子系统,只需要跟VFS提供的统一的接口进行交互即可,而不需要关心各种底层的文件系统的实现细节。
系统调用,VFS,缓存,文件系统以及块存储之间的关系如下:
vfs.png在VFS的下方,linux支持各种类型的文件系统,如Ext4、XFS、NFS等。按照存储位置的不同,文件系统可以分为三类:
-
基于磁盘的文件系统,即把数据直接存储在计算机本地挂载的磁盘中,常见又: Ext4, XFS, OverlayFS等类型的文件系统。
-
基于内存的文件系统,即常说的虚拟文件系统。此类文件系统不需要任何的磁盘分配空间,但会占用内存,常见的有 /proc, /sys(主要向用户空间导出层次化的内核对象)等
-
网络文件系统,即用来访问其他计算机数据的文件系统,如NFS,SMB, iSCSI等。
以上提到的这些文件系统,要先挂载到VFS目录树中的某个子目录(挂载点),然后才能访问其中的文件。如在安装系统时,要先挂载一个根目录(/),在根目录下在把其他文件系统(其他磁盘分区、/proc文件系统、/sys文件系统、NFS等)挂载进来。
文件系统 IO
当把文件系统挂载到挂载点之后,就能够通过挂载点去访问管理的文件了。 VFS提供了一组标准的文件访问接口,以系统调用的方式提供给应用程序使用。
如cat命令,首先调用open()打开一个文件;然后调用read(),读取文件内容;最后调用write()把文件内容输出到控制台的标准输出中。
strace cat vi
execve("/usr/bin/cat", ["cat", "vi"], 0x7ffcc99897c8 /* 48 vars */) = 0
brk(NULL) = 0x55b305e3d000
arch_prctl(0x3001 /* ARCH_??? */, 0x7ffdb0bafeb0) = -1 EINVAL (Invalid argument)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=71314, ...}) = 0
mmap(NULL, 71314, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa9e9c0b000
close(3) = 0
openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\360q\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
fstat(3, {st_mode=S_IFREG|0755, st_size=2029224, ...}) = 0
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa9e9c09000
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 784, 64) = 784
pread64(3, "\4\0\0\0\20\0\0\0\5\0\0\0GNU\0\2\0\0\300\4\0\0\0\3\0\0\0\0\0\0\0", 32, 848) = 32
pread64(3, "\4\0\0\0\24\0\0\0\3\0\0\0GNU\0\t\233\222%\274\260\320\31\331\326\10\204\276X>\263"..., 68, 880) = 68
mmap(NULL, 2036952, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7fa9e9a17000
mprotect(0x7fa9e9a3c000, 1847296, PROT_NONE) = 0
mmap(0x7fa9e9a3c000, 1540096, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x25000) = 0x7fa9e9a3c000
mmap(0x7fa9e9bb4000, 303104, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x19d000) = 0x7fa9e9bb4000
mmap(0x7fa9e9bff000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1e7000) = 0x7fa9e9bff000
mmap(0x7fa9e9c05000, 13528, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7fa9e9c05000
close(3) = 0
arch_prctl(ARCH_SET_FS, 0x7fa9e9c0a580) = 0
mprotect(0x7fa9e9bff000, 12288, PROT_READ) = 0
mprotect(0x55b3058ca000, 4096, PROT_READ) = 0
mprotect(0x7fa9e9c4a000, 4096, PROT_READ) = 0
munmap(0x7fa9e9c0b000, 71314) = 0
brk(NULL) = 0x55b305e3d000
brk(0x55b305e5e000) = 0x55b305e5e000
openat(AT_FDCWD, "/usr/lib/locale/locale-archive", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=5699248, ...}) = 0
mmap(NULL, 5699248, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7fa9e94a7000
close(3) = 0
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0), ...}) = 0
openat(AT_FDCWD, "vi", O_RDONLY) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=60, ...}) = 0
fadvise64(3, 0, 0, POSIX_FADV_SEQUENTIAL) = 0
mmap(NULL, 139264, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fa9e9485000
read(3, "{\n \"registry-mirrors\": [\"http"..., 131072) = 60
write(1, "{\n \"registry-mirrors\": [\"http"..., 60{
"registry-mirrors": ["http://hub-mirror.c.163.com"]
}
) = 60
read(3, "", 131072) = 0
munmap(0x7fa9e9485000, 139264) = 0
close(3) = 0
close(1) = 0
close(2) = 0
exit_group(0) = ?
+++ exited with 0 +++
文件读写方式的各种差异,导致了IO分类的多种多样,最常见的有,缓冲于非缓冲IO,直接于非直接IO,阻塞于非阻塞IO,同步于异步IO。各种分类方式的依据如下:
- 根据是否利用了标准库缓存,可以把文件IO分为缓冲于非缓冲IO。
-
缓冲IO,利用标准库缓存来加速文件的访问,而标准库内部在通过系统调度访问文件。
-
非缓冲IO,直接通过系统调用来访问文件,不经过标准库缓存。
所说的"缓冲",是指标准库内部实现的缓存,如很多程序遇到换行时才真正输出,而换行前的内容就是被标准库暂时缓存了。
无论时缓冲IO还是非缓冲IO,它们最终都需要经过系统调用来访问文件,所以在系统调用后,还会通过页缓存来减少磁盘的IO操作。
- 根据是否利用操作系统的页缓存,可以把文件系统IO分为直接IO与非直接IO。
-
直接IO,是指跳过操作系统的页缓存,直接跟文件系统交互来访问文件。
-
非直接IO,是指进行文件读写时要先经过操作系统的页缓存,然后再由内核或额外的系统调用将文件内容真正写入磁盘。
如果要实现直接IO,需要再系统调用中指定O_DIRECT
标志,如果没有设置,默认是非直接IO。另外不管是直接IO还是非直接IO,本质上都是和文件系统的交互,在其他一些场景中(数据库场景),还会有跳过文件系统直接读写磁盘的场景(裸IO)。
- 根据应用程序是否阻塞自身运行,可以把文件IO分为阻塞IO和非阻塞IO。
-
所谓阻塞IO,是指应用程序执行IO操作后,如果没有获得响应,就会阻塞当前线程,不能执行其他任务。
-
非阻塞IO与之相反,是指应用程序进行IO操作后,不会阻塞当前的线程,可以执行其他任务,随后再通过轮询或者事件通知的形式,获取调用结果。
如访问管道或者网络套接字时,设置了O_NONBLOCK
标志,就表示用非阻塞的方式进行访问,如果不做任何设置,默认是阻塞的方式进行访问。
- 根据是否等待响应结果,可以把文件IO分为同步和异步IO。
-
同步IO,应用程序执行IO操作后,要一直等到整个IO完成后才能获得IO的响应。
-
异步IO,应用程序执行IO操作后,不用等到完成和完成后的响应,而是继续执行即可,等到此次IO完成后,响应会通过事件通知的方式告知应用程序。
如,再操作文件时如果设置了O_SYNC
或者O_DSYNC
标志,就代表同步IO, 如果设置了O_DSYNC
,需要等文件数据写入磁盘后才能返回,而O_SYNC
在O_DSYNC
的基础上,要求文件元数据也要写入磁盘后才能返回。
在访问管道或者套接字时, O_ASYNC
设置选项标志IO是异步操作,此时内核会通过SIGIO 或者SIGPOLL信号来通知进程文件是否可写。
很多IO的概念页出现在网络编程中,如非阻塞IO通常会跟select/poll配合使用。
至此,可以理解“linux 一切皆文件” 的含义:无论是普通文件和块设备、还是网络套接字和管道等,它们都是通过统一的VFS接口来访问的。
网友评论