pidstat -d 1可按线程(tid)监控实时磁盘io,重点关注kb/s异常高(如>50mb/s)且%io接近100%的线程,结合/proc/tid/stack和strace -p tid -e trace=write,fsync,close -t定位卡在write()/fsync()的源头。

用 pidstat -d 看哪个线程在狂写
磁盘IO导致的线程延迟,往往不是“所有线程都慢”,而是某几个线程卡在 write()、fsync() 或 open() 上不动。直接看 top 或 htop 没用,它们只显示 %CPU 和 %MEM,不暴露 IO 等待细节。
真正管用的是:pidstat -d 1(每秒刷新一次)。它会按线程(TID)列出读写速率(kB/s)、I/O 等待时间(%IO),重点关注:
-
MB/s值异常高(比如单个线程持续 >50 MB/s)的 TID -
%IO接近 100% 的线程——说明它大部分时间都在等磁盘响应 - 和你的 C++ 进程 PID 对应的线程中,
Command列是否显示为你的程序名(有时是[your_app]或带pthread后缀)
拿到可疑 TID 后,用 cat /proc/[TID]/stack 查看内核栈,如果看到 __x64_sys_write → ext4_file_write_iter → blk_mq_sched_insert_requests 这类路径,基本锁定是写文件阻塞了。
检查日志线程是否在调用 write() 时卡住
C++ 多线程日志系统是最常见的 IO 延迟源头。问题不在“有没有异步”,而在于后端落盘线程是否真的避开了阻塞点。常见陷阱包括:
- 用了双缓冲,但
write()调用仍传入超大 buffer(如 >128KB),触发 Linux 内核强制回写,延迟陡增 - 开了
O_SYNC或O_DIRECT,但没用posix_memalign()对齐内存,导致write()直接返回-EINVAL并重试,形成隐式自旋 - 日志文件挂载在 ext4 默认
data=ordered模式下,单文件高频顺序写引发 journal 锁争用(iostat -x 1中await > 50ms且%util ≈ 100%是典型信号) - 后端线程在
close()或fsync()时被拖住——尤其当磁盘本身响应慢或有坏块时
验证方法:对日志线程 TID 执行 strace -p [TID] -e trace=write,fsync,close -T,观察每次系统调用耗时(<...></...> 后的秒数)。若某次 write() 耗时 >10ms,基本就是它。
区分是应用层刷盘慢,还是磁盘/文件系统真不行
线程延迟来自 IO,但根因可能在三层:你的代码 → 文件系统 → 物理磁盘。不能一看到 await 高就换 SSD。
先做隔离判断:
- 用
dd if=/dev/zero of=/tmp/test bs=4k count=10000 oflag=direct测裸盘延迟。如果dd也卡住(time dd显示耗时远超预期),说明是存储层问题 - 换成写
/dev/shm/(内存文件系统)同样逻辑的日志路径,如果延迟消失,说明是 ext4/journal 或磁盘瓶颈 - 检查
/proc/sys/vm/dirty_ratio。若压测时该值太低(如默认 20),会导致频繁强制 flush,把write()变成同步操作 - 用
lsof -p [PID] | grep log确认日志文件是否被其他进程(如 logrotate)持有句柄,造成unlink()后文件未释放,持续写入“黑洞”
特别注意:iotop 显示的“IO-BA”列是每个进程/线程的实际磁盘带宽,但它不反映延迟;而 pidstat -d 的 %IO 才是你线程是否被 IO 卡住的直接证据。
避免用 std::ofstream 在多线程里埋雷
很多团队还在用 std::ofstream + std::mutex 做日志,这本身就是延迟放大器:
- 每次
都触发格式化 + 内存分配 + 小块 <code>write(),系统调用频次爆炸 -
std::ofstream内部有锁,即使你加了外层 mutex,仍可能因缓冲区管理竞争导致隐式等待 - 析构时自动
flush(),如果此时磁盘正忙,主线程退出会被卡住
替代方案不是“换个库”,而是绕过它:
- 用
open()+write()+close()(仅初始化/关闭时),全程无格式化、无缓冲、无锁 - 日志格式化必须在前端完成(即写入缓冲区前就拼好二进制数据),后端只做 memcpy + write
- 禁用 stdio 缓冲:
setvbuf(stdout, nullptr, _IONBF, 0),避免printf类接口干扰
真正难的不是实现异步,而是让后端线程的 write() 调用不成为瓶颈——它必须短、快、可预测。一旦出现毫秒级抖动,就要怀疑是不是文件系统参数、内存对齐或单次写入大小惹的祸。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











