Refer to <<linux
内核源代码情景分析
>> and <<Linux kernel Version:2.4.0>>
Having any problems, send mails to viloner@163.com
VFS
的结构
除
Linux
本身的文件系统
Ext2
外,要使
Linux
支持其它各种文件系统,就要把各种不同的文件系统的操作和管理纳入到一个统一的框架中。让内核中的文件系统界面成为一条文件系统“总线”,使得用户程序可以通过同一个文件系统操作界面,也就是同一组系统调用,对各种不同的文件系统(以及文件)进行操作。这样,就可以对用户程序隐去各种不同文件系统的实现细节,为用户程序提供一个统一的、抽象的、虚拟的文件系统界面,这就是所谓的“虚拟文件系统”
VFS
(
Virtual Filesystem Switch
)。这个抽象的界面主要由一组标准的、抽象的文件操作构成,以系统调用的形式提供于用户程序,如
read()
、
write()
、
lseek()
等等。这样,用户程序就可以把所有的文件看作一致的、抽象的“
VFS
文件”,通过这些系统调用对文件进行操作,而无需关心具体的文件属于什么文件系统以及具体文件系统的设计和实现。
如果把内核比拟为
PC
机中的“母板”,把
VFS
比拟为“母板”上的一个“插槽”,那么每个具体的文件系统就好像一块“接口卡”。不同的接口卡上有不同的电子线路,但是它们与插槽的连接有几条线,每条线干什么用则是有明确定义的。同样,不同的文件系统通过不同的程序来实现其各种功能,但是与
VFS
之间的界面则是有明确定义的。这个界面的主体就是一个
file_operations
数据结构,其定义在
include/linux/fs.h
中:
768 /*
769
*NOTE
770
*read,write,poll,fsync,readv,writev can be called
771
* without the big kernel lock held in all filesystems.
772
*/
773
struct file_operations {
774
struct module *owner;
775 loff_t (*llseek) (struct file * ,loff_t, int);
776 ssize_t (*read) (struct file * ,char *, size_t, loff_t *);
777 ssize_t (*write) (struct file * ,const char *, size_t, loff_t *);
……
}
每种文件系统都有自己的
file_operations
数据结构,结构中的成分几乎全是函数指针,所以实际上是个函数跳转表,如果具体的文件系统不支持某项操作,其
file_operations
结构中的相应函数指针就是
NULL
。
每个进程通过“打开文件”(
open()
)与具体的文件建立起连接,或者说建立起一个读写的“上下文”。这种连接以一个
file
数据结构作为代表,结构中有个
file_operations
结构指针
f_op
。将
file
结构中的指针
f_op
设置成指向某个具体的
file_operations
结构,就指定了这个文件所属的文件系统,并且与具体文件系统所提供的一组函数挂上了钩,就好像把具体的“接口卡”插到了“插槽”中。
Linux
内核中对
VFS
与具体文件系统的关系划分可以用下图表示:
―――――――――――
用户空间
|
用户程序(进程)
|
―――――――――――
|
文件系统操作的系统
--------------------------------------------------------------------------
调用界面,包括
read()
、
|
write()
、
open()
、
close()
等等
――――――
系统空间
| VFS |
函数
sys_read()
、
sys_write()
、
sys_open()
等等
――――――
|
通过
file
结构
------------------------------------------------------------------------------
中的
f_op
指针实现
| | | |
的“文件系统总线”
―――――
――――
―――
――――――
| minix | | Ext2 | …… | FAT | ….. |
设备文件
|…..
―――――
――――
―――
――――――
进程与文件的连接,即“已打开文件”,是进程的一项“财产”,归具体的进程所有。代表着这种连接的
file
结构必然与代表着进程的
task_struct
数据结构存在着联系。
Task_struct
数据结构定义如下
(include/linux/sched.h):
277struct task_struct {
……
375
/* filesystem information */
376
struct fs_struct *fs;
377
/* open file information */
378
struct files_struct *files;
……
};
这里有两个指针
fs
和
files,
一个指向
fs_struct
数据结构,是关于文件系统的信息
;
另一个指向
files_struct
数据结构,是关于已打开文件的信息。先看
fs_struct
结构,它的定义在
include/linux/fs_struct.h
中:
5 struct fs_struct{
6
atomic_t count;
7
rwlock_t lock;
8
int umask;
9
struct dentry * root, * pwd, * altroot;
10
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
11
};
结构中有六个指针。前三个是
dentry
结构指针,就是
root,pwd
以及
altroot
。这些指针各自指向代表着一个“目录项”的
dentry
数据结构,里面记录着文件的各项属性,如文件名、访问权限等等。其中
pwd
则指向当前所在目录
;
而
root
所指向的
dentry
结构代表着本进程的“根目录”,那就是当用户登陆进入系统时所“看到”的根目录
;
至于
altroot
则为用户设置的“替换根目录”。后三个指针就各自指向代表着这些“安装”的
vfsmount
数据结构。注意,
fs_struct
结构中的信息都是与文件系统和进程有关的,带有全局性(对具体进程而言),而与具体的已打开文件没有什么关系。
与具体已打开文件有关的信息在
file
结构中,而
files_struct
结构的文体就是一个
file
结构数组。每打开一个文件后,进程就通过一个“打开文件号”
fid
来访问这个文件,而
fid
实际上就是相应
file
结构在数组中的下标。如前所述,每个
file
结构中有个
f_op
,指向该文件所属文件系统的
file_operations
数据结构。同时,
file
结构中还有个指针
f_dentry
,指向该文件的
dentry
数据结构。
每个文件除有一个“目录项”即
dentry
数据结构以外,还有一个“索引节点”即
inode
数据结构,里面记录着文件在存储介质上的位置与分布信息。同时,
dentry
结构中有个
inode
结构指针
d_inode
指向相应的
inode
结构。一个文件的
dentry
结构和
inode
结构都在从不同角度描述这个文件各方面的属性,且它们所描述的目标是不同的。
Dentry
结构所代表的是逻辑意义上的文件,记录的是其逻辑上的属性。而
inode
结构所代表的是物理意义上的文件,记录的是其物理上的属性
;
它们之间的关系是多对一的关系。
前面说过虚拟文件系统
VFS
与具体的文件系统之间的界面的“主体”是
file_operations
数据结构,是因为除此之外还有一些其它的数据结构。其中主要的还有与目录项相关联的
dentry_operations
数据结构和与索引节点相联系的
inode_operations
数据结构。这两个数据结构中的内容也都是一些函数指针,但是这些函数指针大多只是在打开文件的过程中使用,或者仅在文件操作的“底层”使用
(
如分配空间
)
,所以不像
file_operations
结构中那些函数那么常用。
总之,具体文件系统与虚拟文件系统
VFS
间的界面是一组数据结构,包括
file_iperations
、
dentry_operations
、
inode_operations
,还有其他。原则上每种文件系统都必须在内核中提供这些数据结构。
下图显示了文件系统内部结构的基本情况:
文件系统逻辑结构图:
dentry
|->|………|
| |d_inode|-->
进程(用户)根目录
inode
| |………|
fs_struct |
|-> |root |----| dentry inode
| |pwd|-----> |……….| dentry |-->|….|
| |…..| |d_inode|->
当前目录的
inode|--->|……….| | | U |
| |…..| |………| | |d_inode|--| | i_op |-|
task_struct | files | |……….| |….| |
|……….| | file_struct |--->|………| | | d_op |--| |
| fs |-- | |-->|……………| | |………| | |………| | |
|……….| | |……………| | |f_dentry|-----| | |
| file |------ | |……………| | |………| dentry_operations | inode_op |
|fd_array[fid]|---| | f_op |-------| |……| <-- | |…..| <--|
|……………| |………| | |……| |…..|
| |……| |……|
|
| files_operations
|---->|……….|
|read |
|write |
|………|
那么,
Linux
到底支持哪一些具体的文件系统呢?数据结构
inode
中有一个成分
u,
是一个
union
。根据具体文件系统的不同,可以将这个
union
解释成不同的数据结构。例如,当
inode
所代表的文件是个插口
(socket)
时,
u
就用作
socket
数据结构
;
当
inode
所代表的文件属于
Ext2
文件系统时,
u
就用作
Ext2
文件系统的详细描述结构
ext2_inode_info
。所以,看一下这个
union
的定义就可以看出
Linux
目前支持多少种文件系统,这个定义是
inode
数据结构定义的一部分,中文件
include/linux/fs.h
中:
433
union{
434
struct minix_inode_info minix_i;
435
struct ext2_inode_info ext2_i;
436
struct bpfs_inode_info bpfs _i;
437
struct ntfs_inode_info ntfs _i;
438
struct msdos_inode_info msdos_i;
439
struct umsdos_inode_info umsdos _i;
440
struct iso_inode_info iso _i;
441
struct nfs_inode_info nfs _i;
442
struct sysv_inode_info sysv _i;
443
struct affs_inode_info affs _i;
……………………
459 void *generic_ip;
460
}u;
这个
union
定义只是大致地反映了
Linux
目前所支持的各种文件系统,因为这是以
inode
结构中这一部分空间的不同用法和解释为基础的。如果两种文件系统对这个
union
的解释相同,那就不能从这个定义中反映出来了。另一方面,虽然原则上每个文件系统都有其自身的函数跳转表,即
file_operations
数据结构,但是反过来说,每个
file_operations
结构都代表着一个不同的文件系统就不确切了。如就在
Ext2
文件系统的框架中,光是作管道的文件就根据读、写权限的不同而有三个不同的
file_operations
结构。
还有,在
Linux
系统中外部设备是视同文件的,所以从概念上讲每种不同的外部设备就相关于一种不同的文件系统。可是,在这个
union
的定义中却只列出了
usbdev
作为一种独立的文件系统,那么“块设备”又怎样?“字符设备”又怎样?“网络设备”呢?为什么这里都没有。原因就在于这些设备都不要求将
inode
结构中的这个
union
作不同的解释。同样道理,对“特殊文件”,这里列出了
proc
与
socket
,但是用来实现“命名管道”的另一种特殊文件
FIFO
就没有在这里单独列出。
所以,
inode
结构中的这个
union
反映了各种文件系统在部分数据结构上的不同,而
file_operations
结构反映了它们在算法(操作)上的不同。
下面再论述文件系统中的几个重要数据结构:
dentry
、
inode
、
super_block(
超级块
)
和一些重要概念根目录,根设备以及它们之间的关系。
每个文件都有个
inode
。所谓
inode
,也就是“索引节点”
(
或称“
I
节点”
)
的意思。要“访问”一个文件时,一定要通过它的索引才能知道这个文件是什么类型的文件
(
例如,是否设备文件
)
、怎样组织的、文件中存储着多少数据、这些数据在什么地方以及其下层的驱动程序在哪儿等必要的信息。数据结构
inode
的定义在文件
include/linux/fs.h
中给出:
387struct inode {
388 struct list_head i_hash;
389 struct list_head i_list;
390 struct list_head i_dentry;
391
392 struct list_head i_dirty_buffers;
393
394 unsigned long i_ino;
395 atomic_t i_count;
396 kdev_t i_dev;
397 umode_t i_mode;
398 nlink_t i_nlink;
399 uid_t i_uid;
400 gid_t i_gid;
401 kdev_t i_rdev;
402 loff_t i_size;
……
}
每个
inode
都有一个“
i
节点号”
i_ino,
在同一文件系统中每个
i
节点号都是唯一的,内核中有时候会根据
i
节点号的杂凑值寻找其
inode
结构。同时,每个文件都有个“文件主”,最初是创建了这个文件的用户,但是可以改变。系统的每个用户都有一个用户号,即
uid
,并且都属于某一个用户“组”,所以又有个组号
gid
。因此,在
inode
结构中就相应地有
i_uid
和
i_gid
两个成分,以指明文件主的身份。值得注意的是,
inode
结构中有两个设备号,即
i_dev
和
i_rdev
。首先,除特殊文件外,一个索引节点总得存储在某个设备上,这就是
i_dev
。其次,如果索引节点所代表的并不是常规文件,而是某个设备,那就还要有个设备号,那就是
i_rdev
。设备号实际上由两部分组成,即“主设备号”与“次设备号”。主设备号表示设备的种类,例如磁盘就分成软盘、
IDE
硬盘,
SCSI
硬盘等等。次设备号则表示系统内配备的同一种设备中的某个具体设备。每当一个文件受到访问时,系统都要在这个文件的
inode
中留下时间印记,
inode
结构中的
i_atime
、
i_mtime
、
i_ctime
分别为最后一次访问该文件的时间、修改该文件的时间以及最初创建该文件的时间。对于具有数据部分的文件
(
磁盘文件或“普通文件”
)
来说,
I_size
就是其数据部分当前的大小。至于数据所在的位置,则根据文件系统的不同而记录在
inode
中的
union
里面。就像人可以有别名一样,文件也可以有多个文件名,也就是说可以将一个已经创建的文件“连接”
(link)
到另一个文件名。这个“别名”与原来的文件名可以在同一个目录中,也可以在不同的目录中,但是这些不同的目录都指向同一个
inode
。与此相应,在
inode
结构中有个计数器
i_link
,用来记住这个文件有多少个这样的连接。同时,还有个队列头
i_dentry
,用来构成一个
dentry
结构队列,沿着这个队列就可以找到与这个文件相联系的所有
dentry
结构。除了相对静态的信息以外,
inode
结构中还有些成分用于表示一些动态的信息。例如,
i_count
就是
inode
结构的共享计数器,这个数值在系统运行的过程中是常常变化的。又如,
inode
结构可以通过它的几个
list_head
结构动态地链入到内存中的若干队列中,这种关系显然也是动态地变化的。另外,
inode
结构中
union
里面的信息也有很多是动态的。显然,
inode
结构中相对静态的一些信息是需要保存在“不挥发性”介质如磁盘上的。这一点对数据部分的磁盘固不待言,就是对于不具有数据部分的设备文件和特殊文件也是必需的
(
只有少数特殊文件例外,如无名管道文件
)
。所以,磁盘的格式化也考虑到了这个问题。以
Ext2
格式为例,磁盘上的记录块
(
扇区
)
主要分成两部分,一部分用于索引节点,一部分用于文件的数据。给定一个索引节点号,就可以通过磁盘的设备驱动程序将其所在的记录块读入内存中。
虽然在
inode
结构中包含了关于文件的组织和管理的信息,但是还有一项关键性的信息,即文件名,却并不在其内。显然,我们需要一种机制,使得根据一个文件的文件名就可以在磁盘上找到该文件的索引节点,从而在内存中建立起代表该文件的
inode
结构。这种机制就是文件系统的目录树。这棵“树”从系统的“根节点”,即“
/
”开始向下伸展,除最底层的“叶”节点为“文件”以外,其他的中间节点都是“目录”。其实,目录也是一种文件,是一种特殊的磁盘文件。这种文件的“文件名”就是目录名,也有索引节点,并且有数据部分。所不同的是,其数据部分的内容只包括“目录项”。
说到这里,可能要产和一个疑问:要访问一个文件就得先访问一个目录,才能根据文件名从目录中找到该文件的目录项,进而找到其
i
节点
;
可是目录本身也是文件,它本身的目录项又在另一个目录项中,这一来不是成了“先有鸡还是先有蛋”的问题,或者说递归了吗?这个圈子的出口在哪儿呢?我们不妨换一个方式来思考:是否有这样一个目录,它本身的“目录项”不在其他目录中,而可以在一个固定的位置上或者通过一个固定的算法找到,并且从这个目录出发可以找到系统中的任何一个文件?答案是肯定的,这个目录就是系统的根目录“
/
”,或者说“根设备”上的根目录。每一个“文件系统”,即每个个格式化成某种文件系统的存储设备上都有一个根目录,同时又都有一个“超级块”
(super block)
,根目录的位置以及文件系统的其他一些参数都记录在超级块中。超级块在设备上的逻辑位置是固定的,例如,在磁盘上总是在第二个逻辑块
(
第一个逻辑块为引导块
)
,所以在需要再从其他什么地方去“查找”。同时,对于一个特定的文件系统,超级块的格式也是固定的,系统在初始化时要将一个存储设备
(
通常就是从中引导出操作系统的那个设备
)
作为整个系统的“根设备”,它的根目录就成为整个文件系统的“总根”,就是“
/
”。更确切地说,就是把根设备的根目录“安装”在文件系统的总根“
/
”节点上。有了根设备以后,还可以进而把其他存储设备也安装到文件系统中空闲的目录节点上。所谓“安装”,就是从一个设备上读入超级块,在内存中建立起一个
super_block
结构。再进而将此设备上的根目录与文件系统中已经存在的一个空白目录挂上钩。系统初始化时整个文件系统只有一个空目录“
/
”,所以根设备的根目录就安装在这个节点上。这样,从根目录“
/
”开始,根据给定的“全名路径”就可以找到文件系统中的任何一个文件,而不论这个文件是在哪一个存储设备上,只人文件所在的存储设备已经安装就行了。
但是,每次都要提供一个全路径名,并且每次都要从根目录“
/
”开始查找,既不方便也是一种浪费。所以文件系统也提供了从“当前目录”开始查找的手段。第一个进程在每个时刻都有一个“当前工作目录
pwd
”,用户可以改变这个目录,但是永远都有这么个目录存在。这样,就可以只提供一个从
pwd
开始的“相对路径名”来查找一个文件。这就是前面看到过的
fs_struct
数据结构中为什么要有个指针
pwd
原因。这个指针总是指向本进程的“当前工作目录”的
dentry
结构,而进程的
task_struct
结构中的指针
fs
则总是指向一个有效的
fs_struct
结构。每当一个进程通过
chdir()
系统调用进入一个目录,或者在
login
进入用户的原始目录
(
“
Home Directory
”
)
时,内核就使该进程的
pwd
指针指向这个目录在内存中的
dentry
结构。相对路径名还可以用“
../
”开头,表示先向上找到当前目录的父目录,再从那开始查找。相应地,在
dentry
结构中也有个指针
d_parent
,指向其父目录的
dentry
结构。
如前所述,
fs_struct
结构中还有一个指针
root
,指向本进程的根目录“
/
”的
dentry
结构。前面讲到,“
/
”表示整个文件系统的总根,可这只是就一般而言,或者是对早期的
Unix
系统而言。事实上,特权用户可以通过一个系统调用
chroot()
将一个目录设置进程的根目录。从此以后,这个进程以及这个进程所
fook()
的子进程就把这个目录当成了文件系统的根,遇到文件的全名路径名时就从这个目录而不是从真正的文件系统的总根开始查找。例如,要是这个进程执行一个系统调用
chdir(“/”)
,就会转到这个“现在”的根目录而不是真正的根目录。这种特殊的设计也是从实践需求引起的,最初是为了克服
FTP
,特别是匿名
FTP
的一个安全性问题。
FTP
的服务进程
(
所谓“守护神”
daemon)
是特权用户进程。当一个远程的用户与
FTP
服务进程建立起连接以后,就可以在远地发出诸如“
cd /
”、“
get /etc/passwd
”之类的命令。显然,这给系统的安全性造成了一个潜在的缺口,现在有了进程自己的“根目录”以及系统调用
chroot()
,就可以让
FTP
服务进程把另一个目录当成它的根目录,从而当远程用户要求“
get /etc/passwd
”时就会得到“文件找不到”这之类的出错信息,从而保证了
passwd
口令的安全性。而且,
fs_struct
结构中还有一个指针
alroot
,指向本进程的“替换根目录”。
对于普通文件,文件系统层最终要通过磁盘或其他存储设备的驱动程序从存储介质上读或写。主
Ext2
文件系统而言,从磁盘文件的角度来看,对存储介质的访问可以涉及到四种不同的目标,那就是:
(1)
文件中的数据,包括目录的内容,即目录项
ext2_dir_entry-2
数据结构。
(2)
文件的组织与管理信息,即索引节点
ext2_inode
数据结构。
(3)
磁盘的超级块。如果物理的磁盘被划分成若干分区,那就包括每个“逻辑磁盘“的超级块。
(4)
引导块。
每个按
Ext2
格式经过格式化的磁盘
(
或逻辑盘
)
存储介质都相应地被划分成至少
4
个部分。其中引导块永远是介质上的第一个记录块,超级块永远是介质上的第二个记录块,其他两部分的大小则取决于磁盘大小等参数,这些参数都存储在超级块中。
有的文件系统并没有索引节点这么一种数据结构,甚至没有这么一种概念。但是既然构成一个文件系统就必然存在着某种索引机制,从这种机制中就可以抽象出
(
或变换成
)super_block
结构和
inode
结构中的公共信息。同时,
super_block
结构也和
inode
结构一样包含着一个
union
,对这一部分信息要根据具体的文件系统而加以不同的解释和使用。
从磁盘驱动程序的角度来看,则整个介质只是一个由若干记录块组成的一维阵列
(
记录块数组
)
而已,所以这种设备称为“块设备”。当文件系统层要从磁盘上读出一个索引节点时,要根据索引节点号和超级块中提供的信息,计算出这个索引节点在磁盘上的哪一个记录块以及在此记录块中的相对位移。然后,通过磁盘驱动程序读入这个记录块后再根据索引节点在记录块中的相对位移找到这个节点。如前所述,磁盘上的“根目录”是特殊的,其索引节点号保存在该磁盘的超级块中。从磁盘读一个特定文件的内容
(
数据
)
则要稍为麻烦一点。先要读入该文件的索引节点,然后根据索引节点中提供的信息将数据在文件中的位移换算成磁盘上的记录号,再通过磁盘驱动程序从磁盘上读入。
相比之下,作为“设备文件”的磁盘则不存在
(
或看不见
)
这样的逻辑划分,而只是将磁盘看成一个巨大的线性存储空间
(
字节数组
)
。当从作为设备文件的磁盘读出时,只要将数据在此文件中的个位移换算成磁盘上的记录块号,就可以通过磁盘驱动程序读入了。不过,在此之前也要先找到代表着这个设备文件的目录项和索引节点,才能把字符串形式的设备文件名转换成驱动程序所需要的设备号。
在前面我们曾把具体的文件系统比喻作“接口卡”,而把虚拟文件系统
VFS
比喻成一条插槽。因此,
file
结构中的指针
f_op
就可以看作插槽中的一个触点,并且在
dentry
、
inode
等结构中都有类似的触点。所以,如果把整个具体文件系统比喻成“接口卡”的话,那么这种接口卡的“插槽”分成好几段,而
file
结构只是其中最主要的一段。有关的数据结构有:
(1)
文件操作跳转表,即
file_operations
数据结构:
file
结构中的指针
f_op
指向具体的
file_operations
结构,这是
read()
、
write()
等文件操作的跳转表。一种文件系统并不只限于一个
file_operations
结构,如
Ext2
就有两个这样的数据结构,分别用于普通文件和目录文件。
(2)
目录项操作跳转表,即
dentry_operations
数据结构:
dentry
结构中的指针
d_op
指向具体的
dentry_operations
数据结构,这是内核中
hash()
、
compare()
等内部操作的跳转表。如果
d_op
为
0
则表示按
Linux
默认的
(
即
Ext2)
方式办。注意,这里所说的目录项,而不是目录,目录本身是一种特殊用途和具有特殊结构的文件。
(3)
索引节点操作跳转表,即
inode_operations
数据结构:
inode
结构中的指针
i_op
指向具体的
inode_operations
数据结构,这是
mkdir()
、
mknod()
等文件操作以及
lockup()
、
permission()
等内部函数的跳转表。同样,一种文件系统也并不只限于一个
inode_operations
结构。
(4)
超级块操作跳转表,即
super_operations
数据结构:
super_block
结构中的指针
s_op
指向具体的
super_operations
数据结构,这是
read_inode()
、
write_inode()
、
delete_inode()
等内部操作的跳转表。
(5)
超级块本身也因文件系统而异。
由此可见,
file
结构、
dentry
结构、
inode
结构、
super_block
结构以及关于超级块位置的约定都属于
VFS
层。
此外,
inode
结构中还有一个指针
i_fop
,也指向具体的
file_operations
数据结构,实际上
file
结构中的指针
f_op
只是
inode
结构中这个指针的一个副本,在打开文件的时候从目标文件的
inode
结构中复制到
file
结构中。
最后还要指出,虽然每个文件都有目录项和索引节点在磁盘上,但是只有在需要时才在内存中为之建立起相应的
dentry
和
inode
数据结构。