perf_event_open溢出信号

Daw*_*odo 9 linux signals perf

我想计算某些代码的(或多或少)确切数量的指令.此外,我希望在通过特定数量的指令后收到信号.

为此,我使用perf_event_open提供的溢出信号行为 .

我正在使用manpage建议实现溢出信号的第二种方式:

信号溢出

可以设置事件以在超过阈值时传递信号.使用poll(2),select(2),epoll(2)和fcntl(2),系统调用来设置信号处理程序.

[...]

另一种方法是使用PERF_EVENT_IOC_REFRESH ioctl.这个ioctl增加了一个计数器,每次事件溢出时减少.当非零时,溢出时发送POLL_IN信号,但一旦值达到0,就会发送类型为POLL_HUP的信号,并禁用基础事件.

PERF_EVENT_IOC_REFRESH ioctl的进一步说明:

PERF_EVENT_IOC_REFRESH

非继承溢出计数器可以使用它来为参数指定的多个溢出启用计数器,之后禁用它.此ioctl的后续调用将参数值添加到当前计数.设置POLL_IN的信号将在每次溢出时发生,直到计数达到0; 当发生这种情况时,发送设置了POLL_HUP的信号并禁用该事件.使用0的参数被视为未定义的行为.

一个非常小的例子看起来像这样:

#define _GNU_SOURCE 1

#include <asm/unistd.h>
#include <fcntl.h>
#include <linux/perf_event.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

long perf_event_open(struct perf_event_attr* event_attr, pid_t pid, int cpu, int group_fd, unsigned long flags)
{
    return syscall(__NR_perf_event_open, event_attr, pid, cpu, group_fd, flags);
}

static void perf_event_handler(int signum, siginfo_t* info, void* ucontext) {
    if(info->si_code != POLL_HUP) {
        // Only POLL_HUP should happen.
        exit(EXIT_FAILURE);
    }

    ioctl(info->si_fd, PERF_EVENT_IOC_REFRESH, 1);
}

int main(int argc, char** argv)
{
    // Configure signal handler
    struct sigaction sa;
    memset(&sa, 0, sizeof(struct sigaction));
    sa.sa_sigaction = perf_event_handler;
    sa.sa_flags = SA_SIGINFO;

    // Setup signal handler
    if (sigaction(SIGIO, &sa, NULL) < 0) {
        fprintf(stderr,"Error setting up signal handler\n");
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // Configure perf_event_attr struct
    struct perf_event_attr pe;
    memset(&pe, 0, sizeof(struct perf_event_attr));
    pe.type = PERF_TYPE_HARDWARE;
    pe.size = sizeof(struct perf_event_attr);
    pe.config = PERF_COUNT_HW_INSTRUCTIONS;     // Count retired hardware instructions
    pe.disabled = 1;        // Event is initially disabled
    pe.sample_type = PERF_SAMPLE_IP;
    pe.sample_period = 1000;
    pe.exclude_kernel = 1;      // excluding events that happen in the kernel-space
    pe.exclude_hv = 1;          // excluding events that happen in the hypervisor

    pid_t pid = 0;  // measure the current process/thread
    int cpu = -1;   // measure on any cpu
    int group_fd = -1;
    unsigned long flags = 0;

    int fd = perf_event_open(&pe, pid, cpu, group_fd, flags);
    if (fd == -1) {
        fprintf(stderr, "Error opening leader %llx\n", pe.config);
        perror("perf_event_open");
        exit(EXIT_FAILURE);
    }

    // Setup event handler for overflow signals
    fcntl(fd, F_SETFL, O_NONBLOCK|O_ASYNC);
    fcntl(fd, F_SETSIG, SIGIO);
    fcntl(fd, F_SETOWN, getpid());

    ioctl(fd, PERF_EVENT_IOC_RESET, 0);     // Reset event counter to 0
    ioctl(fd, PERF_EVENT_IOC_REFRESH, 1);   // 

// Start monitoring

    long loopCount = 1000000;
    long c = 0;
    long i = 0;

    // Some sample payload.
    for(i = 0; i < loopCount; i++) {
        c += 1;
    }

// End monitoring

    ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);   // Disable event

    long long counter;
    read(fd, &counter, sizeof(long long));  // Read event counter value

    printf("Used %lld instructions\n", counter);

    close(fd);
}
Run Code Online (Sandbox Code Playgroud)

所以基本上我正在做以下事情:

  1. 为SIGIO信号设置信号处理程序
  2. perf_event_open(返回文件描述符)创建一个新的性能计数器
  3. 使用fcntl到的信号发送行为添加到该文件描述符.
  4. 运行有效负载循环以执行许多指令.

执行有效负载循环时,在某些时候sample_interval将执行1000条指令().根据perf_event_open联机帮助页,这会触发溢出,然后会减少内部计数器.一旦此计数器达到零,"发送类型为POLL_HUP的信号,并且基础事件被禁用."

当发送信号时,停止当前进程/线程的控制流程,并执行信号处理程序.场景:

  1. 已执行1000条指令.
  2. 事件被自动禁用并发送信号.
  3. 立即传递信号,停止控制流程并执行信号处理程序.

这种情况意味着两件事:

  • 计数指令的最终数量总是等于根本不使用信号的示例.
  • 为信号处理程序保存的指令指针(并且可以通过它访问ucontext)将直接指向导致溢出的指令.

基本上你可以说,信号行为可以看作是同步的.

这是我想要实现的完美语义.

但是,就我而言,我配置的信号通常是异步的,有些时间可能会通过,直到最终传递并执行信号处理程序.这可能对我来说是个问题.

例如,请考虑以下情形:

  1. 已执行1000条指令.
  2. 事件被自动禁用并发送信号.
  3. 更多指令通过
  4. 传递信号,停止控制流程并执行信号处理程序.

这种情况意味着两件事:

  • 计数指令的最终数量将小于根本不使用信号的示例.
  • 为信号处理程序保存的指令指针将指向导致溢出的指令或其后的任何指令.

到目前为止,我已经对上面的示例进行了大量测试,并且没有遇到支持第一个场景的错过指令.

但是,我真的很想知道,我是否可以依赖这个假设.内核会发生什么?

osg*_*sgx 4

我想计算某段代码的(或多或少)确切的指令数量。此外,我希望在通过特定数量的指令后收到信号。

您有两项可能相互冲突的任务。当您想要计数(某些硬件事件的确切数量)时,只需在计数模式下使用 CPU 的性能监控单元(不要设置sample_period/使用sample_freqperf_event_attr结构)并将测量代码放入目标程序中(就像完成的那样)在你的例子中)。在这种模式下,根据手册页perf_event_open不会产生溢出(CPU的PMU通常是64位宽,并且在使用采样模式时不设置为小负值时不会溢出):

溢出仅由采样事件生成(sample_period 必须为非零值)。

要对程序的一部分进行计数,请使用ioctlperf_event_open 返回的 fd 的 s,如手册页中所述

perf_event ioctl 调用 - 各种 ioctl 作用于 perf_event_open() 文件描述符: PERF_EVENT_IOC_ENABLE ... PERF_EVENT_IOC_DISABLE ... PERF_EVENT_IOC_RESET

您可以使用rdpmc(在 x86 上)或通过readfd 上的系统调用来读取当前值,如手册页中的简短示例所示:

   #include <stdlib.h>
   #include <stdio.h>
   #include <unistd.h>
   #include <string.h>
   #include <sys/ioctl.h>
   #include <linux/perf_event.h>
   #include <asm/unistd.h>

   static long
   perf_event_open(struct perf_event_attr *hw_event, pid_t pid,
                   int cpu, int group_fd, unsigned long flags)
   {
       int ret;

       ret = syscall(__NR_perf_event_open, hw_event, pid, cpu,
                      group_fd, flags);
       return ret;
   }

   int
   main(int argc, char **argv)
   {
       struct perf_event_attr pe;
       long long count;
       int fd;

       memset(&pe, 0, sizeof(struct perf_event_attr));
       pe.type = PERF_TYPE_HARDWARE;
       pe.size = sizeof(struct perf_event_attr);
       pe.config = PERF_COUNT_HW_INSTRUCTIONS;
       pe.disabled = 1;
       pe.exclude_kernel = 1;
       pe.exclude_hv = 1;

       fd = perf_event_open(&pe, 0, -1, -1, 0);
       if (fd == -1) {
          fprintf(stderr, "Error opening leader %llx\n", pe.config);
          exit(EXIT_FAILURE);
       }

       ioctl(fd, PERF_EVENT_IOC_RESET, 0);
       ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);

       printf("Measuring instruction count for this printf\n");
       /* Place target code here instead of printf */

       ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
       read(fd, &count, sizeof(long long));

       printf("Used %lld instructions\n", count);

       close(fd);
   }
Run Code Online (Sandbox Code Playgroud)

此外,我希望在通过特定数量的指令后收到信号。

您真的想获得信号还是只需要每执行 1000 条指令时就有指令指针?如果要收集指针,请使用perf_even_open采样模式,但从其他程序中执行此操作以禁用事件收集代码的测量。此外,如果您不为每个溢出使用信号(具有大量的内核跟踪器交互以及从/到内核的切换),而是使用 perf_events 的功能来收集多个溢出事件,那么它对目标程序的负面影响会更小进入单个 mmap 缓冲区并轮询该缓冲区。当 PMU 溢出中断时,将调用 perf 中断处理程序将指令指针保存到缓冲区中,然后计数将被重置,程序将返回执行。在您的示例中,perf 中断处理程序将唤醒您的程序,它将执行多个系统调用,返回内核,然后内核将重新启动目标代码(因此每个样本的开销大于使用 mmap 并解析它)。使用precise_ip标志,您可以激活 PMU 的高级采样(如果它具有此类模式,例如 intel x86/em64t 中的 PEBS 和 PREC_DIST,用于某些计数器,如 INST_RETIRED、UOPS_RETIRED、BR_INST_RETIRED、BR_MISP_RETIRED、MEM_UOPS_RETIRED、MEM_LOAD_UOPS_RETIRED、MEM_LOAD_UOPS_LLC_HIT_RETIRED 并使用简单的 hackcycles;或者像AMD x86/amd64的IBS;关于PEBS和IBS的论文),当指令地址直接由低速硬件保存时。一些非常先进的 PMU 能够在硬件中进行采样,连续存储多个事件的溢出信息,并在没有软件中断的情况下自动重置计数器(一些描述在precise_ip一篇论文中)。

我不知道在 perf_events 子系统和 CPU 中是否可以同时激活两个 perf_event 任务:两个任务都对目标进程中的事件进行计数,同时从其他进程中进行采样。借助先进的 PMU,这在硬件中是可能的,并且现代内核中的 perf_events 可能允许它。但您没有提供有关您的内核版本以及 CPU 供应商和系列的详细信息,因此我们无法回答这部分。

您还可以尝试其他 API 来访问 PMU,例如 PAPI 或 likwid ( https://github.com/RRZE-HPC/likwid )。其中一些可以直接读取 PMU 寄存器(有时是 MSR),并且可以在启用计数时允许同时采样。