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,然后走设备驱动这一级了?