
文件系统-inode相关操作
inode结构体
struct m_inode {
unsigned short i_mode;// 文件类型和属性(rwx 位)
unsigned short i_uid; // 用户 id(文件拥有者标识符)
unsigned long i_size;// 文件大小(字节数)
unsigned long i_mtime;// 修改时间(自 1970.1.1:0 算起,秒)
unsigned char i_gid;// 组 id(文件拥有者所在的组)
unsigned char i_nlinks; // 链接数(多少个文件目录项指向该 i 节点)
unsigned short i_zone[9]; // 直接(0-6)、间接(7)或双重间接(8)逻辑块号。 zone 是区的意思,可译成区段,或逻辑块。 指向数据块
};
/* these are in memory also */
struct task_struct * i_wait; // 等待该 i 节点的进程
unsigned long i_atime; // 最后访问时间 access time
unsigned long i_ctime; // i 节点自身修改时间 change time
unsigned short i_dev; //存储该文件所在的设备,dev_t中包括主设备号和次设备号
unsigned short i_num; // i 节点号
unsigned short i_count; // i 节点被使用的次数,0 表示该 i 节点空闲
unsigned char i_lock; // 锁定标志
unsigned char i_dirt; // 已修改(脏)标志
unsigned char i_pipe; // 管道标志
unsigned char i_mount; // 安装标志(挂载标志)
unsigned char i_seek; // 搜寻标志(用于lseek)
unsigned char i_update; // 更新标志
};
知道前面7个成员变量就能够知道文件的类型和属性
i_zone:是文件和磁盘的映射
i_zone[0]~i_zone[6]是直接块号,如果文件只使用7个逻辑块,那么数组中的每个元素则存储了一个逻辑块号i_zone[7]是一次间接块号,如果占用的逻辑块较多大于7个且小于512+7个,则占用一次间接块号i_zone[8]是二次间接块号,如果占用逻辑块太多大于512+7且小于512*512+7,则启动二次间接逻辑块
inode中的i_zone具体意义

如上图所示,可以看出来Linux0.1内核中文件最大大小为:7+512+512*512 KB
inode相关操作函数
inode相关操作函数在fs/inode.c中
释放dev设备的所有i节点invalidate_inodes
void invalidate_inodes(int dev)
{
int i;
struct m_inode * inode;
inode = 0+inode_table;
for(i=0 ; i<NR_INODE ; i++,inode++) {
wait_on_inode(inode);
if (inode->i_dev == dev) {
if (inode->i_count)
printk("inode in use on removed disk\n\r");
inode->i_dev = inode->i_dirt = 0; // 释放
}
}
}
使用一个m_inode指向inode_table ,然后逐个遍历数组中的每一个i节点,如果是这个设备的就把i节点的i_dev和i_dirt置为0
invalidate_inodes 函数的作用是在设备被卸载或移除时,逻辑上作废该设备在内存中缓存的所有 i 节点。它只将 i_dev 和 i_dirt 置为 0,而不清除其他字段,这是基于内核生命周期管理和安全性的精心设计
i_dev是识别 i 节点归属的唯一设备主键(配合i_num)。将它设为 0,相当于切断了该内存 i 节点与物理设备的关联。- 后续任何内核操作(如
iget查找、bmap读写)都会先检查i_dev。若为 0,该 i 节点会被视为“不属于任何设备”而被查找逻辑跳过,从而不会被误用于新设备。 - 如果
i_dirt保持为 1,sync_inodes或iput在释放时可能会触发write_inode,试图将内存中过时的 i 节点数据写回磁盘。 - 设备已移除或更换,写入会导致数据损坏或内核崩溃。置 0 直接阻止了这种危险的 I/O 操作。
其他字段都不需要清除
| 字段类型 | 为什么不清除 |
|---|---|
i_count(引用计数) | 绝对不能清零! 进程可能仍持有该文件的文件描述符(struct file 指向该 i 节点)。如果强行清零,后续进程关闭文件时 iput 会认为没人用了,可能触发 free_inode 或 truncate,导致内存损坏或访问已移除设备。只要 i_dev=0,iput 就会安全地跳过写盘操作,仅减少引用计数。 |
i_mode、i_size、i_zone(磁盘元数据) | 这些是数据负载,不是状态标志。保留它们无害,因为只要 i_dev=0,内核就不会再使用它们进行磁盘读写。即使进程通过 fstat 查看,看到的也是旧数据(对于已移除的设备,这通常是可以接受的)。 |
i_num(i节点号) | 查找时同时比对 i_dev 和 i_num。既然 i_dev=0,即使 i_num 没变,也不会被误匹配。 |
i_lock / i_wait(锁和等待队列) | 函数开头调用了 wait_on_inode,确保解锁状态。清零锁状态没有意义,因为该槽位可能很快被 get_empty_inode 复用,而 get_empty_inode 会通过 memset 彻底重置整个结构体。 |
同步i节点信息sync_inodes
// 同步内存与设备上的所有 i 节点信息
void sync_inodes(void)
{
int i;
struct m_inode * inode;
inode = 0+inode_table;
for(i=0 ; i<NR_INODE ; i++,inode++) {
wait_on_inode(inode);
if (inode->i_dirt && !inode->i_pipe)
write_inode(inode); // 写盘
}
}
在函数中遍历inode节点,并且这里的判断inode->i_dirt && !inode->i_pipe表示的是inode节点信息需要更新,并且不是pipe文件,才会去调用write_inode
为什么要不是pipe文件呢,是因为pipe文件是用于进程间通信的存放在内存中的文件,它是不需要写盘的
将i节点写入设备write_inode
将从内存中传入的inode节点信息写入磁盘
//// 将指定 i 节点信息写入设备(写入对应的缓冲区中,待缓冲区刷新时会写入盘中)
static void write_inode(struct m_inode * inode)
首先锁定这个inode,然后判断i_dirt是否为1需要写盘和是不是在某个设备上的
struct super_block * sb;
struct buffer_head * bh;
int block;
lock_inode(inode);
if (!inode->i_dirt || !inode->i_dev) {
unlock_inode(inode);
return;
}
然后获取这个inode所在设备的超级块
if (!(sb=get_super(inode->i_dev)))
panic("trying to write inode without device");
接着就是计算这个inode所对应的块号是多少
block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +
(inode->i_num-1)/INODES_PER_BLOCK;
- 这里的
2是代表的文件系统最开始的引导块和超级块共占两个块号 s_imap_blocks和s_zmap_blocks表示i节点位图的块个数和逻辑块位图块个数i_num是当前i节点的节点号,除以INODES_PER_BLOCK(每个块的i节点个数)则得到当前i节点在i节点中的哪个块
if (!(bh=bread(inode->i_dev,block)))
panic("unable to read i-node block");
((struct d_inode *)bh->b_data)
[(inode->i_num-1)%INODES_PER_BLOCK] =
*(struct d_inode *)inode;
找到块号后就可以去读取当前设备这个块号对应的高速缓冲区(CPU不直接和磁盘打交道,都是通过高速缓冲区的),读取到高速缓冲区块后将当前inode信息写入高速缓冲区
((struct d_inode *)bh->b_data)
[(inode->i_num-1)%INODES_PER_BLOCK] =
在这里这个结构体拷贝是直接一个结构体等于另一个结构体,应该是当时的编译器的原因,在现代C语言编译器是不允许结构体这样赋值的,一般使用memcpy或其他按字节方式拷贝
bh->b_dirt=1;
inode->i_dirt=0;
brelse(bh);
unlock_inode(inode);
最后将bh的b_dirt标志为1,等待下一次sync将高速缓冲区写入磁盘,然后将i_dirt标志为0表示数据已经更新,再让当前进程释放掉bh,解锁这块inode
sys_sync系统调用
在fs/buffer.c中有一个sync函数的系统调用
//系统调用,刷缓存, 同步设备和内存高速缓冲中数据; 将缓存中的数据写入设备?
int sys_sync(void)
{
int i;
struct buffer_head * bh;
sync_inodes(); /* write out inodes into buffers */
bh = start_buffer;
// 扫描所有高速缓冲区,对于已被修改的缓冲块产生写盘请求,将缓冲中数据与设备中同步
for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
wait_on_buffer(bh);
if (bh->b_dirt)
ll_rw_block(WRITE,bh); // 产生写设备块请求
}
return 0;
}
可以看到在这里会调用sync_inodes函数触发同步inode信息,最后调用ll_rw_block写磁盘函数
获取一个空闲i节点get_empty_inode
// 从 i 节点表(inode_table)中获取一个空闲 i 节点项
struct m_inode * get_empty_inode(void)
首先定义一个static全局变量last_inode最开始指向inode_table头
然后通过while循环去获取一个空闲的inode
do {
inode = NULL;
for (i = NR_INODE; i ; i--) {
if (++last_inode >= inode_table + NR_INODE)
last_inode = inode_table;
if (!last_inode->i_count) {
inode = last_inode; // ====== key ======
if (!inode->i_dirt && !inode->i_lock)
break;
}
}
if (!inode) {
for (i=0 ; i<NR_INODE ; i++)
printk("%04x: %6d\t",inode_table[i].i_dev,
inode_table[i].i_num);
panic("No free inodes in mem");
}
wait_on_inode(inode);
while (inode->i_dirt) {
write_inode(inode);
wait_on_inode(inode);
}
} while (inode->i_count);
在这里遍历整个inode_table,并且如果当last_inode指向了inode_table最后一个,那么就让他指向inode_table头,而i = NR_INODE; i ; i--保证不管last_inode起始在哪里都能遍历完整个inode_table数组
如果找到一个i_count为0的节点,并且没有脏数据和没有上锁,那么就跳出循环说明找到了;否则就会去等待直到有空闲的inode出现
memset(inode,0,sizeof(*inode));
inode->i_count = 1;
return inode;
在最后将获得的inode节点内部信息清空然后标识i_count为1表示当前进程在使用,返回这个inode节点
(重难)块映射处理_bmap
/**
* @brief 块映射处理,为指定i节点的指定块号分配逻辑块
*
* @param inode i节点
* @param block 数据块号
* @param create 创建标志,如果置位,则在对应逻辑块不存在时就申请新的磁盘块
* @return int 数据块对应在设备上的逻辑块号
*/
static int _bmap(struct m_inode * inode,int block,int create)
这个函数是为i节点的指定块号分配一个逻辑块
注意:block是“文件内逻辑块号(File Logical Block Number)”,它等同于 文件当前读写位置相对于文件开头的偏移量(字节)除以磁盘块大小(1KB)。
根据上面i_zone的表示图可知,一共分为三种情况
block<7
struct buffer_head * bh;
int i;
if (block<0)
panic("_bmap: block<0");
if (block >= 7+512+512*512) // 如果块号大于直接块数 + 间接块数 + 二次间接块数,超出文件系统表示范围,则死机
panic("_bmap: block>big");
if (block<7) {
if (create && !inode->i_zone[block])
if (inode->i_zone[block]=new_block(inode->i_dev)) { // ======= key =======
inode->i_ctime=CURRENT_TIME;
inode->i_dirt=1;
}
return inode->i_zone[block];
}
如果block小于7那么在i_zone中就是使用的直接块号存储
如果create为1要申请盘块,并且i_zone中对应block位还没有数据,那么就为i_zone[block]申请一块新的盘块,并且inode的i_ctime节点自身修改时间更新,i_dirt表示i节点需要写盘更新
最后返回得到的物理盘块号
7<block<512
block -= 7;
if (block<512) {
if (create && !inode->i_zone[7])
if (inode->i_zone[7]=new_block(inode->i_dev)) {
inode->i_dirt=1;
inode->i_ctime=CURRENT_TIME;
}
if (!inode->i_zone[7])
return 0;
if (!(bh = bread(inode->i_dev,inode->i_zone[7])))
return 0;
i = ((unsigned short *) (bh->b_data))[block];
if (create && !i)
if (i=new_block(inode->i_dev)) {
((unsigned short *) (bh->b_data))[block]=i;
bh->b_dirt=1;
}
brelse(bh);
return i;
}
如果要申请的文件逻辑块号7<block<512,那么就要启用间接逻辑块号
1.首先如果i_zone[7]里面还没有数据,也就是在之前inode节点都还没有用到间接逻辑块号,就先为间接逻辑块号i_zone[7]分配一个盘块,并且更新i节点自身信息
if (!inode->i_zone[7])用于处理异常情况
2.当i_zone[7]有盘块后,就去读取这个盘块的信息到高速缓冲区bread
3.在这里代码有段i = ((unsigned short *) (bh->b_data))[block];
这里表示的查看间接盘块的对应block区域是否已经有值了
- 每一个
blcok在间接盘块对应一项,所以哪怕bh->b_data其他项为0也不影响block这一项有数据(对应文件内逻辑块号block的一个盘块) - 这里先给
i赋值(bh->b_data))[block]是因为有可能本来这一项就有盘块映射了,_bmap除了映射还有查找文件内逻辑块号block映射到了什么盘块号的功能
4.if (create && !i)表示如果要创建映射并且前面i并没有读取到值(之前没有建立这个block的映射),那么下面的逻辑就是创建映射
- 首先在dev上申请一个盘块,返回盘块的逻辑块号
- 然后让
(bh->b_data))[block]存储这个逻辑块号 - 更新间接逻辑块
i_zone[7]的高速缓冲区信息
5.最后释放掉分配的高速缓冲区,然后返回创建/查找到的文件内逻辑块号block映射的盘块号
block>512
block -= 512;
if (create && !inode->i_zone[8])
if (inode->i_zone[8]=new_block(inode->i_dev)) {
inode->i_dirt=1;
inode->i_ctime=CURRENT_TIME;
}
if (!inode->i_zone[8])
return 0;
if (!(bh=bread(inode->i_dev,inode->i_zone[8])))
return 0;
i = ((unsigned short *)bh->b_data)[block>>9];
if (create && !i)
if (i=new_block(inode->i_dev)) {
((unsigned short *) (bh->b_data))[block>>9]=i;
bh->b_dirt=1;
}
brelse(bh);
if (!i)
return 0;
if (!(bh=bread(inode->i_dev,i)))
return 0;
i = ((unsigned short *)bh->b_data)[block&511];
if (create && !i)
if (i=new_block(inode->i_dev)) {
((unsigned short *) (bh->b_data))[block&511]=i;
bh->b_dirt=1;
}
brelse(bh);
return i;
对于block大于512的情况其实本质上和前面相似,只是多了一次分配块号给二级间接块
需要注意的几个地方:
1.读取二级索引表到高速缓冲区,并定位对应的一级索引块号
if (!(bh=bread(inode->i_dev,inode->i_zone[8])))
return 0;
i = ((unsigned short *)bh->b_data)[block>>9];
- 既然
i_zone[8]已有盘块号,就通过bread将这块二级索引表读入高速缓冲区。 - 这里表示查看二级索引表中对应的“一级索引块”区域是否已经有值了
block>>9即block / 512。因为每个一级索引块可以管理 512 个数据块,所以要先用block / 512算出:这个文件逻辑块对应的是第几个一级索引块。bh->b_data里存储的是一个unsigned short数组(共 512 项),[block>>9]取出其中一项,赋值给i。- 这里先给
i赋值,正是为了发挥_bmap的查找功能:如果该项已存在(即之前已经为这一组数据块分配过一级索引表),则直接拿到该一级索引表的盘块号,不需要重复创建。
2.读取一级索引表,定位最终的数据块号
if (!i)
return 0;
if (!(bh=bread(inode->i_dev,i)))
return 0;
i = ((unsigned short *)bh->b_data)[block&511];
- 上一步我们拿到了“一级索引块”的盘块号(存储在
i中)。如果i为 0,说明没有对应的一级索引块,直接返回 0。 - 通过
bread将这一级索引表读入高速缓冲区。 - 这里表示查看一级索引表中对应的数据块区域是否已经有值了
block & 511即block % 512。因为每个一级索引块正好有 512 项,block的低 9 位决定了它属于该一级索引表管理范围内的第几个数据块。- 将该项的值赋给
i,同样是兼顾查找功能:如果此项已存在,则直接拿到了最终文件数据的盘块号。
_bmap函数的上层封装
//// 根据 i 节点信息取数据块 block 在设备上对应的逻辑块号, 不创建
int bmap(struct m_inode * inode,int block)
{
return _bmap(inode,block,0);
}
//// 根据 i 节点信息取数据块 block 在设备上对应的逻辑块号, 创建
int create_block(struct m_inode * inode, int block)
{
return _bmap(inode,block,1);
}
释放一个i节点iput
//// 释放一个 i 节点(回写入设备)
void iput(struct m_inode * inode)
这个函数是让当前调用的进程释放传入的inode,并且如果当前进程是最后一个进程那么就进行inode的资源释放工作
if (!inode)
return;
wait_on_inode(inode); // 等待 inode 节点解锁(如果已上锁的话)
if (!inode->i_count)
panic("iput: trying to free free inode");
// 如果是管道 i 节点,则唤醒等待该管道的进程,引用次数减 1,如果还有引用则返回。否则释放
// 管道占用的内存页面,并复位该节点的引用计数值、已修改标志和管道标志,并返回
if (inode->i_pipe) {
wake_up(&inode->i_wait);
if (--inode->i_count)
return;
free_page(inode->i_size);
inode->i_count=0;
inode->i_dirt=0;
inode->i_pipe=0;
return;
}
//错误情况
if (!inode->i_dev) {
inode->i_count--;
return;
}
首先是获取到inode节点并且如果是管道节点,那么就去唤醒持有这个管道的所有进程
并且如果当前进程是最后一个持有这个管道的,那么就进行资源释放工作,清理inode的内存,以及把几个标志位清零
if (S_ISBLK(inode->i_mode)) {
sync_dev(inode->i_zone[0]);//块设备的inode磁盘映射的第一个直接块号存储他的设备号
wait_on_inode(inode);
}
这里S_ISBLK查看当前inode代表的是否是一个块设备文件
这段代码位于 iput(释放 inode 引用)函数中。当进程关闭一个打开的块设备文件(例如 close(fd))时:
- 内核在释放该 inode 前,需要确保所有针对该设备的内存脏缓冲区都已经写入磁盘。
sync_dev(设备号)的作用就是:强制将该设备的所有脏块(buffer_head)同步回物理磁盘。- 紧接着的
wait_on_inode(inode)是等待该 inode 解锁,确保同步操作完成。
repeat:
if (inode->i_count>1) { // 引用计数递减
inode->i_count--;
return;
}
if (!inode->i_nlinks) { // 链接释放
truncate(inode);
free_inode(inode);
return;
}
if (inode->i_dirt) { // 如果该 i 节点已作过修改,则更新该 i 节点,并等待该 i 节点解锁
write_inode(inode); /* we can sleep - so do again */
wait_on_inode(inode);
goto repeat;
}
inode->i_count--;
return;
repeat段下的内容是进行资源释放的操作
- 首先如果
i_count大于1,就代表当前进程不是最后一个持有这个inode的进程,所以直接把引用计数减1就返回 - 如果当前进程是最后一个持有的,并且
i_nlinks为0表示没有文件链接当前inode, truncate(inode);:释放所有数据块。把i_zone[9]中记录的、以及间接块里指向的所有磁盘数据块全部释放回空闲位图(归还磁盘空间)。此时文件再也无法恢复数据了。free_inode(inode);:释放 inode 自身。在 inode 位图中将该 inode 标记为“空闲”,并把内存inode_table中的该槽位清空(准备复用)。 这段代码是 Linux 内核处理 “已删除(unlink)但仍在打开状态” 的经典逻辑,俗称 “幽灵文件”(Ghost File) 的收尾工作。也就是当你后台打开一个文件编辑的时候,前台又调用了rm删除了这个文件rm命令调用unlink系统调用,由于目录中不再有这个文件名,内核将inode->i_nlinks从1减为0。- 但是! 后台进程依然持有该文件的文件描述符,因此
inode->i_count仍为1(或更大)。
如果i_dirt为1表示需要更新inode,则调用write_inode去更新inode然后等待解锁重新repeat流程
