早期的UNIX系统最重要的两大功能是:文件存储/访问,任务/进程调度(多任务)。由这两大功能衍生出了内存管理,设备管理,用户接口等功能。在这里就来说说其中第一个重要的功能:文件系统。
在UNIX系统上,所有一切都被当成文件来对待,包括设备。因此,就需要一个系统来管理这些文件,并提供一个统一的操作接口。inode就是用来管理文件的一个数据结构(或者说是模型、类)。每个文件在磁盘上都对应一个inode,在访问文件时,inode会被加载到内存中,使用结束后内存中的inode被释放。如果删除文件(unlink),磁盘上的inode及文件的数据都会被释放。
inode是一个文件在系统中的惟一标识。一个文件可以由多个名字(link),但是只占用一个inode。当文件处于active状态时(比如被打开),内存中保留一份inode的拷贝,与inode一起保留在内存中的对象成为inode的核内拷贝(in-core copy,以下用核内inode代替)。
inode结构
如上所示,inode有磁盘inode和核内inode。其实大部分情况下说的都是存放在磁盘上的inode。一个inode对象包含如下成员:
- 文件属主。分成单个属主和组属主。
- 文件类型:普通文件、目录、设备文件、FIFO等
- 访问权限
- 文件访问时间:上次的文件修改/访问/inode修改时间
- 链接数:至少是1
- 文件数据块的地址表
- 文件大小
更改属主、文件类型、访问权限、链接设置,修改文件内容都将导致对inode的修改,而修改文件内容还将更新文件修改时间及访问时间。
核内inode(In-core copy of inode)
除了磁盘inode的信息,核内inode还包括如下成员:
- 状态。指示是否锁定、有进程等待、核内inode是否与磁盘inode不同(包括对文件数据及inode本身的修改)、挂接点
- 逻辑设备号
- inode号
- 指向其他核内inode的指针。主要是用于将其放在hash Q与free list上进行操作。
- 引用计数
其中引用计数是一个相当重要的成员。它用来指示有多少个活动的文件实例,比如当一个文件被第一次打开,inode被加载到内核并放到hash Q上,这是核内inode的引用计数为1。此时另一个进程再打开该文件,则无须再在内核中保留一份拷贝,增加引用计数即可。当它为0时,inode被释放——置入free list中。此时hash Q上依然有该inode,以便下一次访问时不必从硬盘加载数据。
Linux 0.99.15中,inode定义如下(inode和核内inode在此被统一了):
struct inode {
dev_t i_dev; // 设备号,用于核内inode
unsigned long i_ino; // inode号:因为在磁盘上连续存储,通过索引就可找到某个inode
// 用于核内inode
umode_t i_mode;
nlink_t i_nlink; // 链接设置
uid_t i_uid; // 属主信息
gid_t i_gid;
dev_t i_rdev;
off_t i_size;
time_t i_atime; // 时间戳
time_t i_mtime;
time_t i_ctime;
unsigned long i_blksize;
unsigned long i_blocks;
struct inode_operations * i_op; // 对inode及文件的操作集合。该结构体包含对inode及文件
// 操作的函数指针,针对不同的inode,
// 可以设置不同的操作集合。
// 这里有一点多态的影子。做过设备驱动的同学对此会有印象。
struct super_block * i_sb; // 超级块,日后再议
struct wait_queue * i_wait; // 等待队列。用于同步(睡眠/唤醒)
struct file_lock * i_flock;
struct vm_area_struct * i_mmap; // 映射的内存区
struct inode * i_next, * i_prev; // 用于核内inode
struct inode * i_hash_next, * i_hash_prev; // 用于核内inode
// 剩余的信息貌似与本文主题无关,暂不讨论。日后再议
// ……
unsigned short i_count; // 引用计数。用于核内inode
// ……
};
核内inode的分配与回收
分配:
INode iget(int fs_inode_no)
{
while (未完成)
{
if (inode在cache中)
{
if (inode被锁住)
{
sleep (inode解锁事件);
重试;
}
// 对挂接点的特殊处理。日后再议
if (inode在free list上)
从free list移出;
++引用计数;
返回该inode;
}
// inode不在cache中
if (没有可用inode)
返回错误; // 因为inode占用时间较长,从文件打开到关闭通常要很久,
// 此处进程选择等待将导致挂死。这里处理与getblk()不同
从free list移出;
重置inode号、文件系统号;
放到新的hash Q中;
从磁盘上读取bread();
初始化其他成员如引用计数;
返回该inode;
}
}
由于inode的标识是fs NO和inode NO,因此从磁盘上读取时,需要做如下转换(因为一次读取一个磁盘块,而inode通常比磁盘块小):
块号 = ((inode号 -1) / 每块的inode数) + 磁盘inode首块块号
读到inode所在块后,就可以计算inode在块中偏移量:
偏移量 = ((inode号 - 1) % 每块的inode数) * 磁盘inode大小
还是来看看Linux 0.99.15的实现:
struct inode * __iget(struct super_block * sb, int nr, int crossmntp)
{
static struct wait_queue * update_wait = NULL;
struct inode_hash_entry * h;
struct inode * inode;
struct inode * empty = NULL;
if (!sb)
panic("VFS: iget with sb==NULL");
h = hash(sb->s_dev, nr); // 获取fs号
repeat:
for (inode = h->inode; inode ; inode = inode->i_hash_next)
if (inode->i_dev == sb->s_dev && inode->i_ino == nr)
goto found_it; // 在hash Q上找到
// 未在hash Q上找到
if (!empty) {
h->updating++;
empty = get_empty_inode();
if (!--h->updating)
wake_up(&update_wait);
if (empty)
goto repeat;
return (NULL); // free list上无可用inode
}
inode = empty;
// 设置各成员值
inode->i_sb = sb;
inode->i_dev = sb->s_dev;
inode->i_ino = nr;
inode->i_flags = sb->s_flags;
put_last_free(inode);
// 放到新的hash Q上
insert_inode_hash(inode);
// 从磁盘读取inode
read_inode(inode);
goto return_it;
found_it:
if (!inode->i_count)
nr_free_inodes--;
inode->i_count++;
wait_on_inode(inode);
if (inode->i_dev != sb->s_dev || inode->i_ino != nr) {
printk("Whee.. inode changed from under us. Tell Linus\n");
iput(inode);
goto repeat;
}
// 跨挂接点
if (crossmntp && inode->i_mount) {
struct inode * tmp = inode->i_mount;
tmp->i_count++;
iput(inode);
inode = tmp;
wait_on_inode(inode);
}
if (empty)
iput(empty);
return_it:
while (h->updating)
sleep_on(&update_wait);
return inode;
}
回收:
void iput (In_Core_Inode * iNode)
{
加锁;
--引用计数;
if (引用计数为0)
{
if (链接计数为0)
{
释放文件所占磁盘块;
设置文件类型为0;
释放磁盘inode,ifree();
}
if (inode被更新)
将更新写到磁盘inode;
将该核内inode放到free list上;
}
解锁;
}
Linux 0.99.15的实现:
void iput(struct inode * inode)
{
if (!inode)
return;
wait_on_inode(inode); // 等待该inode被释放
// ……
if (inode->i_pipe)
wake_up_interruptible(&PIPE_WAIT(*inode));
repeat:
if (inode->i_count>1) {
inode->i_count--; // 减少引用计数
return;
}
wake_up(&inode_wait); // 解锁
if (inode->i_pipe) {
unsigned long page = (unsigned long) PIPE_BASE(*inode);
PIPE_BASE(*inode) = NULL;
free_page(page);
}
if (inode->i_sb && inode->i_sb->s_op && inode->i_sb->s_op->put_inode) {
inode->i_sb->s_op->put_inode(inode);
if (!inode->i_nlink) // 链接数为0,此处并未释放磁盘inode
return;
}
if (inode->i_dirt) {
write_inode(inode); /* we can sleep - so do again */
wait_on_inode(inode);
goto repeat;
}
inode->i_count--;
nr_free_inodes++;
return;
}
由于Linux中不区分核内inode和磁盘inode,因此实现上与这里描述的算法有较大差别。
参考:
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. 本文可以用于除商业用途外的所有用途。若要用于商业用途,请与作者联系。