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_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_head | set_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 */
寻址空间:
- 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个块)
五、ext2_get_branch
这个函数时根据上一步获取的offset,一步一步读取物理块号。因为间接寻址的场景,数组当中存储的是下一级寻址的地址,所以要根据地址一级一级转换,最终得到实际的物理块号。以下以逻辑块号1234567为例,它的offset和转换过程如下:
offset[4]数组 | 14 | 0 | 204 | 208955 |
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。预分配就是预先分配块空间,避免每次临时分配。
七、
(其他函数,待完善)
参考资料: