问题背景
使用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都能成功唤醒:
/proc/vmstat
问题分析
首先查看sync系统调用卡住时的代码调用流程:
可以看到sync执行下刷page函数后,检查page写完成否则进入D状态,等待page写完成后唤醒自己。
问题重点就是有1个page没有被唤醒还处于正在回写的状态导致进程卡住,不知道什么原因引起的。
查看整个ext4文件系统回写page流程后(这里耗时较多),这里猜想原因可能有3点:
- 出问题的page在fs层没有申请到block层;
- scsi层收到page写请求但没有真正写到磁盘设备;
- 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个数。
- 调度器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文件系统。
启动挂载脚本优化流程图: