前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >初入源码-perf设计文档

初入源码-perf设计文档

作者头像
AshinZ
发布2023-11-01 17:00:23
2860
发布2023-11-01 17:00:23
举报

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

本文是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)
本文参与?腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-04-17,如有侵权请联系?cloudcommunity@tencent.com 删除

本文分享自 程栩的性能优化笔记 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • perf设计
  • 小结
  • 参考资料
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
http://www.vxiaotou.com