注:本分类下文章大多整理自《深入分析linux内核源代码》一书,另有参考其他一些资料如《linux内核完全剖析》、《linux c 编程一站式学习》等,只是为了更好地理清系统编程和网络编程中的一些概念性问题,并没有深入地阅读分析源码,我也是草草翻过这本书,请有兴趣的朋友自己参考相关资料。此书出版较早,分析的版本为2.4.16,故出现的一些概念可能跟最新版本内核不同。

此书已经开源,阅读地址 http://www.kerneltravel.net

一、Ext2 文件系统


(一)、文件系统布局

Ext2文件系统布局,文件数据块寻址,VFS虚拟文件系统-LMLPHP
Ext2文件系统布局,文件数据块寻址,VFS虚拟文件系统-LMLPHP


文件系统中存储的最小单位是块( 
Block
),一个块究竟多大是在格式化时确定的,例如 
mke2fs
的 
-b
选项可以设定块大小为 
1024
、 
2048
或 
4096
字节。而上图中启动块( 
Boot Block
)的大小是确定的,就是 
1KB
,启动块是由 
PC
标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是 
ext2
文件系统的开始, 
ext2
文件系统将整个分区划成若干个同样大小的块组( 
Block Group
),每个块组都由以下部分组成。

超级块(
Super Block


块组描述符表(
GDT
, 
Group Descriptor Table


块位图(
Block Bitmap


inode 
位图(
inode Bitmap


inode 
表(
inode Table

数据块(
Data Block

根据不同的文件类型有以下几种情况:

(二)、数据块寻址



如果一个文件有多个数据块,这些数据块很可能不是连续存放的,应该如何寻址到每个块呢?
事实上,每个文件的inode的索引项一共有 
15
个,从 
Blocks[0]
到 
Blocks[14]
,每个索引项占 
4
字节。前 
12
个索引项都表示块编号,例如若
Blocks[0]
字段保存着 
24
,就表示第 
24
个块是该文件的数据块,如果块大小是 
1KB
,这样可以表示从 
0
字节到 
12KB
的文件。如果剩下的三个索引项 
Blocks[12]
到 
Blocks[14]
也是这么用的,就只能表示最大 
15KB
的文件了,这是远远不够的,事实上,剩下的三个索引项都是间接索引。

索引项
Blocks[12]
所指向的块并非数据块,而是称为间接寻址块( 
Indirect Block
),其中存放的都是类似 
Blocks[0]
这种索引项,再由索引项指向数据块。设块大小是 
b
,那么一个间接寻址块中可以存放 
b/4
个索引项,指向 
b/4
个数据块。所以如果把 
Blocks[0]
到 
Blocks[12]
都用上,最多可以表示 
b/4+12
个数据块,对于块大小是 
1K
的情况,最大可表示 
268K
的文件。如下图所示,注意文件的数据块编号是从 
0
开始的, 
Blocks[0]
指向第 
0
个数据块, 
Blocks[11]
指向第 
11
个数据块, 
Blocks[12]
所指向的间接寻址块的第一个索引项指向第 
12
个数据块,依此类推。
Ext2文件系统布局,文件数据块寻址,VFS虚拟文件系统-LMLPHP
Ext2文件系统布局,文件数据块寻址,VFS虚拟文件系统-LMLPHP

从上图可以看出,索引项 
Blocks[13]
指向两级的间接寻址块,总共最多可表示 
(b/4)^2 
+b/4+12
个数据块,对于 
1K
的块大小最大可表示 
64.26MB
的文件。索引项 
Blocks[14]
指向三级的间接寻址块,总共最多可表示 
(b/4)^3 
+(b/4)^

+b/4+12
个数据块,对于 
1K
的块大小最大可表示 
16.06GB
的文件。

可见,这种寻址方式对于访问不超过 
12
个数据块的小文件是非常快的,访问文件中的任意数据只需要两次读盘操作,一次读 
inode
(也就是读索引项)一次读数据块。而访问大文件中的数据则需要最多五次读盘操作: 
inode
、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。实际上,磁盘中的 
inode(索引节点高速缓存)
和数据块(块高速缓存)往往已经被内核缓存了,读大文件的效率也不会太低。 

 二、VFS 虚拟文件系统

Linux
支持各种各样的文件系统格式,如 
ext2
、 
ext3
、 
reiserfs
、 
FAT
、 
NTFS
、 
iso9660
等等,不同的磁盘分区、光盘或其它存储设备都有不同的文件系统格式,然而这些文件系统都可以 
mount
到某个目录下,使我们看到一个统一的目录树,各种文件系统上的目录和文件我们用 
ls
命令看起来是一样的,读写操作用起来也都是一样的,这是怎么做到的呢? 
Linux
内核在各种不同的文件系统格式之上做了一个抽象层,使得文件、目录、读写访问等概念成为抽象层的概念,因此各种文件系统看起来用起来都一样,这个抽象层称为虚拟文件系统(
VFS

Virtual Filesystem
)。

VFS 是应用程序和具体的文件系统之间的一个层
。不过,在某些情
况下,一个文件操作可能由VFS 本身去执行,无需调用下一层程序。例如,当某个进程关闭
一个打开的文件时,并不需要涉及磁盘上的相应文件,因此,VFS 只需释放对应的文件对象。
类似地,如果系统调用lseek()修改一个文件指针,而这个文件指针指向有关打开的文件与
进程交互的一个属性,那么VFS 只需修改对应的文件对象,而不必访问磁盘上的文件,因此,
无需调用具体的文件系统子程序。从某种意义上说,可以把VFS 看成“通用”文件系统,它
在必要时依赖某种具体的文件系统。

Ext2文件系统布局,文件数据块寻址,VFS虚拟文件系统-LMLPHP
Ext2文件系统布局,文件数据块寻址,VFS虚拟文件系统-LMLPHP

每个进程在
PCB

Process Control Block
)中都保存着一个指向文件描述符表的指针(struct files_struct* files),文件描述符就是这个表的索引,每个表项都有一个指向已打开文件的指针,现在我们明确一下:已打开的文件在内核中用 
file
结构体表示,文件描述符表中的指针指向 
file
结构体。


file
结构体中维护 
File Status Flag
( 
file
结构体的成员 
f_flags
)和当前读写位置( 
file
结构体的成员 
f_pos
)。在上图中,进程 
1
和进程 
2
都打开同一文件,但是对应不同的 
file
结构体,因此可以有不同的 
File Status Flag
和读写位置。 
file
结构体中比较重要的成员还有 
f_count
,表示引用计数( 
Reference Count
),
dup
、 
fork
等系统调用会导致多个文件描述符指向同一个 
file
结构体,例如有 
fd1
和 
fd2
都引用同一个 
file
结构体,那么它的引用计数就是 
2
,当 
close(fd1)
时并不会释放 
file
结构体,而只是把引用计数减到 
1
,如果再 
close(fd2)
,引用计数就会减到 
0
同时释放 
file
结构体,这才真的关闭了文件。

每个
file
结构体都指向一个 
file_operations
结构体,这个结构体的成员都是函数指针,指向实现各种文件操作的内核函数。比如在用户程序中 
read
一个文件描述符, 
read
通过系统调用进入内核,然后找到这个文件描述符所指向的 
file
结构体,找到 
file
结构体所指向的 
file_operations
结构体,调用它的 
read
成员所指向的内核函数以完成用户请求。在用户程序中调
lseek

read

write

ioctl

open
等函数,最终都由内核调用 
file_operations
的各成员所指向的内核函数完成用户请求。 
file_operations
结构体中的 
release
成员用于完成用户程序的 
close
请求,之所以叫 
release
而不叫 
close
是因为它不一定真的关闭文件,而是减少引用计数,只有引用计数减到 
0
才关闭文件。对于同一个文件系统上打开的常规文件来说, 
read
、 
write
等文件操作的步骤和方法应该是一样的,调用的函数应该是相同的,所以图中的三个打开文件的 
file
结构体指向同一个 
file_operations
结构体。如果打开一个字符设备文件,那么它的 
read
、 
write
操作肯定和常规文件不一样,不是读写磁盘的数据块而是读写硬件设备,所以 
file
结构体应该指向不同的 
file_operations
结构体,其中的各种文件操作函数由该设备的驱动程序实现。

每个
file
结构体都有一个指向 
dentry
结构体的指针, 
“dentry”
是 
directory entry
(目录项)的缩写。我们传给 
open
、 
stat
等函数的参数的是一个路径,例如 
/home/akaedu/a
,需要根据路径找到文件的 
inode
。为了减少读盘次数,内核缓存了目录的树状结构,称为 
dentry cache(目录高速缓存)
,其中每个节点是一个 
dentry
结构体,只要沿着路径各部分的 
dentry
搜索即可,从根目录 
/
找到 
home
目录,然后找到 
akaedu
目录,然后找到文件 
a
。 
dentry cache
只保存最近访问过的目录项,如果要找的目录项在 
cache
中没有,就要从磁盘读到内存中。

每个
dentry
结构体都有一个指针指向 
inode
结构体。 
inode
结构体保存着从磁盘 
inode
读上来的信息。在上图的例子中,有两个 
dentry
,分别表示 
/home/akaedu/a
和 
/home/akaedu/b
,它们都指向同一个 
inode
,说明这两个文件互为硬链接。 
inode
结构体中保存着从磁盘分区的 
inode
读上来信息,例如所有者、文件大小、文件类型和权限位等。每个 
inode
结构体都有一个指向 
inode_operations
结构体的指针,后者也是一组函数指针指向一些完成文件目录操作的内核函数。和 
file_operations
不同, 
inode_operations
所指向的不是针对某一个文件进行操作的函数,而是影响文件和目录布局的函数,例如添加删除文件和目录、跟踪符号链接等等,属于同一文件系统的各 
inode
结构体可以指向同一个 
inode_operations
结构体。

inode
结构体有一个指向 
super_block
结构体的指针。 
super_block
结构体保存着从磁盘分区的超级块读上来的信息,例如文件系统类型、块大小等。 
super_block
结构体的 
s_root
成员是一个指向 
dentry
的指针,表示这个文件系统的根目录被 
mount
到哪里,在上图的例子中这个分区被 
mount
到 
/home
目录下。

file
、 
dentry
、 
inode
、 
super_block
这几个结构体组成了 
VFS
的核心概念。对于 
ext2
文件系统来说,在磁盘存储布局上也有 
inode
和超级块的概念,所以很容易和 
VFS
中的概念建立对应关系。而另外一些文件系统格式来自非 
UNIX
系统(例如 
Windows
的 
FAT32
、 
NTFS
),可能没有 
inode
或超级块这样的概念,但为了能 
mount
到 
Linux
系统,也只好在驱动程序中硬凑一下,在 
Linux
下看 
FAT32
和 
NTFS
分区会发现权限位是错的,所有文件都是 
rwxrwxrwx
,因为它们本来就没有 
inode
和权限位的概念,这是硬凑出来的。


04-25 23:20
查看更多