Linux内核-文件系统-inode相关操作
2026/8/2 3:37:34 网站建设 项目流程

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,则启动二次间接逻辑块

原文链接:Linux内核-文件系统-inode相关操作 – kidwjb的小站

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_devi_dirt置为0

invalidate_inodes函数的作用是在设备被卸载或移除时,逻辑上作废该设备在内存中缓存的所有 i 节点。它只将i_devi_dirt置为 0,而不清除其他字段,这是基于内核生命周期管理和安全性的精心设计

  • i_dev是识别 i 节点归属的唯一设备主键(配合i_num)。将它设为 0,相当于切断了该内存 i 节点与物理设备的关联。

  • 后续任何内核操作(如iget查找、bmap读写)都会先检查i_dev。若为 0,该 i 节点会被视为“不属于任何设备”而被查找逻辑跳过,从而不会被误用于新设备

  • 如果i_dirt保持为 1,sync_inodesiput在释放时可能会触发write_inode,试图将内存中过时的 i 节点数据写回磁盘。

  • 设备已移除或更换,写入会导致数据损坏或内核崩溃。置 0 直接阻止了这种危险的 I/O 操作。

其他字段都不需要清除

字段类型为什么不清除
i_count(引用计数)绝对不能清零!进程可能仍持有该文件的文件描述符(struct file指向该 i 节点)。如果强行清零,后续进程关闭文件时iput会认为没人用了,可能触发free_inodetruncate,导致内存损坏或访问已移除设备。只要i_dev=0iput就会安全地跳过写盘操作,仅减少引用计数
i_modei_sizei_zone(磁盘元数据)这些是数据负载,不是状态标志。保留它们无害,因为只要i_dev=0,内核就不会再使用它们进行磁盘读写。即使进程通过fstat查看,看到的也是旧数据(对于已移除的设备,这通常是可以接受的)。
i_num(i节点号)查找时同时比对i_devi_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_blockss_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);

最后将bhb_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区域是否已经有值了

  1. 每一个blcok在间接盘块对应一项,所以哪怕bh->b_data其他项为0也不影响block这一项有数据(对应文件内逻辑块号block的一个盘块)

  2. 这里先给i赋值(bh->b_data))[block]是因为有可能本来这一项就有盘块映射了,_bmap除了映射还有查找文件内逻辑块号block映射到了什么盘块号的功能

4.if (create && !i)表示如果要创建映射并且前面i并没有读取到值(之前没有建立这个block的映射),那么下面的逻辑就是创建映射

  1. 首先在dev上申请一个盘块,返回盘块的逻辑块号

  2. 然后让(bh->b_data))[block]存储这个逻辑块号

  3. 更新间接逻辑块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>>9block / 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 & 511block % 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_nlinks1减为0

  • 但是!后台进程依然持有该文件的文件描述符,因此inode->i_count仍为1(或更大)。

如果i_dirt为1表示需要更新inode,则调用write_inode去更新inode然后等待解锁重新repeat流程

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询