Linux ext2文件系统小结
本文来源:http://learn.akae.cn/media/ch29s02.html 有修改
1.ext2文件系统整体布局
一个磁盘可以划分成多个分区,每个分区必须先用格式化工具(例如某种mkfs
命令)格式化成某种格式的文件系统,然后才能存储文件,格式化的过程会在磁盘上写一些管理存储布局的信息。下图是一个磁盘分区格式化成ext2文件系统后的存储布局。
文件系统中存储的最小单位是块(Block),一个块究竟多大是在格式化时确定的,例如mke2fs
的-b
选项可以设定块大小为1024、2048或4096字节,这些 blocks 被聚在一起分成几个大的 block group。每个 block group 中有多少个 block 是固定的。而上图中启动块(Boot Block)的大小是确定的,就是1KB,启动块是由PC标准规定的,用来存储磁盘分区信息和启动信息,任何文件系统都不能使用启动块。启动块之后才是ext2文件系统的开始,ext2文件系统将整个分区划成若干个同样大小的块组(Block Group),每个块组都由以下部分组成
1).超级块(Super Block)
描述整个分区的文件系统信息,例如块大小、文件系统版本号、上次mount
的时间等等。超级块在每个块组的开头都有一份拷贝。
2).块组描述符表(GDT,Group Descriptor Table)
由很多块组描述符组成,整个分区分成多少个块组就对应有多少个块组描述符。每个块组描述符(Group Descriptor)存储一个块组的描述信息,例如在这个块组中从哪里开始是inode表,从哪里开始是数据块,空闲的inode和数据块还有多少个等等。和超级块类似,块组描述符表在每个块组的开头也都有一份拷贝,这些信息是非常重要的,一旦超级块意外损坏就会丢失整个分区的数据,一旦块组描述符意外损坏就会丢失整个块组的数据,因此它们都有多份拷贝。通常内核只用到第0个块组中的拷贝,当执行e2fsck
检查文件系统一致性时,第0个块组中的超级块和块组描述符表就会拷贝到其它块组,这样当第0个块组的开头意外损坏时就可以用其它拷贝来恢复,从而减少损失。
3).块位图(Block Bitmap)
一个块组中的块是这样利用的:数据块存储所有文件的数据,比如某个分区的块大小是1024字节,某个文件是2049字节,那么就需要三个数据块来存,即使第三个块只存了一个字节也需要占用一个整块;超级块、块组描述符表、块位图、inode位图、inode表这几部分存储该块组的描述信息。那么如何知道哪些块已经用来存储文件数据或其它描述信息,哪些块仍然空闲可用呢?块位图就是用来描述整个块组中哪些块已用哪些块空闲的,它本身占一个块,其中的每个bit代表本块组中的一个块,这个bit为1表示该块已用,这个bit为0表示该块空闲可用。
为什么用df
命令统计整个磁盘的已用空间非常快呢?因为只需要查看每个块组的块位图即可,而不需要搜遍整个分区。相反,用du
命令查看一个较大目录的已用空间就非常慢,因为不可避免地要搜遍整个目录的所有文件。
与此相联系的另一个问题是:在格式化一个分区时究竟会划出多少个块组呢?主要的限制在于块位图本身必须只占一个块。用mke2fs
格式化时默认块大小是1024字节,可以用-b
参数指定块大小,现在设块大小指定为b字节,那么一个块可以有8b个bit,这样大小的一个块位图就可以表示8b个块的占用情况,因此一个块组最多可以有8b个块,如果整个分区有s个块,那么就可以有s/(8b)个块组。格式化时可以用-g
参数指定一个块组有多少个块,但是通常不需要手动指定,mke2fs
工具会计算出最优的数值。
4).inode位图(inode Bitmap)
和块位图类似,本身占一个块,其中每个bit表示一个inode是否空闲可用
5).inode表(inode Table)
我们知道,一个文件除了数据需要存储之外,一些描述信息也需要存储,例如文件类型(常规、目录、符号链接等),权限,文件大小,创建/修改/访问时间等,也就是ls -l
命令看到的那些信息,这些信息存在inode中而不是数据块中。每个文件都有一个inode,一个块组中的所有inode组成了inode表。
inode表占多少个块在格式化时就要决定并写入块组描述符中,mke2fs
格式化工具的默认策略是一个块组有多少个8KB就分配多少个inode。由于数据块占了整个块组的绝大部分,也可以近似认为数据块有多少个8KB就分配多少个inode,换句话说,如果平均每个文件的大小是8KB,当分区存满的时候inode表会得到比较充分的利用,数据块也不浪费。如果这个分区存的都是很大的文件(比如电影),则数据块用完的时候inode会有一些浪费,如果这个分区存的都是很小的文件(比如源代码),则有可能数据块还没用完inode就已经用完了,数据块可能有很大的浪费。如果用户在格式化时能够对这个分区以后要存储的文件大小做一个预测,也可以用mke2fs
的-i
参数手动指定每多少个字节分配一个inode。
6).数据块(Data Block)
根据不同的文件类型有以下几种情况
-
对于常规文件,文件的数据存储在数据块中。
-
对于目录,该目录下的所有文件名和目录名存储在数据块中,注意文件名保存在它所在目录的数据块中,除文件名之外,
ls -l
命令看到的其它信息都保存在该文件的inode中。注意这个概念:目录也是一种文件,是一种特殊类型的文件。 -
对于符号链接,如果目标路径名较短则直接保存在inode中以便更快地查找,如果目标路径名较长则分配一个数据块来保存。
-
设备文件、FIFO和socket等特殊文件没有数据块,设备文件的主设备号和次设备号保存在inode中。
2.数据块寻址
如果一个文件有多个数据块,这些数据块很可能不是连续存放的,应该如何寻址到每个块呢?实际上,根目录的数据块是通过其inode中的索引项Blocks[0]
找到的,事实上,这样的索引项一共有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个数据块,依此类推。
从上图可以看出,索引项Blocks[13]
指向两级的间接寻址块,最多可表示(b/4)2+b/4+12个数据块,对于1K的块大小最大可表示64.26MB的文件。索引项Blocks[14]
指向三级的间接寻址块,最多可表示(b/4)3+(b/4)2+b/4+12个数据块,对于1K的块大小最大可表示16.06GB的文件。
可见,这种寻址方式对于访问不超过12个数据块的小文件是非常快的,访问文件中的任意数据只需要两次读盘操作,一次读inode(也就是读索引项)一次读数据块。而访问大文件中的数据则需要最多五次读盘操作:inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。实际上,磁盘中的inode和数据块往往已经被内核缓存了,读大文件的效率也不会太低。
3.文件和目录操作的系统函数(这里面的"(n)"应该表示参数的个数)
1).stat(2)
函数读取文件的inode,然后把inode中的各种文件属性填入一个struct stat
结构体传出给调用者。stat(1)
命令是基于stat
函数实现的。stat
需要根据传入的文件路径找到inode,假设一个路径是/opt/file
,则查找的顺序是:
-
读出inode表中第2项,也就是根目录的inode,从中找出根目录数据块的位置
-
从根目录的数据块中找出文件名为
opt
的记录,从记录中读出它的inode号 -
读出
opt
目录的inode,从中找出它的数据块的位置 -
从
opt
目录的数据块中找出文件名为file
的记录,从记录中读出它的inode号 -
读出
file
文件的inode
还有另外两个类似stat
的函数:fstat(2)
函数传入一个已打开的文件描述符,传出inode信息,lstat(2)
函数也是传入路径传出inode信息,但是和stat
函数有一点不同,当文件是一个符号链接时,stat(2)
函数传出的是它所指向的目标文件的inode,而lstat
函数传出的就是符号链接文件本身的inode。
2).access(2)
函数检查执行当前进程的用户是否有权限访问某个文件,传入文件路径和要执行的访问操作(读/写/执行),access
函数取出文件inode中的st_mode
字段,比较一下访问权限,然后返回0表示允许访问,返回-1表示错误或不允许访问。
3).chmod(2)
和fchmod(2)
函数改变文件的访问权限,也就是修改inode中的st_mode
字段。这两个函数的区别类似于stat
/fstat
。chmod(1)
命令是基于chmod
函数实现的。
4).chown
(
2
)
/fchown(2)
/lchown(2)
改变文件的所有者和组,也就是修改inode中的User
和Group
字段,只有超级用户才能正确调用这几个函数,这几个函数之间的区别类似于stat
/fstat
/lstat
。chown(1)
命令是基于chown
函数实现的。
5).utime(2)
函数改变文件的访问时间和修改时间,也就是修改inode中的atime
和mtime
字段。touch(1)
命令是基于utime
函数实现的。
6).truncate(2)
和ftruncate(2)
函数把文件截断到某个长度,如果新的长度比原来的长度短,则后面的数据被截掉了,如果新的长度比原来的长度长,则后面多出来的部分用0填充,这需要修改inode中的Blocks
索引项以及块位图中相应的bit。这两个函数的区别类似于stat
/fstat
。
7).link(2)
函数创建硬链接,其原理是在目录的数据块中添加一条新记录,其中的inode号字段和原文件相同。symlink(2)
函数创建一个符号链接,这需要创建一个新的inode,其中st_mode
字段的文件类型是符号链接,原文件的路径保存在inode中或者分配一个数据块来保存。ln(1)
命令是基于link
和symlink
函数实现的。
8).unlink(2)
函数删除一个链接。如果是符号链接则释放这个符号链接的inode和数据块,清除inode位图和块位图中相应的位。如果是硬链接则从目录的数据块中清除一条文件名记录,如果当前文件的硬链接数已经是1了还要删除它,就同时释放它的inode和数据块,清除inode位图和块位图中相应的位,这样就真的删除文件了。unlink(1)
命令和rm(1)
命令是基于unlink
函数实现的。
9).rename(2)
函数改变文件名,需要修改目录数据块中的文件名记录,如果原文件名和新文件名不在一个目录下则需要从原目录数据块中清除一条记录然后添加到新目录的数据块中。mv(1)
命令是基于rename
函数实现的,因此在同一分区的不同目录中移动文件并不需要复制和删除文件的inode和数据块,只需要一个改名操作,即使要移动整个目录,这个目录下有很多子目录和文件也要随着一起移动,移动操作也只是对顶级目录的改名操作,很快就能完成。但是,如果在不同的分区之间移动文件就必须复制和删除inode和数据块,如果要移动整个目录,所有子目录和文件都要复制删除,这就很慢了。
10)readlink(2)
函数读取一个符号链接所指向的目标路径,其原理是从符号链接的inode或数据块中读出保存的数据,这就是目标路径。
11).mkdir(2)
函数创建新的目录,要做的操作是在它的父目录数据块中添加一条记录,然后分配新的inode和数据块,inode的st_mode
字段的文件类型是目录,在数据块中填两个记录,分别是.
和..
,由于..
表示父目录,因此父目录的硬链接数要加1。mkdir(1)
命令是基于mkdir
函数实现的。
12).rmdir(2)
函数删除一个目录,这个目录必须是空的(只包含.
和..
)才能删除,要做的操作是释放它的inode和数据块,清除inode位图和块位图中相应的位,清除父目录数据块中的记录,父目录的硬链接数要减1。rmdir(1)
命令是基于rmdir
函数实现的。
13).opendir(3)
/readdir(3)
/closedir(3)
用于遍历目录数据块中的记录。opendir
打开一个目录,返回一个DIR *
指针代表这个目录,它是一个类似FILE *
指针的句柄,closedir
用于关闭这个句柄,把DIR *
指针传给readdir
读取目录数据块中的记录,每次返回一个指向struct dirent
的指针,反复读就可以遍历所有记录,所有记录遍历完之后readdir
返回NULL
。结构体struct dirent
的定义如下: