初入源码-perf设计文档

2023-11-01 17:00:23 浏览数 (1)

大家好,我是程栩,一个专注于性能的大厂程序员,分享包括但不限于计算机体系结构、性能优化、云原生的知识。

本文是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系统调用来获取该文件描述符:

代码语言:javascript复制
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来提供相关配置信息:

代码语言:javascript复制
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

最不重要

如图所示:

config二进制图

如果raw_type等于1,那么这个性能计数器会对其他63位数据指向的性能计数器进行计数。这种编码取决于具体的机器。

如果raw_type等于0,那么type就会定义需要使用哪一种计数器:

代码语言:javascript复制
enum perf_type_id {
 PERF_TYPE_HARDWARE  = 0,
 PERF_TYPE_SOFTWARE  = 1,
 PERF_TYPE_TRACEPOINT  = 2,
};

如果选择了PERF_TYPE_HARDWARE,那么就会统计由event_id指向的硬件事件:

代码语言:javascript复制
/*
 * 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的软件事件:

代码语言:javascript复制
/*
 * 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_attrirq_period值为0。

通过read()系统调用可以获取到当前计数器的值,以及由read_format表征的可能其他u64的值:

代码语言:javascript复制
/*
 * 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_attrrecord_type控制每次中断的时候记录的数据:

代码语言:javascript复制
/*
 * 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_attrdisabled位表示该计数器是否在开始时是被禁用的,如果是的话,可以通过ioctlprctl来启用。

perf_event_attrinherit位如果启用,就表明计数器还应当统计当前任务的子任务。这里的子任务指的是开启统计以后生成的子任务,而不是在计数器启用时已经存在的子任务。

perf_event_attrpinned位在启用时表名这个计数器应当始终在CPU上。这仅应用于硬件事件和group leaders上。如果一个开启了pinned的计数器不能在CPU上运行了,那么该计数器会进入一个错误状态,也无法从中获取数据,除非其重新启用。之所以不在CPU上可能是因为同时启用的硬件计数器过多,硬件没有这么多计数器提供。

perf_event_attrexclusive位在启用时表示当这个计数器的组在CPU上时,该CPU上应该只有该组在使用计数器。未来将会通过更复杂的监控程序通过extra_config_len来探索CPU硬件监控单元更高阶特性,从而不会互相影响。

perf_event_attrexclude_userexclude_kernelexclude_hv提供了一种不统计userkernelhypervisor模式数据的模式。此外exclude_hostexclude_guest提供了在hypervisor下限制访问上下文的能力。

perf_event_attrmmapmunmap位允许记录程序的mmapmunmap操作,从而能够帮助将用户空间地址和实际的代码联系起来,即使整个进程都结束了,也可以进行这样的操作。这些事件都被记录在ring-buffer中。

perf_event_attrcomm位允许追踪进程创建时候的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 == -1pid == -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

个页(page)。第一个页是一个元数据页(perf_event_mmap_page),用来记录诸如ring-buffer头位置等信息:

代码语言:javascript复制
/*
 * 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数据格式是这样的:

代码语言:javascript复制
#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_attrwakeup_events 来设置每多少次进行通知。未来的工作将包括一个连接到环形缓冲区的 splice() 接口。

计数器可以通过ioctl或者prctl来进行开启和关闭。当计数器被关闭的时候,它不会进行计数或者生成数据,但是它会保持存在和当前值:

代码语言:javascript复制
// 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)

0 人点赞