同进程内NFS写入线程干扰chronyc tracking的问题求助
问题背景
在带RT补丁的Yocto嵌入式Linux系统中,同一进程内存在两个线程:一个线程向NFS文件系统写入文件,另一个线程每秒通过popen执行一次chronyc tracking。异常现象为:当两个线程同属一个进程时,chronyc tracking无法在100ms内返回;但分属不同进程时则正常。按照Linux CFS(完全公平调度器)原理,线程所属进程不应影响调度结果。
已完成的测试
- 移除RT补丁后现象一致
- 测试4.x、5.x、6.x版本内核均出现相同问题
- 替换为CIFS文件系统写入时现象相同
- 写入SD卡或sshfs文件系统时工作正常
- 为
timeout和chronyc tracking设置RT优先级70无改善
复现代码
#include <iostream> #include <fstream> #include <thread> bool done{false}; std::string path; bool command(const char * cmd) { std::array<char, 128> buffer; std::string result; auto pipe = popen(cmd, "r"); if (!pipe) { std::cout << "Failed to open pipe\n"; return false; } while (fgets(buffer.data(), 128, pipe) != nullptr) result += buffer.data(); return pclose(pipe) == EXIT_SUCCESS; } void tracking() { while (!done) { auto res = command("timeout 0.1 chronyc tracking"); if (!res) std::cout << "Timeout waiting for chrony tracking\n"; else std::cout << "Got chrony tracking\n"; std::this_thread::sleep_for(std::chrono::seconds(1)); } } void write_file() { while (!done) { // Open file, do some writing, close file std::ofstream file; file.open(path); for (int i = 0; i < 100000; i++) { file << "lmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvbcdefghjklmnopqrstuvabcdefghjklmnopqrstuvlmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvabcdefghjklmnopqrstuvbcdefghjklmnopqrstuvabcdefghjklmnopqrstuv"; } file.close(); } } int main(int argc, char **argv) { if (argc != 3) { std::cout << argv[0] << " <both/chrony/file> /path/to/file\n"; return 1; } std::string cmd = argv[1]; path = argv[2]; if (cmd == "both") { std::jthread t1(tracking); std::jthread t2(write_file); std::this_thread::sleep_for(std::chrono::minutes(2)); done = true; } else if (cmd == "chrony") { std::jthread t1(tracking); std::this_thread::sleep_for(std::chrono::minutes(2)); done = true; } else if (cmd == "file") { std::jthread t2(write_file); std::this_thread::sleep_for(std::chrono::minutes(2)); done = true; } else { return 1; } return 0; }
perf调度追踪信息
同进程(NFS写入+chronyc)
chronyc 4123/4123 [001] 2057.379201: 1 sched:sched_switch: prev_comm=chronyc prev_pid=4123 prev_prio=120 prev_state=R+ ==> next_comm=rcu_preempt next_pid=15 next_prio=98 c0b5783c __schedule ([kernel.kallsyms]) c0b5783c __schedule ([kernel.kallsyms]) c0b57fb8 preempt_schedule_irq ([kernel.kallsyms]) c0100c0c svc_preempt ([kernel.kallsyms]) c04163f0 nfs_page_clear_headlock ([kernel.kallsyms]) c04167e0 __nfs_pageio_add_request ([kernel.kallsyms]) c0417300 nfs_pageio_add_request ([kernel.kallsyms]) c041b664 nfs_page_async_flush ([kernel.kallsyms]) c041b9d0 nfs_writepages_callback ([kernel.kallsyms]) c026b88c write_cache_pages ([kernel.kallsyms]) c041bb28 nfs_writepages ([kernel.kallsyms]) c026de78 do_writepages ([kernel.kallsyms]) c025fd8c filemap_fdatawrite_wbc ([kernel.kallsyms]) c02604bc filemap_write_and_wait_range ([kernel.kallsyms]) c041c484 nfs_wb_all ([kernel.kallsyms]) c040c9b4 nfs_file_flush ([kernel.kallsyms]) c02e08b4 filp_close ([kernel.kallsyms]) c0309014 put_files_struct ([kernel.kallsyms]) c012b2f8 do_exit ([kernel.kallsyms]) c012bb1c do_group_exit ([kernel.kallsyms]) c012bb80 __wake_up_parent ([kernel.kallsyms])
同进程(CIFS写入+chronyc)
chronyc 9269/9269 [001] 6244.795802: 1 sched:sched_switch: prev_comm=chronyc prev_pid=9269 prev_prio=120 prev_state=R+ ==> next_comm=rcuc/1 next_pid=24 next_prio=98 c0b5783c __schedule ([kernel.kallsyms]) c0b5783c __schedule ([kernel.kallsyms]) c0b57fb8 preempt_schedule_irq ([kernel.kallsyms]) c0100c0c svc_preempt ([kernel.kallsyms]) c07a87e8 macb_start_xmit ([kernel.kallsyms]) c098dc04 dev_hard_start_xmit ([kernel.kallsyms]) c09d43bc sch_direct_xmit ([kernel.kallsyms]) c098e28c __dev_queue_xmit ([kernel.kallsyms]) c0a0ae9c ip_finish_output2 ([kernel.kallsyms]) c0a0c890 __ip_queue_xmit ([kernel.kallsyms]) c0a2da94 __tcp_transmit_skb ([kernel.kallsyms]) c0a2f160 tcp_write_xmit ([kernel.kallsyms]) c0a2ff80 __tcp_push_pending_frames ([kernel.kallsyms]) c0a1b24c tcp_sendmsg_locked ([kernel.kallsyms]) c0a1ba90 tcp_sendmsg ([kernel.kallsyms]) c0964540 sock_sendmsg ([kernel.kallsyms]) c048eb10 smb_send_kvec ([kernel.kallsyms]) c04901f8 __smb_send_rqst ([kernel.kallsyms]) c0490a4c cifs_call_async ([kernel.kallsyms]) c04b85dc smb2_async_writev ([kernel.kallsyms]) c047ef1c cifs_writepages ([kernel.kallsyms]) c026de78 do_writepages ([kernel.kallsyms]) c025fd8c filemap_fdatawrite_wbc ([kernel.kallsyms]) c02604bc filemap_write_and_wait_range ([kernel.kallsyms]) c04814b0 cifs_flush ([kernel.kallsyms]) c02e08b4 filp_close ([kernel.kallsyms]) c0309014 put_files_struct ([kernel.kallsyms]) c012b2f8 do_exit ([kernel.kallsyms]) c012bb1c do_group_exit ([kernel.kallsyms]) c012bb80 __wake_up_parent ([kernel.kallsyms])
注:CIFS场景下仅首次chronyc tracking超时,后续均能在100ms内返回。
问题解答
1. 现象原因
问题核心是进程级资源锁竞争与同步IO阻塞,而非CFS调度逻辑:
- 同进程内,写线程执行
file.close()时,会触发NFS/CIFS的同步刷盘操作(如nfs_file_flush/cifs_flush),该操作会等待内核将脏页写入远程服务器,同时持有进程的files_struct(文件描述符表)相关锁。 pclose会等待chronyc子进程退出,父进程处理子进程退出的路径(do_group_exit/put_files_struct)需要访问同一份files_struct资源,导致锁竞争,进而让chronyc的等待逻辑被阻塞,最终触发timeout 0.1超时。- 分属不同进程时,资源完全隔离,写进程的刷盘操作不会影响
chronyc进程的资源访问;SD卡/sshfs正常是因为前者刷盘逻辑为本地块设备,锁机制不同,后者为用户态文件系统,同步阻塞逻辑不涉及内核进程级锁。
2. 可调整的内核参数
- NFS相关:
echo 0 > /sys/module/nfs/parameters/wbt_enable:关闭写回节流,减少同步刷盘等待时长echo 1 > /sys/module/nfs/parameters/nfs4_disable_idmapping:简化身份映射,降低操作开销- 调整
vm.dirty_writeback_centisecs为更小值(如100,即1秒),加快脏页回写
- CIFS相关:
echo 5 > /sys/module/cifs/parameters/ops_timeout:缩短CIFS操作超时时间echo 128 > /sys/module/cifs/parameters/max_credits:增加并发操作数,缓解单操作阻塞
- 调度相关:
echo 1 > /proc/sys/kernel/sched_child_runs_first:让子进程优先调度,减少chronyc启动延迟
3. 进程层面的限制
存在两类进程级限制:
- 共享资源锁竞争:进程的
files_struct是所有线程共享的核心资源,写线程的刷盘操作会持有该资源的锁,导致同一进程内处理pclose的线程被阻塞。 - 进程上下文同步阻塞:NFS/CIFS的同步刷盘操作是在进程上下文内完成的同步等待,会占用进程的调度时间片,导致同一进程内的其他线程无法及时获得CPU时间,间接延迟
chronyc的执行。
4. 有效的调试方法
- 系统调用追踪:用
perf trace -p <pid>监控目标进程的popen/pclose/close系统调用耗时,定位阻塞点。 - 内核锁追踪:用
ftrace开启lock_acquire/lock_release事件,查看files_struct相关锁的持有、等待时长。 - 线程状态监控:用
pidstat -t -d 1 <pid>实时查看进程内各线程的CPU占用、IO等待时间,对比同进程/不同进程的差异。 - 手动替换
popen:用fork+exec手动实现子进程创建,排除popen封装逻辑的影响,直接监控chronyc的调度延迟。 - 内核代码调试:在
nfs_file_flush、cifs_flush、put_files_struct等关键函数中添加打印,追踪锁的持有周期。
内容的提问来源于stack exchange,提问作者Elias Nässlund
相关产品推荐
相关产品推荐

