VFS基础学习笔记 - 5.读文件过程
目录
- 1. 前言
- 2. buffer_head与page的关联
- 3. ksys_read
- |- -generic_file_read_iter
- |- - -generic_file_buffered_read
- 参考文档
1. 前言
本专题我们开始学习虚拟文件系统VFS的相关内容。本专题主要参考了《存储技术原理分析》、ULA、ULK的相关内容。本文主要记录读文件的过程。
kernel版本:5.10
FS: minix
平台:arm64
注:
为方便阅读,正文标题采用分级结构标识,每一级用一个"-“表示,如:两级为”|- -", 三级为”|- - -“
2. buffer_head与page的关联
之所以介绍buffer_head与page的关系,是因为文件系统读操作时,首先将是从page cache中获取,如果获取不到将从磁盘中获取换入到page,当page需要从磁盘换入时,将执行实际的磁盘读写操作,这个过程中buffer_head就负责管理page与磁盘逻辑块的关系。
后面在submit_bh流程中可以看到有使用buffer_head.
page被划分为多个buffer,每个buffer对应一个buffer_head进行管理,buffer_head维护buffer与磁盘逻辑块的关系;
buffer_head通过b_this_page形成循环链表,它的b_data指向了从磁盘获取的数据;
buffer_head的b_page指向所属的page, 而page作为page cache时,其private指针指向领头的buffer_head
3. ksys_read
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)|--ksys_read(fd, buf, count);|--struct fd f = fdget_pos(fd);|--loff_t pos, *ppos = file_ppos(f.file);|--pos = *ppos; ppos = &pos;|--ret = vfs_read(f.file, buf, count, ppos);| | //*对要访问的文件部分检查是否有冲突的强制锁| |--rw_verify_area(READ, file, pos, count);| | //如果文件系统给出read函数则执行文件系统的read函数| |--if (file->f_op->read)| | ret = file->f_op->read(file, buf, count, pos);| | else if (file->f_op->read_iter)| | ret = new_sync_read(file, buf, count, pos);|--f.file->f_pos = pos;
read主要是通过fd获取到file文件描述符,更新文件的位置,通过调用vfs_read读取,读取完毕更新文件的位置
vfs_read首先做一些基本的检查,如:文件是否可读、读取范围是否超过文件范围、是否实现操作函数集、读取的缓冲区是否可访问,是否持有锁等;
如果file->f_ops已经实现则执行file->f_ops->read函数,否则执行默认的new_sync_read函数。read_iter和read回调至少要实现一个,如果没有定义read回调则一定要定义read_iter回调,minixfs的read_iter回调为generic_file_read_iter,
f.file->f_pos = pos:修改文件当前读写位置
ssize_t new_sync_read(struct file *filp, char __user *buf,size_t len,loff_t *ppos)|--struct iovec iov = { .iov_base = buf, .iov_len = len };| struct kiocb kiocb;|--init_sync_kiocb(&kiocb, filp)|--iov_iter_init(&iter, READ, &iov, 1, len)|--call_read_iter(filp, &kiocb, &iter);|--filp->f_op->read_iter(&kiocb, &iter)|--generic_file_read_iter
struct iovec为了兼容read和readv系统调用,readv可以读取到多个用户缓冲区,因此定义了iovec数据结构,它代表一个用户缓冲区;
struct kiocb 是内核io控制块,读写是在此控制块的控制下进行的,为了重用AIO的代码逻辑而设计;struct iov_iter 是io向量迭代器,用于向前推进io的写入
|- -generic_file_read_iter
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)|--size_t count = iov_iter_count(iter)| if (!count)| goto out;| //>>>>>> 对于直接IO的处理|--if (iocb->ki_flags & IOCB_DIRECT)| struct file *file = iocb->ki_filp;| struct address_space *mapping = file->f_mapping;| struct inode *inode = mapping->host;| filemap_write_and_wait_range(mapping, iocb->ki_pos, iocb->ki_pos + count - 1);| mapping->a_ops->direct_IO(iocb, iter);| //>>>>>> 对于经page cache读的处理,使用缓存页面的情况:将缓存页面page划分成一个个的buffer 块来进行IO.|--generic_file_buffered_read(iocb, iter, retval)
generic_file_read_iter首先会判定iter是否还有剩余字节,如果没有则退出,然后根据文件读写标记分直接IO和通过page cache两种方式。
1.iov_iter_count:获取剩余字节数;
2.针对直接IO的方式,也需要做一些冲刷处理,filemap_write_and_wait_range就是为了将page cache中的数据冲刷进磁盘,之后将利用mapping->a_ops->direct_IO回调执行直接IO读写
3.针对使用缓存页面的情况主要调用了generic_file_buffered_read函数把读入到 page cache中的页面内容拷贝到用户buffer
|- - -generic_file_buffered_read
ssize_t generic_file_buffered_read(struct kiocb *iocb,struct iov_iter *iter, ssize_t written)|--struct file *filp = iocb->ki_filp;| struct address_space *mapping = filp->f_mapping;| struct inode *inode = mapping->host;| struct file_ra_state *ra = &filp->f_ra;| loff_t *ppos = &iocb->ki_pos;| pgoff_t index;| unsigned long offset; /* offset into pagecache page */| //ppos对应的逻辑页索引|--index = *ppos >> PAGE_SHIFT;| //获取本次请求的最后一个字节所在的逻辑页索引| last_index = (*ppos + iter->count + PAGE_SIZE-1) >> PAGE_SHIFT;| //ppos对应的字节在逻辑页内的偏移| offset = *ppos & ~PAGE_MASK;|--for (;;)| cond_resched();
find_page: //>>>>>>查找页面| // 根据index获取page cache页面,如果页面为空则执行预读,然后再次检查| page = find_get_page(mapping, index);| if (!page)| page_cache_sync_readahead(mapping,...)| page = find_get_page(mapping, index);| if (unlikely(page == NULL)) | goto no_cached_page;| //执行异步预读| if (PageReadahead(page))| page_cache_async_readahead(mapping,...)| //如果页面不是最新,则等待其读写操作完成| if (!PageUptodate(page))| wait_on_page_locked_xxx(page, ...);| if (PageUptodate(page))| goto page_ok; | if (!trylock_page(page))| goto page_not_up_to_date;| if (!page->mapping)| goto page_not_up_to_date_locked;| if (!mapping->a_ops->is_partially_uptodate(page, offset, iter->count))| goto page_not_up_to_date_locked;
page_ok: //>>>>>>从address_space中查找到page, 如果不是最新则等待其读取完毕| isize = i_size_read(inode);| end_index = (isize - 1) >> PAGE_SHIFT;| nr = PAGE_SIZE;| nr = nr - offset;| if (mapping_writably_mapped(mapping))| flush_dcache_page(page);| //将页面的PG_referenced或PG_active置位,表示页正在被访问,不应该被置换出| if (prev_index != index || offset != prev_offset) | mark_page_accessed(page);| //读到的数据拷贝给用户缓冲区| copy_page_to_iter(page, offset, nr, iter)| continue;
page_not_up_to_date: //>>>>>> 对于页面不是最新的处理| lock_page_xxx(page, ...);
page_not_up_to_date_locked: //>>>>>>> 页面被删除的处理| //页面被删除| if (!page->mapping)| unlock_page(page);| put_page(page);| continue;| //到此处可能已经有进程将数据从磁盘读取出放入page cache,则解锁页面.否则执行read_page从磁盘中读取| if (PageUptodate(page))| unlock_page(page);| goto page_ok;
readpage: //>>>>> 执行读取操作| // read_page对普通文件一般被封装为mpage_readpage,如ext3| mapping->a_ops->readpage(filp, page);| //此处锁住页面,由于前面在find page时已经持有锁,因此此处会被阻塞,| //直到bio->end_io回调将页面标记为最新才会解锁,这就体现出读是同步的| if (!PageUptodate(page))| lock_page__xxx(page, ...);| goto page_ok;
readpage_error:| put_page(page);| goto out;
no_cached_page: //>>>>> 如果没有分配到page页面,则要进行分配| page = page_cache_alloc(mapping);| add_to_page_cache_lru(page, mapping, index,...);| goto readpage;
generic_file_buffered_read首先获取起始位置所在的页面和偏移,然后执行读操作
find_page: 从page cache中查找给定索引的page页面,其中find_get_page根据index获取page cache页面,如果页面为空,接下来会执行预读,然后再次通过find_get_page检查,如果发现页面为最新则转向page_ok;尝试给页面加锁,如果无法加锁则认为页面正处于读写操作,转向page_not_up_to_date;如果通过预读后仍然查找到的页面为空,则最终转向no_cached_page;
page_ok:标记查找到的页面正在访问,以防换出,之后将page页面读取到用户空间;
page_not_up_to_date:此处将等待页面读写完成;
page_not_up_to_date_locked:如果发现枷锁的页面在之前已经被删除了,则解锁,继续下次循环,也可能有其它进程填充了新的页面,此时跳到page_ok处理
readpage:执行从磁盘到page的读取,在调用完毕后会锁住页面,由于前面在page_not_up_to_date时已经持有锁,因此此处会被阻塞, 同步就体现在这个地方,后面在minix_readpage->submit_bio执行完毕后会执行bio->bi_end_io回调来唤醒当前阻塞的进程。
注:minix_readpage是基于缓存buffer block构造bio,ext3_readpage是基于缓存页page来构造bio(TODO)no_cached_page:分配新的page,并加入到page cache中
static int minix_readpage(struct file *file, struct page *page)|--block_read_full_page(page,minix_get_block);|--struct inode *inode = page->mapping->host;| sector_t iblock, lblock;| struct buffer_head *bh, *head, *arr[MAX_BUF_PER_PAGE];| //如果没有为page创建buffers则创建buffers|--head = create_page_buffers(page, inode, 0);|--iblock = (sector_t)page->index << (PAGE_SHIFT - bbits); | lblock = (i_size_read(inode)+blocksize-1) >> bbits;| bh = head;| //通过get_block为每一个相对文件开始的文件块编号和相对磁盘的逻辑块号进行映射|--do {| if (buffer_uptodate(bh))| continue;| if (!buffer_mapped(bh))| if (iblock < lblock)| get_block(inode, iblock, bh, 0);| if (!buffer_mapped(bh))| zero_user(page, i * blocksize, blocksize);| continue;| if (buffer_uptodate(bh))| continue;| } while (i++, iblock++, (bh = bh->b_this_page) != head);| //lock the buffers|--for (i = 0; i < nr; i++)| bh = arr[i];| lock_buffer(bh);| mark_buffer_async_read(bh)| // start the IO|--for (i = 0; i < nr; i++)bh = arr[i];if (buffer_uptodate(bh))end_buffer_async_read(bh, 1);elsesubmit_bh(REQ_OP_READ, 0, bh)//提交读请求,为读取磁盘块到缓冲区buffer|--bio_alloc(GFP_NOIO, 1)|--初始化bio,其中bio->bi_end_io = end_bio_bh_io_sync;|--submit_bio(bio)//至此被传递到IO子系统
准备工作
(1)通过create_empty_buffers将page划分为多个buffer,每个buffer的大小与磁盘逻辑块大小相同,每个逻辑块由buffer_head进行管理
(2)根据page得到当前正在处理的文件逻辑块号iblock和文件的结束逻辑块号lblock,
(3)通过一个循环处理步骤(1)对page所划分的所有buffers,调用get_block->minix_get_block:对相对文件开始的文件块编号和相对磁盘的逻辑块号进行映射(每个文件块或每个磁盘逻辑块就对应一个buffer)并将需要磁盘块填充的buffer保存到attr数组;lock_buffer:锁定page的每一个buffer;
submit_bh:实际启动IO。如果已经是最新直接调用end_buffer_async_read,如果buffer中的内容不是最新则需要通过submit_bh来从磁盘中读取到buffer
static void end_bio_bh_io_sync(struct bio *bio)|--bh->b_end_io(bh, !bio->bi_status);|--end_buffer_async_read(struct buffer_head *bh, int uptodate)|--page = bh->b_page;| 读取成功标记bh管理的buffer数据为最新|--if (uptodate)| set_buffer_uptodate(bh);| //所有buffer读取成功则将整个page标记为最新|--if (page_uptodate && !PageError(page))| SetPageUptodate(page);| //唤醒所有等待操作此page的线程,与前面readpage后的lock_page_xxx呼应|--unlock_page(page);
从前面执行我们知道通过submit_bh->submit_bio最终会将磁盘数据读取到buffer_head->b_data中,完成之后会调用初始化bio时设置的回调bio->bi_end_io = end_bio_bh_io_sync。end_bio_bh_io_sync实质的工作是会调用bh->b_end_io也就是 end_buffer_async_read。在submit_bio执行完毕,真正将磁盘内容读取到buffer中后会将释放锁,这里就会唤醒所有等待操作此 page的进程
注:对于直接根据页面而不是buffer_head构建IO请求的情况暂不分析
参考文档
《存储技术原理分析》
VFS基础学习笔记 - 5.读文件过程相关推荐
- Python3 基础学习笔记 C09【文件和异常】
CSDN 课程推荐:<8小时Python零基础轻松入门>,讲师齐伟,苏州研途教育科技有限公司CTO,苏州大学应用统计专业硕士生指导委员会委员:已出版<跟老齐学Python:轻松入门& ...
- 【Python】DS的基础学习笔记7:文件、异常和模块
文章目录 文件.异常和模块 7.1 文件的读写 7.1.1 文件的打开 1. 文件路径 2. 打开模式 3. 字符编码 7.1.2 文件的读取 1. 读取整个内容--f.read() 2. 逐行进行读 ...
- _int64_在Linux对应对文件,LInux 编程基础学习笔记 持续ing 文件读写
一.文件读写 1.创建文件: 头文件: 函数:creat(char *path,int mode) mode:S_I(R|W|X)(USR|GRP|OTH) 对不同用户组的不同读写执行 2.打开 ...
- guido正式发布python年份_Python 基础学习笔记.docx
Python 基础学习笔记 基于<Python语言程序设计基础(第2版)> 第一部分 初识Python语言 第1章 程序设计基本方法 1.1 计算机的概念 计算机是根据指令操作数据的设备, ...
- 8. SpringBoot基础学习笔记
SpringBoot基础学习笔记 课程前置知识说明 1 SpringBoot基础篇 1.1 快速上手SpringBoot SpringBoot入门程序制作 1.2 SpringBoot简介 1.2.1 ...
- C基础学习笔记——01-C基础第02天(用户权限、VI操作、Linux服务器搭建)
在学习C基础总结了笔记,并分享出来.有问题请及时联系博主:Alliswell_WP,转载请注明出处. 01-C基础第02天(用户权限.VI操作.Linux服务器搭建) 打开终端:ctrl+alt+t ...
- 多人网络游戏服务器开发基础学习笔记 II: 帧同步 | 游戏客户端预测原理分析 | FPS 游戏状态同步
这篇是对书本 网络多人游戏架构与编程 的学习第二篇(第一篇:多人网络游戏服务器开发基础学习笔记 I:基本知识 | 游戏设计模式 | 网游服务器层次结构 | 游戏对象序列化 | 游戏 RPC 框架 | ...
- Objective-C基础学习笔记
Objective-C基础学习笔记 day01-基础语法 NSString NS前缀 如何定义类 1)类的三要素 2)定义类的语法 3)注意 创建类的对象 使用对象 方法的声明和调用 定义 无参数方法 ...
- 8.Python基础学习笔记day8-正则表达式、网络编程、进程与线程
8.Python基础学习笔记day8-正则表达式.网络编程.进程与线程 一.正则表达式 ''' 1. [1,2,3,4]中任意取3个元素排列: A43 = 4x3x2 = 24itertools.pe ...
最新文章
- 【黑科技】在alv中设置字体样式
- mybatis中传入参数的几种方式
- 【TensorFlow】tf.nn.softmax_cross_entropy_with_logits 函数:求交叉熵损失
- 慎用url重写(转)
- 怎么自定义字体_自定义字体@fontface的常见应用
- [ACL2020]Generalizing Natural Language Analysis through Span-relation Representations
- SpringCloud工作笔记050---关于同一账号多人同时登录的token重复问题
- Java中构造方法的执行顺序
- ajax提交不能获取数据,django无法收到ajax的请求数据
- JS弹出窗口的运用与技巧
- 超全的 Python 可视化教程,收藏
- 软件测试岗位职责和划分
- 把一个人的特点写具体作文_把一个人的特点写具体作文400字
- 如何无损把mp4视频格式转换成mp3音频格式
- 全球光纤接头闭合器(FOSC)收入预计2028年达到42.159亿美元
- MJB,阿里又一次成功的营销?
- 困在“墙”里的中年程序员
- BUUCTF黑客帝国
- Axure 9 选择组的使用
- Geoscene Enterprise2.1 windows环境下的安装部署