Linux内核读文件流程
主要描述从用户态启动文件读开始,直到磁盘驱动,linux内核代码所走过的流程。阅读者需要对linux内核的内存管理、ext系列的文件系统,块设备,页高速缓存等有一定了解,不了解也没关系,顺着代码读可能会吃力点而已,鉴于不可能将代码全贴出来,中间缺失的部分,请大家自行脑补吧。至于写文章的原因,作为一名高效的客服,电话咨询来临时,就要把链接给出来,没有现成的就自己造了。
linux内核版本号:3.0.13-0.27 sles11sp2版本。
首先看系统调用处代码:
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
然后系统通过vfs_read函数进入,通过vfs的文件对象操作函数read,进行操作。
if (file->f_op->read)
ret = file->f_op->read(file, buf, count, pos);
else
ret = do_sync_read(file, buf, count, pos);
其中read函数是根据不同的文件系统定义初始化不同的,其中ext3文件系统初始化如下:
const struct file_operations ext3_file_operations = {
.llseek = generic_file_llseek,
.read = do_sync_read,
.write = do_sync_write,
.aio_read = generic_file_aio_read,
.aio_write = generic_file_aio_write,
.unlocked_ioctl = ext3_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = ext3_compat_ioctl,
#endif
.mmap = generic_file_mmap,
.open = dquot_file_open,
.release = ext3_release_file,
.fsync = ext3_sync_file,
.splice_read = generic_file_splice_read,
.splice_write = generic_file_splice_write,
};
read被定义为do_sync_read函数,在do_sync_read函数中初始化了iovec,kiocb结构体,将一些函数入参藏到里面,需要注意调用到里面会释放出来,注意要内外对应。此时系统进入了一个死循环,开始以一个页大小方式反复读数据,最后完成读任务。
for (;;) {
ret = filp->f_op->aio_read(&kiocb, &iov, 1, kiocb.ki_pos);
if (ret != -EIOCBRETRY)
break;
wait_on_retry_sync_kiocb(&kiocb);
}
系统调用aio_read函数,前面可知是generic_file_aio_read函数,然后进入到里面。需要注意的是,内核很多函数命名是不规范了,往往函数名不能表意,很多情况下按单词理解函数刚好反了。
generic_file_aio_read并不是异步读,函数中的一些检查操作请自动忽略,函数通过文件对象标准flag判断读文件是不是O_DIRECT形式,如果是,则进入直读模式,也就是跳过页缓存,实际代码实现上差别很大,O_DIRECT放到后面再写,先说其他的。函数则再次初始化了desc结构体,又藏了一些入参。
进入do_generic_file_read,内核通过read的偏移量ppos,获取的基树的索引index,这个时候就根据索引查找页高速缓存:
find_page:
page = find_get_page(mapping, index);
if (!page) {
page_cache_sync_readahead(mapping,
ra, filp,
index, last_index - index);
page = find_get_page(mapping, index);
if (unlikely(page == NULL))
goto no_cached_page;
}
if (PageReadahead(page)) {
page_cache_async_readahead(mapping,
ra, filp, page,
index, last_index - index);
}
if (!PageUptodate(page)) {
if (inode->i_blkbits == PAGE_CACHE_SHIFT ||
!mapping->a_ops->is_partially_uptodate)
goto page_not_up_to_date;
if (!trylock_page(page))
goto page_not_up_to_date;
/* Did it get truncated before we got the lock? */
if (!page->mapping)
goto page_not_up_to_date_locked;
if (!mapping->a_ops->is_partially_uptodate(page,
desc, offset))
goto page_not_up_to_date_locked;
unlock_page(page);
}
读取的页有可能不在页高速缓存中,page_cache_sync_readahead函数看是否在预读中找到,(关于文件预读机制,后面单独写),如果没有找到,就进入no_cached_page里,为准备读取的页分配缓存,然后从文件系统上读取数据。
error = mapping->a_ops->readpage(filp, page);
页的磁盘读取是通过调用readpage函数完成的,由于ext3的日志处理有三种方式,所以初始化的函数也有3个,即ext3_ordered_aops、ext3_writeback_aops、ext3_journalled_aops。因为ext3默认的日志写入方式是ordered,所以后面都是以ordered为准。虽然读操作和日志没有关系。
static const struct address_space_operations ext3_ordered_aops = {
.readpage = ext3_readpage,
.readpages = ext3_readpages,
.writepage = ext3_ordered_writepage,
.write_begin = ext3_write_begin,
.write_end = ext3_ordered_write_end,
.bmap = ext3_bmap,
.invalidatepage = ext3_invalidatepage,
.releasepage = ext3_releasepage,
.direct_IO = ext3_direct_IO,
.migratepage = buffer_migrate_page,
.is_partially_uptodate = block_is_partially_uptodate,
.error_remove_page = generic_error_remove_page,
};
ext3_readpage中将ext3_get_block传入给mpage_readpage,先记下了。同时在do_mpage_readpage中将ext3_get_block作为get_block传递进去。
static struct bio *
do_mpage_readpage(struct bio *bio, struct page *page, unsigned nr_pages,
sector_t *last_block_in_bio, struct buffer_head *map_bh,
unsigned long *first_logical_block, get_block_t get_block)
{
..........
while (page_block < blocks_per_page) {
map_bh->b_state = 0;
map_bh->b_size = 0;
if (block_in_file < last_block) {
map_bh->b_size = (last_block-block_in_file) << blkbits;
/*下面直接调用ext3_get_block读取相应的物理块,其中buffhead中会
将对应的物理块号存放到b_blocknr字段中*/
if (get_block(inode, block_in_file, map_bh, 0))
goto confused;
*first_logical_block = block_in_file;
/*判断块是不是连续的,连续的则进行合并提交一个bio*/
if (page_block && blocks[page_block-1] != map_bh->b_blocknr-1)
goto confused;
nblocks = map_bh->b_size >> blkbits;
for (relative_block = 0; ; relative_block++) {
if (relative_block == nblocks) {
clear_buffer_mapped(map_bh);
break;
} else if (page_block == blocks_per_page)
break;
/*最终将b_blocknr存放到了blocks数组中*/
blocks[page_block] = map_bh->b_blocknr+relative_block;
page_block++;
block_in_file++;
}
}
/*bio开始为0,后续分配*/
if (bio && (*last_block_in_bio != blocks[0] - 1))
bio = mpage_bio_submit(READ, bio);
alloc_new:
if (bio == NULL) {
/*将blocks开始的物理块号附加到bio*/
bio = mpage_alloc(bdev, blocks[0] << (blkbits - 9),
min_t(int, nr_pages, bio_get_nr_vecs(bdev)),
GFP_KERNEL);
if (bio == NULL)
goto confused;
}
/*向设备层提交bio*/
bio = mpage_bio_submit(READ, bio);
/*不连续的块则针对每个块分配一个bio进行提交*/
confused:
if (bio)
bio = mpage_bio_submit(READ, bio);
if (!PageUptodate(page))
block_read_full_page(page, get_block);
else
unlock_page(page);
goto out;
}
do_mpage_readpage大致就是通过传递进来的page结构体通过
block_in_file = (sector_t)page->index << (PAGE_CACHE_SHIFT - blkbits);
获取page对应文件的块相对位置,然后通过ext3_get_block获取磁盘的绝对块号,见块号赋值给BIO,然后提交给设备层读取,即完成了一次读文件的过程。
Linux内核读文件流程来自于OENHAN
链接为:https://oenhan.com/linux-kernel-read/
请问大神,上述代码中的 PageReadahead 函数是做什么的?
找了半天也没有找到函数定义
@UCSHELL Kernel对应的版本不一样,最新的函数去掉了,你翻到对应的版本看吧
请问大神 linux读裸盘的流程是不是和这个不一样呢?是不是把裸盘整个空间看成一个文件了。
@PENGZHIXI 读裸盘?dd设备?如果是,本质上就是读取一个文件内容了,和这个流程不一样
@OENHAN 就是一个普通的硬盘 不进行格式化 也不挂载。直接通过open(“/dev/sda”,O_RDWR)这样的方式来读写
@PENGZHIXI 本质就是对一个(设备)文件,流程完全不一样,走的是设备文件的路径
@OENHAN 也就是说走vfs,然后走设备驱动这一级了?