当前位置: 首页 > 工具软件 > unit_perf > 使用案例 >

系统性能分析工具perf(2 of 2):perf工作原理简析

齐英朗
2023-12-01

背景

此前工作中,笔者使用perf测过CPU的CPI[1],cache miss, 内存带宽等性能指标。另外,还移植过perf uncore[2]相关的补丁。这些让我很好奇:perf大概是怎么工作的? 带着这个问题,笔者谨希望把自己的一点经验分享出来。

perf-list

perf list列出的event有这几类:1. hardware,如cache-misses; 2. software, 如context switches; 3. cache, 如L1-dcache-loads;4. tracepoint; 5. pmu。 但是,perf list仅仅把有符号名称的事件列出来了,而缺了很多硬件相关的事件。这些硬件相关事件叫作Raw Hardware Event, man perf-list有介绍。

举个例子,PMU是一组监控CPU各种性能的硬件,包括各种core, offcore和uncore事件。单说perf uncore, Intel处理器就提供了各种的性能监控单元,如内存控制器(IMC), 电源控制(PCU)等等,详见《Intel® Xeon® Processor E5 and E7 v4 Product Families Uncore Performance Monitoring Reference Manual》[3]。这些uncore的PMU设备,注册在MSR space或PCICFG space[4],可以通过下面命令看到(抹掉同类别设备):

$ls /sys/devices/ | grep uncore
uncore_cbox_0
uncore_ha_0
uncore_imc_0
uncore_pcu
uncore_qpi_0
uncore_r2pcie
uncore_r3qpi_0
uncore_ubox

但是,使用perf list只能显示IMC相关事件:

$perf list|grep uncore
  uncore_imc_0/cas_count_read/                       [Kernel PMU event]
  uncore_imc_0/cas_count_write/                      [Kernel PMU event]
  uncore_imc_0/clockticks/                           [Kernel PMU event]
 ...                    
  uncore_imc_3/cas_count_read/                       [Kernel PMU event]
  uncore_imc_3/cas_count_write/                      [Kernel PMU event]
  uncore_imc_3/clockticks/                           [Kernel PMU event]

为什么perf list没有显示其他uncore事件呢?从代码分析来看,perf list会通过sysfs去读取uncore设备所支持的event, 见linux/tools/perf/util/pmu.c:pmu_aliases():

/*
 * Reading the pmu event aliases definition, which should be located at:
 * /sys/bus/event_source/devices/<dev>/events as sysfs group attributes.
 */
 static int pmu_aliases(const char *name, struct list_head *head)

再看perf uncore的驱动代码,发现只有iMC uncore设备注册了events相关属性, 见arch/x86/events/intel/uncore_snbep.c:hswep_uncore_imc_events:

static struct uncore_event_desc hswep_uncore_imc_events[] = {
        INTEL_UNCORE_EVENT_DESC(clockticks,      "event=0x00,umask=0x00"),
        INTEL_UNCORE_EVENT_DESC(cas_count_read,  "event=0x04,umask=0x03"),
        INTEL_UNCORE_EVENT_DESC(cas_count_read.scale, "6.103515625e-5"),
        INTEL_UNCORE_EVENT_DESC(cas_count_read.unit, "MiB"),
        INTEL_UNCORE_EVENT_DESC(cas_count_write, "event=0x04,umask=0x0c"),
        INTEL_UNCORE_EVENT_DESC(cas_count_write.scale, "6.103515625e-5"),
        INTEL_UNCORE_EVENT_DESC(cas_count_write.unit, "MiB"),
        { /* end: all zeroes */ },
};

从实用性看,在所有uncore设备中,系统工程师可能最常用的就是iMC提供的内存带宽监测。其它不常用到的uncore PMU事件,可以通过Raw Hardware Event的方式,查看Intel Uncore手册[5]来指定。

在使用过程中,发现一个perf list存在的bug,iMC channel的编号不正确,发了个补丁得到了Intel工程师review,upstream还没有merge,见perf/x86/intel/uncore: allocate pmu index for pci device dynamically[6]。这是一个很明显的问题,刚开始我不相信上游或Intel会允许这样明显的问题存在,虽然问题不大,通过解决这个问题的感受是perf可能隐藏一些问题,需要在测试中提高警惕,最好能通过其他测量方式进行粗略的对比验证。

perf-stat

perf-stat是最常用到的命令,用man手册的原话就是Run a command and gathers performance counter statistics from it。perf-record命令可看做是perf-stat的一种包装,核心代码路径与perf-stat一样,加上周期性采样,用一种可被perf-report解析的格式将结果输出到文件。因此,很好奇perf-stat是如何工作的。

perf是由用户态的perf tool命令和内核态perf驱动两部分,加上一个连通用户态和内核态的系统调用sys_perf_event_open组成。

最简单的perf stat示例

perf工具是随内核tree一起维护的,构建和调试都非常方便:

$cd linux/tools/perf
$make
...
$./perf stat ls
...
 Performance counter stats for 'ls':

          1.011337      task-clock:u (msec)       #    0.769 CPUs utilized
                 0      context-switches:u        #    0.000 K/sec
                 0      cpu-migrations:u          #    0.000 K/sec
               105      page-faults:u             #    0.104 M/sec
         1,105,427      cycles:u                  #    1.093 GHz
         1,406,263      instructions:u            #    1.27  insn per cycle
           282,440      branches:u                #  279.274 M/sec
             9,686      branch-misses:u           #    3.43% of all branches

       0.001314310 seconds time elapsed

以上是一个非常简单的perf-stat命令,运行了ls命令,在没有指定event的情况下,输出了几种默认的性能指标。下面,我们以这个简单的perf-stat命令为例分析其工作过程。

用户态工作流

如果perf-stat命令没有通过-e参数指定任何event,函数add_default_attributes()会默认添加8个events。 event是perf工具的核心对象,各种命令都是围绕着event工作。perf-stat命令可以同时指定多个events,由一个核心全局变量struct perf_evlist *evsel_list组织起来,以下仅列出几个很重要的成员:

struct perf_evlist {
        struct list_head entries;
        bool             enabled;
                struct {
                int     cork_fd;
                pid_t   pid;
        } workload;
        struct fdarray   pollfd;
        struct thread_map *threads;
        struct cpu_map    *cpus;
        struct events_stats stats;
        ...
}     
  • entries: 所有events列表, 即struct perf_evsel对象;
  • pid: 运行cmd的进程pid, 即运行ls命令的进程pid;
  • pollfd: 保存sys_perf_event_open()返回的fd;
  • threads: perf-stat可以通过-t参数指定多个线程,仅在这些线程运行时进行计数;
  • cpus: perf-stat能通过 -C参数指定多个cpu, 仅当程序运行在这些cpu上时才会计数;
  • stats: 计数统计结果,perf-stat从mmap内存区读取counter值后,还要做一些数值转换或聚合等处理

perf_evlist::entries是一个event链表,链接的对象是一个个event,由struct perf_evsel表示,其中非常重要的成员如下:

struct perf_evsel {
char                    *name;
struct perf_event_attr  attr;
struct perf_counts      *counts;
struct xyarray          *fd;
struct cpu_map          *cpus;
struct thread_map       *threads;
}
  • name: event的名称;
  • attr: event的属性,传递给perf系统调用非常重要的参数;
  • cpus, threads, fd: perf-stat可以指定一些对event计数的限制条件,只统计哪些task或哪些cpu, 其实就是一个由struct xyarray表示的二维表格,最终的计数值被分解成cpus*threads个小的counter,sys_perf_event_open()请求perf驱动为每个分量值创建一个子counter,并分别返回一个fd;
  • counts: perf_counts::values保存每个分量计数值,perf_counts::aggr保存最终所有分量的聚合值。

perf的性能计数器本质上是一些特殊的硬件寄存器,perf对这样的硬件能力进行抽象,提供针对event的per-CPU和per-thread的64位虚机计数器("virtual" 64-bit counters)。当perf-stat不指定任何thread或cpu时,这样的一个二维表格就变成一个点,即一个event对应一个counter,对应一个fd。

简单介绍了核心数据结构,终于可以继续看看perf-stat的工作流了。perf-stat的工作逻辑主要在__run_perf_stat()中,大致是这样: a. fork一个子进程,准备用来运行cmd,即示例中的ls命令;b. 为每一个event事件,通过sys_perf_event_open()系统调用,创建一个counter; c. 通过管道给子进程发消息,exec命令, 即运行示例中的ls命令, 并立即enable计数器; d. 当程序运行结束后,disable计数器,并读取counter。 用户态的工作流大致如下:

__run_perf_stat()
  perf_evlist__prepare_workload()
  create_perf_stat_counter()
     sys_perf_event_open()
  enable_counters()
     perf_evsel__run_ioctl(evsel, ncpus, nthreads, PERF_EVENT_IOC_DISABLE)
        ioctl(fd, ioc, arg)
  wait()
  disable_counters()
     perf_evsel__run_ioctl(evsel, ncpus, nthreads, PERF_EVENT_IOC_ENABLE)
  read_counters()
    perf_evsel__read(evsel, cpu, thread, count)
      readn(fd, count, size)

用户态工作流比较清晰,最终都可以很方便通过ioctl()控制计数器,通过read()读取计数器的值。而这样方便的条件都是perf系统调sys_perf_event_open()用创造出来的,已经迫不及待想看看这个系统调用做了些什么。

perf系统调用

perf系统调用会为一个虚机计数器(virtual counter)打开一个fd,然后perf-stat就通过这个fd向perf内核驱动发请求。perf系统调用定义如下(linux/kernel/events/core.c):

/**
 * sys_perf_event_open - open a performance event, associate it to a task/cpu
 *
 * @attr_uptr:  event_id type attributes for monitoring/sampling
 * @pid:                target pid
 * @cpu:                target cpu
 * @group_fd:           group leader event fd
 */
SYSCALL_DEFINE5(perf_event_open,
                struct perf_event_attr __user *, attr_uptr,
                pid_t, pid, int, cpu, int, group_fd, unsigned long, flags)

特别提一下, struct perf_event_attr是一个信息量很大的结构体,kernel中有文档详细介绍[7]。其它参数如何使用,man手册有详细的解释,并且手册最后还给出了用户态编程例子,见man perf_event_open

sys_perf_event_open()主要做了这几件事情:

a. 根据struct perf_event_attr,创建和初始化struct perf_event, 它包含几个重要的成员:

/**
 * struct perf_event - performance event kernel representation:
 */
struct perf_event {
	struct pmu                      *pmu; //硬件pmu抽象
	local64_t                       count; // 64-bit virtual counter
	u64                             total_time_enabled;
	u64                             total_time_running;
	struct perf_event_context       *ctx; // 与task相关
...
}

b. 为这个event找到或创建一个struct perf_event_context, context和event是1:N的关系,一个context会与一个进程的task_struct关联,perf_event_count::event_list表示所有对这个进程感兴趣的事件, 它包括几个重要成员:

struct perf_event_context {
        struct pmu                      *pmu;
        struct list_head                event_list;
        struct task_struct              *task;
        ...
}

c. 把event与一个context进行关联,见perf_install_in_context();

d. 最后,把fd和perf_fops进行绑定:

static const struct file_operations perf_fops = {
        .llseek                 = no_llseek,
        .release                = perf_release,
        .read                   = perf_read,
        .poll                   = perf_poll,
        .unlocked_ioctl         = perf_ioctl,
        .compat_ioctl           = perf_compat_ioctl,
        .mmap                   = perf_mmap,
        .fasync                 = perf_fasync,
};

perf系统调用大致的调用链如下:

sys_perf_event_open()
	get_unused_fd_flags()
 	perf_event_alloc()
 	find_get_context()
   		alloc_perf_context()
 	anon_inode_getfile()
 	perf_install_in_context()
   		add_event_to_ctx()
 	fd_install(event_fd, event_file)

内核态工作流

perf event有两种方式:计数(counting)和采样(sampled)。

计数方式会对发生在所有指定cpu和指定进程的事件次数进行求和,对事件数值通过read()获得。

采样方式会周期性地把计数结果放在由mmap()创建的ring buffer中。回到开始的简单perf-stat示例,用的是计数(counting)方式。

接下来,我们主要了解这几个问题:

  1. 怎么enable和disable计数器?
  2. 进行计数的时机在哪里?
  3. 如何读取计数结果?

回答这些问题的入口,基本都在perf实现的文件操作集中:

static const struct file_operations perf_fops = {
        .read                   = perf_read,
        .unlocked_ioctl         = perf_ioctl,
...

首先,我们看一下怎样enable计数器的,主要步骤如下:

perf_ioctl()
	__perf_event_enable()
		ctx_sched_out() IF ctx->is_active
		ctx_resched()
			perf_pmu_disable()
			task_ctx_sched_out()
			cpu_ctx_sched_out()
			perf_event_sched_in()
				event_sched_in()
					event->pmu->add(event, PERF_EF_START)
			perf_pmu_enable()
				pmu->pmu_enable(pmu)

这个过程有很多调度相关的处理,使整个逻辑显得复杂,我们暂且不关心太多调度细节。硬件的PMU资源是有限的,当event数量多于可用的PMC时,多个virtual counter就会复用硬件PMC。因此, PMU先把event添加到激活列表(pmu->add(event, PERF_EF_START)), 最后enable计数(pmu->pmu_enable(pmu) )。PMU是CPU体系结构相关的,可以想象它有一套为event分配具体硬件PMC的逻辑,我们暂不深究。

我们继续了解如何获取计数器结果,大致的callchain如下:

perf_read()
	perf_read_one()
		perf_event_read_value()
			__perf_event_read()
				pmu->start_txn(pmu, PERF_PMU_TXN_READ)
				pmu->read(event)
				pmu->commit_txn(pmu)

PMU最终会通过rdpmcl(counter, val)获得计数器的值,保存在perf_event::count中。关于PMU各种操作说明,可以参考include/linux/perf_event.h:struct pmu{}。PMU操作的实现是体系结构相关的,x86上的read()的实现是arch/x86/events/core.c:x86_pmu_read()

event可以设置限定条件,仅当指定的进程运行在指定的cpu上时,才能进行计数,这就是上面提到的计数时机问题。很容易想到,这样的时机发生在进程切换的时候。当目标进程切换出目标CPU时,PMU停止计数,并将硬件寄保存在内存变量中,反之亦然,这个过程类似进程切换时对硬件上下文的保护。在kernel/sched/core.c, 我们能看到这些计数时机。

在进程切换前:

prepare_task_switch()
	perf_event_task_sched_out()
		__perf_event_task_sched_out() // stop each event and update the event value in event->count
			perf_pmu_sched_task()
				pmu->sched_task(cpuctx->task_ctx, sched_in)

进程切换后:

finish_task_switch()
	perf_event_task_sched_in()
		perf_event_context_sched_in()
			perf_event_sched_in()

小结

通过对perf-list和perf-stat这两个基本的perf命令进行分析,引出了一些有意思的问题,在尝试回答这些问题的过程中,基本上总结了目前我对perf这个工具的认识。但是,本文仅对perf的工作原理做了很粗略的梳理,也没有展开对PMU层,perf uncore等硬件相关代码进行分析,希望以后能补上这部分内容。

最后,能坚持看到最后的亲们都是希望更深了解性能测试的,作为福利给大家推荐本书: 《system performance: enterprise and the cloud》 书的作者是一位从事多年性能优化工作的一线工程师,想必大家都听说过他写的火焰图程序: perf Examples

Cheers!

参考索引

  1. Cycles per instruction: https://en.wikipedia.org/wiki/Cycles_per_instruction
  2. uncore: https://en.wikipedia.org/wiki/Uncore
  3. 《Intel® Xeon® Processor E5 and E7 v4 Product Families Uncore Performance Monitoring Reference Manual》
  4. 《Linux设备驱动程序》中第二章PCI驱动程序
  5. https://patchwork.kernel.org/patch/10412883/
  6. linux/tools/perf/design.txt
 类似资料: