分类: LINUX
2008-04-29 15:37:48
UNIX内 核把普通文件与目录文件存放在称为块设备的磁盘或磁带上,一个系统可以有若干个物理磁盘,一块物理磁盘上可以定义若干个文件系统——这样做易于管理磁盘数 据。但内核在逻辑一级上仅涉及文件系统,而不涉及物理磁盘,也即,在内核上,将每一个文件系统理解成一个逻辑磁盘,或者说设备,由一个唯一的逻辑设备号标 识。而从逻辑设备到物理磁盘的地址映射则由磁盘驱动程序负责完成。
具体到一个文件系统,它是由一系列的逻辑块组成的,每个逻辑块为512,1024,2048 bytes或者是任何合适的512 bytes的 倍数。这些值可以在生成系统时直接指定,但同一个文件系统的逻辑块大小总是固定不变的,而不同的文件系统之间则允许不同的大小。使用较大的逻辑块,可以增 加磁盘和内存之间的有效数据传输率,因为内核在每次磁盘操作时可以传输更多的数据,以减少用于这类操作的时间消耗(每寻道一次可以读取几个扇区,从而减少寻道时间)。当然,由此带来的代价也是显而易见的,逻辑块越大,越多的有效存储空间就会被放弃,这就是所谓的磁盘碎片问题,有人作过统计分析,发现4KB大小的逻辑块可能导致45%的空间被浪费。System V 支持512和1024 bytes的逻辑块,为了方便,本文假定一个逻辑块1024 bytes。
一个逻辑设备的空间通常按下图被划分为若干用途各不相同的部分:
l 引导块——占据文件系统的开始部分,通常都是第一个扇区(逻辑设备的),用于读入并启动。尽管只需要在根文件系统中含有引导块,为统一起见,每个文件系统都有一个引导块(可能空闲)。
l 超级块——该部分对文件系统具有决定意义,记录了逻辑盘的当前状态,包括逻辑盘有多大,能存放多少文件,哪里可以找到空闲空间以及其它文件系统信息。
l 索引节点区——存放文件系统的索引节点表,一个文件(包括目录)占据一个索引节点。该表的大小由系统管理员在格式化文件系统时指定,其中文件系统的根目录通常就占据第一个索引节点,通过它可以将一个文件系统挂到另一个上面,并访问整个目录结构。
l 数据区——存放文件数据或管理信息,一个被分配的逻辑块属于且只属于一个文件。
超级块对文件系统的维护至关重要,在挂载文件系统后,其内容常驻内存。超级块的抽象数据结构及其解释如下:
struct filsys {
ushort s_isize ; // 索引节点区所占逻辑块数
daddr_t s_fsize ; // 整个文件系统的逻辑块数
short s_nfree ; // 空闲块表中的空闲块数
daddr_t s_free[NICFREE] ; // 空闲块表
short s_ninode ; // 空闲索引节点表中的空闲索引节点数
ino_t s_inode[NICINOD] ; // 空闲索引节点表
char s_flock ; // 处理空闲块表时的加锁标志
char s_ilock ; // 处理空闲索引节点表时的加锁标志
char s_fmod ; // 超级块被修改标志
char s_ronly ; // 只读挂载标志
time_t s_time ; // 超级块上次被修改时间
short s_dinfo[4] ; // 设备信息
daddr_t s_tfree ; // 空闲逻辑块总数
ino_t s_tnode ; // 空闲索引节点总数
char s_fname[6] ; // 文件系统名称
char s_fpack[6] ;
long s_fill[13] ; // 填充filsys,使其大小为512 bytes
long s_magic ;
long s_type ; // 文件系统类型 }
需要指出的是,在结构中的空闲块表大多时候不包括所有的空闲块,他的大小是NICFREE,同理空闲索引节点表的大小是NICINOD,由于整个结构限于512bytes,故这NICFREE是50,而NICINOD是100。之所以只有512bytes是为了兼容各种不同大小的逻辑块,而使其最小。当空闲块表或空闲索引节点表用完时,如何找到另外的空闲块或空闲索引节点,将在分析块与节点的分配算法时具体介绍。
从结构中可以看到,System V的文件系统大小在逻辑上最大可以达到2^32*1KB = 4TB,索引节点表则最多占用2^16 = 64K个逻辑块。UNIX中每个文件有一个唯一的索引节点,以静态的形式存放在磁盘上,故又称为磁盘索引节点。在System V中实现如下:
struct dinode {
ushort di_mode ; // 文件类型及访问权限标志
short di_nlink ; // 文件链接数
ushort di_uid ; // 文件拥有者的用户标识
ushort di_gid ; // 文件拥有者的组标识
off_t di_size ; // 文件大小
char di_addr[40] ; // 文件数据占用的逻辑块明细表
time_t di_atime ; // 文件最近一次访问时间
time_t di_mtime ; // 文件最近一次修改时间time_t di_ctime ; // 文件创建时间 }
在上述所列结构中,di_size实际为文件最后一个字节距离文件起始点的偏移量,故真正的文件大小应该是di_size+1;另外,一个文件在磁盘上不是连续存放的,这样可以减少磁盘碎片,也因此需要di_addr来描述文件所占用的逻辑块的具体分布,这个数组实际使用39个字节,每3个字节一组,记录一个逻辑块号,因此一个文件系统的数据区最多有2^24 = 16M个数据块,这里所以使用40,是为了使整个结构保持64 bytes的大小,以便一个逻辑块可以存放整数个索引节点,这里是16个,所以整个文件系统最多有64K*16 = 1M = 1048576个文件,显然足够大了。
粗看di_addr[40],你可能会误以为一个文件最多只有1K*13 = 13 K的大小,这显然很荒谬,实际上UNIX使用了另一种安排即所谓的直接块,一次间接块,二次间接块和三次间接块,这种办法在大大增加文件长度的上限的同时,极大地缩短了索引节点的长度。如下图所示:
其中,一次间址包含256个直接块,二次间址又包含256个一次间址,三次间址又包含256个二次间址,从这种递归关系中不难看出,一个UNIX普通文件的最大长度可作如下推算:
10个直接块,每块1KB,最多容纳10KB ;
一次间址所指向的数据块含有256个逻辑块号,故又指向256个直接块,最多容纳 256KB,注意,这里的逻辑块号是以4字节的整数编址,而不是di_addr的三个字节。
二,三次间址与一次间址类似,分别有 256*256*1KB = 64MB 和 256*256*256*1KB = 16GB 。当然,di_size为32位长整型,故一个文件最多不超过4GB。采用间址的方法可以大大增加文件的最大长度,但同时增加了大文件的读写时间开销。好在UNIX中大部分的文件都小于10KB,一份统计分析显示,85%的文件小于8KB,48%的文件小于4KB,同时实验也表明这个结果基本可信。磁盘索引节点反映了文件的静态特征,为了实现文件的读写,在系统的内核还维护着一张内存索引节点表,由若干个内存索引节点组成,除了复制磁盘索引节点的信息外,还包含当前打开文件的动态信息。
struct inode {
char i_flag; // 内存索引节点状态
cnt_t i_count; // 引用计数
dev_t i_dev; // 相应文件所在设备号
ino_t i_number; // 索引节点号
ushort i_mode; // 文件类型及访问权限标志
short i_ nlink; // 文件链接数
ushort i_uid; // 文件拥有者的用户标识
ushort i_gid; // 文件拥有者的组标识
off_t i_size; // 文件大小
char i_ addr[40]; // 文件数据占用的逻辑块明细表
time_t i_atime; // 文件最近一次访问时间
time_t i_mtime; // 文件最近一次访问时间
time_t i_ctime; // 文件创建时间
};
当打开一个文件时,内核用文件对应的设备号及索引节点号在内存索引节点表中查找,若找到一个相应表目,表明,该文件被别的进程打开了,这时,只需要将i_count加1.
否则找一个空表目,将磁盘索引节点中的内容拷贝到该表目,将文件对应的设备号和索引结点号填入对应域中,然后i_count置1.
一个文件可以被同一进程或不同进程同时打开,但这些进程对文件的操作可能不同,读写操作的位置也可能不同,而索引节点中基本只包含文件物理结构(索引表).为此,文件系统设置了一个全局数据结构: 系统打开文件表,其表项结构描述如下:
struct file{
char f_flag; // 读写标志
cnt_t f_count; //引用计数
struct inode *f_inode; // 指向内存索引节点指针
off_t f_offset; // 读写位置 }
系统打开文件表为I/O重定向,管道及文件共享等重要功能提供了实现依据。对文件的全部操作都离不开该表。同时,内核还为每个进程维护了一张用户文件描述符表,或则说进程打开文件表,这张表实际上就是指向file结构的指针数组,其指向的每个file结构都是系统打开文件表的一个表项。当应用程序打开或创建一个文件时,返回的文件描述符就是这个数组的下标,顺着该指针可以找到存放在文件中的数据和其他文件信息。
第一章已提到,对应用程序而言,从内核返回的文件数据都是字节流,目录文件与普通文件的不同就在于系统预先给出了目录文件数据流的解释格式。在具体实现中,目录文件也使用磁盘索引节点表示,只是在dinode的di_mode域中标明了是目录。而其数据流由目录登记项组成,每个登记项具有以下结构:
#define DIRSIZ 14
struct direct {
ino_t d_ino; // 该登记项对应的索引节点号
char d_name[DIRSIZ]; // 该登记项对应的文件的名称
};
目录在路径名到索引节点号的转化过程中起了非常重要的作用,从其结构也可以看出为什么早期的UNIX系统如System V只允许最多14个字符的文件名
这里应当提及一类特殊的目录登记项,即硬链接。在UNIX中允许一个文件有若干别名,甚至只要不在同一个目录中,也可以有相同的名字(因此路径名是唯一的)。以一个文件为例,假设在/usr/目录下创建了一个file.c的文件,分配了一个磁盘索引节点,其索引节点号为702。此时,在目录文件usr中相应地创建了一项登记项,其
该表反映了各个文件系统之间的链接关系,每一个挂载的文件系统都要在该表中占据一项。
struct mount {
int m_flags ; // 文件系统状态
dev_t m_dev ; // 逻辑设备号
struct inode * m_inodep ; // 挂载目录的内存索引节点指针
struct buf * m_bufp ; // 存放被挂载文件系统的超级块
struct inode * m_mount ; // 被挂载文件系统的根索引节点指针 }
d_ino为702,d_name为字符串” file.c”。如果现在要在/usr下为file.c取一个别名,即建立一个到file.c的链接file1.c,系统并不真的创建一个新的文件,而仅仅在目录文件usr中添加一项d_ino为702,但d_name为”file1.c”的新登记项。类似的,若要在别的目录下创建一个到file.c的链接,也只需在该目录文件中添加d_ino为702,但d_name为链接名的登记项。
由硬链接的实现可以看出,由于d_ino仅能够对本文件系统内的索引节点进行索引,因此,不能够在两个文件系统之间创建硬链接(不能保证d_ino唯 一)。为了解决这个问题,引入了符号链接。与硬链接不同,创建一个符号链接的同时也创建了一个新文件,即分配了一个新的磁盘索引节点。只不过,该文件的内 容是所要链接的文件在系统中的路径名,故,要访问原文件,首先要读取这个路径名,再由其查找到原文件的磁盘索引节点,也因此,倘若原文件所在的文件系统的 挂载点发生变化,那么该链接就不再可用。