内核更新后Linux read()读取perf计数器偶发返回errno10(无子进程)
内核更新后perf_event_open的read()调用随机返回ECHILD错误问题
我在CentOS 9 Stream系统上运行一个C++程序,通过perf_event_open和read方法收集硬件计数器数据。程序在内核版本5.14.0-319.el9.x86_64下完全正常,但更新至5.14.0-472.el9.x86_64(以及略旧的5.14.0-467.el9.x86_64)后,read()方法偶尔会返回-1,错误时机随机:有时收集数百个样本才触发,有时仅20个左右就出现,也存在全程无错误的情况。
调用read()后立即保存的errno值为10,通过strerror_r获取的错误描述为No child processes(无子进程)。
初始化硬件计数器的示例代码
#define TOTAL_HW_COUNTERS 4 const char * pmu_names[TOTAL_HW_COUNTERS] = {"instructions", "cpu-cycles", "branch-misses","cache-misses"}; unsigned int pmu_offsets[TOTAL_HW_COUNTERS] = {PERF_COUNT_HW_INSTRUCTIONS, PERF_COUNT_HW_CPU_CYCLES, PERF_COUNT_HW_BRANCH_MISSES, PERF_COUNT_HW_CACHE_MISSES}; unsigned int pmu_types[TOTAL_HW_COUNTERS] = {PERF_TYPE_HARDWARE, PERF_TYPE_HARDWARE, PERF_TYPE_HARDWARE, PERF_TYPE_HARDWARE}; int initializePerf(pid_t child_pid) { static struct perf_event_attr pe; int fd; /* The first object is the leader */ memset(&pe, 0, sizeof(pe)); pe.type = pmu_types[0]; pe.size = sizeof(pe); pe.config = pmu_offsets[0]; pe.disabled = 1; pe.inherit = 1; pe.exclude_kernel = 0; pe.exclude_hv = 0; pe.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID; fd = perf_event_open(&pe, child_pid, -1, -1, 0); if (fd == -1) { int errsv = errno; std::cerr << "Error opening leader " << pe.config << " errno: " << errsv << std::endl; exit(EXIT_FAILURE); } /* Add the rest of the perf_event_attr objects in a group with the leader */ for (int i = 1; i < TOTAL_HW_COUNTERS; i++) { memset(&pe, 0, sizeof(pe)); pe.type = pmu_types[i]; pe.size = sizeof(pe); pe.config = pmu_offsets[i]; pe.disabled = 1; pe.inherit = 1; pe.exclude_kernel = 0; pe.exclude_hv = 0; pe.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID; if (perf_event_open(&pe, child_pid, -1, fd, 0) == -1) { int errsv = errno; std::cerr << "Error opening group counter " << pe.config << " errno: " << errsv << std::endl; exit(EXIT_FAILURE); } } return fd; } int main(int argc, char* argv[]) { initializePerf(0); // Do stuff return 0; }
读取硬件计数器的代码
int readPerfCounters(int bytes_read, char * read_pmu_buf, int fd, int count) { char * bufptr = read_pmu_buf + bytes_read; int errsv; int size_read = read(fd, bufptr, count); errsv = errno; if (size_read == -1) { std::cerr << "Error reading perf counters at fd " << fd; std::cerr << " errno: " << errsv << std::endl; char buffer[ 256 ]; char * errorMsg = strerror_r(errsv, buffer, 256 ); std::cerr << errorMsg << std::endl; // Output: "No child processes" } return size_read; }
疑惑与解答
1. "No child processes"错误与read()调用有何关联?
当你调用perf_event_open时传入child_pid=0,表示监控当前进程自身,加上pe.inherit=1会同时监控该进程创建的子进程。在较新的内核版本中,perf子系统的read操作增加了对监控目标存活状态的严格检查:
- 如果程序运行过程中有子进程创建后快速退出,内核在采样周期内检测到对应计数器的目标实体失效,就会返回ECHILD
- 使用
PERF_FORMAT_GROUP时,组内任意一个计数器的目标进程/线程不存在,都会导致整个组的read调用返回该错误 - 即使没有显式创建子进程,进程内部的线程短暂销毁重建(比如线程池的动态调整),也可能触发内核的状态检查返回ECHILD
2. 不同内核版本间的相关变更
CentOS 9内核从5.14.0-319到5.14.0-467/472的更新中,perf子系统的关键变更包括:
- 修复了perf_event在进程退出时的资源泄漏问题,同时新增了已退出进程的文件描述符有效性检查逻辑
- 优化了
inherit模式下的计数器继承机制,子进程退出时内核会立即标记对应计数器状态为无效 - 完善了
read操作的错误处理流程,将之前被忽略的"目标进程不存在"场景明确映射为ECHILD错误返回
临时解决方案
- 关闭inherit模式:如果不需要监控子进程,将
pe.inherit设置为0,避免子进程生命周期变化影响主进程计数器读取 - 错误重试机制:遇到ECHILD错误时,短暂延迟(比如1ms)后重新执行
read操作,多数情况下能成功获取数据 - 重新初始化计数器:如果重试失败,可以关闭当前perf文件描述符,重新调用
initializePerf创建新的计数器实例
内容的提问来源于stack exchange,提问作者esal26
相关产品推荐
相关产品推荐

