在inode里存放了文件数据所在磁盘数据块号,文件越大,所需要的块号就越多,这是因为文件在磁盘上的存放是不连续的。那为什么不用连续存放?这样只需要一个起始块号以及文件大小就可以描述整个文件的数据位置了。这回带来一些问题,包括很难增长文件大小,以及很容易产生磁盘碎片。
因此,还是采用存储每个块号的方式来存放文件数据在磁盘上的索引。当然,要想允许大文件且保持inode足够小,可以采用分级机制。在inode中有13个整数用来存放块号。前10个数是直接指向文件数据的块号,第11个数单级间接指向文件数据,即在第11个块号所指向的的数据块存放的是一个块号表。第12个数双级间接指向文件数据,即第12个块号指向一个单级间接块号表。第13个块号指向的是一个指向双级间接块号表。如下图所示:
|double |--->-----------------
|indirect| |single direct 0|--->...
---------- |single direct 1|--->...
|... | ...
|single direct n|--->...
-----------------
……
这种方式能够存放多少数据呢?计算一下。假设一个磁盘块能存放1K字节,一个块号占用4个字节,那么:
10 direct blocks: 10 * 1K = 10K
1 indirect block with 256 direct blocks: 256 * 1K = 256K
1 double indirect block with 256 indirect blocks: 256 * 256K = 64M
1 triple indirect block with 256 double indirect blocks: 256 * 64M = 16G。
然而一个4字节整数能寻址的最大空间为4G,因此inode的这种块号组织方式足够用了,除非使用的是64位系统。
进程通过字节偏移量来访问文件中的数据,那么就需要将偏移量转换成磁盘块号。这里需要考虑分级层次,因此在实现起来会有一定的麻烦。内核提供bmap()函数来将偏移量转换成磁盘块号。处理如下:
struct Location
{
BlockNo blkNo;
ByteOffset off;
Bytes nIO;
BlockNo readAheadNo;
};
Location * bmap(INode * anINode, ByteOffset off)
{
根据偏移量计算逻辑块号;
计算块中的起始字节;
计算要拷贝给用户的字节数;
检查是否要预读,标记inode;
确定间接索引的等级;
while (不是在一个需要的等级)
{
根据逻辑块号来计算inode或间接块的索引;
获取磁盘块号;
brelse(未释放的磁盘块);
若没有更多的间接索引等级则返回块号;
读取间接索引磁盘块;
根据间接索引的等级调整逻辑块号;
}
}
这个算法在实现起来比较复杂,在Linux的实现当中比较分散,此处就不列举代码。
将路径名映射到inode
目录与普通文件类似,只不过目录中数据存放的是目录项,该项包含的是inode号以及在该目录中的一个文件名(可以是目录)。路径名是一个以“/”分割的字符串。每个被“/”分开的部分称为一个组件。在UNIX system V中,组件最长为14个字节。
那么,如何将路径名映射到inode呢?每个进程都有一个u area,存放一个指向当前目录的指针。另外有一个全局变量存放根目录的inode。有了两个inode,映射就很简单了。
若一个进程要打开“/etc/passwd”文件,内核解析该路径名时,发现“/”,这样就将根目录作为工作目录,然后从该目录中(inode所包好的数据块中)查找etc,找到了匹配后得到了etc的inode号,然后读取etc的内容,在其中查找passwd,找到之后就可以返回对应的inode。处理过程如下:
INode * namei(String pathName) // 返回的是锁住的inode
{
INode * cwINode = NULL;
if (pathName[0] == '/')
cwINode = iget(rootINodeNO);
else
cwINode = iget(currentDirINode);
while (pathName中还有组件)
{
从pathName中读取下一个组件component;
ISDIR(component) && 进程对该inode有对应的权限(否则返回权限错误);
if (*cwINode is root INode && component == "..")
continue;
while (cwINode的数据还没有结束)
{
bmap()得到数据块号; bread()读取数据块;
if (在cwINode的数据中找到component)
{
从该项中读取inode号matchINodeNo;
iput(cwINode);
cwINode = iget(matchINodeNo);
brelse()释放数据块;
break;
}
else
{
brelse()释放数据块;
return (no inode);
}
}
}
return (cwINode);
}
在Linux 0.99.15的实现中,namei采用了递归的方式,有兴趣的可以参考其代码。
The Design of The UNIX Operation System, by Maurice J. Bach
Linux Kernel Source Code v0.99.15, by Linus Torvalds
Linux Kernel Source Code v2.6.22, by Linus Torvalds and Linux community.
Understanding The Linux Kernel, 3rd edition, by Daniel P. Bovet, Marco Cesati
Copyleft (C) 2007 raof01. 本文可以用于除商业用途外的所有用途。若要用于商业用途,请与作者联系。