Linux ext4文件系统多线程写文件sync卡住分析

问题背景

       使用SanDisk 8G SD卡接多摄像头录制视频,大概率会在剩余容量较低时出现sync同步卡住或者删除旧文件失败问题,内核版本3.10.y。

问题复现

       手动实现6进程同时写SD卡文件脚本,写完文件后执行sync同步到磁盘,同时在SD卡剩余容量低于500MB时开始删除最旧的10个文件。一般情况下在删除三次后sync就会出现卡住状态。查看sync此时为D状态,这时6个sync全部卡住并进入D状态:

       查看sync进程卡住时的状态信息:

       查看内存cache的page状态可以看到还有一个page处于writeback状态,导致了没有唤醒sync而进入D状态;其他的page都能成功唤醒:

问题分析

       首先查看sync系统调用卡住时的代码调用流程:

       可以看到sync执行下刷page函数后,检查page写完成否则进入D状态,等待page写完成后唤醒自己。

       问题重点就是有1个page没有被唤醒还处于正在回写的状态导致进程卡住,不知道什么原因引起的。

       查看整个ext4文件系统回写page流程后(这里耗时较多),这里猜想原因可能有3点:

  1. 出问题的page在fs层没有申请到block层;
  2. scsi层收到page写请求但没有真正写到磁盘设备;
  3. scsi层写完page但没有中断返回导致没有唤醒;

       下面对于上面的3个猜想逐步验证:

调试方法

       由于fs/block层不适合添加打印跟踪代码流程,所以在/sys/class目录下创建文件,使用全局变量来计数page调用流程中的个数,看出现问题后是哪一步的page少了一个,这样就能大致的定位问题在那一层;然后继续加计数器跟踪在哪一层的哪个代码函数中。

1、申请下去的page个数统计:

       函数submit_bio中bio_sectors(bio);统计了每个bio里面sector个数,1 page=8 sector。

2、bio合并到request的sector在blk_queue_bio函数统计:

       bio合并主要有3个地方尝试合并:attempt_plug_merge,elv_merge,init_request_from_bio,可以在这3个地方统计申请到调度器里面的sector个数。

  1. 调度器plug提交到scsi进行真正写磁盘设备的setcor个数在scsi_request_fn函数统计:

       blk_peek_request函数中调用了sd_prep_fn进行request组装成scsi cmd形式的命令,这里可以统计从调度器里面出来的sector个数。

4、真正写入磁盘成功的sector在scsi_io_completion函数统计:

scsi_io_completion函数中good_bytes统计了真正写入磁盘成功的字节数,可以换算成sector的个数。

5、每次scsi完成后会发一个中断可以统计:

       __blk_complete_request函数可以统计每次scsi完成一个request回写后发送的中断个数,这个数也是scsi_io_completion调用的次数。

       在统计的过程中,以sector为单位的个数整个流程都是一致的,但是以page为单位时在scsi组装request到cmd时却少了1个,所以重点查看scsi组装request到cmd函数sd_prep_fn。

       在sd_prep_fn函数中发现,有些SD卡物理地址最后的1个page不支持连续访问,需要拆分成单个sector进行访问,所以最后一个page被拆分为8个sector下发到scsi,在统计page个数就少了一个。

       增加测试代码记录最后一个page在cache中的内存地址,在函数end_page_writeback中检查最后一个page地址是否被wake_up_page(page, PG_writeback)唤醒;发现最后一个page没有进行唤醒。

       增加测试代码记录最后一个page在cache中的内存地址,在函数end_page_writeback中检查最后一个page地址是否被wake_up_page(page, PG_writeback)唤醒;发现最后一个page没有进行唤醒。

问题原因

       最后一个page因为拆分为8个sector单独访问,所以在req_bio_endio中检查bio是否完成时,传入bio_advance函数的nbytes是sector 512字节而不是page 4096字节,导致bv_offset在page内部进行偏移512字节,最后一个page写完时bv_offset偏移了3584,bv_len被减了3584字节还剩512字节:

       在最后ext4_end_bio函数中处理每个bio写完情况时,发现最后一个page的buffer_head异常,认为还处于正在写的状态,且async标志没有被清除所以需要跳过唤醒最后一个page。

       这里可以看出是内核的bug,在处理最后1个page拆分为sector时,没有处理好边界的问题。

问题解决

       查看内核版本更新记录,发现在3.13版本后不用struct bio_vec结构体来计算buffer_head,而是额外增加一个结构体struct bvec_iter,所以在检查bvec->bv_offset(不会偏移自加)条件时永远成立的,所以后面的版本在写物理地址边界时不会出现最后1个page不唤醒的情况。

       由于内核变动太大,不适合把后面版本的代码移植过来,且直接修改内核代码规避此问题风险太高,不知道后面会出什么问题,所以经过评估得出解决方案:启动挂载SD卡脚本中检查分区防止访问到边界以避免这个问题。

       启动挂载SD卡脚本会检查分区是否到边界,到边界后删除分区重新创建分区,创建分区时预留空间防止到边界,最后格式化为ext4文件系统。

       启动挂载脚本优化流程图:

猜你喜欢

转载自blog.csdn.net/TSZ0000/article/details/89317315
今日推荐