11 ext2文件系统IO流程:读写文件read/write

ext2使用page页面缓存来完成对文件的读写。这些页面的管理是通过inode的字段i_mapping来完成,也就是地址空间。所以在创建inode时,要指定i_mapping的操作表a_ops,帮助地址空间完成页面操作。

参考:014 Linux文件系统数据结构详解:地址空间struct address_space

地址空间操作表a_ops中,需要指定读page、写page等多种页面操作的函数指针,但是具体的块操作(读取、写入)、buffer操作、VM页面操作,其实文件系统不用太关注,因为Linux内核提供了大量公共函数(参考:buffer.c和mpage.c),文件系统可以直接调用完成读取和写入。但是,文件系统需要提供块的映射方法,帮助完成文件系统逻辑块号(就是在文件中的偏移量)到实际块设备块号映射,最后填充到buffer_head中。

// inode.c

const struct address_space_operations ext2_aops = {
	.set_page_dirty		= __set_page_dirty_buffers,
	.readpage		= ext2_readpage,
	.readahead		= ext2_readahead,
	.writepage		= ext2_writepage,
	.write_begin		= ext2_write_begin,
	.write_end		= ext2_write_end,
	.bmap			= ext2_bmap,
	.direct_IO		= ext2_direct_IO,
	.writepages		= ext2_writepages,
	.migratepage		= buffer_migrate_page,
	.is_partially_uptodate	= block_is_partially_uptodate,
	.error_remove_page	= generic_error_remove_page,
};

一、page页面基本操作函数

以下代码是ext2文件系统inode地址空间的操作函数,从操作函数的实现可以看出,都是调用了内核提供的函数,比如:mpage_readpage、block_write_begin、generic_write_end、generic_block_bmap等。

// inode.c

static int ext2_writepage(struct page *page, struct writeback_control *wbc)
{
     return block_write_full_page(page, ext2_get_block, wbc);
}

static int ext2_readpage(struct file *file, struct page *page)
{
     return mpage_readpage(page, ext2_get_block);
}

static void ext2_readahead(struct readahead_control *rac)
{
     mpage_readahead(rac, ext2_get_block);
}

static int
ext2_write_begin(struct file *file, struct address_space *mapping,
          loff_t pos, unsigned len, unsigned flags,
          struct page **pagep, void **fsdata)
{
     int ret;

     ret = block_write_begin(mapping, pos, len, flags, pagep,
                    ext2_get_block);
     if (ret < 0)
          ext2_write_failed(mapping, pos + len);
     return ret;
}

static int ext2_write_end(struct file *file, struct address_space *mapping,
               loff_t pos, unsigned len, unsigned copied,
               struct page *page, void *fsdata)
{
     int ret;

     ret = generic_write_end(file, mapping, pos, len, copied, page, fsdata);
     if (ret < len)
          ext2_write_failed(mapping, pos + len);
     return ret;
}

static sector_t ext2_bmap(struct address_space *mapping, sector_t block)
{
     return generic_block_bmap(mapping,block,ext2_get_block);
}

在调用这些函数时,都携带了一个函数指针ext2_get_block,那这个函数是什么作用呢?下面我给出了函数原型,一共是4个参数,3个入参1个出参。其实这个函数,就是根据传入的iblock逻辑块号,因为虽然文件中看起来块是连续的,但是因为实际存储不连续,需要做一次转换,最终返回buffer映射结果(包含了实际块号)。

// fs.h

typedef int (get_block_t)(struct inode *inode, sector_t iblock,
			struct buffer_head *bh_result, int create);

参数说明:

  • ① inode:入参,文件的inode
  • ② iblock:入参,类型是sector_t就是块号
  • ③ bh_result:出参,块映射到的buffer
  • ④ create:入参,是否创建新块

二、ext2_get_block调用关系图

ext2_get_block()函数的实现非常复杂,我们先要理清调用关系图,然后对各个函数详细分析:

ext2_get_block调用关系图
ext2_get_block调用关系图
函数主要作用
ext2_get_blocks将逻辑块号映射成物理块号主函数
ext2_block_to_path计算逻辑块号所在位置,转换成寻址数组类型和下标直接寻址:<EXT2_NDIR_BLOCKS

ext2_get_branch一级一级得获取物理块号1)可能全部获取到
2)也可能部分获取到(需要再分配)
ext2_init_block_alloc_info初始化inode的i_block_alloc_info字段里面包含预留窗口和数据块分配信息。
ext2_find_goal
ext2_find_near
ext2_blks_to_allocate
ext2_alloc_branch
ext2_alloc_blocks
ext2_new_blocks
ext2_splice_branch
map_bh将找到的物理块号设置给buffer_headset_buffer_mapped(bh);
bh->b_bdev = sb->s_bdev;
bh->b_blocknr = block;
bh->b_size = sb->s_blocksize;

三、ext2数据块寻址结构

ext2文件系统inode结构ext2_inode中,i_block字段存储了文件关联的数据块号信息,通过这些块号信息,可以寻址到文件关联的数据块,进而完成文件的读取和写入。

i_block这个字段是一个定长数组,一共15个元素,其中前12个元素(下标0 – 11)是直接寻址方式,也就是里面直接存储了块号。如果ext2块大小是4092B,那么前12个元素可以存储4092*12=48KB文件。

而为了支持更大的文件,后三个元素,也就是13、14、15号元素(下标12、13、14)采用间接寻址。其中第13号一级间接寻址,也就是说13号元素存储了一个地址,这个地址指向一个数组,这个数组存储了块号。按照ext2计算方法,间接寻址的数组大小为blocksize/4,也就是1024个元素,也就是说支持4092*1024KB,也就是4MB文件。

13号元素是一级间接寻址,而14号元素是二级间接寻址,需要跳跃两个数组才能找到块号,而15号元素是三级间接寻址,需要跳跃三个数组才能找到块号。

#define	EXT2_NDIR_BLOCKS		12
#define	EXT2_IND_BLOCK			EXT2_NDIR_BLOCKS
#define	EXT2_DIND_BLOCK			(EXT2_IND_BLOCK + 1)
#define	EXT2_TIND_BLOCK			(EXT2_DIND_BLOCK + 1)
#define	EXT2_N_BLOCKS			(EXT2_TIND_BLOCK + 1)

__le32	i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
ext2块寻址方式
ext2块寻址方式

寻址空间:

  • 1)直接寻址:12块
  • 2)一级间接寻址:1024块
  • 3)二级间接寻址:1024*1024=1024576块
  • 4)三级间接寻址:1024*1024*1024=1049165824块

四、ext2_block_to_path

ext2_block_to_path()函数用于计算在给定逻辑块号情况下,寻址深度以及各级深度的数组下标。其中几个关键变量,这里做一个汇总,以块大小为4096KB为例:

  • ptrs:单个间接寻址支持的块数,blocksize/4,那么ptrs就是1024
  • ptrs_bits:prts以2为底的对数log2(ptrs),那么ptrs_bits就是10
  • direct_blocks:直接寻址块总数12个
  • indirect_blocks:一级间接寻址块总数等于ptrs,也就是1024个
  • double_blocks:二级间接寻址时,每块相当于再分解出1024块,那么就有1024*1024=1048,576块,也就是1左移两个10位,一共左移20位
  • boundary:最后一个块的下标
  • n(返回值):寻址深度
  • offset[0]:直接寻址下标
  • offset[1]:一级间接寻址下标
  • offset[2]:二级间接寻址下标,平级的所有都算,是整体的偏移量
  • offset[3]:三级间接寻址下标,平级的所有都算,是整体的偏移量
static int ext2_block_to_path(struct inode *inode,
            long i_block, int offsets[4], int *boundary)
{
    int ptrs = EXT2_ADDR_PER_BLOCK(inode->i_sb);
    int ptrs_bits = EXT2_ADDR_PER_BLOCK_BITS(inode->i_sb);
    const long direct_blocks = EXT2_NDIR_BLOCKS,
        indirect_blocks = ptrs,
        double_blocks = (1 << (ptrs_bits * 2));
    int n = 0;
    int final = 0;

    if (i_block < 0) {
        ext2_msg(inode->i_sb, KERN_WARNING,
            "warning: %s: block < 0", __func__);
    } else if (i_block < direct_blocks) {
        offsets[n++] = i_block;
        final = direct_blocks;
    } else if ( (i_block -= direct_blocks) < indirect_blocks) {
        offsets[n++] = EXT2_IND_BLOCK;
        offsets[n++] = i_block;
        final = ptrs;
    } else if ((i_block -= indirect_blocks) < double_blocks) {
        offsets[n++] = EXT2_DIND_BLOCK;
        offsets[n++] = i_block >> ptrs_bits;
        offsets[n++] = i_block & (ptrs - 1);
        final = ptrs;
    } else if (((i_block -= double_blocks) >> (ptrs_bits * 2)) < ptrs) {
        offsets[n++] = EXT2_TIND_BLOCK;
        offsets[n++] = i_block >> (ptrs_bits * 2);
        offsets[n++] = (i_block >> ptrs_bits) & (ptrs - 1);
        offsets[n++] = i_block & (ptrs - 1);
        final = ptrs;
    } else {
        ext2_msg(inode->i_sb, KERN_WARNING,
            "warning: %s: block is too big", __func__);
    }
    if (boundary)
        *boundary = final - 1 - (i_block & (ptrs - 1));

    return n;
}

举例说明:

1)逻辑块号为5

offset[0]=5,offset[1]=0,offset[2]=0,offset[3]=0

2)逻辑块号为99

offset[0]=12,i_block=99-12,offset[1]=87,offset[2]=0,offset[3]=0

3)逻辑块号为1234

offset[0]=13,i_block=1234-12-1024=198,offset[1]=198>>10=0,offset[2]=198,offset[3]=0

4)逻辑块号为1234567

offset[0]=14,i_block=1234567-12-1024-1024576=208955,offset[1]=208955>>20=0,offset[2]=(208955>>10) & 1023=204,offset[3]=208955

验算:

直接寻址:12

一级间接寻址:1024

二级间接寻址:1024*1024=1024576

三级间接寻址:1024576+208955=208896

加总方法1:12+1024+1024576+208896=1234567

加总方法2:12+1024+1024*1024+1*204*1024+208955%1024=1234567

示意图:(红色表示已寻找的逻辑块,注意最后一个三级寻址不满1024,只有59个块

逻辑块号:1234567的寻址过程
逻辑块号1234567的寻址过程

五、ext2_get_branch

这个函数时根据上一步获取的offset,一步一步读取物理块号。因为间接寻址的场景,数组当中存储的是下一级寻址的地址,所以要根据地址一级一级转换,最终得到实际的物理块号。以下以逻辑块号1234567为例,它的offset和转换过程如下:

offset[4]数组140204208955

1.转换步骤:

  • 1)先初始化chain[0],存入地址i_data+offset[0],其实就是取第14号元素内容
  • 2)根据1)中得到的地址,读取块数据,也就是一级间接寻址数组的首地址
  • 3)把首地址+偏移量,以及对应位置的数据存入chain[1]
  • 4)根据chain[1]的数据,读取块数据,也就是二级寻址数组的首地址
  • 5)把首地址+偏移量,以及对应位置的数据存入chain[2]
  • 6)根据chain[1]的数据,读取块数据,也就是三级寻址数组的首地址
  • 7)把首地址+偏移量,以及对应位置的数据存入chain[3],存入就是最终的物理块号

2.返回值

如果所有地址全部成功读取,那么将会返回NULL;如果其中某一级为NULL,说明空间还不够,需要再分配。

  • 1)如果全部寻址成功(空间足够):主流程设置对应块号到bno(出参,物理块号),这样主流程结束
  • 2)如果部分寻址不成功(空间不足):需要分配块号
static Indirect *ext2_get_branch(struct inode *inode,
                 int depth,
                 int *offsets,
                 Indirect chain[4],
                 int *err)
    ......
    /* i_data is not going away, no lock needed */
    add_chain (chain, NULL, EXT2_I(inode)->i_data + *offsets);
    if (!p->key)
        goto no_block;
    while (--depth) {
        bh = sb_bread(sb, le32_to_cpu(p->key));
        if (!bh)
            goto failure;
        read_lock(&EXT2_I(inode)->i_meta_lock);
        if (!verify_chain(chain, p))
            goto changed;
        add_chain(++p, bh, (__le32*)bh->b_data + *++offsets);
        read_unlock(&EXT2_I(inode)->i_meta_lock);
        if (!p->key)
            goto no_block;
    }
    ......
}

六、ext2_init_block_alloc_info

如果inode还没有分配块空间,那么先初始化预分配窗口,并且链接到inode。预分配就是预先分配块空间,避免每次临时分配。

七、

(其他函数,待完善)

参考资料:

VFS

ext2_get_branch解析

ext2文件系统inode.c分析

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注