文件系统负责存储和组织文件,以便可以方便地对文件进行查找和访问,我们要能优雅地访问磁盘上的数据就得用到文件系统。不同的文件系统有不同的文件存储和组织方式。
Linux从各种各样的文件系统中提取它们的共同部分,设计出一个抽象层,让上层应用程序可以通过统一的界面进行操作,当需要具体文件系统介入时,由抽象层调用具体文件系统提供的回调函数来处理。这个抽象层叫做虚拟文件系统开关(Virtual Filesystem Switch)层,简称为虚拟文件系统(VFS)。它是具体文件系统和上层应用间的接口层,将各种不同文件系统的操作和管理纳入一个统一的框架,使得用户不需要关心各种不同文件系统的实现细节。
VFS有点类似于JAVA中的JDBC,JDBC负责制定一套访问数据库的标准接口,而具体如何访问数据库则由具体的数据库厂商来实现,应用层无需关系底层的具体实现,只需要调用JDBC的标准接口即可。
但是严格说来,VFS并不是一种实际的文件系统,它只存在于内存中,不存在于任何外存空间。VFS在系统启动时建立,系统关闭时消亡。
在VFS中,树的根节点是称为根文件系统(Root File System)的顶层文件系统,它代表了整个系统的根目录。根文件系统下面可以挂载(Mount)其他的文件系统,这些文件系统作为根文件系统的子文件系统,可以通过挂载点(Mount Point)的方式加入到整个树形结构中。
每个文件系统都有自己的根目录,它们可以包含子目录和文件,形成一个层次化的文件结构。这些文件系统可以是硬盘上的实际文件系统,也可以是网络上的远程文件系统,甚至可以是虚拟的文件系统,如tmpfs(内存文件系统)。
在VFS中对文件使用一种层次的方式来管理,层次中的节点被称为目录(Directory),而叶子就是文件。目录包含了一组文件或其他目录,包含在另一个目录下的目录被称为子目录,前者被称为父目录,这样就形成了一个层次的树状结构,其根节点被称为根(Root)目录。
每个文件系统并不是独立使用的。相反,系统有一个全局文件系统树,要访问一个文件系统中的文件,必须先将这个文件系统放在全局文件系统树的某个目录下,也就是我们熟悉的挂载mount过程。
文件通过路径Path来标识,路径指的是从文件系统树的一个节点开始,到达另一个节点的通路。路径通常表示成中间所经过的节点(目录或文件)的名字,加上分隔符/
,连接成字符串形式。
Linux下虚拟文件目录树的结构如下:
$ sudo ls /
bin dev home initrd.img.old lib64 media opt root sbin srv tmp var vmlinuz.old
boot etc initrd.img lib lost+found mnt proc run snap sys usr vmlinuz
看起来虽然所有的文件都位于同一个根目录下,但是其中的部分文件来源于不同的分区中,例如/boot
目录来源于分区/dev/sda1
:
$ df -h
Filesystem Size Used Avail Use% Mounted on
udev 428M 0 428M 0% /dev
tmpfs 93M 6.8M 86M 8% /run
/dev/sda3 124G 3.8G 113G 4% /
tmpfs 461M 0 461M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 461M 0 461M 0% /sys/fs/cgroup
/dev/sda1 462M 152M 282M 35% /boot
tmpfs 93M 0 93M 0% /run/user/1000
计算机引导的时候是先挂载了分区/dev/sda1
,又挂载了分区/dev/sda3
,为了方便查看内核启动时的一些配置,又将/dev/sda1
挂载到了/
目录下,所以/boot
目录已经完成了他的使命,是可以被卸载的。
下面演示/boot
的卸载与挂载:
$ ls /boot
config-4.15.0-156-generic initrd.img-4.15.0-156-generic System.map-4.15.0-156-generic vmlinuz-4.15.0-213-generic
config-4.15.0-213-generic initrd.img-4.15.0-213-generic System.map-4.15.0-213-generic
grub lost+found vmlinuz-4.15.0-156-generic
$ sudo umount /boot
$ sudo ls /boot
$ sudo df -h
Filesystem Size Used Avail Use% Mounted on
udev 428M 0 428M 0% /dev
tmpfs 93M 6.8M 86M 8% /run
/dev/sda3 124G 3.8G 113G 4% /
tmpfs 461M 0 461M 0% /dev/shm
tmpfs 5.0M 0 5.0M 0% /run/lock
tmpfs 461M 0 461M 0% /sys/fs/cgroup
tmpfs 93M 0 93M 0% /run/user/1000
$ sudo mount /dev/sda1 /boot
$ ls /boot
config-4.15.0-156-generic initrd.img-4.15.0-156-generic System.map-4.15.0-156-generic vmlinuz-4.15.0-213-generic
config-4.15.0-213-generic initrd.img-4.15.0-213-generic System.map-4.15.0-213-generic
grub lost+found vmlinuz-4.15.0-156-generic
一个相同的文件系统可以被挂载到不同的路径下(生成不同的超级块实例,分别访问),一个相同超级块实例也可以被挂载到不同的路径下(相同超级块实例,共同访问),即一个文件系统类型可能有多个超级块实例,而每个超级块实例又可以有多个挂载实例。
/dev/sda1
和/dev/sda2
都被格式化为Minix文件系统,当/dev/sda1
和/dev/sda2
上的文件系统实例先后被挂载到系统时,假设在/mnt/d1
和/mnt/d2
下生成了两个超级块实例,分别对应一个挂载实例。透过/mnt/d1
和/mnt/d2
所做的改动分别反映到/dev/sda1
和/dev/sda2
上,然后,倘若/dev/sda1
又被挂载到了/mnt/d11
下,则/mnt/d1
和/mnt/d11
所作的改动都会被反映到/dev/sda1
上的文件系统实例中。
虚拟文件系统在磁盘中并没有对应的存储的信息。尽管Linux支持多达几十种文件系统,但这些真实的文件系统并不是一下子都挂在系统中的,它们实际上是按需挂载的。另外,这些实的文件系统只有安装到系统中,VFS才予以认可,也就是说,VFS只管理挂载到系统中的实际文件系统 。
VFS有4个主要对象:
/home/morris/myfile
中,根目录是/
,而home
,morris
和文件myfile
都是目录项。超级块是对一个文件系统的描述;索引节点是对一个文件物理属性的描述;而目录项是对一个文件逻辑属性的描述。
超级块用来描述整个文件系统的信息,包括文件系统的大小、有多少是空的和已经填满的占多少,以及他们各自的总数和其他诸如此类的信息。超级块占用1号物理块,就是文件系统的控制块,要使用一个分区来进行数据访问,那么第一个要访问的就是超级块。所以,超级块坏了,那磁盘也就基本没救了。
当内核在对一个文件系统进行初始化和注册时 在内存为其分配一个超级块。此时的超级块为VFS超级块。也就是说,VFS超级块是各种具体文件系统在安装时建立的,并在这些文件系统卸载时被自动删除。VFS超级块只存放在内存中 。
对于每个具体的文件系统来说,都有各自的超级块,如Ext2超级块和Ext3超级块,它存放在磁盘上,内容包括:文件系统的大小、空闲块数目、空闲块索引表、空闲i节点数目、空闲i节点索引表、封锁标记等。
超级块是系统为文件分配存储空间、回收存储空间的依据。这一部分的拓扑结构如下图:
如何查看linux上一个文件系统的超级块super block的信息:
$ sudo dumpe2fs /dev/sda1
dumpe2fs 1.44.1 (24-Mar-2018)
Filesystem volume name: <none>
Last mounted on: /boot
Filesystem UUID: 0fa78116-cd56-42d2-9282-a526cafa0469
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index filetype needs_recovery extent 64bit flex_bg sparse_super large_file huge_file dir_nlink extra_isize metadata_csum
Filesystem flags: signed_directory_hash
Default mount options: user_xattr acl
Filesystem state: clean
Errors behavior: Continue
Filesystem OS type: Linux
Inode count: 124928
Block count: 498688
Reserved block count: 24934
Free blocks: 317378
Free inodes: 1246
。。。 。。。
文件系统处理文件所需要的所有信息都存放在索引节点中。在同一个文件系统中,每个索引节点号都是唯一的。具体文件系统的索引节点是存放在磁盘上,是一种静态结构,要使用它,必须调入内存,填写VFS的索引节点,因此,也称VFS索引节点是动态节点。
我们的磁盘在进行分区、格式化的时候会分为两个区域,一个是数据区,用于存储文件中的数据;另一个是inode区,用于存放inode table(inode表),inode table中存放的是一个一个的inode(也称为inode节点),不同的inode就可以表示不同的文件,每一个文件都必须对应一个inode,inode实质上是一个结构体,这个结构体中有很多的元素,不同的元素记录了文件了不同信息,譬如:
所以由此可知,inode table本身也需要占用磁盘的存储空间。在同一个文件系统中,每一个文件都有唯一的一个inode,每一个 inode都有一个与之相对应的数字编号,内核可以根据inode节点号的哈希值查找其inode结构,前提是内核要知道inode节点号和对应文件所在文件系统的超级块对象的地址。
在Linux系统下,我们可以通过ls -i
命令查看文件的inode编号,如下所示:
$ sudo ls -li
total 12
4456463 -rw-rw-r-- 1 vagrant vagrant 433 Dec 7 10:37 app.py
4456478 -rw-rw-r-- 1 vagrant vagrant 388 Dec 8 07:24 docker-compose.yml
4456464 -rw-rw-r-- 1 vagrant vagrant 305 Dec 7 10:39 Dockerfile
上图中ls打印出来的信息中,每一行前面的一个数字就表示了对应文件的inode编号。
除此之外,还可以使用stat命令查看,用法如下:
$ stat app.py
File: app.py
Size: 433 Blocks: 8 IO Block: 4096 regular file
Device: 803h/2051d Inode: 4456463 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ vagrant) Gid: ( 1000/ vagrant)
Access: 2023-12-07 10:39:58.051136118 +0000
Modify: 2023-12-07 10:37:51.154374450 +0000
Change: 2023-12-07 10:37:51.154374450 +0000
Birth: -
和超级块和inode节点不同,目录项并不是实际存在于磁盘上的。在使用的时候在内存中创建目录项对象,其实通过索引节点已经可以定位到指定的文件,但是索引节点对象的属性非常多,在查找,比较文件时,直接用索引节点效率不高,所以引入了目录项的概念。
引入目录项的概念主要是出于方便查找文件的目的。一个路径的各个组成部分,不管是目录还是普通的文件,都是一个目录项对象。
例如在路径/home/foo/test.c
中,目录/
、home
、foo
和文件test.c
都对应一个目录项对象。不同于前面的两个对象,目录项对象没有对应的磁盘数据结构,VFS在遍历路径名的过程中现场将它们逐个地解析成目录项对象。
每个目录项对象都有3种状态:
目录项的目的就是提高文件查找,比较的效率,所以访问过的目录项都会缓存在slab中。
slab中缓存的名称一般就是dentry,可以通过如下命令查看:
$ sudo cat /proc/slabinfo | grep dentry
dentry 58861 60627 192 21 1 : tunables 0 0 0 : slabdata 2887 2887 0
文件对象是进程打开的文件在内存中的实例。Linux用户程序可以通过open()系统调用来打开一个文件,通过close()系统调用来关闭一个文件。由于多个进程可以同时打开和操作同一个文件,所以同一个文件,在内存中也存在多个对应的文件对象,但对应的索引节点和目录项是唯一的。
文件对象表示进程已打开的文件,从用户角度来看,我们在代码中操作的就是一个文件对象。
文件对象反过来指向一个目录项对象(目录项反过来指向一个索引节点),其实只有目录项对象才表示一个已打开的实际文件,虽然一个文件对应的文件对象不是唯一的,但其对应的索引节点和目录项对象却是唯一的。
文件描述符:File descriptor,简称fd,当应用程序请求内核打开/新建一个文件(资源)时,内核会返回一个文件描述符用于对应这个打开/新建的文件,其fd本质上就是一个非负整数。
实际上,文件描述符是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。系统为每一个进程维护了一个文件描述符表,该表的值都是从0开始的,所以在不同的进程中你会看到相同的文件描述符,这种情况下相同文件描述符有可能指向同一个文件,也有可能指向不同的文件。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。
每一个进程都会有下面三个文件描述符:
怎么查看一个进程打开了那些文件呢?可以使用lsof命令。
lsof是list open files的简称,它的作用主要是列出系统中打开的文件,基本上linux系统中所有的对象都可以看作文件,lsof可以查看用户和进程操作了哪些文件,也可以查看系统中网络的使用情况,以及设备的信息。
例如可以使用ls -p $$
查看当前shell打开了哪些文件,其中node列表示的是文件的inode号 :
$ sudo lsof -p $$
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
bash 2693 vagrant cwd DIR 8,3 4096 4456450 /home/vagrant
bash 2693 vagrant rtd DIR 8,3 4096 2 /
bash 2693 vagrant txt REG 8,3 1113504 5767339 /bin/bash
bash 2693 vagrant 0u CHR 136,0 0t0 3 /dev/pts/0
bash 2693 vagrant 1u CHR 136,0 0t0 3 /dev/pts/0
bash 2693 vagrant 2u CHR 136,0 0t0 3 /dev/pts/0
bash 2693 vagrant 255u CHR 136,0 0t0 3 /dev/pts/0
。。。。。。
下面通过一个例子模拟文件的读与写来理解文件描述符:
创建一个ooxx.txt
文件,并填充点内容:
$ echo -e "hello\n" > ooxx.txt
创建一个文件描述符8
,用来读取ooxx.txt
文件:
$ exec 8< ooxx.txt
可以看到当前shell的fd目录下多了个文件描述符为8
的文件:
$ ll /proc/$$/fd
。。。。。。
lr-x------ 1 vagrant vagrant 64 Dec 20 07:38 8 -> /home/vagrant/ooxx.txt
查看ooxx.txt
文件的inode对象,可以发现Links数为1,说明有1个文件描述符引用它:
$ sudo stat ooxx.txt
File: ooxx.txt
Size: 7 Blocks: 8 IO Block: 4096 regular file
Device: 803h/2051d Inode: 4456465 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ vagrant) Gid: ( 1000/ vagrant)
Access: 2023-12-20 11:34:29.164038985 +0000
Modify: 2023-12-20 11:34:27.604024767 +0000
Change: 2023-12-20 11:34:27.604024767 +0000
Birth: -
lsof命令加上-o参数的话,会显示一列OFFSET,表示当前读文件位置的指针。
$ lsof -op $$
bash 2693 vagrant 8r REG 8,3 0t0 4456465 /home/vagrant/ooxx.txt
lsof的node列的编号与ooxx.txt
的inode号一致,offfset列的偏移量为0。
使用read命令来读取文件ooxx.txt
的内容:
$ read a 0<&8
$ echo $a
hello
定义了一个变量a,将文件描述符8
的内容读取到标准输入0
,并赋值给a。
再次查看offfset列的偏移量为6,由于read命令遇到换行符就会停止,所以一共读出了hello\n
6个字符,offset的位置变成了6。
$ lsof -op $$
bash 2693 vagrant 8r REG 8,3 0t6 4456465 /home/vagrant/ooxx.txt
新开一个shell,用一个新的文件描述符6,去读取ooxx.txt证明两个进程读取文件时,不会相互影响:
$ exec 6<ooxx.txt
$ ll /proc/$$/fd
total 0
dr-x------ 2 vagrant vagrant 0 Dec 20 11:48 ./
dr-xr-xr-x 9 vagrant vagrant 0 Dec 20 11:48 ../
lrwx------ 1 vagrant vagrant 64 Dec 20 11:48 0 -> /dev/pts/1
lrwx------ 1 vagrant vagrant 64 Dec 20 11:48 1 -> /dev/pts/1
lrwx------ 1 vagrant vagrant 64 Dec 20 11:48 2 -> /dev/pts/1
lrwx------ 1 vagrant vagrant 64 Dec 20 11:48 255 -> /dev/pts/1
lr-x------ 1 vagrant vagrant 64 Dec 20 11:48 6 -> /home/vagrant/ooxx.txt
$ lsof -op $$
。。。。。。
bash 7792 vagrant 6r REG 8,3 0t0 4456465 /home/vagrant/ooxx.txt
$ stat ooxx.txt
File: ooxx.txt
Size: 7 Blocks: 8 IO Block: 4096 regular file
Device: 803h/2051d Inode: 4456465 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1000/ vagrant) Gid: ( 1000/ vagrant)
Access: 2023-12-20 11:34:29.164038985 +0000
Modify: 2023-12-20 11:34:27.604024767 +0000
Change: 2023-12-20 11:34:27.604024767 +0000
Birth: -
从上面的结果可以发现两个进程读取同一个文件时的偏移量时不一样的,互相不影响,但是inode是同一个。
在Linux中,所有内容都是以文件的形式保存和管理的,即一切皆文件,普通文件是文件,目录(Windows下称为文件夹)是文件,硬件设备(键盘、监视器、硬盘、打印机等)都是文件,就连套接字(socket)、网络通信等资源也都是文件。
Linux为应用程序访问文件提供了统一的接口,如read、write、open等,也称为
虚拟文件系统(VFS,Virtual File System)。虚拟文件系统其实就是一个目录树,树上不同的节点可以映射到物理的文件地址,也可以进行挂载,相当于一个解耦层,在具体的文件系统之上抽象的一层,为能够给各种文件系统提供一个通用的接口,使上层的应用程序能够使用通用的接口访问不同文件系统、不同的驱动。
逻辑上抽象为一棵树,物理上可以来自于不同的资源。