<返回更多

linux性能工具perf工作原理简析

2019-09-20    
加入收藏
地址: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)方式。

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

  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》(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!

参考索引

  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
声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>