为此,我使用了提供的溢出信号行为
perf_event_open.
我正在使用manpage建议实现溢出信号的第二种方式:
Signal overflow
Events can be set to deliver a signal when a threshold
is crossed. The signal handler is set up using the poll(2),select(2),
epoll(2) and fcntl(2),system calls.[…]
The other way is by use of the PERF_EVENT_IOC_REFRESH ioctl. This
ioctl adds to a counter that decrements each time the event overflows.
When nonzero,a POLL_IN signal is sent on overflow,but once the value
reaches 0,a signal is sent of type POLL_HUP and the underlying event
is disabled.
PERF_EVENT_IOC_REFRESH ioctl的进一步说明:
PERF_EVENT_IOC_REFRESH
Non-inherited overflow counters can use this to enable a
counter for a number of overflows specified by the argument,
after which it is disabled. Subsequent calls of this ioctl
add the argument value to the current count. A signal with
POLL_IN set will happen on each overflow until the count
reaches 0; when that happens a signal with POLL_HUP set is
sent and the event is disabled. Using an argument of 0 is
considered undefined behavior.
一个非常小的例子看起来像这样:
- #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,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,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,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,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);
- }
所以基本上我正在做以下事情:
>为SIGIO信号设置信号处理程序
>使用perf_event_open创建一个新的性能计数器(返回文件描述符)
>使用fcntl将信号发送行为添加到文件描述符.
>运行有效负载循环以执行许多指令.
执行有效负载循环时,在某些时候将执行1000条指令(sample_interval).根据perf_event_open manpage,这会触发溢出,然后递减内部计数器.
一旦该计数器达到零,“发送一个类型为POLL_HUP的信号,并且基础事件被禁用.”
当发送信号时,停止当前进程/线程的控制流程,并执行信号处理程序.场景:
>已执行1000条指令.
>事件自动禁用,并发送信号.
>立即传递信号,停止控制流程并执行信号处理程序.
这种情况意味着两件事:
>计数指令的最终数量总是等于根本不使用信号的示例.
>为信号处理程序保存的指令指针(可通过ucontext访问)将直接指向导致溢出的指令.
基本上你可以说,信号行为可以看作是同步的.
这是我想要实现的完美语义.
但是,就我而言,我配置的信号通常是异步的,并且可能会经过一段时间,直到它最终被传递并执行信号处理程序.这可能对我造成问题.
例如,请考虑以下情形:
>已执行1000条指令.
>事件自动禁用,并发送信号.
>更多指令通过
>传递信号,停止控制流程并执行信号处理程序.
这种情况意味着两件事:
>计算指令的最终数量将小于根本不使用信号的示例.
>为信号处理程序保存的指令指针将指向导致溢出的指令或其后的任何指令.
到目前为止,我已经对上面的示例进行了大量测试,并且没有遇到支持第一个场景的错过指令.
但是,我真的很想知道,我是否可以依赖这个假设.
内核会发生什么?
解决方法
I want to count the (more or less) exact amount of instructions for some piece of code. Additionally,I want to receive a Signal after a specific amount of instructions passed.
你有两个任务可能会相互冲突.当您想要计数(某些硬件事件的确切数量)时,只需在计数模式下使用cpu的性能监视单元(不要设置所使用的perf_event_attr结构的sample_period / sample_freq)并将测量代码放在目标程序中(如它是在你的例子中完成的).在根据man page of perf_event_open
的这种模式下,不会产生溢出(cpu的PMU通常为64位宽,当使用采样模式时未设置为小的负值时不会溢出):
Overflows are generated only by sampling events (sample_period must a nonzero value).
要计算程序的一部分,请使用返回fd的perf_event_open的ioctls,如man page所述
perf_event ioctl calls – VarIoUs ioctls act on perf_event_open() file descriptors: PERF_EVENT_IOC_ENABLE … PERF_EVENT_IOC_DISABLE … PERF_EVENT_IOC_RESET
您可以使用rdpmc(在x86上)或在fd上读取系统调用来读取当前值,如the man page中的简短示例所示:
- #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,unsigned long flags)
- {
- int ret;
- ret = syscall(__NR_perf_event_open,hw_event,flags);
- return ret;
- }
- int
- main(int argc,char **argv)
- {
- struct perf_event_attr pe;
- long long count;
- int fd;
- memset(&pe,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,-1,0);
- if (fd == -1) {
- fprintf(stderr,pe.config);
- exit(EXIT_FAILURE);
- }
- ioctl(fd,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,0);
- read(fd,&count,sizeof(long long));
- printf("Used %lld instructions\n",count);
- close(fd);
- }
Additionally,I want to receive a Signal after a specific amount of instructions passed.
你真的想获得信号,或者你只需要执行1000条指令的指令指针吗?如果要收集指针,请使用带采样模式的perf_even_open,但可以从其他程序执行此操作以禁用对事件收集代码的测量.此外,它对您的目标程序的负面影响较小,如果您不使用每个溢出的信号(具有大量的内核跟踪器交互和从内核切换),而是使用perf_events的功能来收集多个溢出事件进入单个mmap缓冲区并在此缓冲区上进行轮询.来自PMU的溢出中断将调用中断处理程序将指令指针保存到缓冲区中,然后重置计数,程序将返回执行状态.在您的示例中,perf中断处理程序将唤醒您的程序,它将执行多个系统调用,返回内核,然后内核将重新启动目标代码(因此每个示例的开销大于使用mmap并解析它).使用precise_ip标志,您可以激活PMU的高级采样(如果它具有这样的模式,如针对some counters的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以及simple hack也可以循环;或者像AMD x86 / amd64的IBS;关于PEBS and IBS的论文),当指令地址由具有低滑动的硬件直接保存时.一些非常先进的PMU能够在硬件中进行采样,在行中存储多个事件的溢出信息,并且无需软件中断即可自动复位计数器(precision_ip上的一些描述为in the same paper).
我不知道perf_events子系统和cpu中是否有可能同时激活两个perf_event任务:目标进程中的计数事件和同时从其他进程中采样.使用高级PMU,这可以在硬件中实现,现代内核中的perf_events可以允许它.但是,您没有提供有关内核版本以及cpu供应商和系列的详细信息,因此我们无法回答这一部分.
您也可以尝试其他API来访问PMU,如PAPI或likwid(https://github.com/RRZE-HPC/likwid).其中一些可以直接读取PMU寄存器(有时是MSR),并且可以在启用计数时同时进行采样.