上面一篇文章我们主要介绍了性能事件管道的初始化函数,这一篇文章我们主要介绍如何将性能事件给拉取出来。
signal(SIGINT, term);
signal(SIGINT, term);
这行代码的意图是设置当进程接收到 SIGINT 信号时调用 term 函数。term 应该是一个用户定义的函数,用来处理 SIGINT 信号,例如清理资源或优雅地退出程序。
int evpipe_loop(evpipe_t *evp, int *sig, int strict) {
int cpu, err, ready;
//性能事件循环
for (;!(*sig);) {
//监测性能事件
ready = poll(evp->poll, evp->ncpus, -1);
if (ready <= 0) return ready ? : 0;
//如果准备好了, 我们直接读取对应的性能事件
for (cpu = 0; ready && (cpu < evp->ncpus); cpu++) {
if (!(evp->poll[cpu].revents & POLLIN))
continue;
//不断的提取
err = evqueue_drain(&evp->q[cpu], strict);
ready--;
}
}
return 0;
}
上面我们使用了evqueue_drain
这个函数,这个函数主要的作用是从我们之前在init函数中初始化的mem中读取对应的数据。
我们首先设置三个参数,分别是内存大小,初始地址, 偏移量,
size = q->mem->data_size;
offs = q->mem->data_offset;
base = (uint8_t *)q->mem + offs;
首先我们设置一下对应的结构体:
struct perf_event_mmap_page {
__u32 version; //版本号码
__u32 compat_version; /* lowest version this is compat with */
__u32 lock; /* seqlock for synchronization */
__u32 index; /* hardware counter identifier */
__s64 offset; /* add to hardware counter value */
__u64 time_enabled; /* time event active */
__u64 time_running; /* time event on CPU */
union {
__u64 capabilities;
struct {
__u64 cap_usr_time / cap_usr_rdpmc / cap_bit0 : 1,
cap_bit0_is_deprecated : 1,
cap_user_rdpmc : 1,
cap_user_time : 1,
cap_user_time_zero : 1,
};
};
__u16 pmc_width;
__u16 time_shift;
__u32 time_mult;
__u64 time_offset;
__u64 __reserved[120]; /* Pad to 1 k */
__u64 data_head; /* head in the data section */
__u64 data_tail; /* user-space written tail */
__u64 data_offset; /* where the buffer starts */
__u64 data_size; /* data buffer size */
__u64 aux_head;
__u64 aux_tail;
__u64 aux_offset;
__u64 aux_size;
}
这个结构体是perf
工具的一部分,perf
是Linux内核提供的一个强大的性能分析工具,用于监控和测量系统和应用程序的性能。通过这个结构体,应用程序可以高效地访问硬件性能计数器的数据,进行性能分析和优化。
然后我们遍历这段内存, 我们主要使用到下面两个辅助函数:
-
__get_head
函数用于获取perf_event_mmap_page
结构体中data_head
字段的值。这是一个指向下一个可读数据块的指针。函数首先通过volatile
关键字声明的指针来读取data_head
,确保编译器不会对读取操作进行优化,从而保证每次都是直接从内存中读取最新的值。然后使用asm volatile
语句来创建一个内存屏障,防止编译器和处理器对代码进行重排序,确保在读取data_head
之前的所有操作都已经完成。 -
__set_tail
函数用于设置perf_event_mmap_page
结构体中的data_tail
字段的值。data_tail
通常用于指向下一个可写入的数据块。同样地,这里使用asm volatile
语句来创建一个内存屏障,确保在设置data_tail
之前的所有操作都已经完成,防止重排序。
static inline uint64_t __get_head(struct perf_event_mmap_page *mem)
{
uint64_t head = *((volatile uint64_t *)&mem->data_head);
asm volatile("" ::: "memory");
return head;
}
static inline void __set_tail(struct perf_event_mmap_page *mem, uint64_t tail)
{
asm volatile("" ::: "memory");
mem->data_tail = tail;
}
然后就是通过这两个辅助函数进行遍历对应的内存空间:
for (head = __get_head(q->mem); //首先获取对应内存地址的头
q->mem->data_tail != head; //判断是否已经到了结尾
//更新我们的head
__set_tail(q->mem, q->mem->data_tail + ev->hdr.size))
我们下面一步做的事情是从对应的地址空间中读取出对应的数据,
首先将队列当前的尾部位置赋值给局部变量tail, 并且根据尾部的值计算出当前的位置, 然后我们得到当前的事件。然后我们继续计算next的值。
tail = q->mem->data_tail;
this = base + (tail % size);
ev = (void *)this;
next = base + ((tail + ev->hdr.size) % size);
但是我们需要注意的一点是, 如果下一个位置next在当前位置this之前,说明已经绕到了队列的开头,需要处理内存的环绕情况。
我们需要去处理对应的内存环绕的问题,首先我们需要计算计算从当前位置this到队列末尾的距离,这个距离表示数据项在队列末尾的部分的长度。
然后重新分配q->buf的内存,使其大小等于数据项的总大小ev->hdr.size,并且将从当前位置this到队列末尾的数据复制到新分配的缓冲区q->buf的前left个字节。
将队列开头到数据项开始前的部分复制到q->buf的剩余部分。ev->hdr.size - left是这部分数据的长度。
q->buf = realloc(q->buf, ev->hdr.size);
memcpy(q->buf, this, left);
memcpy(q->buf + left, base, ev->hdr.size - left);
ev = q->buf;
然后我们根据对应的handle钩子函数去处理对应的事件, 分别有两种对应的事件, 第一种是PERF_RECORD_SAMPLE
, 我们直接调用对应的钩子函数。
switch (ev->hdr.type) {
case PERF_RECORD_SAMPLE:
err = event_handle(ev, ev->hdr.size);
break;
case PERF_RECORD_LOST:
lost = (void *)ev;
if (strict) {
_e("lost %"PRId64" events", lost->lost);
err = -EOVERFLOW;
} else {
_w("lost %"PRId64" events", lost->lost);
}
break;
default:
_e("unknown perf event %#"PRIx32, ev->hdr.type);
err = -EINVAL;
break;
}