通过轮询perf_event_open返回的FD监控CPU周期消耗无响应问题
问题与解决方案
现有代码用信号通知perf事件时能正常工作,但多线程/全进程场景下信号使用繁琐,想通过poll监控perf_event_open返回的FD实现无信号监控。添加mmap后poll仍一直不返回,预期的revents: <POLLIN值>从未出现。
原代码
#include <linux/perf_event.h> #include <sys/syscall.h> #include <unistd.h> #include <stdint.h> #include <string.h> #include <sys/mman.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <poll.h> #include <sys/ioctl.h> void * sweat(void * p) { uint64_t z=1; for (int b=0;b<10;b++) { for (int a=0;a<99999999;a++) { //sched_yield(); z*=a; } } return (void*)z; } void * sweat_forever(void * p) { while(1) sweat(0); } pthread_t background(void * (*f)(void *)) { pthread_t pid; pthread_create(&pid, 0, f, 0); return pid; } int main() { struct perf_event_attr pe; memset(&pe, 0, sizeof(pe)); pe.type = PERF_TYPE_HARDWARE; pe.size = sizeof(pe); pe.config = PERF_COUNT_HW_CPU_CYCLES; pe.sample_type = PERF_SAMPLE_IP; pe.wakeup_events = 1; pe.sample_period = 2000000; // threshold pe.disabled = 1; pe.exclude_kernel = 0; pe.exclude_hv = 1; pid_t pid = getpid(); //Whole process //pid_t pid = 0; //This thread int fd = syscall(__NR_perf_event_open, &pe, pid, -1, -1, 0); if (fd == -1) { perror("perf_event_open"); exit(1); } size_t page_size = sysconf(_SC_PAGESIZE); struct perf_event_mmap_page * meta = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (meta == MAP_FAILED) { perror("mmap"); exit(1); } ioctl(fd, PERF_EVENT_IOC_REFRESH, 100); ioctl(fd, PERF_EVENT_IOC_ENABLE, 0); background(sweat_forever); printf("Monitor waiting for PMU events...\n"); struct pollfd pfd = { .fd = fd, .events = POLLIN | POLLERR }; while (1) { int ret = poll(&pfd, 1, -1); //This doesn't return if (ret < 0) { perror("poll"); exit(1); } printf("revents:%x\n", pfd.revents ); usleep(1000); } return 0; }
问题根源
- 唤醒配置不全:仅设
wakeup_events=1不够,内核需要明确的水位线触发条件才会标记FD为POLLIN。 - 事件启用顺序错误:先刷新再启用事件,导致刷新的计数未生效,事件很快耗尽。
- 未处理缓冲区数据:内核缓冲区满后若不消费数据,可能不会再次触发POLLIN。
修改后的代码
#include <linux/perf_event.h> #include <sys/syscall.h> #include <unistd.h> #include <stdint.h> #include <string.h> #include <sys/mman.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <poll.h> #include <sys/ioctl.h> void * sweat(void * p) { uint64_t z=1; for (int b=0;b<10;b++) { for (int a=0;a<99999999;a++) { //sched_yield(); z*=a; } } return (void*)z; } void * sweat_forever(void * p) { while(1) sweat(0); } pthread_t background(void * (*f)(void *)) { pthread_t pid; pthread_create(&pid, 0, f, 0); return pid; } int main() { struct perf_event_attr pe; memset(&pe, 0, sizeof(pe)); pe.type = PERF_TYPE_HARDWARE; pe.size = sizeof(pe); pe.config = PERF_COUNT_HW_CPU_CYCLES; pe.sample_type = PERF_SAMPLE_IP; pe.wakeup_events = 1; // 设置水位线:缓冲区有至少1个事件时触发POLLIN pe.wakeup_watermark = 1; pe.sample_period = 2000000; // 采样阈值 pe.disabled = 1; pe.exclude_kernel = 0; pe.exclude_hv = 1; pid_t pid = getpid(); // 监控整个进程 //pid_t pid = 0; // 仅监控当前线程 int fd = syscall(__NR_perf_event_open, &pe, pid, -1, -1, 0); if (fd == -1) { perror("perf_event_open"); exit(1); } size_t page_size = sysconf(_SC_PAGESIZE); struct perf_event_mmap_page *meta = mmap(NULL, page_size * 2, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (meta == MAP_FAILED) { perror("mmap"); exit(1); } // 先启用事件,再无限刷新(-1表示持续采样,不会因计数耗尽停止) ioctl(fd, PERF_EVENT_IOC_ENABLE, 0); ioctl(fd, PERF_EVENT_IOC_REFRESH, -1); background(sweat_forever); printf("Monitor waiting for PMU events...\n"); struct pollfd pfd = { .fd = fd, .events = POLLIN | POLLERR }; while (1) { int ret = poll(&pfd, 1, -1); if (ret < 0) { perror("poll"); exit(1); } printf("revents:%x\n", pfd.revents ); // 消费缓冲区数据:更新tail告知内核数据已处理,否则可能不再触发POLLIN uint64_t head = meta->data_head; __sync_synchronize(); // 此处可添加采样数据解析逻辑,示例省略 __sync_synchronize(); meta->data_tail = head; usleep(1000); } return 0; }
核心修改点
- 新增
pe.wakeup_watermark = 1:明确内核触发POLLIN的条件。 - 调整
ioctl顺序:先启用事件再无限刷新,保证采样持续进行。 - 添加缓冲区消费逻辑:更新
data_tail,让内核知道可以继续写入新数据并触发下一次POLLIN。
内容的提问来源于stack exchange,提问作者Adrian May
相关产品推荐
相关产品推荐

