目录
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);
else
submit_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请求的情况暂不分析
参考文档
《存储技术原理分析》