Linux内核-文件系统-inode相关操作

文件系统-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具体意义

izone总览drawiopng

如上图所示,可以看出来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流程

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇