You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通过轮询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;
}

问题根源

  1. 唤醒配置不全:仅设wakeup_events=1不够,内核需要明确的水位线触发条件才会标记FD为POLLIN。
  2. 事件启用顺序错误:先刷新再启用事件,导致刷新的计数未生效,事件很快耗尽。
  3. 未处理缓冲区数据:内核缓冲区满后若不消费数据,可能不会再次触发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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 18:44:51