背景
此前工作中,笔者使用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)方式。
接下来,我们主要了解这几个问题:
- 怎么enable和disable计数器?
- 进行计数的时机在哪里?
- 如何读取计数结果?
回答这些问题的入口,基本都在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!
参考索引
- Cycles per instruction: https://en.wikipedia.org/wiki/Cycles_per_instruction
- uncore: https://en.wikipedia.org/wiki/Uncore
- 《Intel® Xeon® Processor E5 and E7 v4 Product Families Uncore Performance Monitoring Reference Manual》
- 《Linux设备驱动程序》中第二章PCI驱动程序
- https://patchwork.kernel.org/patch/10412883/
- linux/tools/perf/design.txt