Linux内核-文件系统-位图操作

Linux内核-文件系统-位图操作

inode节点概念

文件与磁盘的映射

文件系统概念

不管读取什么磁盘上的资源,都是先getblk(获取该资源对应的设备和块号的高速缓冲区),然后在bread(确认有效数据的高速缓冲区),最后再进行区域内存的拷贝

一个设备的文件系统框架

一个设备的文件系统框架png

所有块的大小都是固定的,称为盘块或磁盘块

扇区:块设备上一个长度为512B的数据块

在不同的文件系统中扇区和盘块的对应关系不同

  • 两个扇区对应一个盘块–>1024B
  • 四个扇区对应一个盘块–>2048B

i节点位图块中的一个比特对应一个i节点,如果是两个扇区一个盘块,那么i节点就是1024*8 = 8191个i节点,因为i节点位图的第0位不用

逻辑块位图中一个比特对应一个逻辑块


超级块结构体

在超级块结构体中有

// 内存中磁盘超级块结构
struct super_block {
    unsigned short s_ninodes;        // 节点数
    unsigned short s_nzones;         // 逻辑块数
    unsigned short s_imap_blocks;    // i 节点位图所占用的数据块数
    unsigned short s_zmap_blocks;    // 逻辑块位图所占用的数据块数
    unsigned short s_firstdatazone;  // 第一个数据逻辑块号,相对于引导块而言
    unsigned short s_log_zone_size;  // log(数据块数/逻辑块)
    unsigned long s_max_size;        // 文件最大长度
    unsigned short s_magic;
/* These are only in memory */
    struct buffer_head * s_imap[8];    // i 节点位图缓冲块指针数组(占用 8 块,可表示 64M)
    struct buffer_head * s_zmap[8];    // 逻辑块位图缓冲块指针数组
    unsigned short s_dev;              // 超级块所在的设备号
    struct m_inode * s_isup;           // 被安装的文件系统根目录的 i 节点。(isup-super i)
    struct m_inode * s_imount;         // 被安装到的 i 节点
    unsigned long s_time;              // 修改时间
    struct task_struct * s_wait;       // 等待该超级块的进程
    unsigned char s_lock;              // 被锁定标志
    unsigned char s_rd_only;           // 只读标志
    unsigned char s_dirt;              // 已修改标志(脏标志)
};

其中的

struct buffer_head * s_imap[8];    // i 节点位图缓冲块指针数组(占用 8 块,可表示 64M)
struct buffer_head * s_zmap[8];    // 逻辑块位图缓冲块指针数组

指的是指向i节点位图和指向逻辑块位图的buffer_head指针数组,在这里每个数组都有八项,说明在当前linux0.1内核中一个文件系统支持8个i节点位图8个逻辑块位图

那么对应逻辑块的最大大小:

8(个缓冲区头)* 1024(一个逻辑块位图1024字节) * 8(一个字节8个bit)* 1024(1个bit对应一个块) = 64M

i节点同理

位图操作源码bitmap.c

释放逻辑块free_block

 //// 释放设备 dev 上数据区中的逻辑块 block。 
 // 复位指定逻辑块 block 的逻辑块位图比特位
void free_block(int dev, int block)

释放逻辑块总共分为两个操作:

释放掉指定的逻辑块

    struct super_block * sb;
    struct buffer_head * bh;

    if (!(sb = get_super(dev)))       //get super block
        panic("trying to free block on nonexistent device");
    if (block < sb->s_firstdatazone || block >= sb->s_nzones)  // 若逻辑块号小于首个逻辑块号或者大于设备上总逻辑块数
        panic("trying to free block not in datazone");
    bh = get_hash_table(dev,block);        // 在hash表中找到该数据块
    if (bh) {
        if (bh->b_count != 1) {
            printk("trying to free block (%04x:%d), count=%d\n",
                dev,block,bh->b_count);
            return;
        }
        bh->b_dirt=0;            // 复位脏(已修改)标志位
        bh->b_uptodate=0;        // 复位更新标志
        brelse(bh);                // 释放缓冲块
    }

在最开始调用get_super函数获取得到这个设备对应的超级块,然后检查传入的逻辑块号是否是合理的

接着就使用get_hash_table在哈希表中获取得到对应设备异或逻辑块号的高速缓冲区块

如果得到了逻辑块,查看是否只有当前进程使用这个逻辑块,如果不是的话说明还有进程正在使用,还不能释放;如果只有当前进程使用,那么就将b_dirtb_uptodate复位,并且释放缓冲块

注意:这里为什么没有调用getblk获取对应的高速缓冲区块,应该是本来getblk就会进行复位操作,这样子就多此一举的

复位指定逻辑块对应的逻辑块位图中的比特位

    block -= sb->s_firstdatazone - 1 ;
    // 计算 block 在数据区开始算起的逻辑块号(从 1 开始计数)。然后对逻辑块(区段)位图进行操作,复位对应的比特位
    if (clear_bit(block&8191,sb->s_zmap[block/8192]->b_data)) {
        printk("block (%04x:%d) ",dev,block+sb->s_firstdatazone-1);
        panic("free_block: bit already cleared");
    }
    // 置相应逻辑块位图所在缓冲区已修改标志。
    sb->s_zmap[block/8192]->b_dirt = 1;
  • block -= sb->s_firstdatazone - 1 ;这里是算出block相对于逻辑块开始的块号s_firstdatazone的数值,因为逻辑块号是相对于引导块而言的
  • clear_bit(block&8191,sb->s_zmap[block/8192]->b_data)这里使用了一个嵌入式汇编宏clear_bit(nr,addr)
//// 复位指定地址开始的第 nr 位偏移处的比特位。返回原比特位的反码(1 或 0)。 
// 输入:%0 - eax(返回值),%1 - eax(0);%2 - nr,位偏移值;%3 - (addr),addr 的内容。
#define clear_bit(nr,addr) 

参数第一项:block&8191,这里是让与操作得到的结果只能是[0,8191]

参数第二项:sb->s_zmap[block/8192]->b_data,这里block/8192是每8192个block作为数组的一项

二者结合起来看,是因为struct buffer_head * s_zmap[8];逻辑块位图一共有八个,对于每一个单独的逻辑块位图他们的bit位置是相同的,所以找到了对于s_zmap指针数组,他指向的b_data就是一块逻辑块位图,然后第一项得到要复位这块逻辑块位图的第几位

最后记得将这个逻辑块位图对应高速缓冲区头中的b_dirt置1,表明这个高速缓冲区已经被修改了

申请逻辑块new_block

 ////向设备 dev 申请一个逻辑块(磁盘块,区段)。返回逻辑块号。 
 // 置位指定逻辑块 block 的逻辑块位图比特位。
int new_block(int dev)

该函数用于向指定的设备申请一个逻辑块

主要分为三个操作

获取逻辑块位图空闲比特位

    struct buffer_head * bh;
    struct super_block * sb;
    int i,j;

    if (!(sb = get_super(dev)))
        panic("trying to get new block from nonexistant device");
    j = 8192;
    for (i=0 ; i<8 ; i++)
        if (bh=sb->s_zmap[i])
            if ((j=find_first_zero(bh->b_data))<8192)
                break;
    if (i>=8 || !bh || j>=8192)
        return 0;

首先也是先从根据设备号获取得到指定的超级块,因为超级块存储着逻辑块位图和i节点位图信息

然后就是通过一个for循环去寻找逻辑块位图中第一个有空闲的比特位

  • for (i=0 ; i<8 ; i++)是因为s_zmap数组**一共八项,**每一项管理一块逻辑块位图,然后让bh=sb->s_zmap[i]在每一块位图上试
  • j=find_first_zero(bh->b_data))<8192表示如果找到了第一个为0的比特位,并且没有超过这个位图块的大小,那么就是找到了

最后进行一个简单的判断查看找到的比特是否在合理的位置

置位逻辑块位图

    if (set_bit(j,bh->b_data))
        panic("new_block: bit already set");
    bh->b_dirt = 1;
    j += i*8192 + sb->s_firstdatazone-1;
    if (j >= sb->s_nzones)
        return 0;

经过上面for循环找到了比特位和对应的逻辑块位图的高速缓冲区,那么就通过set_bit宏去设置这个比特位

//// 置位指定地址开始的第 nr 个位偏移处的比特位(nr 可以大于 32!)。返回原比特位(0 或 1)。 
// 输入:%0 - eax(返回值),%1 - eax(0);%2 - nr,位偏移值;%3 - (addr),addr 的内容。
#define set_bit(nr,addr)

置位后说明高速缓冲区有数据更新,将b_dirt置1

j += i*8192 + sb->s_firstdatazone-1是计算出该比特位在整个设备内存中的位置,j表示他指向的逻辑块号,如果超过了最大的逻辑块号就报错

获取逻辑块号对应的高速缓冲区块

    if (!(bh=getblk(dev,j)))         //==============key============
        panic("new_block: cannot get block");
    if (bh->b_count != 1)
        panic("new block: count is != 1");
    clear_block(bh->b_data);
    bh->b_uptodate = 1;
    bh->b_dirt = 1;
    brelse(bh);
    return j;

最后是获取设备号和该逻辑块号对应的高速缓冲区块getblk,拿到这个对应的高速缓冲区块之后会去判断是否b_count为1


它不是为了处理正常情况,而是为了捕捉文件系统逻辑上的严重错误(Bug)。 在正确的内核逻辑下,getblk 返回的 b_count 必须等于 1

new_block 的作用是分配一个全新的、空闲的磁盘块。它的查找依据是逻辑块位图(s_zmap

  • 在调用 getblk 之前:函数已经通过 find_first_zero 在位图中找到了一个明确标记为“空闲(0)” 的比特位,并立即调用 set_bit 将其置为“占用(1)”。
  • 关键推论:既然这个磁盘块号 j 在位图中是“空闲”的,那就意味着当前文件系统中没有任何一个文件(inode)指向这个块号。因此,理论上绝对不可能有任何进程正在通过文件系统路径访问这个块

既然没有进程通过文件系统访问它,那么当 getblk 去哈希表里查找 (dev, j) 时,可能出现的只有两种情况:

情况哈希表中是否存在?b_count 原本是多少?getblk 后的 b_count结果
情况 A(缓存残留)存在(之前用过,释放后还在缓存里)0(无人使用,但数据残留)0 → 1正常
情况 B(全新分配)不存在从空闲链表拿新块,置为 1正常

在这两种正常场景下,getblk 返回时 b_count 都是 1


然后调用clear_block宏将这块高速缓冲区数据清空

// 将指定地址(addr)处的一块内存清零。嵌入汇编程序宏。 256B
// 输入:eax = 0,ecx = 数据块大小 BLOCK_SIZE/4,edi = addr。
#define clear_block(addr) 

清空后表明这块高速缓冲区数据更新了,把b_uptodateb_dirt都置1。然后释放掉这块内存的占用brelse

释放指定i节点free_inode

 //// 释放指定的 i 节点。 
 // 复位对应 i 节点位图比特位
void free_inode(struct m_inode * inode)
{
    struct super_block * sb;
    struct buffer_head * bh;

    if (!inode)
        return;
    if (!inode->i_dev) {
        memset(inode,0,sizeof(*inode));
        return;
    }
    if (inode->i_count>1) {
        printk("trying to free inode with count=%d\n",inode->i_count);
        panic("free_inode");
    }
    if (inode->i_nlinks)
        panic("trying to free inode with links");
    if (!(sb = get_super(inode->i_dev)))
        panic("trying to free inode on nonexistent device");
    if (inode->i_num < 1 || inode->i_num > sb->s_ninodes)
        panic("trying to free inode 0 or nonexistant inode");
    if (!(bh=sb->s_imap[inode->i_num>>13]))
        panic("nonexistent imap in superblock");
    if (clear_bit(inode->i_num&8191,bh->b_data))
        printk("free_inode: bit already cleared.\n\r");
    bh->b_dirt = 1;
    memset(inode,0,sizeof(*inode));
}

这里的流程和前面释放指定逻辑块的流程和思路都是一样的

先把i节点对应的i节点位图中的bit置0,然后情况i节点位图中的信息

创建inode

 //// 为设备 dev 建立一个新 i 节点。返回该新 i 节点的指针。 
 // 在内存 i 节点表中获取一个空闲 i 节点表项,并从 i 节点位图中找一个空闲 i 节点。
struct m_inode * new_inode(int dev)
{
    struct m_inode * inode;
    struct super_block * sb;
    struct buffer_head * bh;
    int i,j;

    if (!(inode=get_empty_inode()))         //=======key=========
        return NULL;
    if (!(sb = get_super(dev)))
        panic("new_inode with unknown device");
    j = 8192;
    for (i=0 ; i<8 ; i++)
        if (bh=sb->s_imap[i])
            if ((j=find_first_zero(bh->b_data))<8192)
                break;
    if (!bh || j >= 8192 || j+i*8192 > sb->s_ninodes) {
        iput(inode);
        return NULL;
    }
    if (set_bit(j,bh->b_data))
        panic("new_inode: bit already set");
    bh->b_dirt = 1;
    inode->i_count=1;
    inode->i_nlinks=1;
    inode->i_dev=dev;
    inode->i_uid=current->euid;
    inode->i_gid=current->egid;
    inode->i_dirt=1;
    inode->i_num = j + i*8192;
    inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;
    return inode;
}
  1. 通过super_block找到对应的inode信息(i节点位图,逻辑块位图)
  2. 通过inode操作函数找到对应的inode的分配内存
  3. 设置inode位图中对应的位1
  4. 返回设置好的inode
上一篇
下一篇