引言
一、read涉及的子系统
用户态调用SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t,count) 进入vfs。vfs先把数据存放在内核page中,然后从page中拷贝需要的数据到用户态缓存中。
二、页缓存page cache
- 对于read而言,如果page cache没有需要的数据页,或者数据页为PG_dirty,那就需要启动一次磁盘IO操作把磁盘上的数据读到page中,并把page状态设为PG_uptodate,最后把page中对应的数据拷贝到用户态buf中。
- 对于write而言,如果page cache中没有需要的数据页,同read一样需要把磁盘中的数据读到page中,然后对page执行写操作。如果数据页为PG_dirty,这说明page中的数据一定比磁盘块数据新(nfs不一定),所以只需对page直线写操作即可,也不用考虑磁盘数据。
文件的偏移(file->f_pos)转换成page index,radix_tree通过index来管理radix tree中的page。page index从bit0到bit31分为几段,每段6个bit(最后一个段2bit),被用来做radix_tree_node的slot数组索引,代码中用radix_tree_node->slots[RADIX_TREE_MAP_SIZE]来表示这个数组。在上面图中,树的高度为2,所以只需要用到2个6bit的位段,高位段(bit11~bit6)用来表示第一层节点中slot数组的索引,低位段(bit5~bit0)用来表示第二层节点中slot数组索引,第二层数组slot中的元素是叶子节点,表示page。
3.10.14版本中为每个中间节点都包含子节点的一些状态标记,radix_tree_node->tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS],一共有3个:
- PAGECACHE_TAG_DIRTY 表示子节点中存在脏页
- PAGECACHE_TAG_WRITEBACK 表示子节点中有页正在回写到磁盘
- PAGECACHE_TAG_TOWRITE 表示子节点中有页将要被回写到磁盘
三、映射层mapper layer
当数据需要从磁盘上读取时,就需要经过mapper layer将文件偏移量对应的逻辑块号转换成磁盘上的物理块号,然后才能从磁盘上读取数据。不同的磁盘文件系统,mapper layer机制不一样,以ext3为例,它采用了直接寻址与间接寻址结合的方式。
文件偏移量file->f_pos转换成page index后,乘以blocks per page,得到逻辑块号。inode->i_data[15]前12个元素存放的是数据物理块号,i_data[12]存放的是一个间址块号,这个间址块按4字节划分成,每个小块中存放的物理数据物理块号,由于用到了一个间址块,所以称为一级间址。i_data[13]是二级间址,i_data[14]是三级间址。
四、磁盘文件系统disk filesystem
五、块设备io层block io layer
块设备io层用来对提交的io请求根据sector号排序、合并,使磁头以一个合理的次序访问这些物理扇区,减少磁头移动距离,从而到达较优的性能。比如第一个io请求sector号为5,第7二个io请求扇区号为20,第三个请求扇区号为7,如果不排序,那么磁头以5→20→7这样的次序移动,而排序后只需要以5→7→20这样移动。合并后的bio生成request,request存放在request queue中(又称为dispatch list),每个磁盘有一个这样的request queue,这个队列中的request会被转换成底层驱动的command下发到驱动中。
现在的磁盘支持scatter-gather模式,在读操作中控制器从相邻磁盘扇区中获得数据,然后将他们存放在不同的内存区中。bio->bi_sector描述了磁盘上的起始扇区号。bio->bi_io_vec是一个数组,每一个元素称为段,描述的是内存数据位置,对应一个内存区,如果是scatter-gather模式,那就可能有多个段。
- submit_bio流程:
submit_bio-->generic_make_request--> q->make_request_fn(q,bio)
每个block初始化时会设置make_request_fn,对于scsi磁盘:
scsi_alloc_queue-->__scsi_alloc_queue--> blk_init_queue--> blk_init_queue_node --> blk_init_allocated_queue--> blk_queue_make_request(q, blk_queue_bio)
所以q->make_request_fn即blk_queue_bio
- bio的合并:
因为磁盘的机械特性,移动磁头的时间比较长,所以需要根据bio读写的磁盘扇区号对bio进行排序合并,合并的原则一般是bio的扇区号与已有的request是否连续。当bio合并到request中后,这个request有与相邻的request存在扇区号连续的可能,所以需要对request再尝试合并一次。先尝试合并到pluglist中,合并失败的话再尝试合并到调度器中。
- 引入plug list是因为这个list是每个进程自己的,合并bio时,不需要获取q lock,避免锁争用。
- bio无法合并到plug list,需要通过elv_merge将bio合并到调度器某个request中,bio合并成功的话还需要再通过elv_merged_request继续对request合并。bio合并完成后,这个request肯定是在调度器中了,系统后继肯定会从调度器中取出这个io放到磁盘的queue中,并将queue中的request逐个下发到驱动层。
- request的申请:
如果bio不能完成上面的合并,就会通过get_request申请一个新的request结构,然后通过init_request_from_bio初始化把bio加入到这个新的request。
- 如果打开了plug list,那么把这个request暂时放在plug list中。
- 如果打开了plug list,那么把这个request暂时放在plug list中。 如果没有打开plug list,通过add_acct_request把新request加入到调度器中。接着执行__blk_run_queue-->__blk_run_queue_uncond-->q->request_fn这个函数是scsi_request_fn
六、块设备驱动层
scsi_request_fn循环处理gendisk的rq,将rq封装成cmd传给low-level driver。当gendisk没有rq需要处理,或者low-level driver不能再接受更多的rq时,退出循环。
scsi_request_fn首先从request_queue(也就是dispatchlist)头部至尾依次取出一个rq封装成cmd,然后传给low-level drive处理。在取每个rq前,会判断low-level drive是否busy(不能再处理更多的请求了),如果busy那么scsi_request_fn退出循环。这个过程见图中1如果request_queue为空,并且low-leveldirve可以继续处理请求,那么会找到当前request_queue对应的cfq_data,这个结构中维护了一些rbtree,从而可以找到下一次待处理的rq,并把这个rq添加到request_queue尾部,见图中2,然后继续执行中1的流程。
七、io完成后硬件发送中断通知设备驱动程序
这里针对single queue linux block layer描述(从linux3.13版本开始合入了multi-queue)。
read操作由于等待磁盘io数据,所以当前进程阻塞睡眠在某个cpu等待队列上。当io完成后,硬件发送中断通知系统中的某个cpu(现在的系统,中断都是发送给某个cpu,而不是所有的cpu),但接收到中断的cpu可能不是read进程睡眠的cpu,如果用接收到硬件中断的cpu去唤醒其他cpu上的进程,开销比较大。在中断中,会触发软中断,由软中断去做io完成后的处理工作比如唤醒进程,所以只要在提交io请求的cpu上触发软中断,就可以唤醒本cpu上睡眠的进程。代码blk-softirq.c中通过req->cpu跟踪req之前是由哪个cpu提交的,触发软中断时会判断当前cpu与req->是否相同,如果相同就在本cpu上触发软中断,否则在req->cpu上触发软中断。软中断会调用回调函数bio->bi_end_io,即mpage_end_io,这个回调函数会释放page锁唤醒read进程,详细过程其他文章再说。
- 所有cpu共享一个request queue,对request_queue->queue_lock的竞争比较多。
- 大多数情况下,完成一次io需要两次中断,一个是硬件中断,一个是IPI核间中断用于触发其他cpu上的软中断。
- 如果提交io请求的cpu不是接收到硬件中断的cpu,还存在远端内存访问的问题。