1 引言
很详细地了解某个操作系统的实际工作方式是非常困难的,因为大多数操作系统的源代码都是严格保密的。在以实际使用为目标的操作系统中,让任何人都可以自由获取系统源代码,无论目的是要了解、学习还是修改,这样的系统并不多。本论文的主题就是这些少数操作系统中的一个:Linux。
Linux是一个性能稳定、功能强大、效率高的操作系统。它在功能特性方面与Unix系统相似,同时又具有多任务、多用户、多平台等若干特性。Linux的源代码是开放的,阅读Linux源代码,无疑是深入学习Linux的最好方法。
文件系统是Linux操作系统的重要组成部分,Linux文件具有强大的功功能。文件系统中的文件是数据的集合,文件系统不仅包含着文件中的数据而且还有文件系统的结构,所有Linux 用户和程序看到的文件、目录、软连接及文件保护信息等都存储在其中。
Linux 最早的文件系统是Minix,但是专门为Linux 设计的文件系统——扩展文件系统第二版或EXT2被设计出来并添加到Linux中,这对Linux产生了重大影响。EXT2文件系统功能强大、易扩充、性能上进行了全面优化优化,也是现在所以Linux发布和安装的标准文件系统类型。
每个实际文件系统从操作系统和系统服务中分离出来,它们之间通过一个接口层:虚拟文件系统或VFS来通讯。VFS使得Linux可以支持多个不同的文件系统,每个表示一个VFS 的通用接口。由于软件将Linux 文件系统的所有细节进行了转换,所以Linux核心的其它部分及系统中运行的程序将看到统一的文件系统。Linux 的虚拟文件系统允许用户同时能透明地安装许多不同的文件系统。
在Linux文件系统中,作为一种特殊类型/proc文件系统只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。/proc文件系统是一个伪文件系统,用户和应用程序可以通过/proc得到系统的信息,并可以改变内核的某些参数。
在Linux文件系统中,EXT2文件系统、虚拟文件系统、/proc文件系统是三个具有代表性的文件系统,本论文试图通过对他们的分析来研究Linux文件系统机制。并且在分析这三种文件系统的基础上对Linux文件系统操作进行了解、研究(本论文选取了open和close两种操作进行研究)。在第二部分中将介绍EXT2文件系统;第三部分论述虚拟文件系统的特点;第四部分简要介绍/proc文件系统;最后,介绍两种具体文件系统操作的实现。
2 EXT2文件系统
在Linux中普通文件和目录文件保存在称为块物理设备的磁盘或者磁带上。一套Linux系统支持若干物理盘,每个物理盘可定义一个或者多个文件系统。(类比于微机磁盘分区)。每个文件系统由逻辑块的序列组成,一个逻辑盘空间一般划分为几个用途各不相同的部分,即引导块、超级块、inode区以及数据区等。
引导块:在文件系统的开头,通常为一个扇区,其中存放引导程序,用于读入并启动操作系统;超级块:用于记录文件系统的管理信息。特定的文件系统定义了特定的超级块;inode区(索引节点):一个文件或目录占据一个索引节点。第一个索引节点是该文件系统的根节点。利用根节点,可以把一个文件系统挂在另一个文件系统的非叶节点上;数据区:用于存放文件数据或者管理数据。
Linux最早引入的文件系统类型是MINIX。MINIX文件系统由MINIX操作系统定义,有一定的局限性,如文件名最长14个字符,文件最长64M字节。第一个专门为Linux设计的文件系统是EXT(Extended File System),但目前流行最广的是EXT2。
第二代扩展文件系统由Rey Card 设计,其目标是为Linux 提供一个强大的可扩展文件系统。它同时也是Linux界中设计最成功的文件系统。通过VFS的超级块(struct ext2_sb_info ext2_sb)可以访问EXT2的超级块,通过VFS的inode(struct ext2_inode_info ext2_i)可以访问EXT2的inode。
文件系统EXT2的源代码在/usr/src/linux/fs/ext2目录下,它的数据结构在文件/usr/src/linux/include/linux/ext2_fs.h以及同一目录下的文件ext2_fs_i.h和ext2_fs_sb.h中定义。
EXT2文件系统将它所占用的逻辑分区划分成块组(block group),如下图所示:
组0
组1
…………
组N
↓
超级块
组描述符表
块位图
inode位图
inode表
数据块
图1 EXT2文件系统逻辑分区
和很多文件系统一样, EXT2 建立在数据被保存在数据块中的文件内这个前提下。这些数据块长度相等且这个长度可以变化,某个EXT2 文件系统的块大小在创建(使用mke2fs)时设置。每个文件的大小和刚好大于它的块大小正数倍相等。如果块大小为1024 字节而一个1025 字节长的文件将占据两个1024 字节大小的块。这样你不得不浪费差不多一般的空间。我们通常需要在CPU 的内存利用率和磁盘空间使用上进行折中。而大多数操作系统,包括Linux 在内,为了减少CPU 的工作负载而被迫选择相对较低的磁盘空间利用率。并不是文件中每个块都包含数据,其中有些块被用来包含描叙此文件系统结构的信息。EXT2通过一个inode 结构来描叙文件系统中文件并确定此文件系统的拓扑结构。inode 结构描叙文件中数据占据哪个块以及文件的存取权限、文件修改时间及文件类型。EXT2 文件系统中的每个文件用一个inode 来表示且每个inode 有唯一的编号。文件系统中所有的inode都被保存在inode 表中。 EXT2 目录仅是一个包含指向其目录入口指针的特殊文件(也用inode表示)。
对文件系统而言文件仅是一系列可读写的数据块。文件系统并不需要了解数据块应该放置到物理介质上什么位置,这些都是设备驱动的任务。无论何时只要文件系统需要从包含它的块设备中读取信息或数据,它将请求底层的设备驱动读取一个基本块大小整数倍的数据块。EXT2 文件系统将它所使用的逻辑分区划分成数据块组。每个数据块组将那些对文件系统完整性最重要的信息复制出来, 同时将实际文件和目录看作信息与数据块。为了发生灾难性事件时文件系统的修复,这些复制非常有必要。
2.1 The EXT2 Inode
Double Indirect
Mode
Owner info
Size
Timestamps
Direct Blocks
Indirect Blocks
Triple indirect
Data
Data
Data
Data
Data
Data
Data
Data
图2 EXT2 Inode
在EXT2 文件系统中inode 是基本块,文件系统中的每个文件与目录由唯一的inode 来描叙.每个数据块组的EXT2 inode 被保存在inode 表中,同时还有一个位图被系统用来跟踪已分配和未分配的inode.图中给出了EXT2 inode的格式,它包含以下几个域:
⑴mode:它包含两类信息;inode 描叙的内容以及用户使用权限。EXT2 中的inode 可以表示一个文件、目录、符号连接、块设备、字符设备或FIFO。
⑵Owner Information:表示此文件或目录所有者的用户和组标志符。文件系统根据它可以进行正确的存取。
⑶Size:以字节计算的文件尺寸。
⑷Timestamps:inode 创建及最后一次被修改的时间。
⑸Datablocks:指向此inode 描叙的包含数据的块指针。前12 个指针指向包含由inode 描叙的物理块, 最后三个指针包含多级间接指针。例如两级间接指针指向一块指针,而这些指针又指向一些数据块。这意味着访问文件尺寸小于或等于12个数据块的文件将比访问大文件快得多。EXT2 inode 还可以描叙特殊设备文件。虽然它们不是真正的文件, 但可以通过它们访问设备。所有那些位于/dev 中的设备文件可用来存取Linux 设备。例如mount 程序可把设备文件作为参数。
2.2 EXT2 超级块
超级块中包含了描叙文件系统基本尺寸和形态的信息。文件系统管理器利用它们来使用和维护文件系统。通常安装文件系统时只读取数据块组0 中的超级块,但是为了防止文件系统被破坏,每个数据块组都包含了复制拷贝。超级块包含如下信息:
⑴Magic Number:文件系统安装软件用来检验是否是一个真正的EXT2 文件系统超级块。 当前EXT2 版本中为0xEF53
⑵Revision Level:这个主从修订版本号让安装代码能判断此文件系统是否支持只存在于某个特定版本文件系统中的属性。同时它还是特性兼容标志以帮助安装代码判断此文件系统的新特性是否可以安全使用。
⑶Mount Count and Maximum Mount Count:系统使用它们来决定是否应对此文件系统进行全面检查。每次文件系统安装时此安装记数将递增,当它等于最大安装记数时系统将显示一条警告信息“maxumal mount count reached,running e2fsck is recommended”。
⑷Block Group Number:超级块的拷贝。
⑸Block Size:以字节记数的文件系统块大小如1024 字节。
⑹Blocks per Group:每个组中块数目当文件系统创建时此块大小被固定下来。
⑺Free Blocks:文件系统中空闲块数
⑻Free Inodes:文件系统中空闲Inode数
⑼First Inode:文件系统中第一个inode号。EXT2 根文件系统中第一个inode将是指向'/'目录的目录入口。
2.3 EXT2 目录
11
15
5
file
12
40
14
very_long_name
0 15 55
inode table
图3 EXT2目录
在EXT2 文件系统中目录是用来创建和包含文件系统中文件存取路径的特殊文件。图中给出了内存中的目录入口布局。
目录文件是一组目录入口的链表它们包含以下信息:
⑴inode:对应每个目录入口的inode。它被用来索引储存在数据块组的Inode表中的inode 数组。在上图中file文件的目录入口中有一个对inode号11的引用。
⑵name length:以字节记数的目录入口长度。
⑶name:目录入口的名称。
每个目录的前两个入口总是"."和".." 它们分别表示当前目录和父目录。
2.4 EXT2 组标志符
每个数据块组都拥有一个描叙它结构。象超级块一样,所有数据块组中的组描叙符被复制到每个数据块组中以防文件系统崩溃。每个组描叙符包含以下信息:
⑴Blocks Bitmap:对应此数据块组的块分配位图的块号。在块分配和回收时使用。
⑵Inode Bitmap:对应此数据块组的inode 分配位图的块号。在inode 分配和回收时使用。
⑶Inode Table:对应数据块组的inode表的起始块号。每个inode用下面的EXT2 inode结构来表示。
⑷Free blocks count, Free Inodes count, Used directory count:组描叙符放置在一起形成了组描叙符表。每个数据块组在超级块拷贝后包含整个组描叙符表。EXT2 文件系统仅使用第一个拷贝(在数据块组0中)。其它拷贝都象超级块拷贝一样用来防止主拷贝被破坏。
2.5 在EXT2 文件系统中搜寻文件
Linux文件名的格式与Unix 类似,是一系列以"/"隔开的目录名并以文件名结尾。/home/rusling/.cshrc中/home和/rusling都是目录名而文件名为.cshrc。象Unix系统一样,Linux并不关心文件名格式本身,它可以由任意可打印字符组成。为了寻找EXT2 文件系统中表示此文件的inode,系统必须将文件名从目录名中分离出来。
我们所需要的第一个inode 是根文件系统的inode,它被存放在文件系统的超级块中。为读取某个EXT2 inode,我们必须在适当数据块组的inode表中进行搜寻。如果根inode 号为42则我们需要数据块组0 inode表的第42个inode。此根inode对应于一个EXT2目录,即根inode 的mode域将它描叙成目录且其数据块包含EXT2 目录入口。home目录是许多目录的入口同时此目录给我们提供了大量描叙/home目录的inode。我们必须读取此目录以找到rusling 目录入口,此入口又提供了许多描叙/home/rusling 目录的inode。最后读取由/home/rusling目录描叙的inode 指向的目录入口以找出.cshrc 文件的inode 号并从中取得包含在文件中信息的数据块。
2.6 改变EXT2 文件系统中文件的大小
文件系统普遍存在的一个问题是碎块化。一个文件所包含的数据块遍布整个文件系统,这使得对文件数据块的顺序访问越来越慢。EXT2 文件系统试图通过分配一个和当前文件数据块在物理位置上邻接或者至少位于同一个数据块组中的新块来解决这个问题。只有在这种分配策略失败时才在其它数据块组中分配空间。
当进程准备写某文件时, Linux 文件系统首先检查数据是否已经超出了文件最后一个被分配的块空间。如果是则必须为此文件分配一个新数据块。进程将一直等待到此分配完成然后将其余数据写入此文件。EXT2 块分配例程所作的第一件事是对此文件系统的EXT2超级块加锁。这是因为块分配和回收将导致超级块中某些域的改变,Linux 文件系统不能在同一时刻为多个进程进行此类服务。如果另外一个进程需要分配更多的数据块时它必须等到此进程完成分配操作为止。在超级块上等待的进程将被挂起直到超级块的控制权被其当前使用者释放。对超级块的访问遵循先来先服务原则,一旦进程取得了超级块的控制则它必须保持到操作结束为止。如果系统中空闲块不多则此分配的将失败,进程会释放对文件系统超级块的控制。
如果EXT2 文件系统被设成预先分配数据块则我们可以从中取得一个。预先分配块实际上并不存在,它们仅仅包含在已分配块的位图中。我们试图为之分配新数据块文件所对应的VFS inode 包含两个EXT2 特殊域:prealloc_block 和prealloc_count,它们分别代表第一个预先分配数据块的块号以及各自的数目。如果没有使用预先分配块或块预先分配数据块策略,则EXT2 文件系统必须分配一个新块。它首先检查此文件最后一个块后的数据块是否空闲。从逻辑上来说这是让其顺序访问更快的最有效块分配策略。如果此块已被使用则它会在理想块周围64 个块中选择一个。这个块虽然不是最理想但和此文件的其它数据块都位于同一个数据块组中。
如果此块还是不空闲则进程将在所有其它数据块组中搜寻,直到找到一空闲块。块分配代码将在某个数据块组中寻找一个由8 个空闲数据块组成的簇。如果找不到那么它将取更小的尺寸。如果使用了块预先分配则它将更新相应的prealloc_block和prealloc_count。
找到空闲块后块分配代码将更新数据块组中的位图并在buffer cache中为它分配一个数据缓存。这个数据缓存由文件系统支撑设备的标志符以及已分配块的块号来标志。缓存中的数据被置0 且缓存被标记成dirty 以显示其内容还没有写入物理磁盘。最后超级块也被标记为dirty 以表示它已被更新并解锁了。如果有进程在等待这个超级块则队列中的第一个进程将得到运行并取得对超级块的独占控制。如果数据块被填满则进程的数据被写入新数据块中,以上的整个过程将重复且另一个数据块被分配。
3 虚拟文件系统(VFS)
虚拟文件系统(VFS)是物理文件系统与服务之间的一个接口层,它对Linux的每个文件系统的所有细节进行抽象,使得不同的文件系统在Linux核心以及系统中运行的其他进程看来,都是相同的。严格说来,VFS并不是一种实际的文件系统。它只存在于内存中,不存在于任何外存空间。VFS在系统启动时建立,在系统关闭时消亡。
VFS使Linux同时安装、支持许多不同类型的文件系统成为可能。VFS拥有关于各种特殊文件系统的公共界面,如超级块、inode、文件操作函数入口等。实际文件系统的细节,统一由VFS的公共界面来索引,它们对系统核心和用户进程来说是透明的。
VFS的功能包括:纪录可用的文件系统的类型;将设备同对应的文件系统联系起来;处理一些面向文件的通用操作;涉及到针对文件系统的操作时,VFS把它们影射到与控制文件、目录以及inode相关的物理文件系统。
当某个进程发布了一个面向文件的系统调用时,核心将调用VFS中相应的函数,这个函数处理一些与物理结构无关的操作,并且把它重定向为真实文件系统中相应的函数调用,后者则用来处理那些与物理结构相关的操作。
VFS与实际文件系统的封装关系如下图所示:
VFS
MINIX FS
VFSinode缓存
VFS目录缓存
EXT FS
EXT2 FS
MSDS FS
缓冲存储
I/O设备驱动
图4 VFS与实际文件系统的封装关系
VFS的源代码集中在/usr/src/linux/fs目录下,关于它的数据结构的描述在文件/usr/src/lunux/include/linux/fs.h中。
3.1 VFS超级块
VFS使用了与EXT2文件系统类似的方式:超级块和索引节点inode描述文件系统。
VFS超级块是各种逻辑文件系统在安装时建立的,并在这些文件系统卸载时自动删除,它只存在于内存中。VFS中保存了系统中挂接的文件系统的链表以及这些文件系统对应的VFS超级块。系统启动后所有被初始化的文件系统都要向VFS登记。每个已安装的文件系统由一个VFS 超块表示它包含如下信息:
⑴Device:表示文件系统所在块设备的设备标志符。例如系统中第一个IDE 硬盘的设备标志符为0x301。
⑵Inode pointers:这个mounted inode指针指向文件系统中第一个inode。而covered inode指针指向此文件系统安装目录的inode。根文件系统的VFS超块不包含covered指针。
⑶Blocksize:以字节记数的文件系统块大小,如1024 字节。
⑷Superblock operations:指向此文件系统一组超块操纵例程的指针。这些例程被VFS 用来读写inode和超块。
⑸File System type:这是一个指向已安装文件系统的file_system_type结构的指针。
⑹File System specific:指向文件系统所需信息的指针。
3.2 The VFS Inode
和EXT2 文件系统相同,VFS 中的每个文件、目录等都用且只用一个VFS inode表示。每个VFS inode 中的信息通过文件系统相关例程从底层文件系统中得到。VFS inode仅存在于核心内存并且保存只要对系统有用,它们就会被保存在在VFS inode cache中。每个VFS inode包含下列域:
⑴device:包含此文件或此VFS inode 代表的任何东西的设备的设备标志符。
⑵inode number:文件系统中唯一的inode号。在虚拟文件系统中device和inode号的组合是唯一的。
⑶mode:和EXT2 中的相同, 表示此VFS inode 的存取权限。
⑷user ids:所有者的标志符。
⑸times:VFS inode 创建、修改和写入时间。
⑹block size:以字节计算的文件块大小,如1024 字节。
⑺inode operations:指向一组例程地址的指针。这些例程和文件系统相关且对此inode 执行操作,如截断此inode表示的文件。
⑻count:使用此VFS inode 的系统部件数。一个count为0 的inode可以被自由的丢弃或重新使用。
⑼lock: 用来对某个VFS inode加锁,如用于读取文件系统时。
⑽dirty:表示这个VFS inode是否已经被写过,如果是则底层文件系统需要更新。
3.3 注册文件系统
用户可以通过两种途径向内核注册文件系统:一是在编译内核时确定可支持的文件系统类型,并在系统初始化时通过内嵌的函数调用在VFS中进行注册;二是把某个文件系统当作一个模块,利用模块的加载和卸载特征向注册表登记类型或从注册表注销。
文件系统类型的注册函数为:int register filesystem (struct file_system_type *fs)
每个文件系统都有一个初始化例程,文件系统通过它在VFS中进行注册,即填写file_system_type数据结构。该结构包含了文件系统的名称及一个指向对应VFS超级块读取例程的地址。所有已注册文件系统的file_system_type结构形成了一个注册链表,如下图所示:
file_system_type file_system_type file_system_type
*read_super()
name
owner
kem_mnt
next
*read_super()
name
owner
kem_mnt
next
*read_super()
name
owner
kem_mnt
next
图5
file_system_type的数据结构在include/linux/fs.h中定义如下:
struct file_system_type {
const char *name;
//文件系统的类型名,如EXT2。这些名称出现在Linux中的/proc/filesystems中且必须是唯一的。
int fs_flags;
//fs_flags的取值可能有很多种。例如,文件系统标识FS_REQUIRES_DEV表示文件系统只能加载在一个块设备上;FS_SINGLE表示文件系统只能有一个超级块;FS_NOMOUNT表示文件系统不能安装在用户空间上。
struct super_block *(*read_super) (struct super_block *, void *, int);
//read_super所指的函数用于读出该文件系统在外存的超级块。
struct module *owner;
//如果实现该文件系统的程序段是由module动态载入的,则指向该module;如果实现该文件系统的程序段是在内核编译时生成的,则owner = NULL。
struct vfsmount * kem_mnt;
//只为标识为FS_SINGLE的文件系统使用(For kernel mount)
struct file_system_type * next;
//文件系统类型链表的后续指针。
};
3.4 安装文件系统
文件系统注册后便在设备上按一定格式建立文件系统,但是此时设备上的文件和节点都还不是可访问的,还不能按照一定的路径名访问其中特定的节点或文件。只有把它安装到文件系统中某个节点上,才能使设备上的文件和节点可被访问。因此注册了wej系统只代表Linux系统支持这种文件系统的应用,要真正使用该文件系统还必须安装它。
文件系统的安装必须调用mount命令,把其他子系统安装到已经存在于文件系统的空闲节点上。该命令使用系统的mount()调用:asmlinkage ling sys_mount(char * dev_name, char * dir_name, char * type, unsigned long flags, void * data)
其中dev_name是要安装的文件系统的磁盘分区的路径名,如/dev/hda5。参数dir_name是要安装的文件系统的目录名;type指定磁盘分区上的文件系统类型;flags指定该文件系统如何被安装;data是指向任意的信息结构的指针,其内容依赖于被安装的特定文件系统类型。
使用mount命令后,VFS通过file_systems在file_system_type链表中根据指定的文件系统名称搜索文件系统类型信息。而函数get_fs_type()根据具体文件系统的类型名在内核中找到相应的file_system_type结构:
struct file_system_type *get_fs_type(const char *name)
{
struct file_system_type *fs;
read_lock(&file_systems_lock);
fs = *(find_filesystem(name));
if (!fs && (request_module(name) == 0)) {
read_lock(&file_systems_lock);
fs = *(find_filesystem(name));
if (fs && !try_inc_mod_count(fs->owner))
fs = NULL;
read_unlock(&file_systems_lock);
}
return fs;
}
其中函数find_filesystem(name)扫描file_system对列,找到所需文件系统类型的数据结构。
3.5 卸载文件系统
超级用户卸载文件系统使用umount命令。
卸载过程必须检查文件系统及其超级块的状态。如果文件系统正被其他进程使用该文件系统就不能被卸载。如果文件系统的文件或目录正在使用,则VFS索引节点缓存中可能包含相应的VFS索引节点。检查代码在该缓存中,根据文件系统所在的设备标识符查找是否有来自该文件系统的VFS索引节点。如果有且使用计数大于0则说明该文件系统正在使用,不能被删除。如果文件系统的超级块为“脏”,即被修改,则应先将它写回到磁盘上。
文件系统允许在被删除后,对应的VFS超级块被释放,vfsmount数据结构从vfsmntlist链表中断开并被释放。
4 /proc文件系统
/proc文件系统提供了一个功能强大的访问任意地址空间的接口。它允许用户用标准文件系统接口和系统调用来读取和修改其他进程的地址空间,并对其进行若干控制操作。访问控制是使用读-写-执行权限实现的,在默认情况下只有超级用户的进程所有者才能读写/proc文件。
在/proc文件系统中每个进程由一个子目录代表,目录的名称由进程标识符的十进制表示。每个进程的子目录都包含下述入口点:cmdline:命令行参数。environ:环境变量的值。fd:所有文件的描述符。mem:该进程使用的内存。stat:进程状态。status:可读形式的进程状态。cwd:链接到当前工作目录。exe:链接到该进程运行状态。maps:内存分配。root:链接到该进程的根目录。statm:进程内存状态。
事实上/proc文件系统并不物理地存在于任何设备上,因此它的安装过程是特殊的。对/proc文件系统不能直接通过mount()函数来安装,而要先由系统内核在内核初始化时自动地通过一个函数kern_mount()来安装一次,然后再由处理系统初始化的进程通过mount()函数重新安装。
/proc文件系统中最重要的数据结构是一个叫PROC_DIR_ENTRY的结构类型(include/linux/proc_fs.h),所有该文件系中的文件及目录都除了通用文件操作用的inode,file,dentry等结构外,都必须首先注册自己的一个PROC_DIR_ENTRY,在其中定义了针对自己的各项属性和操作函数指针。
struct proc_dir_entry {
unsigned short low_ino;
/*注册inode号,实际inode的低16位*/
unsigned short namelen;
/*名称字符串的长度*/
const char *name;
/*文件名称*/
mode_t mode;
/*文件权限*/
nlink_t nlink;
/*连接到此文件的目录项个数*/
uid_t uid;
/*用户标识*/
gid_t gid;
/*组标识*/
unsigned long size;
/*文件大小,均为0*/
struct inode_operations * ops;
/*inode操作的函数指针*/
int (*get_info)(char *, char **, off_t, int, int);
void (*fill_inode)(struct inode *, int);
/*填补inode信息的函数指针*/
struct proc_dir_entry *next, *parent, *subdir;
/*兄弟,父亲,子文件指针*/
void *data;
int (*read_proc)(char *page, char **start, off_t off, int count,
int *eof, void *data);
int (*write_proc)(struct file *file, const char *buffer,
unsigned long count, void *data);
int (*readlink_proc)(struct proc_dir_entry *de, char *page);
unsigned int count;
/*使用数*/
int deleted;
/*删除标志*/
};
其次就是针对inode,file和super block的各种操作函数指针,这些结构与文件系统中使用的完全相同,此处不再赘述。
/proc文件系统真正显示了Linux虚拟文件系统的能了。事实上不管是/proc目录还是其子目录和文件都不真正的存在,但是/proc文件系统像一个真正的文件系统一样将向虚拟文件系统注册。访问/proc中的文件和目录时,VFS系统从内核的数据中临时构造这些文件和目录。例如内核的/proc/devices文件是从描述其设备的内核数据结构中产生出来。/proc文件系统为用户提供了一个在系统内部工作的可读窗口。
5 Open操作和Close操作
5.1 Open操作
系统调用open是由函数sys_open(fs/open.c)实现的。该系统调用用来获得欲访问文件的文件描述符。如果文件不存在则可以用它创建一个新文件。打开的文件不存在时,使用三个参数形式:filename,falges,mode。
⑴参数filename是指向所要打开文件的路径名指针。
⑵参数flags指定打开文件的方式,它必须包含下列三个值之一:O_RDONLY(只读打开);O_WRONLY(只写打开);O_RDWR(读/写打开)
⑶参数mode指定对文件所有者、文件用户组及系统中所有其他用户的访问权限位。
具体实现如下:
asmlinkage long sys_open(const char * filename, int flags, int mode)
{
char * tmp;
int fd, error;
#if BITS_PER_LONG != 32
flags |= O_LARGEFILE;#endif
tmp = getname(filename);
/* 调用getname()函数作路径名的合法性检查。getname()在检查名字错误的同时要
把filename从用户区拷贝到核心资料区。它会检查给出的地址是否在当前进程的虚
存段内,在核心空间中申请一页,并不断把filename字符的内容拷贝到该页中去。
这样是为了使系统效率提高,减少不比要的操作。*/
fd = PTR_ERR(tmp);
if (!IS_ERR(tmp)) {
fd = get_unused_fd();
/* 调用get_unused_fd(),获取一个文件描述符。*/
if (fd >= 0) {
struct file *f = filp_open(tmp, flags, mode);
/* 调用filp_open(),获取文件对应的filp结构。*/
error = PTR_ERR(f);
if (IS_ERR(f))
goto out_error;
/* 如果所获file结构有错误,跳到out_error处释放文件描述符,返回出错。*/
fd_install(fd, f);
/* 调用fd_install()将打开文件的file结构装入当前进程打开文件表中。*/
}
out:
putname(tmp);
}
return fd;
out_error:
put_unused_fd(fd);
fd = error;
goto out;
}
int get_unused_fd(void)
{
struct files_struct * files = current->files;
/* 获取当前进程打开文件表。*/
int fd, error;
error = -EMFILE;
write_lock(&files->file_lock);
repeat:
fd = find_next_zero_bit(files->open_fds,
/* files->open_fds是打开文件描述符的位图,files->open_fds为下一个可以打开
文件的描述符,files->max_fdset为可以打开文件的最大数目。通过这些参数,我们
调用find_next_zero_bit()来获取一个空闲的文件描述符。*/
files->max_fdset,
files->next_fd);
/*
* N.B. For clone tasks sharing a files structure, this test
* will limit the total number of files that can be opened.
*/
if (fd >= current->rlim[RLIMIT_NOFILE].rlim_cur)
/* 如果fd大于进程可用最大文件数目直接退出。*/
goto out;
/* Do we need to expand the fdset array? */
if (fd >= files->max_fdset) {
/* 如果fd大于files->max_fdset,那调用expand_fdset()扩展files->open_fds
与files->close_on_exec,用于标记更多的文件。如果扩展成功,那要跳回上面,重
新调用find_next_zero_bit()来获取一个文件描述符。如果出错则退出。*/
error = expand_fdset(files, fd);
if (!error) {
error = -EMFILE;
goto repeat;
}
goto out;
}
/*
* Check whether we need to expand the fd array.
*/
if (fd >= files->max_fds) {
/* 如果fd大于files->max_fds,那调用expand_fd_array()扩展file结构数组fd
(这个fd是指files_struct中的struct file * * fd,与该程序中的变量fd 不同)。
如果扩展成功,那要跳回到上面重新调用find_next_zero_bit()来获取一个文件描述
符。如果出错则退出。*/
error = expand_fd_array(files, fd);
if (!error) {
error = -EMFILE;
goto repeat;
}
goto out;
}
FD_SET(fd, files->open_fds);
/* 将files->open_fds中相应的fd位置位。*/
FD_CLR(fd, files->close_on_exec);
/* 清除files->close_on_exec中的相应位。*/
files->next_fd = fd + 1;
#if 1
/* Sanity check */
if (files->fd[fd] != NULL) {
/*如果files->fd[fd]不为空,那打印出提示信息并将其置空。*/
printk(KERN_WARNING "get_unused_fd: slot %d not NULL!\n", fd);
files->fd[fd] = NULL;
}
#endif
error = fd;
out:
write_unlock(&files->file_lock);
return error;
}
struct file *filp_open(const char * filename, int flags, int mode)
{
int namei_flags, error;
struct nameidata nd;
namei_flags = flags;
if ((namei_flags+1) & O_ACCMODE)
namei_flags++;
if (namei_flags & O_TRUNC)
namei_flags |= 2;
error = open_namei(filename, namei_flags, mode, &nd);
if (!error)
return dentry_open(nd.dentry, nd.mnt, flags);
/* filp_open()的工作大体分成两部分1.调用open_namei(),通过路径名获取其相
应的dentry与vfsmount结构。 2.调用dentry_open(),通过open_namei得到dentry
与vfsmount来得到file结构。(下面马上介绍。)*/
return ERR_PTR(error);
}
调用dentry_open(),通过open_namei得到dentry与vfsmount来得到file结构。
struct file *dentry_open(struct dentry *dentry, struct vfsmount *mnt, int flags)
{
struct file * f;
struct inode *inode;
static LIST_HEAD(kill_list);
int error;
error = -ENFILE;
f = get_empty_filp();
/* 调用get_empty_filp(),创建一个file结构,并将这个结构链入anon_list链表。*/
if (!f)
goto cleanup_dentry;
/* 如果创建file结构失败,那释放dentry与vfsmount,返回出错。*/
f->f_flags = flags;
f->f_mode = (flags+1) & O_ACCMODE;
/*对file结构中的f_flags、f_mode赋值。*/
inode = dentry->d_inode;
if (f->f_mode & FMODE_WRITE) {
/* 检查对打开文件是否以写模式打开。若是则调用get_write_access()获取对文件的写权限。get_write_access()注意通过对inode 的i_writecount项的操作来实现其功能。*/
error = get_write_access(inode);
if (error)
goto cleanup_file;
}
f->f_dentry = dentry;
/ *对file结构中的相关项赋值。*/
f->f_vfsmnt = mnt;
f->f_pos = 0;
f->f_reada = 0;
f->f_op = fops_get(inode->i_fop);
file_move(f, &inode->i_sb->s_files);
/* 将file结构挂到超级块打开文件链表上。*/
/* preallocate kiobuf for O_DIRECT */
f->f_iobuf = NULL;
f->f_iobuf_lock = 0;
if (f->f_flags & O_DIRECT) {
error = alloc_kiovec(1, &f->f_iobuf);
if (error)
goto cleanup_all;
}
if (f->f_op && f->f_op->open) {
/* 将file结构挂到超级块打开文件链表上。*/
error = f->f_op->open(inode,f);
if (error)
goto cleanup_all;
}
f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);
/* 改变file结构的标志项。*/
return f;
cleanup_all:
if (f->f_iobuf)
free_kiovec(1, &f->f_iobuf);
fops_put(f->f_op);
if (f->f_mode & FMODE_WRITE)
put_write_access(inode);
file_move(f, &kill_list); /* out of the way.. */
f->f_dentry = NULL;
f->f_vfsmnt = NULL;
cleanup_file:
put_filp(f);
cleanup_dentry:
dput(dentry);
mntput(mnt);
return ERR_PTR(error);
}
open操作用到的主要函数的调用关系结构如下图所示
sys_open
get_unused_fd
file_open
fd_install
open_namei
dentry_open
path_walk
real_lookup
图6
5.2 Close操作
关闭文件时要调用sys_close()来实现。关闭打开的文件描述符, int close (unsigned int fd),函数调用成功返回0值,否则返回-1值。由errno存放错误编号EBAD表示参数是一个有效的打开的文件描述符。
系统调用open()打开一个文件时系统就分配给文件一个文件描述符,同时为打开文件描述符的引用计数加1。调用close()关闭文件时打开文件描述符的引用计数减1。引用计数为0时才彻底关闭打开的文件。
具体实现如下:
asmlinkage long sys_close(unsigned int fd)
{
struct file * filp;
struct files_struct *files = current->files;
/* 将局部变量files指向当前进程的打开文件表。*/
write_lock(&files->file_lock);
/* 对该表设置读写自旋锁。*/
if (fd >= files->max_fds)
/* 如果函数传入参数fd(打开文件描述符)大于该表的最大打开文件数目则返回出错。*/
goto out_unlock;
filp = files->fd[fd];
/* 根据文件描述符,从打开文件表中得到相应文件的file结构。*/
if (!filp)
/* 如果对应的file结构指针为空,那返回出错。*/
goto out_unlock;
files->fd[fd] = NULL;
/* 将打开文件表中文件描述符所对应的指针置为空。*/
FD_CLR(fd, files->close_on_exec);
/* 将close_on_exec中的fd位置为0。*/
__put_unused_fd(files, fd);
/* 将open_fds项的fd位置为0。并且比较一下fd与next_fd,那将fd的值赋给next_fd,作为未使用的文件描述符提供给后面打开的文件。*/
write_unlock(&files->file_lock);
/* 解开读写自旋锁。*/
return filp_close(filp, files);
/* 调用file_close()做余下的工作。*/
out_unlock:
write_unlock(&files->file_lock);
return -EBADF;
}
int filp_close(struct file *filp, fl_owner_t id)
{
int retval;
if (!file_count(filp)) {
/* 读取文件引用数。若为0直接返回。*/
printk(KERN_ERR "VFS: Close: file count is 0\n");
return 0;
}
retval = 0;
if (filp->f_op && filp->f_op->flush) {
lock_kernel();
retval = filp->f_op->flush(filp);
unlock_kernel();
}
/* 如果文件系统有自己的操作函数flush,那直接调用该函数。在这个操作时要设置内核锁。*/
fcntl_dirnotify(0, filp, 0);
locks_remove_posix(filp, id);
fput(filp);
/* 调用fput()释放文件对应的file结构。*/
return retval;
}
void fput(struct file * file)
{
struct dentry * dentry = file->f_dentry;
struct vfsmount * mnt = file->f_vfsmnt;
struct inode * inode = dentry->d_inode;
if (atomic_dec_and_test(&file->f_count)) {
/* 对文件引用数减1,并判断&file->f_count减1后是否为0。若为0则做后面的工作,否则结束该函数。*/
locks_remove_flock(file);
/* 除去文件锁。*/
if (file->f_op && file->f_op->release)
file->f_op->release(inode, file);
/* 如果文件系统有自己的release函数,那直接调用该函数来释放该文件。*/
fops_put(file->f_op);
/* 减少文件操作函数模块的引用次数。*/
file->f_dentry = NULL;
file->f_vfsmnt = NULL;
/* 将file->f_dentry、file->f_vfsmnt置为NULL, 但此时并没有释放相对应的dentry与vfsmount结构,局部变量dentry和mnt还指向这两个结构。*/
if (file->f_mode & FMODE_WRITE)
put_write_access(inode);
/* 如果f_mode的FMODE_WRITE位被置位了,那么调用put_write_access,将文件对应的inode的i_writecount项减1。*/
dput(dentry);
/* 调用dput释放dentry。*/
if(mnt);
/* 如果mnt存在,调用mntput释放vfsmount。*/
mntput(mnt);
file_list_leck();
list_del(&file->f_list);
list_add(&file->f_list, &free_list);
files_stat.nr_free_files++;
/* 将该节点从超级块中的打开文件链表中删除,加到free_list链表上,并对files_stat.nr_free_files做加1操作。*/
file_list_unlock();
}
}
void dput(struct dentry *dentry)
{
if (!dentry)
/* 如果dentry为空,那立刻返回。*/
return;
repeat:
if (!atomic_dec_and_lock(&dentry->d_count, &dcache_lock))
/* 这里只对dentry->d_count做减1操作,如果减1后dentry->d_count不为0,那立刻返回。*/
return;
/* dput on a free dentry? */
if (!list_empty(&dentry->d_lru))
/* 如果该dentry在d_count= =0的队列(即dentry_unused)上,那么调用BUG()
BUG(); */
/*
* AV: ->d_delete() is _NOT_ allowed to block now.
*/
if (dentry->d_op && dentry->d_op->d_delete) {
/* 如果该dentry有自己的d_delete()函数,那直接调用它,指向完之后,调到unhash_it。*/
if (dentry->d_op->d_delete(dentry))
goto unhash_it;
}
/* Unreachable? Get rid of it */
if (list_empty(&dentry->d_hash))
/* 如果该dentry不在hash表上,那跳到kill_it。*/
goto kill_it;
list_add(&dentry->d_lru, &dentry_unused);
/* 将dentry加到dentry_unused队列上。*/
dentry_stat.nr_unused++;
/*
*Update the timestamp
*/
dentry->d_reftime = jiffies;
/* 更新dentry的时间戳,记录它最后一次引用的时间。*/
spin_unlock(&dcache_lock);
return;
unhash_it:
list_del_init(&dentry->d_hash);
/* 将dentry从hash表中删除。*/
kill_it: {
struct dentry *parent;
list_del(&dentry->d_child);
/* 将该dentry从其同一级目录链表中删除。*/
/* drops the lock, at that point nobody can reach this dentry */
dentry_iput(dentry);
/* 用于删除dentry对应的inode。*/
parent = dentry->d_parent;
d_free(dentry);
/* 释放dentry的内存空间。*/
if (dentry == parent)
return;
dentry = parent;
/* 将变量dentry指向目前目录的父目录的dentry结构,跳到repeat。这样是为了将父目录dentry结构的引用次数减1,直到根目录为止。*/
goto repeat;
}
}
static inline void dentry_iput(struct dentry * dentry)
{
struct inode *inode = dentry->d_inode;
/* 判断一下dentry中的inode是否为空。*/
if (inode) {
dentry->d_inode = NULL;
/* 将dentry->d_inode指向空。*/
list_del_init(&dentry->d_alias);
/* 将该dentry结构从inode的i_dentry链表中删去。i_dentry链表将通过硬链接指向同一个inode的dentry结构。*/
spin_unlock(&dcache_lock);
if (dentry->d_op && dentry->d_op->d_iput)
/* 如果该dentry中带有自己的d_iput的函数,则调用它。否则调用iput函数。*/
dentry->d_op->d_iput(dentry, inode);
else
iput(inode);
} else
spin_unlock(&dcache_lock);
}
close操作用到的主要函数的调用关系结构如下图所示:
sys_close
_put_unused_fd
file_close
fput
dput
dentry_iput
mvtput
iput
图7
结 论
本论文通过对EXT2文件系统、虚拟文件系统、/proc文件系统的分析,以及对具体文件系统操作的分析,对Linux文件系统机制进行了一定的研究。
EXT2文件系统是Linux内核2.4版本的标准文件系统类型,在众多可使用的文件系统类型中,EXT2是Linux自行设计并具有较高效率的一种文件系统类型,本论文对它的索引节点、超级块、组描述符、目录和两个具体文件操作进行了分析,从而对EXT2文件系统的内涵有了一定理解。
操作Linux的最重要特征就是支持多种文件系统,而虚拟文件系统(VFS)的存在就是为了实现对多种文件系统类型的支持。VFS能够使用户程序通过一个文件系统操作界面,对不同文件系统和文件进行操作。这样就可以对用户程序隐去各种文件系统的实现细节,为用户程序提供一个统一、抽象、虚拟的文件系统界面。论文对它的基本结构进行了分析,并通过几个文件系统的控制操作进一步研究VFS的作用。
Linux在发展过程中产生了/proc文件系统。/proc文件系统是一个特殊的,由软件创建的文件系统,内核使用此文件系统来向外部输出信息,它在Linux系统中被广泛的应用。在目前的Linux发行版中,有许多命令像ps,top及uptime,都是通过/proc获取它们所需的信息。许多设备驱动程序也会通过/proc向外界输出信息。论文简要地介绍了/proc文件系统的原理和关键数据结构。
Linux 的内核源码用树形结构组织得非常合理、科学,把功能相关联的文件都放在同一个子目录下,这样使得程序更具可读性。然而,Linux 的内核源码实在是太大而且非常复杂,即便采用了很合理的文件组织方法,在不同目录下的文件之间还是有很多的关联,分析核心的一部分代码通常会要查看其它的几个相关的文件,而且可能这些文件还不在同一个子目录下。
体系的庞大复杂和文件之间关联的错综复杂,可能就是很多人对其望而生畏的主要原因。当然,这种令人生畏的劳动所带来的回报也是非常令人着迷的:不仅可以从中学到很多的计算机的底层的知识(如下面将讲到的系统的引导),体会到整个操作系统体系结构的精妙和在解决某个具体细节问题时,算法的巧妙;而且更重要的是:在源码的分析过程中,就会被一点一点地、潜移默化地专业化;甚至,只要分析十分之一的代码后,就会深刻地体会到,什么样的代码才是一个专业的程序员写的,什么样的代码是一个业余爱好者写的。
致 谢
本次毕业设计是在刘冬梅老师的悉心指导下完成的,在刘老师的指导下,我才完成了毕业设计的各项内容,并且在毕业设计中学到了很多的知识。在这里我对刘老师一个学期的帮助、指导表示诚挚的感谢。
顺利地完成本论文也得益于计算机科学与技术学院的有关老师,正是由于他们的精心安排、细心组织,我才能按部就班地完成每一项任务。为此,向张功萱老师,以及所有为本次毕业设计工作付出心血的老师致以崇高的敬意和由衷的感谢。
参 考 文 献
1 赵炯. Linux内核完全注释. 机械工业出版社,2004
2 陈莉君. Linux操作系统内核分析. 人民邮电出版社,2000
3 Abraham Silberschatz Peter Baer Galvin Greg Gagne郑扣根译. 操作系统概念. 第6版. 高等教育出版社,2004
4 博嘉科技. Linux内核分析与实例应用. 国防工业出版社,2002
5 郭卫东,王非非. Linux操作系统结构分析. 西安电子科技大学出版社,2002
6 王锋. Linux操作系统分析. 重庆大学出版社,2001
7 史志才,毛玉萃. 操作系统原理:Linux技术实现. 高等教育出版社,2004
8 刘胤杰,岳浩等. Linux操作系统教程. 机械工业出版社,2005
9 陈莉君,康华. Linux操作系统原理与应用. 清华大学出版社,2006
10 周明德. UNIX/Linux核心. 清华大学出版社,2004
11 Scott Maxwell. Linux内核源代码分析. 机械工业出版社,2000
12 冯秀芳,马季兰. 操作系统原理与Linux系统. 人民邮电出版社,1999
13 黄庆生. Linux基础教程. 人民邮电出版社,1999
14 陈俊宏. Linux完全安装DIY:进阶安装与设置. 人民邮电出版社,2000
15 徐进明, 施红芹. Linux系统管理. 电子工业出版社,2002