大家好,我是程栩,一个专注于性能的大厂程序员,分享包括但不限于计算机体系结构、性能优化、云原生的知识。
本文是perf系列的第五篇文章,后续会继续介绍perf,包括用法、原理和相关的经典文章。
引
今天我们接着聊perf,开始尝试边阅读源码边理解perf。perf的用户态源码位于tools/perf
目录下,通过调用perf_event_open
系统调用来获取内核的支持从而得到数据。
这篇文章主要基于内核文档翻译而成,目录为:tools/perf/design.txt
。介绍了perf
的部分设计,但是关于perf_event_open
的部分有点陈旧,可以参看最新文档。
perf设计
现代CPU中的性能计数器(performance counters)是特殊的硬件寄存器。这些寄存器在不影响内核或应用性能的情况下统计诸如指令执行、cache miss、分支预取失败等硬件事件。如果我们给它们传递具体的周期数,这些性能计数器也可以在计数到达该周期时触发中断,从而对此时CPU上运行的应用进行采样剖析(Profiling)。
Linux 性能计数器子系统(Linux Performance Counter subsystem)提供了这些硬件能力的抽象(接口),可以帮助我们获取CPU、进程等维度的数据,并且在这些能力之上,提供了事件能力。同时,其提供了虚拟的64位计数器,无论底层硬件的位宽是多少,其都可以兼容。
性能计数器可以通过特殊的文件描述符(file descriptors)来进行访问,我们可以通过sys_perf_event_open
系统调用来获取该文件描述符:
int sys_perf_event_open(struct perf_event_attr *hw_event_uptr,
pid_t pid, int cpu, int group_fd,
unsigned long flags);
该系统调用会返回新的文件描述符。我们可以通过VFS
相关的系统调用来访问,比如通过read()
来读取计数器,fcntl()
来设置模式等。
多个计数器可以被同时开启,此时他们可以被轮询访问。
当我们创建一个新的文件描述符的时候,我们需要传入perf_event_attr
来提供相关配置信息:
struct perf_event_attr {
/*
* The MSB of the config word signifies if the rest contains cpu
* specific (raw) counter configuration data, if unset, the next
* 7 bits are an event type and the rest of the bits are the event
* identifier.
*/
__u64 config;
__u64 irq_period;
__u32 record_type;
__u32 read_format;
__u64 disabled : 1, /* off by default */
inherit : 1, /* children inherit it */
pinned : 1, /* must always be on PMU */
exclusive : 1, /* only group on PMU */
exclude_user : 1, /* don't count user */
exclude_kernel : 1, /* ditto kernel */
exclude_hv : 1, /* ditto hypervisor */
exclude_idle : 1, /* don't count when idle */
mmap : 1, /* include mmap data */
munmap : 1, /* include munmap data */
comm : 1, /* include comm data */
__reserved_1 : 52;
__u32 extra_config_len;
__u32 wakeup_events; /* wakeup every n events */
__u64 __reserved_2;
__u64 __reserved_3;
};
其中,config
表示需要统计哪个计数器。config
被切分为三个模块:
属性名 | 位数 | 地位 |
---|---|---|
raw_type | 1 | 最重要 |
type | 7 | 次要 |
event_id | 56 | 最不重要 |
如图所示:
如果raw_type
等于1,那么这个性能计数器会对其他63位数据指向的性能计数器进行计数。这种编码取决于具体的机器。
如果raw_type
等于0,那么type
就会定义需要使用哪一种计数器:
enum perf_type_id {
PERF_TYPE_HARDWARE = 0,
PERF_TYPE_SOFTWARE = 1,
PERF_TYPE_TRACEPOINT = 2,
};
如果选择了PERF_TYPE_HARDWARE
,那么就会统计由event_id
指向的硬件事件:
/*
* Generalized performance counter event types, used by the hw_event.event_id
* parameter of the sys_perf_event_open() syscall:
*/
enum perf_hw_id {
/*
* Common hardware events, generalized by the kernel:
*/
PERF_COUNT_HW_CPU_CYCLES = 0,
PERF_COUNT_HW_INSTRUCTIONS = 1,
PERF_COUNT_HW_CACHE_REFERENCES = 2,
PERF_COUNT_HW_CACHE_MISSES = 3,
PERF_COUNT_HW_BRANCH_INSTRUCTIONS = 4,
PERF_COUNT_HW_BRANCH_MISSES = 5,
PERF_COUNT_HW_BUS_CYCLES = 6,
PERF_COUNT_HW_STALLED_CYCLES_FRONTEND = 7,
PERF_COUNT_HW_STALLED_CYCLES_BACKEND = 8,
PERF_COUNT_HW_REF_CPU_CYCLES = 9,
};
以上是在Linux上实现了性能计数器的所有CPU都需要支持的硬件事件,尽管在不同的CPU上可能具体的统计项可能有变化,例如有些CPU会统计多级缓存的缓存指向和失效情况。如果CPU不支持选定的硬件事件,那么系统调用会返回-EINVAL
。
现在也支持其他的硬件事件,不过这些硬件事件是基于不同的CPU的,而且是通过直接的event_id
来进行访问。例如在Intel的Core芯片上,我们可以在设置raw_type=1
的时候传入0x4064
来统计External bus cycles while bus lock signal asserted
事件。
如果选择了PERF_TYPE_SOFTWARE
,就会统计基于event_id
的软件事件:
/*
* Special "software" counters provided by the kernel, even if the hardware
* does not support performance counters. These counters measure various
* physical and sw events of the kernel (and allow the profiling of them as
* well):
*/
enum perf_sw_ids {
PERF_COUNT_SW_CPU_CLOCK = 0,
PERF_COUNT_SW_TASK_CLOCK = 1,
PERF_COUNT_SW_PAGE_FAULTS = 2,
PERF_COUNT_SW_CONTEXT_SWITCHES = 3,
PERF_COUNT_SW_CPU_MIGRATIONS = 4,
PERF_COUNT_SW_PAGE_FAULTS_MIN = 5,
PERF_COUNT_SW_PAGE_FAULTS_MAJ = 6,
PERF_COUNT_SW_ALIGNMENT_FAULTS = 7,
PERF_COUNT_SW_EMULATION_FAULTS = 8,
};
计数器有两种类型:计数计数器(counting counter)和采样计数器(sampling counter)。计数计数器用来统计事件发生的次数,其perf_event_attr
的irq_period
值为0。
通过read()
系统调用可以获取到当前计数器的值,以及由read_format
表征的可能其他u64
的值:
/*
* Bits that can be set in hw_event.read_format to request that
* reads on the counter should return the indicated quantities,
* in increasing order of bit value, after the counter value.
*/
enum perf_event_read_format {
PERF_FORMAT_TOTAL_TIME_ENABLED = 1,
PERF_FORMAT_TOTAL_TIME_RUNNING = 2,
};
使用这些额外的值可以建立一个特定计数器的过度使用率,从而帮助我们考虑到时间片轮转调度的因素。
采样计数器是一个每发生N次事件就产生一次中断的计数器,这个N就是我们前面说到的irq_period
。采样计数器的irq_period
值大于零。(由于产生了中断,所以其开销比较大并且会有采样数据不准确的情况出现,PEBS特性可以帮助减少这种情况,后续我们会介绍)
perf_event_attr
的record_type
控制每次中断的时候记录的数据:
/*
* Bits that can be set in hw_event.record_type to request information
* in the overflow packets.
*/
enum perf_event_record_format {
PERF_RECORD_IP = 1U << 0,
PERF_RECORD_TID = 1U << 1,
PERF_RECORD_TIME = 1U << 2,
PERF_RECORD_ADDR = 1U << 3,
PERF_RECORD_GROUP = 1U << 4,
PERF_RECORD_CALLCHAIN = 1U << 5,
};
这些事件数据通过ring-buffer
被记录,可以被用户态通过mmap()
访问。
perf_event_attr
的disabled
位表示该计数器是否在开始时是被禁用的,如果是的话,可以通过ioctl
或prctl
来启用。
perf_event_attr
的inherit
位如果启用,就表明计数器还应当统计当前任务的子任务。这里的子任务指的是开启统计以后生成的子任务,而不是在计数器启用时已经存在的子任务。
perf_event_attr
的pinned
位在启用时表名这个计数器应当始终在CPU上。这仅应用于硬件事件和group leaders
上。如果一个开启了pinned
的计数器不能在CPU上运行了,那么该计数器会进入一个错误状态,也无法从中获取数据,除非其重新启用。之所以不在CPU上可能是因为同时启用的硬件计数器过多,硬件没有这么多计数器提供。
perf_event_attr
的exclusive
位在启用时表示当这个计数器的组在CPU上时,该CPU上应该只有该组在使用计数器。未来将会通过更复杂的监控程序通过extra_config_len
来探索CPU硬件监控单元更高阶特性,从而不会互相影响。
perf_event_attr
的exclude_user
、exclude_kernel
和exclude_hv
提供了一种不统计user
、kernel
和hypervisor
模式数据的模式。此外exclude_host
和exclude_guest
提供了在hypervisor
下限制访问上下文的能力。
perf_event_attr
的mmap
和munmap
位允许记录程序的mmap
和munmap
操作,从而能够帮助将用户空间地址和实际的代码联系起来,即使整个进程都结束了,也可以进行这样的操作。这些事件都被记录在ring-buffer
中。
perf_event_attr
的comm
位允许追踪进程创建时候的comm
数据,这些也被记录在ring-buffer
。
sys_perf_event_open()
系统调用的pid
参数表示了任务的目标:
pid | 含义 |
---|---|
pid == 0 | 计数器统计当前进程 |
pid < 0 | 计数器统计全部的进程 |
pid > 0 | 如果当前进程有足够权限,则添加到特定pid的进程上 |
sys_perf_event_open()
系统调用的cpu
参数表示了CPU的目标:
cpu | 含义 |
---|---|
cpu >= 0 | 计数器仅统计某个CPU |
cpu == -1 | 统计全部CPU |
值得注意的是,cpu == -1
和pid == -1
的组合是无效的。
组合 | 含义 |
---|---|
cpu == -1 && pid > 0 | 创建一个针对单任务的计数器,这个计数器会随着任务进行调度切换 |
pid == -1 && cpu == x | 创建一个针对单cpu的计数器,只针对x号CPU,需要CAP_PERFMON和CAP_SYS_ADMIN权限 |
sys_perf_event_open()
系统调用的flags
参数尚未使用。
sys_perf_event_open()
系统调用的group_fd
参数允许计数器设置组。每一个组中都有一个group leader
。这个组长被首先创建,创建时其传入的参数group_fd
是-1,其他的组员被顺序创建,他们的group_fd
是组长的fd
。如果组中只有一个计数器,那么就认为这是一个只有一个人的组。
一个计数器组会被CPU作为一个单元来调度,只有当全部的计数器可以被放到CPU上时才会被调度上去。这意味着他们可以被有意的进行比较、组合和分开,毕竟他们都是做的一件事情。
统计的事件数据会被放到ring-buffer
中,由mmap
创建和访问。mmap
的大小是
1
+
2
n
1+2^n
1+2n个页(page)。第一个页是一个元数据页(perf_event_mmap_page
),用来记录诸如ring-buffer
头位置等信息:
/*
* Structure of the page that can be mapped via mmap
*/
struct perf_event_mmap_page {
__u32 version; /* version number of this structure */
__u32 compat_version; /* lowest version this is compat with */
/*
* Bits needed to read the hw counters in user-space.
*
* u32 seq;
* s64 count;
*
* do {
* seq = pc->lock;
*
* barrier()
* if (pc->index) {
* count = pmc_read(pc->index - 1);
* count += pc->offset;
* } else
* goto regular_read;
*
* barrier();
* } while (pc->lock != seq);
*
* NOTE: for obvious reason this only works on self-monitoring
* processes.
*/
__u32 lock; /* seqlock for synchronization */
__u32 index; /* hardware counter identifier */
__s64 offset; /* add to hardware counter value */
/*
* Control data for the mmap() data buffer.
*
* User-space reading this value should issue an rmb(), on SMP capable
* platforms, after reading this value -- see perf_event_wakeup().
*/
__u32 data_head; /* head in the data section */
};
请注意硬件计数器用户空间位是特定的,并且只在powerpc中实现。
接下来的ring-buffer
数据格式是这样的:
#define PERF_RECORD_MISC_KERNEL (1 << 0)
#define PERF_RECORD_MISC_USER (1 << 1)
#define PERF_RECORD_MISC_OVERFLOW (1 << 2)
struct perf_event_header {
__u32 type;
__u16 misc;
__u16 size;
};
enum perf_event_type {
/*
* The MMAP events record the PROT_EXEC mappings so that we can
* correlate userspace IPs to code. They have the following structure:
*
* struct {
* struct perf_event_header header;
*
* u32 pid, tid;
* u64 addr;
* u64 len;
* u64 pgoff;
* char filename[];
* };
*/
PERF_RECORD_MMAP = 1,
PERF_RECORD_MUNMAP = 2,
/*
* struct {
* struct perf_event_header header;
*
* u32 pid, tid;
* char comm[];
* };
*/
PERF_RECORD_COMM = 3,
/*
* When header.misc & PERF_RECORD_MISC_OVERFLOW the event_type field
* will be PERF_RECORD_*
*
* struct {
* struct perf_event_header header;
*
* { u64 ip; } && PERF_RECORD_IP
* { u32 pid, tid; } && PERF_RECORD_TID
* { u64 time; } && PERF_RECORD_TIME
* { u64 addr; } && PERF_RECORD_ADDR
*
* { u64 nr;
* { u64 event, val; } cnt[nr]; } && PERF_RECORD_GROUP
*
* { u16 nr,
* hv,
* kernel,
* user;
* u64 ips[nr]; } && PERF_RECORD_CALLCHAIN
* };
*/
};
请注意:PERF_RECORD_CALLCHAIN
取决于特定的架构,目前仅在x86
上实现。
我们可以通过poll()
、select()
、epoll()
和fcntl()
来管理信号通知新事件。通常当一页数据写满的时候会进行通知,我们也可以通过设置perf_event_attr
的wakeup_events
来设置每多少次进行通知。未来的工作将包括一个连接到环形缓冲区的 splice() 接口。
计数器可以通过ioctl
或者prctl
来进行开启和关闭。当计数器被关闭的时候,它不会进行计数或者生成数据,但是它会保持存在和当前值:
// ioctl开启和关闭计数器
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
// prctl开启和关闭计数器
prctl(PR_TASK_PERF_EVENTS_ENABLE);
prctl(PR_TASK_PERF_EVENTS_DISABLE);
对于一组计数器,在参数中传递PERF_IOC_FLAG_GROUP
可以开启或者关闭组长计数器从而开启或者关闭这一组计数器。当组长计数器关闭时,整个组的计数器都不会进行计数;关闭非组长计数器时,不会影响到其他计数器的运行。
小结
今天我们阅读了一篇Linux文档,从而了解到了一些关于perf
执行的过程,还有更多的内容等待我们去探索。
参考资料
- design.txt(https://github.com/torvalds/linux/blob/master/tools/perf/design.txt)
- perf_event_open(https://man7.org/linux/man-pages/man2/perf_event_open.2.html)
些关于perf
执行的过程,还有更多的内容等待我们去探索。
[外链图片转存中…(img-WoVWIQPU-1681822791088)]
参考资料
- design.txt(https://github.com/torvalds/linux/blob/master/tools/perf/design.txt)
- perf_event_open(https://man7.org/linux/man-pages/man2/perf_event_open.2.html)