linux文件IO全景解析
本文说明以下内容:
- 文件系统分层视图
- 数据在硬盘上是如何组织的?
- 读写文件时,如何从文件开始寻址到磁盘扇区?
- read,write内核中完整流程
- directIO不经过pagecache,落盘之前也需要将用户态数据拷贝到内核态,这个内存与page cache有何区别 (块缓存buffer cache与页缓存 page cache???)
Buffer cache和page cache的区别 : 简单说,文件系统操作文件都是面向内存的,根本够不着块这一层;directIO或裸设备读写也会分配一些内存(buffer cache)作为用户态内存和磁盘设备之间的媒介;而非directIO因为已经有了page cache,所以直接用了page cache,不用再特别分配buffer cache.
Linux内核Page Cache和Buffer Cache关系及演化历史
文件系统分层视图
vfs
|
具体文件系统
|
page cache (directio 不走page cache)
|
通用块层
|
IO调度层
|
设备驱动程序
|
设备控制器 (硬件)
磁盘数据组织
磁盘会被划分为多个块组,每个块组都如下述一样组织
大致分为5块区域
superblock --- data bitmap --- inode bitmap --- inode --- data
superblock: 一般在磁盘开始的固定位置上,其中存放了后面各个区域的起始终止位置,例如data bitmap从哪个扇区开始
data bitmap: 标记data区哪些位置有数据,哪些是空闲的
inode bitmap:标记inode区域哪些位置有数据,哪些是空闲的
inode:存放inode
data: 存放数据
dumpe2fs 挂载点 可以查看文件系统信息
vfs 主要构成
superblock
整个具体文件系统的描述信息,包含了关键信息包括super_operations(读写inode等操作)
file
进程角度看到的文件信息,里面存放了当前读写偏移(f_pos)
- fd存放在task_struct的fdtable中,通过fd找到file结构 -> file结构中包含dentry结构 -> dentry结构包含inode
- file结构体中包含了另一个重要信息是file_operations,由各个磁盘文件系统自己实现(如ext4等)
inode
- 磁盘上inode超集,里面存放了文件存放在哪些扇区上等信息
- inode中关键信息有:address_space(将lba映射到page), inode_operations(创建、删除、查找inode等), 指向表示硬盘的i_rdev, i_bdev等结构
为什么把关于inode的操作一部分放到super_operations里面,一部分放到inode_operations里面呢
dentry
- 是文件的逻辑表示,没有对应的磁盘存储
- 在打开文件时/home/xx/a.txt,会以此访问文件/, /home, /home/xx/(目录也是文件,有对应的inode保存在磁盘上),最后才是/home/xx/a.txt;根据文件路径一层层访问磁盘上inode时,在内存中构建dentry结构,最终形成一个树形结构
- 存放了dentry_operations(比对文件名等操作就是用这个实现的)
综上,通过file -> dentry -> inode 就把进程眼中的文件映射到了磁盘上的某些扇区。
更多阅读:
Linux文件系统2---VFS的四个主要对象
计算机底层知识拾遗(四)理解文件系统
寻址过程
进程看到的文件地址空间,是连续的、平坦的,从0开始增加;磁盘上是按照扇区存储文件的,可能连续也可能分散
- 当进程打开一个文件,vfs会创建一个file结构,里面存放了f_pos
- get_block_t 是一个函数指针类型,用于完成文件逻辑块号到磁盘物理块号的映射
- address_space address_space_operations包含读写page等, directIO等操作操作
在io通过submit_bio提交到通用块层之前, - lba -> 文件逻辑块号 -> page (ext4_file_operations)
- page -> 物理块号 (address_space_operations中的readpage/writepage/directIO等 会调用get_block_t完成page到物理块号的映射,并组装成bio,准备提交给通用块层)
更多阅读
linux异步IO浅析
最后,整理一下direct-io异步读操作的处理流程:
io_submit。对于提交的iocbpp数组中的每一个iocb(异步请求),调用io_submit_one来提交它们;
io_submit_one。为请求分配一个kiocb结构,并且在对应的kioctx的ring_info中为它预留一个对应的io_event。然后调用aio_rw_vect_retry来提交这个读请求;
aio_rw_vect_retry。调用file->f_op->aio_read。这个函数通常是由generic_file_aio_read或者其封装来实现的;
generic_file_aio_read。对于非direct-io,会调用do_generic_file_read来处理请求(见《linux文件读写浅析》)。而对于direct-io,则是调用mapping->a_ops->direct_IO。这个函数通常就是blkdev_direct_IO;
blkdev_direct_IO。调用filemap_write_and_wait_range将相应位置可能存在的page cache废弃掉或刷回磁盘(避免产生不一致),然后调用direct_io_worker来处理请求;
direct_io_worker。一次读可能包含多个读操作(对应于类readv系统调用),对于其中的每一个,调用do_direct_IO;
do_direct_IO。调用submit_page_section;
submit_page_section。调用dio_new_bio分配对应的bio结构,然后调用dio_bio_submit来提交bio;
dio_bio_submit。调用submit_bio提交请求。后面的流程就跟非direct-io是一样的了,然后等到请求完成,驱动程序将调用 bio->bi_end_io来结束这次请求。对于direct-io下的异步IO,bio->bi_end_io等于dio_bio_end_aio;
dio_bio_end_aio。调用wake_up_process唤醒被阻塞的进程(异步IO下,主要是io_getevents的调用者)。然后调用aio_complete;
aio_complete。将处理结果写回到对应的io_event中;
正在回写中的脏页会被锁上,此时后续的这个页上的IO会被阻塞
读写流程
文件系统层 & page cache
ssize_t __vfs_read(struct file *file, char __user *buf, size_t count,
loff_t *pos)
{
if (file->f_op->read)
return file->f_op->read(file, buf, count, pos);
else if (file->f_op->read_iter)
return new_sync_read(file, buf, count, pos);
else
return -EINVAL;
}
ssize_t __vfs_write(struct file *file, const char __user *p, size_t count,
loff_t *pos)
{
if (file->f_op->write)
return file->f_op->write(file, p, count, pos);
else if (file->f_op->write_iter)
return new_sync_write(file, p, count, pos);
else
return -EINVAL;
}
const struct file_operations ext4_file_operations = {
......
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
......
}
ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
......
if (iocb->ki_flags & IOCB_DIRECT) {
......
struct address_space *mapping = file->f_mapping;
......
retval = mapping->a_ops->direct_IO(iocb, iter);
}
......
retval = generic_file_buffered_read(iocb, iter, retval);
}
ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
......
if (iocb->ki_flags & IOCB_DIRECT) {
......
written = generic_file_direct_write(iocb, from);
......
} else {
......
written = generic_perform_write(file, from, iocb->ki_pos);
......
}
}
static const struct address_space_operations ext4_aops = {
......
.direct_IO = ext4_direct_IO,
......
};
/*
* This is a library function for use by filesystem drivers.
*/
static inline ssize_t
do_blockdev_direct_IO(struct kiocb *iocb, struct inode *inode,
struct block_device *bdev, struct iov_iter *iter,
get_block_t get_block, dio_iodone_t end_io,
dio_submit_t submit_io, int flags)
{......}
ext4_direct_IO 最终会调用到 __blockdev_direct_IO->do_blockdev_direct_IO,这就跨过了缓存层,到了通用块层,最终到了文件系统的设备驱动层。
写流程
ssize_t generic_perform_write(struct file *file,
struct iov_iter *i, loff_t pos)
{
struct address_space *mapping = file->f_mapping;
const struct address_space_operations *a_ops = mapping->a_ops;
do {
struct page *page;
unsigned long offset; /* Offset into pagecache page */
unsigned long bytes; /* Bytes to write to page */
status = a_ops->write_begin(file, mapping, pos, bytes, flags,
&page, &fsdata);
copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
flush_dcache_page(page);
status = a_ops->write_end(file, mapping, pos, bytes, copied,
page, fsdata);
pos += copied;
written += copied;
balance_dirty_pages_ratelimited(mapping);
} while (iov_iter_count(i));
}
这个函数里,是一个 while 循环。我们需要找出这次写入影响的所有的页,然后依次写入。对于每一个循环,主要做四件事情:
- 对于每一页,先调用 address_space 的 write_begin 做一些准备;调用grab_cache_page_write_begin找到page,并调用get_block_t将文件逻辑块号与磁盘物理快照映射起来;如果write数据没有写整个buffer,需下盘读
ll_rw_block
。
static const struct address_space_operations ext4_aops = {
......
.write_begin = ext4_write_begin,
.write_end = ext4_write_end,
......
}
- 调用 iov_iter_copy_from_user_atomic,将写入的内容从用户态拷贝到内核态的页中;先将分配好的页面调用 kmap_atomic 映射到内核里面的一个虚拟地址,然后将用户态的数据拷贝到内核态的页面的虚拟地址中,调用 kunmap_atomic 把内核里面的映射删除。
size_t iov_iter_copy_from_user_atomic(struct page *page,
struct iov_iter *i, unsigned long offset, size_t bytes)
{
char *kaddr = kmap_atomic(page), *p = kaddr + offset;
iterate_all_kinds(i, bytes, v,
copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
v.bv_offset, v.bv_len),
memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
)
kunmap_atomic(kaddr);
return bytes;
}
- 调用 address_space 的 write_end 完成写操作;调用 ext4_write_end 完成写入。这里面会调用 ext4_journal_stop 完成日志的写入,会调用 block_write_end->__block_commit_write->mark_buffer_dirty,将修改过的缓存标记为脏页。可以看出,其实所谓的完成写入,并没有真正写入硬盘,仅仅是写入缓存后,标记为脏页。
- 调用 balance_dirty_pages_ratelimited,看脏页是否太多,需要写回硬盘。所谓脏页,就是写入到缓存,但是还没有写入到硬盘的页面。
读流程
vfs_read -> new_sync_read
- struct iovec记录用户态内存地址和len;构造kiocb记录filp和相关的flag;构造iov_iter将iovec塞进去
-> ext4_file_read_iter (调用file_operation.read_iter,ext4_file_operation实现为ext4_file_read_iter) -> generic_file_read_iter -> do_generic_file_read - find_get_page: 根据address_space、offset检查是否已经在缓存中;
- 如果在缓存中,检查page是否是uptodate,如果不是,将page锁住(),然后调用ext4_aops.read_page下盘去读这一页数据;数据从磁盘读到后,中断处理中调用bio_endio将page设置为uptodate
- page是uptodate,然后将数据拷贝到用户态内存,至此IO已经完成
page dirty表示写请求将数据写到page上,还没有sync到磁盘;page not uptodate一般表示page cache中已经有了这个page,但数据还未从磁盘上读上来
-> page_cache_sync_readahead(没命中缓存的情景) - 调用ext4_map_blocks找到page对应到磁盘上的物理块号
- 创建bio,将物理块号、page等信息填充进bio,最后调用submit_bio
static 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;
for (;;) {
struct page *page;
pgoff_t end_index;
loff_t isize;
page = find_get_page(mapping, index);
if (!page) {
if (iocb->ki_flags & IOCB_NOWAIT)
goto would_block;
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);
}
/*
* Ok, we have the page, and it's up-to-date, so
* now we can copy it to user space...
*/
ret = copy_page_to_iter(page, offset, nr, iter);
}
}
更多阅读
Ext3文件系统读写过程分析
通用块层
block_device 表示逻辑块设备
gendisk 表示磁盘对象,管理请求队列和磁盘通用操作
request_queue 包含elevator, softirq_done_fn 等
buffer_head 磁盘按照block size被划分为一系列连续的块,一个文件也按照block size被划分为一系列块,进程io操作时将lba转化为逻辑块号(文件视角的块号),逻辑块号对应磁盘上哪个物理块号是有buffer_head管理的,它管理了这个映射关系
https://blog.51cto.com/alanwu/1122077
bio 是文件系统与通用块层进行IO的容器,bio指向一些page
submit_bio -> generic_make_request
- 通过bio上关联的block_device找到gendisk,然后找到request_queue
- request_queue包含elivator, make_request_fn, request_fn(处理请求), softirq_done_fn(请求被硬件处理完后,软中断中调用)
更多阅读
Linux kernel学习-block层
Linux IO请求处理流程-bio和request
io调度层
blk_queue_bio(scsi驱动添加磁盘设备时scsi_alloc_sdev->scsi_alloc_queue->blk_init_queue_node, 这其中会调用elevator_init 初始化elivator)
调度层有三个队列
- 线程的unplog list
- elivator
- gendisk.request_queue
bio 并不是直接挂到设备的请求队列上,他首先尝试挂到unplog list,不行再尝试合并到elivator中的request;如果无法合并,新建request放入unplog list
调度层主要解决两类问题:
- Bio的合并问题。主要考虑bio是否可以和scheduler中的某个request进行合并。因为从磁盘的角度来看,临近的请求需要合并,所有的IO需要顺序化处理,这样磁头才能往一个方向运行,避免无规则的乱序运行。
- Request的调度问题。request在何时可以从scheduler中取出,并且送入底层驱动程序继续进行处理?不同的应用可能需要不同的带宽资源,读写请求的带宽、延迟控制也可以不一样,因此,需要解决request的调度处理,从而可以更好的控制IO的QoS。
如果io塞到了某个request_queue上,函数就对上层响应了,并没有等待驱动层处理结束
更多阅读
一个IO的传奇一生(8) -- elevator子系统
计算机底层知识拾遗(五)理解块IO层
Linux deadline电梯调度算法
驱动层
- 把读取数据内存地址、命令等信息提交给设备控制器
- 设备控制器将驱动发过来的请求翻译成DMA控制器可识别的形式,由DMA控制器执行磁盘到内存的数据传输
- 传输完成时,由DMA控制器触发中断通知CPU读写完毕
scsi_request_fn(scsi驱动添加磁盘设备时scsi_alloc_sdev->scsi_alloc_queue->blk_init_queue_node )
组装scsi cmnd,调用scsi_dispatch_cmd发送给scsi底层驱动,注意scsi cmnd上设置了scsi_done函数
static void scsi_request_fn(struct request_queue *q)
__releases(q->queue_lock)
__acquires(q->queue_lock)
{
struct scsi_device *sdev = q->queuedata;
struct Scsi_Host *shost;
struct scsi_cmnd *cmd;
struct request *req;
/*
* To start with, we keep looping until the queue is empty, or until
* the host is no longer able to accept any more requests.
*/
shost = sdev->host;
for (;;) {
int rtn;
/*
* get next queueable request. We do this early to make sure
* that the request is fully prepared even if we cannot
* accept it.
*/
req = blk_peek_request(q);
......
/*
* Remove the request from the request list.
*/
if (!(blk_queue_tagged(q) && !blk_queue_start_tag(q, req)))
blk_start_request(req);
.....
cmd = req->special;
......
/*
* Dispatch the command to the low-level driver.
*/
cmd->scsi_done = scsi_done;
rtn = scsi_dispatch_cmd(cmd);
......
}
return;
......
}
scsi_dispatch_cmd调用scsi lower level注册的host template模板上的queuecommand将请求下发到scsi底层
scsi_done是scsi中层中断处理函数,它会在底层处理完毕后被调用,该函数中会调用__blk_complete_request触发BLOCK_SOFTIRQ,最终会调用scsi_softirq_done
try_to_wake_up
native_sched_clock
default_wake_function
autoremove_wake_function
wake_bit_function
__wake_up_common
__wake_up
__wake_up_bit
unlock_page
mpage_end_io_read
bio_endio
req_bio_endio
blk_update_request
blk_update_bidi_request
blk_end_bidi_request
blk_end_request
scsi_io_completion
scsi_finish_command
scsi_softirq_done
blk_done_softirq
__do_softirq
handle_IRQ_event
call_softirq
do_softirq
irq_exit
do_IRQ
ret_from_intr
acpi_idle_enter_cl
cpuidle_idle_call
cpu_idle
start_secondary
scsi_done可能是在磁盘控制器或者DMA控制器在IO完成后,触发的硬件中断中调用 -> 参考 Linux内核I/O scsi_done()及__blk_complete_request()调用栈信息
<IRQ> [<ffffffff8162a629>] dump_stack+0x19/0x1b
[<ffffffff812c96d4>] __blk_complete_request+0x144/0x150
[<ffffffff812c9701>] blk_complete_request+0x21/0x30
[<ffffffff81417033>] scsi_done+0x53/0xa0
[<ffffffffa00ef34b>] _scsih_io_done+0x1ab/0xb60 [mpt3sas]
[<ffffffffa00e4530>] ? mpt3sas_base_free_smid+0x120/0x240 [mpt3sas]
[<ffffffffa00e487c>] _base_interrupt+0xbc/0x8c0 [mpt3sas]
[<ffffffff81087bdc>] ? get_next_timer_interrupt+0xec/0x270
[<ffffffff81058e96>] ? native_safe_halt+0x6/0x10
[<ffffffff81114e6e>] handle_irq_event_percpu+0x3e/0x1e0
[<ffffffff8111504d>] handle_irq_event+0x3d/0x60
[<ffffffff81117ce7>] handle_edge_irq+0x77/0x130
[<ffffffff81016ecf>] handle_irq+0xbf/0x150
[<ffffffff810d9f9a>] ? tick_check_idle+0x8a/0xd0
[<ffffffff8163676a>] ? atomic_notifier_call_chain+0x1a/0x20
[<ffffffff8163d1ef>] do_IRQ+0x4f/0xf0
[<ffffffff8163252d>] common_interrupt+0x6d/0x6d
<EOI> [<ffffffff81058e96>] ? native_safe_halt+0x6/0x10
[<ffffffff8101dbcf>] default_idle+0x1f/0xc0
[<ffffffff8101e4d6>] arch_cpu_idle+0x26/0x30
[<ffffffff810cf305>] cpu_startup_entry+0x245/0x290
[<ffffffff8161a347>] rest_init+0x77/0x80
[<ffffffff81a80057>] start_kernel+0x429/0x44a
[<ffffffff81a7fa37>] ? repair_env_string+0x5c/0x5c
[<ffffffff81a7f120>] ? early_idt_handlers+0x120/0x120
[<ffffffff81a7f5ee>] x86_64_start_reservations+0x2a/0x2c
[<ffffffff81a7f742>] x86_64_start_kernel+0x152/0x175
一些问题
scsi_device.request_queue与gendisk.request_queue是什么关系?
是同一个队列
IO的请求队列何来何往
/dev/sdx inode 与 bdev的关系
什么时候调用sd_probe
SCSI Upper Layer 与LLD的联系——sd_probe
Linux那些事儿之我是SCSI硬盘(2)依然probe
- os起来时,初始化sd driver,会扫描总线上所有的device,进行driver & device绑定,这里面会调用sd_probe,为device创建gendisk等结构
- 添加磁盘时,会调用sd_probe扫描总线
裸设备读写
打开/dev/sdx
直接读写场景,IO也区分directIO/非directIO,可以参考init_special_inode
中inode.fops赋值的file_operations读写函数。
把/dev/sdx
当作一个普通文件读写来理解,那么这里涉及一个问题,在文件逻辑块号映射到磁盘物理块号时,这个文件算是在哪个文件系统上呢?
更多阅读
linux驱动子系统--SCSI
scsi块设备驱动层处理
scsi命令的执行
Linux那些事儿 之 我是Block层
linux kernel的中断子系统之(八):softirq
Linux内核读取文件流程源码及阻塞点超详解
《大话存储》——1. 磁盘控制器、驱动器控制电路和磁盘控制器驱动程序
文章汇总(包括NVMe SPDK vSAN Ceph xfs等)