地址:https://my.oschina.NET/u/2475751/blog/1823736
背景
此前工作中,笔者使用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; ... }
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; }
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)方式。
接下来,我们主要了解这几个问题:
回答这些问题的入口,基本都在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》(https://pan.baidu.com/s/1yyPsJxi0XWSwIKOrAWm-Vg?errno=0&errmsg=Auth%20Login%20Sucess&&bduss=&ssnerror=0&traceid=) 书的作者是一位从事多年性能优化工作的一线工程师,想必大家都听说过他写的火焰图程序: perf Examples【http://www.brendangregg.com/perf.html】
Cheers!
参考索引