perf 后端:硬件 PMU(下)

转自:https://zhuanlan.zhihu.com/p/678581247
苏里南公牛
公众号:窗有老梅

  1. 目录

  2. 综述

  3. 事件设计

  4. 编码设计

3.1 PMC ID 编码空间划分

3.2 PMU 抽象

3.3 事件抽象

3.3.1 业务层抽象

3.3.2 硬件层抽象

3.4 设计大图

  1. 工程实践

4.1 模型

4.2 PMU 初始化

4.3 事件初始化

4.3.1 my_perf_event_init

4.3.2 my_sys_perf_event_open

4.3.3 intel_pmu_hw_config

4.4 事件使能

4.4.1 my_perf_event_enable

4.4.2 x86_assign_hw_event

4.4.3 intel_pmu_enable_event

4.5 事件读取

4.6 事件禁能

  1. 总结

  2. 伏笔

  3. 附录

  4. 综述
    本文乃内核 perf 框架解构系列文章第三篇。

《[perf 2] perf 后端:硬件 PMU(上)》一文,我们讨论了 PMU 硬件的基本使用范式,架构相关的概念,以及寄存器层面的基本操作及编程。

本文在上文基础上进行编码实践,目的是展示 Intel x86 架构下硬件 PMU 的编程操作,并从实践中提炼 perf 框架所面临和要解决的问题。

本文在 4.9 内核、skylake 超线程平台上编写一个内核模块,模块的功能是使用硬件 PMU 采集相应事件。本文代码与 OS、处理器平台关系不是很大,可以移植到其他平台。

阅读本文之前,强烈建议先阅读《[perf 2] perf 后端:硬件 PMU(上)》。

本文所有代码在第 7 章附录中,读者自行编译成内核模块运行验证(请不要直接在生产环境中干这事)。

  1. 事件设计
    本文通过硬件 PMU,采集 CPU 3 上的 ‘Instruction Retired’ 及 ‘UnHalted Core Cycles’ 事件。

之所以选择此二者事件,是因为这二者不仅可以通过 generic PMC(指定 umask + eventsel 的方式)采集,亦可通过 fixed PMC 采集,如此我们可以同时利用两种 PMC 来对此二者事件进行监控,通过相互对比采集的事件数值确认程序的正确性(符合预期的情况是,两种 PMC 采集出来的事件数值几乎相等)。

根据 Intel SDM Chapter 18 “Performance Monitoring”,此二者事件是 architecture 的。所谓 architecture,指的是在不同架构之间皆相同,其反义词是 model specific。

‘Instruction Retired’ 事件的 umask 和 eventsel 分别是 00H 和 C0H;‘UnHalted Core Cycles’ 事件的 umask 和 eventsel 分别是 00H 和 3CH:
在这里插入图片描述
图 1

同时,此二者还可以通过第 0 和 第 1 个 fixed PMC 采集:

在这里插入图片描述
图 2

若要查询非 architecture 事件的 umask、eventsel,自行参阅 Intel SDM 19 “Performance Monitoring Events”,不在本文话题范围内。

  1. 编码设计
    按说事件设计好,知道怎么用内核接口做 MSR 读写、发起 smp_call 以及起 hrtimer,基本上就可以开干了。

但为了让代码看起来不是这么的 naive,我们稍微做一点点设计。

3.1 PMC ID 编码空间划分
如你所知,generic PMC 和 fixed PMC,在实际编程中都是通过 ID 来索引的,也就是第几个 generic/fixed PMC。而此二者 ID 编码都是从 0 开始。

为了不让二者的 ID 编码混淆,我们对这二者 PMC 的 ID 做编码空间划分。根据《[perf 2] perf 后端:硬件 PMU(上)》“4.3 PMU 的配置及使能”一节,“至少从目前的 Intel CPU 设计来说,其为 generic PMC 预留的数量为 32”,一个合理的编码空间划分方案如下:

0 - 31:用于 generic PMC 的 ID 编码。
32 - :用于 fixed PMC 的 ID 编码。
我们参考内核的实现,这里将 32 这个魔术字定义为宏:

#define INTEL_PMC_IDX_FIXED		

所以,编码为 32 的 fixed PMC,对应的是第 0 个 fixed PMC,以此类推。

3.2 PMU 抽象
数据结构如下(之所以加个 my_ 前缀,是因为内核中也有 x86_pmu 的数据结构,虽然本人很反感 my_ 前缀的 naming,但先将就着。下文的数据结构同理。):

struct my_x86_pmu {
  int version;
  unsigned eventsel;
  unsigned perfctr;
  int num_counters;
  int num_counters_fixed;
  int cntval_bits;
  int (*rdpmc_index)(int index);

  void (*enable)(struct my_perf_event *);
  void (*disable)(struct my_perf_event *);
  void (*read)(struct my_perf_event *event);
  void (*hw_config)(struct my_perf_event *event);
};

version:PMU 的版本。
eventsel:PMU IA32_PERFEVTSELx 寄存器起始地址。实际上,eventsel 是寄存器组,起始地址为 186H,一个 IA32_PERFEVTSELx 对应一个 generic PMC,用于对此 generic PMC 进行配置。
perfctl:PMU 的 IA32_PMCx 寄存器起始地址。实际上,perfctl 是一个寄存器组,起始地址为 0C1H,可以通过 IA32_PMCx 读出一个 generic PMC 的数值(本文采用与内核相同的方式,使用 RDPMC 指令,而不是从 IA32_PMCx 中读取)。
num_counters:generic PMC 数量。
num_counters_fixed:fixed PMC 数量。
cntval_bits:PMC 数据读出宽度。不同 PMU 实现,其 PMC 数据宽度可能是 48 bit 或其他,但内核的 perf 框架侧,会统一将其转成 64 bit 输出,方法是符号扩展,具体符号扩展算法需要依赖 PMC 的实际数据宽度,也就是 cntval_bits。
rdpmc_index:如前文所言,内核 perf 框架中读取 PMC 数据时,使用的是 RDPMC 指令,此指令需要传入一个 index,rdpmc_index 函数将 PMC 的 ID 转成 RDPMC 指令所需的 index。实际上,此函数并未实现,Intel 平台下,读取 generic PMC 数值的 RDPMC index 参数,就是此 PMC 的 ID。fixed PMC index 的计算略有不同,参考下文“4.4.2 x86_assign_hw_event”一节。
enable/disable/read:使能、禁能、读取一个 event。
hw_config:此函数获取一个事件在寄存器层面的配置,参考下文“4.3.3 intel_pmu_hw_config”一节。

3.3 事件抽象
3.3.1 业务层抽象
所谓的业务层,指的是用户通过 perf 前端接口传进来的事件属性。

struct my_perf_event_attr {
	u64 config;
};

struct my_perf_event {
  local64_t count;
  struct my_perf_event_attr attr;
  struct my_hw_perf_event hw;
};

my_perf_event_attr.config:这是业务层传入的事件属性,是业务层语义,注意与下一小节中硬件层抽象 config 的区别。
count:该事件的具体数值,用户读取一个事件的具体数值时,返回的就是这个值,参考下文“4.5 事件读取”一节。
attr:事件中保存的业务层所传入的属性。
hw:一个业务层的事件,其具体采集任务需要一个具体的硬件 PMC,struct my_hw_perf_event 即是对硬件 PMC 的抽象。参考下一小节。

3.3.2 硬件层抽象

struct my_hw_perf_event {
  struct { /* hardware */
    u64 config;
    unsigned long config_base;
    unsigned long event_base;
    int event_base_rdpmc;
    int idx;
  };

  local64_t prev_count;
};

config:事件的硬件层配置,对应一个 PMC 配置寄存器的配置,比如是否使能用户态(USER)下的事件采集。
config_base:此 PMC 的配置寄存器,对于 generic PMC,其是 IA32_PERFEVTSELx,对于 fixed PMC,其是 IA32_FIXED_CTR_CTRL 。
event_base:此 PMC 的读数寄存器,对于 generic PMC,其是 IA32_PMCx,对于 fixed PMC,其是 IA32_FIXED_CTRx。
event_base_rdpmc:使用 RDPMC 指令读取此 PMC 数值时,应该传入的 index 参数。
idx:此 PMC 的 ID 编码,注意,这是经过 ID 编码空间划分后的 ID。
prev_count:上一次从该 PMC 读出的事件计数值。

3.4 设计大图

在这里插入图片描述
图 3

  1. 工程实践
    4.1 模型
    代码的模型,是在一个 kthread 中,监控 3 号 CPU 的 instruction 和 cycles 事件,并同时采用 generic 和 fixed PMC 两种方案:
static int test_loop(void *__)
{
  unsigned long long prev_count[4] = { 0 }, now_count[4];
  struct my_perf_event events[4];
  int target_cpu = 3, i;

  // suppose we are using the #2 generic pmc for 'Instruction Retired', umask 00h, event C0h
  // suppose we are using the #3 generic pmc for 'UnHalted Core Cycles', umask 00h, event 3Ch
  my_perf_event_init(&events[0], 0x00, 0xc0, 2, false);
  my_perf_event_init(&events[1], 0x00, 0x3c, 3, false);

  // suppose we are using the #0 fixed pmc for 'Instruction Retired', event C0h
  // suppose we are using the #1 fixed pmc for 'UnHalted Core Cycles', event 3Ch
  my_perf_event_init(&events[2], 0x00, 0xc0, 0, true);
  my_perf_event_init(&events[3], 0x00, 0x3c, 1, true);

  for (i = 0; i < ARRAY_SIZE(events); ++i)
  	my_perf_event_enable(target_cpu, &events[i]);

  for (i = 0; i < ARRAY_SIZE(events); ++i)
  	prev_count[i] = my_perf_event_read(target_cpu, &events[i]);

  while (!kthread_should_stop()) {
    for (i = 0; i < ARRAY_SIZE(events); ++i) {
      now_count[i] = my_perf_event_read(target_cpu, &events[i]);
      printk("event %d: %llu\n", i, now_count[i] - prev_count[i]);
      prev_count[i] = now_count[i];
    }

    msleep_interruptible(5000);
  }

  for (i = 0; i < ARRAY_SIZE(events); ++i)
  	my_perf_event_disable(target_cpu, &events[i]);
  
  return 0;
}

static int __init test_module_init(void)
{
  intel_pmu_init();
  loop_task = kthread_run(test_loop, NULL, "test_loop");

  return 0;
}

static void __exit test_module_exit(void)
{
	kthread_stop(loop_task);
}

模型总体逻辑如下:

事件初始化(7 - 15 行):调用 my_perf_event_init 接口初始化四个 PMC 监控事件,前两个采用 generic PMC 来监控,后两个采用 fixed PMC 来监控。
事件使能(17 - 18 行):调用 my_perf_event_enable 接口使能上一步初始化的四个事件。
事件初始值读取(20 - 21 行):调用 my_perf_event_read 接口读出四个事件的初始值。注意,PMC 第一次使用时,其中可能有残留的值(上一次被使用后残留的值),为了获取准确的读数,我们先读出其初始值,后续取每两次读数的差值,也即净增长值。
事件净增长值读取(23 - 31 行):调用 my_perf_event_read 接口读出四个事件的当前值,并与上一次的读数做差获取净增长值。本文模型下,每 5 秒采集一次。
事件禁能(33 - 34 行):事件禁能。
PMU 初始化(41 行):module 的 init 函数中,调用 intel_pmu_init 初始化 PMU。
下面,逐个拆解函数的具体实现。

4.2 PMU 初始化
my_x86_pmu 是对 x86 体系结构 PMU 的抽象,其各项回调函数,由具体处理器平台实现。

本文实验是在 Intel x86 平台上,底层函数是 intel_pmu_ 前缀的 Intel PMU 实现。

static struct my_x86_pmu x86_pmu = {
  .eventsel = MSR_ARCH_PERFMON_EVENTSEL0,
  .perfctr = MSR_ARCH_PERFMON_PERFCTR0,

  .enable = intel_pmu_enable_event,
  .disable = intel_pmu_disable_event,
  .read = intel_pmu_read_event,
  .hw_config = intel_pmu_hw_config,
};

intel_pmu_init 中做 PMU 的基本信息获取:

void intel_pmu_init(void)
{
  union cpuid10_edx edx;
  union cpuid10_eax eax;
  union cpuid10_ebx ebx;
  unsigned int unused;
  int version;

	cpuid(10, &eax.full, &ebx.full, &unused, &edx.full);

	version = eax.split.version_id;

  x86_pmu.version = version;
  x86_pmu.num_counters = eax.split.num_counters;
  x86_pmu.cntval_bits = eax.split.bit_width;

  if (version > 1) {
  	int assume = 3 * !boot_cpu_has(X86_FEATURE_HYPERVISOR);

    x86_pmu.num_counters_fixed =
    				max((int)edx.split.num_counters_fixed, assume);
  }

  printk("... version:                %d\n", x86_pmu.version);
  printk("... bit width:              %d\n", x86_pmu.cntval_bits);
  printk("... generic registers:      %d\n", x86_pmu.num_counters);
  printk("... fixed-purpose events:   %d\n", x86_pmu.num_counters_fixed);
  printk("... x86_model:              %d\n", boot_cpu_data.x86_model);
}

简单讲一下:

9 行:通过 cpuid 获取 PMU 基本信息。
17 - 22 行:根据 PMU 版本信息,计算 num_counters_fixed,也就是 fixed PMC 的数量。具体算法有点 specific,不必深究。
24 - 28 行:打印 PMU 信息概览。
在我的机器上:

... version:                4
... bit width:              48
... generic registers:      4
... fixed-purpose events:   3
... x86_model:              85

4.3 事件初始化
总体逻辑:

static void intel_pmu_hw_config(struct my_perf_event *event)
{
  event->hw.config = ARCH_PERFMON_EVENTSEL_INT;

  event->hw.config |= ARCH_PERFMON_EVENTSEL_USR;
  event->hw.config |= ARCH_PERFMON_EVENTSEL_OS;
  event->hw.config |= event->attr.config & X86_RAW_EVENT_MASK;
}

static void x86_pmu_event_init(struct my_perf_event *event)
{
	x86_pmu.hw_config(event);
}

static void perf_init_event(struct my_perf_event *event)
{
	x86_pmu_event_init(event);
}

static void my_sys_perf_event_open(struct my_perf_event *event, struct my_perf_event_attr *attr, int idx, bool fixed)
{
  memset(event, 0, sizeof(*event));

  event->attr = *attr;

  if (fixed)
  	event->hw.idx = idx + INTEL_PMC_IDX_FIXED;
  else
  	event->hw.idx = idx;

  perf_init_event(event);
}

static void my_perf_event_init(struct my_perf_event *event, u64 umask, u64 eventsel, int idx, bool fixed)
{
  struct my_perf_event_attr attr;

  memset(&attr, 0, sizeof(attr));
  attr.config = (umask << 4) | eventsel;

  my_sys_perf_event_open(event, &attr, idx, fixed);
}

下面逐个讲解重要的函数。

4.3.1 my_perf_event_init
此函数接受 4 个参数,事件的抽象数据结构、事件的 umask/eventsel、事件底层 PMC ID 编号,此事件底层 PMC 是否是 fixed。

框架侧调用点的注释写的很清楚了,使用第 2、3 个 generic PMC,分别对 instruction、cycles 两个事件做监控;同时使用第 0、1 个 fixed PMC,分别对 instruction、cycles 两个事件做监控:

// suppose we are using the #2 generic pmc for 'Instruction Retired', umask 00h, event C0h
  // suppose we are using the #3 generic pmc for 'UnHalted Core Cycles', umask 00h, event 3Ch
  my_perf_event_init(&events[0], 0x00, 0xc0, 2, false);
  my_perf_event_init(&events[1], 0x00, 0x3c, 3, false);

  // suppose we are using the #0 fixed pmc for 'Instruction Retired', event C0h
  // suppose we are using the #1 fixed pmc for 'UnHalted Core Cycles', event 3Ch
  my_perf_event_init(&events[2], 0x00, 0xc0, 0, true);
  my_perf_event_init(&events[3], 0x00, 0x3c, 1, true);

这里要注意,两个 generic PMC 的 ID 是我随便选的(我的机器上不大于 generic PMC 的最大编号,也就是 3 即可),两个 fixed PMC 的 ID 需要严格与 SDM 上的一致,也就是 0、1。参考本文“2. 事件设计”一节。

my_perf_event_init 函数中将 umask、eventsel 按照 architecture specific 的约束,构造成事件属性的 config 域,最后调用 my_sys_perf_event_open 接口打开此事件。

4.3.2 my_sys_perf_event_open
此接口:

将事件属性保存到事件数据结构中。
根据是否是 fixed PMC,对 PMC 的 ID 做编码空间上的隔离,注意这里的编码信息记录到事件的硬件抽象层,也就是 my_hw_perf_event 中。
最后调用 perf_init_event,此函数最终调用到 intel_pmu_hw_config,对事件的硬件层抽象做初始化。
4.3.3 intel_pmu_hw_config
为什么要配置成这样,参考下面两个寄存器:

在这里插入图片描述
在这里插入图片描述
在我们的 intel_pmu_hw_config 实现中,使能了如下 bit 位:

INT:如果 PMC overflow,则处理器通过其 LAPIC 触发一个 exception。
USR:使能 CPU 在 ring 1、2、3 特权级下的事件计数(简单理解为,使能 CPU 运行在用户态时的事件计数)。
OS:使能 CPU 在 ring 0 特权级下的事件计数(简单理解为,使能 CPU 运行在内核态时的事件计数)。
UMASK + EventSelect:将业务层传入的事件 umask、eventsel,转成硬件层 PMC 的配置。
此寄存器 bit 位详解,参考 SDM “18.2.1.1 Architectural Performance Monitoring Version 1 Facilities”。

注意:我们的代码模型下,hardcode 了寄存器的这几个 bit,实际的内核 perf 框架中,当然不是 hardcode 的,而是根据 perf 前端接口所传入的事件属性,决定对应 bit 位是否置上。

比如 USER、OS bit 位是否置上,取决于 perf_event_attr 中的 exclude_user、exclude_kernel 配置:在这里插入图片描述
4.4 事件使能
总体逻辑:

static void __x86_pmu_enable_event(struct my_hw_perf_event *hwc, u64 enable_mask)
{
	wrmsrl(hwc->config_base, hwc->config | enable_mask);
}

static void intel_pmu_enable_fixed(struct my_perf_event *event)
{
  u64 ctrl_val, mask, bits = 0;
  struct my_hw_perf_event *hwc = &event->hw;
  int idx = hwc->idx;

  bits |= 0x8;
  if (hwc->config & ARCH_PERFMON_EVENTSEL_USR)
  	bits |= 0x2;
  if (hwc->config & ARCH_PERFMON_EVENTSEL_OS)
  	bits |= 0x1;

  idx -= INTEL_PMC_IDX_FIXED;
  bits <<= (idx * 4);
  mask = 0xfULL << (idx * 4);

  rdmsrl(hwc->config_base, ctrl_val);
  ctrl_val &= ~mask;
  ctrl_val |= bits;
  wrmsrl(hwc->config_base, ctrl_val);
}

static void intel_pmu_enable_event(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;

  switch (hwc->idx) {
  case 0 ... INTEL_PMC_IDX_FIXED - 1:
    __x86_pmu_enable_event(hwc, ARCH_PERFMON_EVENTSEL_ENABLE);
    break;
  case INTEL_PMC_IDX_FIXED ... INTEL_PMC_IDX_FIXED + 15:
    intel_pmu_enable_fixed(event);
    break;
  };
}

static void x86_pmu_enable(struct my_perf_event *event)
{
  x86_assign_hw_event(event);
  x86_pmu.enable(event);
}

static void __my_perf_event_enable(void *info)
{
  struct my_perf_event *event = info;

  x86_pmu_enable(event);
}

static void my_perf_event_enable(int target_cpu, struct my_perf_event *event)
{
	(void)smp_call_function_single(target_cpu, __my_perf_event_enable, event, 1);
}

下面逐个讲解重要的函数。

4.4.1 my_perf_event_enable
因为 PMU 是 per-cpu 的,要使能目标 CPU 上的 event(本质就是使能目标 CPU 上的某个 PMC),需要通过 smp_call 跨核调过去,回调是 __my_perf_event_enable,此回调中干了两件事:

x86_assign_hw_event:为 event 分配一个 PMC,并初始化此 PMC 的寄存器信息。在咱们的模型下,event 的 PMC 是代码中手动指定的(my_perf_event_init 中指定了 PMC 的 ID)。
intel_pmu_enable_event:写配置寄存器的 enable bit,从硬件上使能 PMC。
4.4.2 x86_assign_hw_event
根据事件硬件层抽象中的 idx,区分其是 generic 还是 fixed PMC,并分别调用相应逻辑:

如果是 generic PMC(0 … INTEL_PMC_IDX_FIXED - 1):config_base 是 PMC 对应的 IA32_PERFEVTSELx 寄存器,event_base 是 PMC 对应的 IA32_PMCx 寄存器(实际上此寄存器压根没用到),event_base_rdpmc 就是 PMC 的编号(generic PMC ID 编址空间下)。
如果是 fixed PMC(INTEL_PMC_IDX_FIXED … INTEL_PMC_IDX_FIXED + 15):config_base 是 IA32_FIXED_CTR_CTRL 寄存器(所有 fixed PMC 皆是通过这一个寄存器来配置),event_base 是 PMC 对应的 IA32_FIXED_CTRx 寄存器(实际上此寄存器压根没用到),event_base_rdpmc 的计算算法见代码。
如果你对这些寄存器感到困惑,请参阅上一篇文章“4.2 事件(PMC)的配置及读取”一节。

此函数说白了,就是将一个事件与一个硬件 PMC 建立关系(assign)。

static inline unsigned int x86_pmu_config_addr(int index)
{
	return x86_pmu.eventsel + index;
}

static inline unsigned int x86_pmu_event_addr(int index)
{
	return x86_pmu.perfctr + index;
}

static inline int x86_pmu_rdpmc_index(int index)
{
	return x86_pmu.rdpmc_index ? x86_pmu.rdpmc_index(index) : index;
}

static void x86_assign_hw_event(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;

  switch (hwc->idx) {
  case 0 ... INTEL_PMC_IDX_FIXED - 1:
    hwc->config_base = x86_pmu_config_addr(hwc->idx);
    hwc->event_base = x86_pmu_event_addr(hwc->idx);
    hwc->event_base_rdpmc = x86_pmu_rdpmc_index(hwc->idx);
    break;
  case INTEL_PMC_IDX_FIXED ... INTEL_PMC_IDX_FIXED + 15:
    hwc->config_base = MSR_ARCH_PERFMON_FIXED_CTR_CTRL;
    hwc->event_base = MSR_ARCH_PERFMON_FIXED_CTR0;
    hwc->event_base_rdpmc = (hwc->idx - INTEL_PMC_IDX_FIXED) | INTEL_PMC_FIXED_RDPMC_BASE;
    break;
  default:
  	return;
  };
}

4.4.3 intel_pmu_enable_event
x86_assign_hw_event 函数中,为事件分配了一个硬件 PMC,并获取了 PMC 的配置、读数寄存器。

intel_pmu_enable_event 函数,基于上一步成果,真正地写入寄存器,让 PMC 硬件实际生效。

对于 generic PMC,使能 PMC 的代码(__x86_pmu_enable_event)非常简单,将 config 带上 EN bit,然后写入 IA32_PERFEVTSELx 寄存器,参考图 4。
对于 fixed PMC,略显复杂,但实际上也很简单,其本质就是写入 IA32_FIXED_CTR_CTRL 寄存器,参考图 5。
4.5 事件读取
my_perf_event_read 接口做了两件事:

perf_event_read:此函数通过 smp_call,一路调用到 PMC 底层寄存器读操作(x86_perf_event_update),通过 rdpmcl(底层是 RDPMC 指令),根据事件的 event_base_rdpmc,读出 PMC 硬件中的值,并将读出的硬件数据,做符号扩展至 64 bit,并将数值存到事件的 count 域中。
perf_event_count:基于上一步成果,将 event 的 count 域返回。

u64 x86_perf_event_update(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;
  int shift = 64 - x86_pmu.cntval_bits;
  u64 prev_raw_count, new_raw_count;
  u64 delta;

again:
  prev_raw_count = local64_read(&hwc->prev_count);
  rdpmcl(hwc->event_base_rdpmc, new_raw_count);

  if (local64_cmpxchg(&hwc->prev_count, prev_raw_count,
										new_raw_count) != prev_raw_count)
  	goto again;

  /*
   * Now we have the new raw value and have updated the prev
   * timestamp already. We can now calculate the elapsed delta
   * (event-)time and add that to the generic event.
   *
   * Careful, not all hw sign-extends above the physical width
   * of the count.
   */
  delta = (new_raw_count << shift) - (prev_raw_count << shift);
  delta >>= shift;

	local64_add(delta, &event->count);

	return new_raw_count;
}

static void intel_pmu_read_event(struct my_perf_event *event)
{
	x86_perf_event_update(event);
}

static void x86_pmu_read(struct my_perf_event *event)
{
	x86_pmu.read(event);
}

static void __perf_event_read(void *info)
{
	struct my_perf_event *event = info;

	x86_pmu_read(event);
}

static void perf_event_read(int target_cpu, struct my_perf_event *event)
{
	(void)smp_call_function_single(target_cpu, __perf_event_read, event, 1);
}

static u64 perf_event_count(struct my_perf_event *event)
{
	return local64_read(&event->count);
}

u64 my_perf_event_read(int target_cpu, struct my_perf_event *event)
{
  perf_event_read(target_cpu, event);
  return perf_event_count(event);
}

4.6 事件禁能
重点看 x86_pmu_disable_event(generic PMC)、intel_pmu_disable_fixed(fixed PMC),主体逻辑与事件使能类似,只不过是反着来的,操作的是同一个寄存器。读者自行理解,不啰嗦了。

static inline void x86_pmu_disable_event(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;

  wrmsrl(hwc->config_base, hwc->config);
}

static void intel_pmu_disable_fixed(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;
  u64 ctrl_val, mask;
  int idx = hwc->idx;

  mask = 0xfULL << ((idx - INTEL_PMC_IDX_FIXED) * 4);
  rdmsrl(hwc->config_base, ctrl_val);
  ctrl_val &= ~mask;
  wrmsrl(hwc->config_base, ctrl_val);
}

static void intel_pmu_disable_event(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;

  switch (hwc->idx) {
  case 0 ... INTEL_PMC_IDX_FIXED - 1:
    x86_pmu_disable_event(event);
    break;
  case INTEL_PMC_IDX_FIXED ... INTEL_PMC_IDX_FIXED + 15:
    intel_pmu_disable_fixed(event);
    break;
  };
}

static void x86_pmu_disable(struct my_perf_event *event)
{
	x86_pmu.disable(event);
}

static void __perf_event_disable(void *info)
{
  struct my_perf_event *event = info;

  x86_pmu_disable(event);
}

static void my_perf_event_disable(int target_cpu, struct my_perf_event *event)
{
	smp_call_function_single(target_cpu, __perf_event_disable, event, 1);
}
  1. 总结
    我们的模型下,通过查阅 SDM,获取所要监控事件的 umask、eventsel,并手动指定通过哪个 generic/fixed PMC 来监控对应事件。
    模块初始化函数中,获取 PMU 信息,比较重要的是 PMU 的数据宽度(cntval_bits)。
    事件初始化逻辑,将业务层传入的事件属性做记录,并生成事件硬件层抽象的基本配置(基于 umask、eventsel),并将 PMC 的 id 做编码空间隔离。
    事件使能逻辑,根据 PMC 的 id,进一步设置事件硬件层抽象(本质上对应的就是一个 PMC)的寄存器信息,并写入硬件寄存器,实际使能此 PMC。
    事件读取逻辑,通过 RDPMC 读出事件对应 PMC 的数值,并做 64 bit 符号扩展。
    事件禁能逻辑,与事件使能逻辑相反,写入硬件寄存器禁能此 PMC。
  2. 伏笔
    行文至此,本文不仅没有解答上一篇文章“5. 伏笔”一节的任何问题,反而引入了更多问题:

我们所监控事件的 umask、eventsel 都是自己查 SDM 得来的,内核还支持通过事件的通用 ID 来指定,它是怎么实现的(《[perf 1] perf 前端》“3.2 事件类型”)?
我们对事件的 PMC 分配采用的是手动方式(手动指定 ID 以及是否使用 fixed PMC),内核 perf 框架显然不可能也这么干,它是怎么实现的?
本文指定的两个事件,即可以通过 generic PMC,也可以通过 fixed PMC,如果是内核 perf 框架,它会怎么做选择?
我们指定的事件之间并无事件组关系,而我们知道内核 perf 框架还支持事件组的采集,它是怎么实现的?
我们的模型只是采集某个 cpu 的 PMU 事件,perf 框架还支持 per-task 的 PMU 事件采集,它是怎么实现的?
这些问题的解答留待内核 perf 框架的后续解构文章。

实际上,搞清楚了问题是什么,就已经搞清楚了问题总体的百分之八十。

  1. 附录
#include <linux/delay.h>
#include <linux/module.h>
#include <linux/kthread.h>
#include <linux/perf_event.h>

static struct task_struct *loop_task;

/*
 * Performance event hw details:
 */
#define MSR_ARCH_PERFMON_EVENTSEL0				0x186
#define MSR_ARCH_PERFMON_PERFCTR0					0xc1

#define ARCH_PERFMON_EVENTSEL_EVENT				0x000000FFULL
#define ARCH_PERFMON_EVENTSEL_UMASK				0x0000FF00ULL
#define ARCH_PERFMON_EVENTSEL_USR					(1ULL << 16)
#define ARCH_PERFMON_EVENTSEL_OS					(1ULL << 17)
#define ARCH_PERFMON_EVENTSEL_EDGE				(1ULL << 18)
#define ARCH_PERFMON_EVENTSEL_PIN_CONTROL	(1ULL << 19)
#define ARCH_PERFMON_EVENTSEL_INT					(1ULL << 20)
#define ARCH_PERFMON_EVENTSEL_ANY					(1ULL << 21)
#define ARCH_PERFMON_EVENTSEL_ENABLE			(1ULL << 22)
#define ARCH_PERFMON_EVENTSEL_INV					(1ULL << 23)
#define ARCH_PERFMON_EVENTSEL_CMASK				0xFF000000ULL

#define X86_RAW_EVENT_MASK				\
  (ARCH_PERFMON_EVENTSEL_EVENT |	\
   ARCH_PERFMON_EVENTSEL_UMASK |  \
   ARCH_PERFMON_EVENTSEL_EDGE  |  \
   ARCH_PERFMON_EVENTSEL_INV   |  \
   ARCH_PERFMON_EVENTSEL_CMASK)

/*
 * All the fixed-mode PMCs are configured via this single MSR:
 */
#define MSR_ARCH_PERFMON_FIXED_CTR_CTRL	0x38d
#define MSR_ARCH_PERFMON_FIXED_CTR0			0x309

/* RDPMC offset for Fixed PMCs */
#define INTEL_PMC_FIXED_RDPMC_BASE			(1 << 30)

#define INTEL_PMC_IDX_FIXED							32

struct my_hw_perf_event {
  struct { /* hardware */
    u64 config;
    unsigned long config_base;
    unsigned long event_base;
    int event_base_rdpmc;
    int idx;
  };
  
  local64_t prev_count;
};

struct my_perf_event_attr {
	u64 config;
};

struct my_perf_event {
  local64_t count;
  struct my_perf_event_attr attr;
  struct my_hw_perf_event hw;
};

struct my_x86_pmu {
  int version;
  unsigned eventsel;
  unsigned perfctr;
  int num_counters;
  int num_counters_fixed;
  int cntval_bits;
  int (*rdpmc_index)(int index);

  void (*enable)(struct my_perf_event *);
  void (*disable)(struct my_perf_event *);
  void (*read)(struct my_perf_event *event);
  void (*hw_config)(struct my_perf_event *event);
};

static void intel_pmu_enable_event(struct my_perf_event *event);
static void intel_pmu_disable_event(struct my_perf_event *event);
static void intel_pmu_read_event(struct my_perf_event *event);
static void intel_pmu_hw_config(struct my_perf_event *event);

static struct my_x86_pmu x86_pmu = {
  .eventsel = MSR_ARCH_PERFMON_EVENTSEL0,
  .perfctr = MSR_ARCH_PERFMON_PERFCTR0,

  .enable = intel_pmu_enable_event,
  .disable = intel_pmu_disable_event,
  .read = intel_pmu_read_event,
  .hw_config = intel_pmu_hw_config,
};

static inline unsigned int x86_pmu_config_addr(int index)
{
	return x86_pmu.eventsel + index;
}

static inline unsigned int x86_pmu_event_addr(int index)
{
	return x86_pmu.perfctr + index;
}

static inline int x86_pmu_rdpmc_index(int index)
{
	return x86_pmu.rdpmc_index ? x86_pmu.rdpmc_index(index) : index;
}

static void x86_assign_hw_event(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;

  switch (hwc->idx) {
  case 0 ... INTEL_PMC_IDX_FIXED - 1:
    hwc->config_base = x86_pmu_config_addr(hwc->idx);
    hwc->event_base = x86_pmu_event_addr(hwc->idx);
    hwc->event_base_rdpmc = x86_pmu_rdpmc_index(hwc->idx);
    break;
  case INTEL_PMC_IDX_FIXED ... INTEL_PMC_IDX_FIXED + 15:
    hwc->config_base = MSR_ARCH_PERFMON_FIXED_CTR_CTRL;
    hwc->event_base = MSR_ARCH_PERFMON_FIXED_CTR0;
    hwc->event_base_rdpmc = (hwc->idx - INTEL_PMC_IDX_FIXED) | INTEL_PMC_FIXED_RDPMC_BASE;
    break;
  default:
  	return;
  };
}

void intel_pmu_init(void)
{
  union cpuid10_edx edx;
  union cpuid10_eax eax;
  union cpuid10_ebx ebx;
  unsigned int unused;
  int version;

	cpuid(10, &eax.full, &ebx.full, &unused, &edx.full);

	version = eax.split.version_id;

  x86_pmu.version = version;
  x86_pmu.num_counters = eax.split.num_counters;
  x86_pmu.cntval_bits = eax.split.bit_width;

  if (version > 1) {
  	int assume = 3 * !boot_cpu_has(X86_FEATURE_HYPERVISOR);

    x86_pmu.num_counters_fixed =
    				max((int)edx.split.num_counters_fixed, assume);
  }

  printk("... version:                %d\n", x86_pmu.version);
  printk("... bit width:              %d\n", x86_pmu.cntval_bits);
  printk("... generic registers:      %d\n", x86_pmu.num_counters);
  printk("... fixed-purpose events:   %d\n", x86_pmu.num_counters_fixed);
  printk("... x86_model:              %d\n", boot_cpu_data.x86_model);
}

///
// read logic
u64 x86_perf_event_update(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;
  int shift = 64 - x86_pmu.cntval_bits;
  u64 prev_raw_count, new_raw_count;
  u64 delta;

again:
  prev_raw_count = local64_read(&hwc->prev_count);
  rdpmcl(hwc->event_base_rdpmc, new_raw_count);

  if (local64_cmpxchg(&hwc->prev_count, prev_raw_count,
										new_raw_count) != prev_raw_count)
  	goto again;

  /*
   * Now we have the new raw value and have updated the prev
   * timestamp already. We can now calculate the elapsed delta
   * (event-)time and add that to the generic event.
   *
   * Careful, not all hw sign-extends above the physical width
   * of the count.
   */
  delta = (new_raw_count << shift) - (prev_raw_count << shift);
  delta >>= shift;

	local64_add(delta, &event->count);

	return new_raw_count;
}

static void intel_pmu_read_event(struct my_perf_event *event)
{
	x86_perf_event_update(event);
}

static void x86_pmu_read(struct my_perf_event *event)
{
	x86_pmu.read(event);
}

static void __perf_event_read(void *info)
{
	struct my_perf_event *event = info;

	x86_pmu_read(event);
}

static void perf_event_read(int target_cpu, struct my_perf_event *event)
{
	(void)smp_call_function_single(target_cpu, __perf_event_read, event, 1);
}

static u64 perf_event_count(struct my_perf_event *event)
{
	return local64_read(&event->count);
}

u64 my_perf_event_read(int target_cpu, struct my_perf_event *event)
{
  perf_event_read(target_cpu, event);
  return perf_event_count(event);
}

///
// disable logic
static inline void x86_pmu_disable_event(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;

  wrmsrl(hwc->config_base, hwc->config);
}

static void intel_pmu_disable_fixed(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;
  u64 ctrl_val, mask;
  int idx = hwc->idx;

  mask = 0xfULL << ((idx - INTEL_PMC_IDX_FIXED) * 4);
  rdmsrl(hwc->config_base, ctrl_val);
  ctrl_val &= ~mask;
  wrmsrl(hwc->config_base, ctrl_val);
}

static void intel_pmu_disable_event(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;

  switch (hwc->idx) {
  case 0 ... INTEL_PMC_IDX_FIXED - 1:
    x86_pmu_disable_event(event);
    break;
  case INTEL_PMC_IDX_FIXED ... INTEL_PMC_IDX_FIXED + 15:
    intel_pmu_disable_fixed(event);
    break;
  };
}

static void x86_pmu_disable(struct my_perf_event *event)
{
	x86_pmu.disable(event);
}

static void __perf_event_disable(void *info)
{
  struct my_perf_event *event = info;

  x86_pmu_disable(event);
}

static void my_perf_event_disable(int target_cpu, struct my_perf_event *event)
{
	smp_call_function_single(target_cpu, __perf_event_disable, event, 1);
}

///
// enable logic
static void __x86_pmu_enable_event(struct my_hw_perf_event *hwc, u64 enable_mask)
{
	wrmsrl(hwc->config_base, hwc->config | enable_mask);
}

static void intel_pmu_enable_fixed(struct my_perf_event *event)
{
  u64 ctrl_val, mask, bits = 0;
  struct my_hw_perf_event *hwc = &event->hw;
  int idx = hwc->idx;

  bits |= 0x8;
  if (hwc->config & ARCH_PERFMON_EVENTSEL_USR)
  	bits |= 0x2;
  if (hwc->config & ARCH_PERFMON_EVENTSEL_OS)
  	bits |= 0x1;

  idx -= INTEL_PMC_IDX_FIXED;
  bits <<= (idx * 4);
  mask = 0xfULL << (idx * 4);

  rdmsrl(hwc->config_base, ctrl_val);
  ctrl_val &= ~mask;
  ctrl_val |= bits;
  wrmsrl(hwc->config_base, ctrl_val);
}

static void intel_pmu_enable_event(struct my_perf_event *event)
{
  struct my_hw_perf_event *hwc = &event->hw;

  switch (hwc->idx) {
  case 0 ... INTEL_PMC_IDX_FIXED - 1:
    __x86_pmu_enable_event(hwc, ARCH_PERFMON_EVENTSEL_ENABLE);
    break;
  case INTEL_PMC_IDX_FIXED ... INTEL_PMC_IDX_FIXED + 15:
    intel_pmu_enable_fixed(event);
    break;
  };
}

static void x86_pmu_enable(struct my_perf_event *event)
{
  x86_assign_hw_event(event);
  x86_pmu.enable(event);
}

static void __my_perf_event_enable(void *info)
{
  struct my_perf_event *event = info;

  x86_pmu_enable(event);
}

static void my_perf_event_enable(int target_cpu, struct my_perf_event *event)
{
	(void)smp_call_function_single(target_cpu, __my_perf_event_enable, event, 1);
}

///
// init logic
static void intel_pmu_hw_config(struct my_perf_event *event)
{
  event->hw.config = ARCH_PERFMON_EVENTSEL_INT;

  event->hw.config |= ARCH_PERFMON_EVENTSEL_USR;
  event->hw.config |= ARCH_PERFMON_EVENTSEL_OS;
  event->hw.config |= event->attr.config & X86_RAW_EVENT_MASK;
}

static void x86_pmu_event_init(struct my_perf_event *event)
{
	x86_pmu.hw_config(event);
}

static void perf_init_event(struct my_perf_event *event)
{
	x86_pmu_event_init(event);
}

static void my_sys_perf_event_open(struct my_perf_event *event, struct my_perf_event_attr *attr, int idx, bool fixed)
{
  memset(event, 0, sizeof(*event));

  event->attr = *attr;

  if (fixed)
  	event->hw.idx = idx + INTEL_PMC_IDX_FIXED;
  else
  	event->hw.idx = idx;

  perf_init_event(event);
}

static void my_perf_event_init(struct my_perf_event *event, u64 umask, u64 eventsel, int idx, bool fixed)
{
  struct my_perf_event_attr attr;

  memset(&attr, 0, sizeof(attr));
  attr.config = (umask << 4) | eventsel;

  my_sys_perf_event_open(event, &attr, idx, fixed);
}

static int test_loop(void *__)
{
  unsigned long long prev_count[4] = { 0 }, now_count[4];
  struct my_perf_event events[4];
  int target_cpu = 3, i;

  // suppose we are using the #2 generic pmc for 'Instruction Retired', umask 00h, event C0h
  // suppose we are using the #3 generic pmc for 'UnHalted Core Cycles', umask 00h, event 3Ch
  my_perf_event_init(&events[0], 0x00, 0xc0, 2, false);
  my_perf_event_init(&events[1], 0x00, 0x3c, 3, false);

  // suppose we are using the #0 fixed pmc for 'Instruction Retired', event C0h
  // suppose we are using the #1 fixed pmc for 'UnHalted Core Cycles', event 3Ch
  my_perf_event_init(&events[2], 0x00, 0xc0, 0, true);
  my_perf_event_init(&events[3], 0x00, 0x3c, 1, true);

  for (i = 0; i < ARRAY_SIZE(events); ++i)
  	my_perf_event_enable(target_cpu, &events[i]);

  for (i = 0; i < ARRAY_SIZE(events); ++i)
  	prev_count[i] = my_perf_event_read(target_cpu, &events[i]);

  while (!kthread_should_stop()) {
    for (i = 0; i < ARRAY_SIZE(events); ++i) {
      now_count[i] = my_perf_event_read(target_cpu, &events[i]);
      printk("event %d: %llu\n", i, now_count[i] - prev_count[i]);
      prev_count[i] = now_count[i];
    }

    msleep_interruptible(5000);
  }

  for (i = 0; i < ARRAY_SIZE(events); ++i)
  	my_perf_event_disable(target_cpu, &events[i]);
  
  return 0;
}

static int __init test_module_init(void)
{
  intel_pmu_init();
  loop_task = kthread_run(test_loop, NULL, "test_loop");

  return 0;
}

static void __exit test_module_exit(void)
{
	kthread_stop(loop_task);
}

module_init(test_module_init);
module_exit(test_module_exit);

MODULE_LICENSE("GPL");
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值