初入源码-perf设计文档

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

本文是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_type1最重要
type7次要
event_id56最不重要

如图所示:

config二进制图

如果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_attrirq_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_attrrecord_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_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 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_attrwakeup_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)
google-perftools 简介 google-perftools 是一款针对 C/C++ 程序的性能分析工具,它是一个遵守 BSD 协议的开源项目。使用该工具可以对 CPU 时间片、内存等系统资源的分配和使用进行分析,本文将重点介绍如何进行 CPU 时间片的剖析。 google-perftools 对一个程序的 CPU 性能剖析包括以下几个步骤。 1. 编译目标程序,加入对 google-perftools 库的依赖。 2. 运行目标程序,并用某种方式启动 / 终止剖析函数并产生剖析结果。 3. 运行剖结果转换工具,将不可读的结果数据转化成某种格式的文档(例如 pdf,txt,gv 等)。 安装 您可以在 google-perftools 的网站 (http://code.google.com/p/google-perftools/downloads/list) 上下载最新版的安装包。为完成步骤 3 的工作,您还需要一个将剖析结果转化为程序员可读文档的工具,例如 gv(http://www.gnu.org/software/gv/)。 编译与运行 您需要在原有的编译选项中加入对 libprofiler.so 的引用,这样在目标程序运行时会加载工具的动态库。例如本例中作者的系统中,libprofiler.so 安装在"/usr/lib"目录下,所以需要在 makefile 文件中的编译选项加入“-L/usr/lib -lprofiler”。 google-perftools 需要在目标代码的开始和结尾点分别调用剖析模块的启动和终止函数,这样在目标程序运行时就可以对这段时间内程序实际占用的 CPU 时间片进行统计和分析。工具的启动和终止可以采用以下两种方式。 a. 使用调试工具 gdb 在程序中手动运行性能工具的启动 / 终止函数。 gdb 是 Linux 上广泛使用的调试工具,它提供了强大的命令行功能,使我们可以在程序运行时插入断点并在断点处执行其他函数。具体的文档请参照 http://www.gnu.org/software/gdb/,本文中将只对用到的几个基本功能进行简单介绍。使用以下几个功能就可以满足我们性能调试的基本需求,具体使用请参见下文示例。 命令 功能 ctrl+c 暂停程序的运行 c 继续程序的运行 b 添加函数断点(参数可以是源代码中的行号或者一个函数名) p 打印某个量的值或者执行一个函数调用 b. 在目标代码中直接加入性能工具函数的调用,该方法就是在程序代码中直接加入调试函数的调用。 两种方式都需要对目标程序重新编译,加入对性能工具的库依赖。对于前者,他的好处是使用比较灵活,但工具的启动和终止依赖于程序员的手动操作,常常需要一些暂停函数(比如休眠 sleep)的支持才能达到控制程序的目的,因此精度可能受到影响。对于后者,它需要对目标代码的进行修改,需要处理函数声明等问题,但得到的结果精度较高,缺点是每次重新设置启动点都需要重新编译,灵活度不高,读者可以根据自己的实际需求采用有效的方式。 示例详解 该程序是一个简单的例子,文中有两处耗时的无用操作,并且二者间有一定的调用关系。 清单 1. 示例程序 void consumeSomeCPUTime1(int input){ int i = 0; input++; while(i++ < 10000){ i--; i++; i--; i++; } }; void consumeSomeCPUTime2(int input){ input++; consumeSomeCPUTime1(input); int i = 0; while(i++ < 10000){ i--; i++; i--; i++; } }; int stupidComputing(int a, int b){ int i = 0; while( i++ < 10000){ consumeSomeCPUTime1(i); } int j = 0; while(j++ < 5000){ consumeSomeCPUTime2(j); } return a+b; }; int smartComputing(int a, int b){ return a+b; }; void main(){ int i = 0; printf("reached the start point of performance bottle neck\n"); sleep(5); //ProfilerStart("CPUProfile"); while( i++ MyProfile.pdf 转换后产生的结果文档如下图。图中的数字和框体的大小代表了的某个函数的运行时间占整个剖析时间的比例。由代码的逻辑可知,stupidComputing,stupidComputing2 都是费时操作并且它们和 consumeSomeCPUTime 存在着一定的调用关系。 图 1. 剖析结果 结束语 本文介绍了一个 Linux 平台上的性能剖析工具 google-perftools,并结合实例向读者展示了如何使用该工具配置、使用及分析性能瓶颈。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值