PS:要转载请注明出处,本人版权所有。
PS: 这个只是基于《我自己》的理解,
如果和你的原则及想法相冲突,请谅解,勿喷。
环境说明
无
前言
前段时间做了很多的基于sdk/demo的大模型程序性能评估,例如一个大模型运行的瓶颈是什么?这里的评估要分为两个部分:
- 异构设备运行性能评估:异构设备的执行时间,例如高通的NSP设备的运行一个模型的性能评估。
- cpu运行性能评估:sdk/demo执行一次模型推理的性能评估。
对于异构设备来说(例如高通dsp,运行高通自己的rtos),有他专门的profile工具。对于cpu设备来说(Linux系统),有一个常用的性能评估工具,就是perf。
对于perf来说,我相信只要是做开发的,多多少少都听过火焰图这个概念,而构建火焰图的一个重要前提是,我们需要通过perf来对目标进程进行cpu cycle采样,然后基于这个采样获取ip/tid/cpu核/callchain等信息。perf除了支持cpu cycle采样外,还支持其他的各种各样的性能参数采样。
以前,用perf来做性能分析,只知道能获取这些信息,但是怎么获取的,一知半解。因此,趁着这个机会,基于linux-7.1.6的内核源码,以采集cpu cycle为例,本文尝试从用户态、内核态两个角度来分析perf实现的基本原理。
perf 用户态实现原理
这里我在AI的辅助下,实现了一个和perf工具非常相似的例子,可以传入pid,对目标进程进行采样,获取ip/tid/cpu核/callchain等信息。
下面是一些代码片段分析:
perf_event.c
c
#include "perf_event.h"
#include <linux/perf_event.h>
#include <sys/ioctl.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <errno.h>
/* ------------------------------------------------------------------ */
/* low-level syscall wrapper */
/* ------------------------------------------------------------------ */
static long
_perf_event_open(const struct perf_event_attr *attr,
pid_t pid,
int cpu,
int group_fd,
unsigned long flags)
{
return syscall(__NR_perf_event_open, attr, pid, cpu, group_fd, flags);
}
/* ------------------------------------------------------------------ */
/* public API */
/* ------------------------------------------------------------------ */
int
perf_open(const struct perf_event_attr *attr,
pid_t pid,
int cpu,
int group_fd,
unsigned long flags)
{
if (attr == NULL) {
errno = EINVAL;
return -1;
}
return (int) _perf_event_open(attr, pid, cpu, group_fd, flags);
}
void
perf_close(int fd)
{
if (fd >= 0) {
close(fd);
}
}
int
perf_start(int fd)
{
if (fd < 0) {
errno = EINVAL;
return -1;
}
if (ioctl(fd, PERF_EVENT_IOC_RESET, 0) < 0) {
return -1;
}
if (ioctl(fd, PERF_EVENT_IOC_ENABLE, 0) < 0) {
return -1;
}
return 0;
}
int
perf_stop(int fd, uint64_t *count)
{
if (fd < 0) {
errno = EINVAL;
return -1;
}
if (ioctl(fd, PERF_EVENT_IOC_DISABLE, 0) < 0) {
return -1;
}
uint64_t val = 0;
ssize_t n = read(fd, &val, sizeof(val));
if (n != sizeof(val)) {
return -1;
}
if (count != NULL) {
*count = val;
}
return 0;
}
perf_event.h
c
#ifndef PERF_EVENT_H
#define PERF_EVENT_H
#include <linux/perf_event.h>
#include <stdint.h>
#include <unistd.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* perf_open -- open a perf event counter.
*
* Direct wrapper for the perf_event_open(2) syscall --- all parameters are
* passed through unchanged.
*
* attr : fully initialised perf_event_attr.
* pid : pid to monitor (0 = calling thread, -1 = all threads).
* cpu : cpu to monitor (-1 = any cpu).
* group_fd : group leader fd (-1 to create a new group).
* flags : PERF_FLAG_FD_NO_GROUP, PERF_FLAG_FD_OUTPUT, etc. (0 for none).
*
* Returns fd >= 0 on success, -1 on failure (errno is set).
*/
int perf_open(const struct perf_event_attr *attr,
pid_t pid,
int cpu,
int group_fd,
unsigned long flags);
/*
* perf_close -- close a perf event fd. No-op when fd < 0.
*/
void perf_close(int fd);
/*
* perf_start -- reset counter to zero and enable. Returns 0 / -1.
*/
int perf_start(int fd);
/*
* perf_stop -- disable counter and read accumulated value into *count.
* Returns 0 / -1.
*/
int perf_stop(int fd, uint64_t *count);
#ifdef __cplusplus
}
#endif
#endif /* PERF_EVENT_H */
perf_ringbuf.c
c
#include "perf_ringbuf.h"
#include <linux/perf_event.h>
#include <sys/mman.h>
#include <unistd.h>
#include <stdint.h>
#include <stddef.h>
int
perf_ringbuf_open(int fd,
int nr_pages,
void **base,
struct perf_event_mmap_page **meta,
size_t *data_size)
{
if (base == NULL || meta == NULL || data_size == NULL || nr_pages < 1) {
return -1;
}
long page_size = sysconf(_SC_PAGESIZE);
size_t mmap_size = (1 + (size_t) nr_pages) * page_size;
void *addr = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (addr == MAP_FAILED) {
return -1;
}
*base = addr;
*meta = (struct perf_event_mmap_page *) addr;
*data_size = (size_t) nr_pages * page_size;
return 0;
}
void
perf_ringbuf_head_tail(const struct perf_event_mmap_page *meta,
uint64_t *head,
uint64_t *tail)
{
uint64_t h = meta->data_head;
__sync_synchronize();
uint64_t t = meta->data_tail;
if (head != NULL) {
*head = h;
}
if (tail != NULL) {
*tail = t;
}
}
void
perf_ringbuf_close(void *base, size_t data_size)
{
if (base == NULL) {
return;
}
long page_size = sysconf(_SC_PAGESIZE);
long mmap_size = page_size + (long) data_size;
munmap(base, mmap_size);
}
perf_ringbuf.h
c
#ifndef PERF_RINGBUF_H
#define PERF_RINGBUF_H
#include <linux/perf_event.h>
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/*
* perf_ringbuf_open -- mmap the perf event ring buffer.
*
* fd : perf event file descriptor.
* nr_pages : number of data pages (must be power-of-2, usually 8).
* base : [out] mmap base address (metadata page).
* meta : [out] pointer to struct perf_event_mmap_page.
* data_size : [out] size of data area in bytes (= nr_pages * page_size).
*
* Returns 0 on success, -1 on failure.
*/
int perf_ringbuf_open(int fd,
int nr_pages,
void **base,
struct perf_event_mmap_page **meta,
size_t *data_size);
/*
* perf_ringbuf_head_tail -- read head and tail with correct memory barrier.
* Caller must pass the meta pointer from perf_ringbuf_open.
* head is written by the kernel, tail is written by userspace.
*/
void perf_ringbuf_head_tail(const struct perf_event_mmap_page *meta,
uint64_t *head,
uint64_t *tail);
/*
* perf_ringbuf_close -- munmap the ring buffer.
*
* base : the base address returned by perf_ringbuf_open.
* data_size : the data_size value returned by perf_ringbuf_open.
*/
void perf_ringbuf_close(void *base, size_t data_size);
#ifdef __cplusplus
}
#endif
#endif /* PERF_RINGBUF_H */
demo_sample.c
c
#define _GNU_SOURCE
#include "perf_event.h"
#include "perf_ringbuf.h"
#include <linux/perf_event.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <dlfcn.h>
#include <poll.h>
#include <time.h>
#define SAMPLE_COUNT 10
#define MAX_MAPS 512
/* ── memory map entry ─────────────────────────────────────────────── */
typedef struct {
uint64_t start;
uint64_t end;
char path[256];
} mem_map_t;
static mem_map_t maps[MAX_MAPS];
static int nr_maps;
/* ── helpers ──────────────────────────────────────────────────────── */
/* read value at ptr, then advance ptr by sizeof(raw_type) */
#define NEXT_FIELD(ptr, raw_type) \
(*(raw_type *)(ptr)); (ptr) = (void *)((char *)(ptr) + sizeof(raw_type))
/* ── parse /proc/<pid>/maps ───────────────────────────────────────── */
static int
parse_maps(pid_t pid)
{
char path[64];
snprintf(path, sizeof(path), "/proc/%d/maps", pid);
FILE *f = fopen(path, "r");
if (f == NULL) {
perror("fopen /proc/.../maps");
return -1;
}
nr_maps = 0;
char line[512];
while (fgets(line, sizeof(line), f) != NULL && nr_maps < MAX_MAPS) {
uint64_t start, end;
char perms[8], fpath[256] = {0};
/* format: start-end perms offset dev inode path */
int n = sscanf(line, "%lx-%lx %7s %*s %*s %*u %255s",
&start, &end, perms, fpath);
if (n < 2) continue; /* skip malformed lines */
maps[nr_maps].start = start;
maps[nr_maps].end = end;
if (n >= 4 && fpath[0] != '\0') {
/* keep only the basename */
const char *name = strrchr(fpath, '/');
snprintf(maps[nr_maps].path, sizeof(maps[nr_maps].path),
"%s", name ? name + 1 : fpath);
} else {
/* anonymous mapping -- try to identify by permissions */
if (strchr(perms, 'x')) {
snprintf(maps[nr_maps].path, sizeof(maps[nr_maps].path),
"[anon_exec:%lx]", start);
} else {
snprintf(maps[nr_maps].path, sizeof(maps[nr_maps].path),
"[anon]");
}
}
nr_maps++;
}
fclose(f);
return 0;
}
/* ── look up which mapping an address falls into ──────────────────── */
static const mem_map_t *
find_map(uint64_t addr)
{
for (int i = 0; i < nr_maps; i++) {
if (addr >= maps[i].start && addr < maps[i].end) {
return &maps[i];
}
}
return NULL;
}
/* ── resolve symbol name ──────────────────────────────────────────── */
/*
* When pid == our own process → use dladdr(3) to get function names.
* When pid != self → fall back to /proc/<pid>/maps and
* print <binary>+<offset>.
*/
static void
resolve_symbol(pid_t my_pid, pid_t target_pid, uint64_t addr,
char *buf, size_t bufsz)
{
/* dladdr only works for addresses in our own address space */
if (target_pid == my_pid || target_pid == 0) {
Dl_info info;
if (dladdr((void *) (uintptr_t) addr, &info) != 0) {
const char *name = info.dli_sname ? info.dli_sname : "???";
uint64_t off = addr - (uint64_t) (uintptr_t) info.dli_saddr;
const char *file = info.dli_fname ? info.dli_fname : "???";
const char *base = strrchr(file, '/');
snprintf(buf, bufsz, "%s+0x%lx (%s)",
name, off, base ? base + 1 : file);
return;
}
/* dladdr failed -- fall through to maps-based lookup */
}
/* kernel-space address */
if (addr >= 0xffff000000000000ULL) {
snprintf(buf, bufsz, "[kernel]");
return;
}
/* maps-based fallback for cross-process or dladdr failure */
const mem_map_t *m = find_map(addr);
if (m != NULL) {
uint64_t off = addr - m->start;
snprintf(buf, bufsz, "%s+0x%lx", m->path, off);
} else {
snprintf(buf, bufsz, "[nomap:0x%lx]", addr);
}
}
/* ── usage ────────────────────────────────────────────────────────── */
static void
usage(const char *prog)
{
fprintf(stderr, "Usage: %s [pid]\n", prog);
fprintf(stderr, " pid -- monitor <pid> (default: 0 = calling thread)\n");
}
/* =================================================================== */
int
main(int argc, char *argv[])
{
pid_t target_pid = 0; /* default: own thread */
pid_t my_pid = getpid();
if (argc > 1) {
if (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0) {
usage(argv[0]);
return 0;
}
target_pid = (pid_t) atoi(argv[1]);
}
/* ── parse /proc/<pid>/maps for symbol resolution ── */
pid_t maps_pid = (target_pid == 0) ? my_pid : target_pid;
if (parse_maps(maps_pid) != 0) {
return -1;
}
printf("loaded %d memory mappings for pid %d\n", nr_maps, maps_pid);
/* ── perf event attr ──────────────────────────────────────────── */
struct perf_event_attr attr;
memset(&attr, 0, sizeof(attr));
attr.size = sizeof(attr);
attr.type = PERF_TYPE_HARDWARE;
attr.config = PERF_COUNT_HW_CPU_CYCLES;
attr.sample_period = 10000;
attr.disabled = 1;
attr.exclude_kernel = 0;
attr.exclude_hv = 1;
attr.wakeup_events = 1;
/* Note: inherit=1 would cover forked children but prevents mmap on
* older ARM kernels. Use pid=<tgid> to cover all threads. */
attr.sample_type =
PERF_SAMPLE_IP |
PERF_SAMPLE_TID |
PERF_SAMPLE_TIME |
PERF_SAMPLE_CPU |
PERF_SAMPLE_CALLCHAIN;
attr.sample_max_stack = 20;
/* ── 1. perf_open ─────────────────────────────────────────────── */
int fd = perf_open(&attr, target_pid, -1, -1, 0);
if (fd < 0) {
perror("perf_open");
return -1;
}
printf("perf fd=%d target_pid=%d\n", fd, target_pid);
/* ── 2. perf_ringbuf_open ─────────────────────────────────────── */
void *base;
struct perf_event_mmap_page *meta;
size_t data_size;
if (perf_ringbuf_open(fd, 8, &base, &meta, &data_size) != 0) {
perror("perf_ringbuf_open");
perf_close(fd);
return -1;
}
long page_size = sysconf(_SC_PAGESIZE);
char *data = (char *) base + page_size;
printf("page_size=%ld data_size=%zu\n", page_size, data_size);
printf("start sampling (%d samples)...\n", SAMPLE_COUNT);
/* ── 3. perf_start ────────────────────────────────────────────── */
if (perf_start(fd) != 0) {
perror("perf_start");
perf_ringbuf_close(base, data_size);
perf_close(fd);
return -1;
}
/* ── produce workload and collect N samples ───────────────────── */
int sample_count = 0;
int is_self = (target_pid == 0 || target_pid == my_pid);
struct pollfd pfd = { .fd = fd, .events = POLLIN };
/*
* Self-monitoring: we must burn CPU ourselves to generate PMU events.
* → poll with short timeout, burn CPU in between.
*
* Cross-process: the target generates events; we just wait passively.
* → poll blocks until data arrives or total timeout expires.
*/
int poll_timeout = is_self ? 10 : 5000; /* ms */
struct timespec t_start, t_now;
clock_gettime(CLOCK_MONOTONIC, &t_start);
volatile unsigned long x = 0;
while (sample_count < SAMPLE_COUNT) {
if (is_self) {
/* burn CPU while waiting for poll --- generates our own samples */
for (int i = 0; i < 200000; i++) {
x += i;
}
}
int ret = poll(&pfd, 1, poll_timeout);
if (ret < 0) {
perror("poll");
break;
}
if (ret == 0) {
/* timeout: check total elapsed (safety against infinite hang) */
clock_gettime(CLOCK_MONOTONIC, &t_now);
double elapsed = (t_now.tv_sec - t_start.tv_sec) +
(t_now.tv_nsec - t_start.tv_nsec) / 1e9;
if (elapsed >= 30.0) {
printf("\ntimeout after %.0fs, collected %d samples\n",
elapsed, sample_count);
break;
}
continue;
}
/* fd is readable --- drain the ring buffer */
uint64_t head, tail;
perf_ringbuf_head_tail(meta, &head, &tail);
while (tail < head) {
size_t offset = tail & (data_size - 1);
struct perf_event_header *header =
(struct perf_event_header *) (data + offset);
if (header->type == PERF_RECORD_SAMPLE) {
sample_count++;
void *p = (char *) header +
sizeof(struct perf_event_header);
/* kernel output order (fixed, not sorted by bit):
* IP, TID, TIME, ID, CPU, CALLCHAIN(no nr, fills rest)
*/
uint64_t ip = NEXT_FIELD(p, uint64_t);
uint32_t pid = NEXT_FIELD(p, uint32_t);
uint32_t tid = NEXT_FIELD(p, uint32_t);
uint64_t time = NEXT_FIELD(p, uint64_t);
uint64_t id = NEXT_FIELD(p, uint64_t);
uint32_t cpu = NEXT_FIELD(p, uint32_t);
(void) NEXT_FIELD(p, uint32_t); /* reserved */
char *record_end = (char *) header + header->size;
uint64_t nr = ((uint64_t)(record_end - (char *) p))
/ sizeof(uint64_t);
uint64_t *ips = (uint64_t *) p;
char sym[256];
printf("\n--- sample %d/%d ---\n", sample_count, SAMPLE_COUNT);
resolve_symbol(my_pid, target_pid, ip, sym, sizeof(sym));
printf("IP = 0x%lx %s\n", ip, sym);
printf("PID = %u\n", pid);
printf("TID = %u\n", tid);
printf("TIME = %lu\n", time);
printf("ID = %lu\n", id);
printf("CPU = %u\n", cpu);
printf("CALLCHAIN (%lu frames):\n", nr);
for (uint64_t i = 0; i < nr; i++) {
resolve_symbol(my_pid, target_pid, ips[i], sym, sizeof(sym));
printf(" [%lu] 0x%lx %s\n", i, ips[i], sym);
}
if (sample_count >= SAMPLE_COUNT) {
tail += header->size;
meta->data_tail = tail;
goto done;
}
}
tail += header->size;
meta->data_tail = tail;
}
}
done:
/* ── 4. perf_stop ─────────────────────────────────────────────── */
uint64_t count;
perf_stop(fd, &count);
printf("\nfinal count = %lu\n", count);
/* ── 5,6. cleanup ─────────────────────────────────────────────── */
perf_ringbuf_close(base, data_size);
perf_close(fd);
return 0;
}
上面的代码有几个重要的关注点:
- 首先,perf的核心就是调用syscall __NR_perf_event_open(perf_event.c),调用的时候,传入对应的想采样的数据类型、采样模式等参数即可,然后得到了一个fd。
- 在我们开始采样之前,我们还需要通过上面的fd+mmap申请一片空间来作为ring buf,方便perf驱动写入采样数据到我们申请的mmap空间
- 开始采样,然后等待ringbuf有数据,触发我们来读取perf+pmu驱动写入的采样数据。
- 我们获取的数据有ip/tid/cpu核/callchain等,然后callchain就是调用堆栈的地址信息,通过/proc/target_pid/maps,我们可以解析出这些callchain地址属于哪个地址模块,以及这个地址模块的偏移
- 上面的地址模块+地址偏移,根据符号表,可以解析出是哪个函数,这样就得到了调用堆栈(这部分不了解不重要,这部分不是本文重点)
perf 内核态实现原理
一个常见的linux的驱动,基本都是在vfs上的基础上暴露相关的接口给用户使用。下面我们通过从open/mmap/ioctl等常见的接口来分析perf驱动。
__NR_perf_event_open 实现基本原理
perf驱动稍微特殊一点,其不是通过vfs暴露设备节点到用户态,直接调用open来获取资源,他是直接提供了一个__NR_perf_event_open系统调用。下面简单介绍一下这个syscall的实现:
代码是linux-7.1.6/kernel/events/core.c
c
static const struct file_operations perf_fops = {
.release = perf_release,
.read = perf_read,
.poll = perf_poll,
.unlocked_ioctl = perf_ioctl,
.compat_ioctl = perf_compat_ioctl,
.mmap = perf_mmap,
.fasync = perf_fasync,
};
/**
* 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
* @flags: perf event open flags
*/
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 *group_leader = NULL, *output_event = NULL;
struct perf_event_pmu_context *pmu_ctx;
struct perf_event *event, *sibling;
struct perf_event_attr attr;
struct perf_event_context *ctx;
struct file *event_file = NULL;
struct task_struct *task = NULL;
struct pmu *pmu;
int event_fd;
int move_group = 0;
int err;
int f_flags = O_RDWR;
int cgroup_fd = -1;
// ... ...
// 这个地方先在当前进程申请一个没有使用的fd,后面将perf的数据挂在fd上面
event_fd = get_unused_fd_flags(f_flags);
if (event_fd < 0)
return event_fd;
// ... ...
// 这个地方根据pid得到重要的进程结构体:task
if (pid != -1 && !(flags & PERF_FLAG_PID_CGROUP)) {
task = find_lively_task_by_vpid(pid);
if (IS_ERR(task)) {
err = PTR_ERR(task);
goto err_fd;
}
}
// ... ...
/*
这个地方是核心,后续的所有操作都是访问的这个核心结构体struct perf_event,主要做的事情是:
1 通过 struct perf_event *event __free(__free_event) = kmem_cache_alloc_node(perf_event_cache, GFP_KERNEL | __GFP_ZERO, node); 申请内存
2 初始化一些结构体, 设置我们传递的cpu/attr等给event保存
3 将目标进程的task结构体保存到event->hw.target
4 设置事件溢出时的回调:event->overflow_handler = perf_event_output_forward;(这个就是真正的写数据到ring buf的地方,后面还会提到)
5 通过pmu = perf_init_event(event);-> pmu = idr_find(&pmu_idr, PERF_TYPE_RAW) 查询到对应的pmu驱动,并挂在perf_event里面。
第5点非常重要,这里的pmu_idr是一个全局的注册表,然后可以遍历查询到对应的pmu驱动
pmu_idr 是通过perf_pmu_register来注册的pmu驱动的,本文将以armv8 pmu驱动为例来说明
*/
event = perf_event_alloc(&attr, cpu, task, group_leader, NULL,
NULL, NULL, cgroup_fd);
// ... ...
// 这个地方:1 申请一个匿名的文件节点event_file 2 给这个event_file填写文件操作ops 3 给这个event_file填写文件private_data为event
// 这里的作用就是,等会儿后面我们返回的fd作为句柄,然后可以通过fd来访问perf_fops里面的方法,以及前面我们构造好的event
event_file = anon_inode_getfile("[perf_event]", &perf_fops, event, f_flags);
if (IS_ERR(event_file)) {
err = PTR_ERR(event_file);
event_file = NULL;
goto err_context;
}
// ... ...
// 将perf event 挂到目标进程
perf_install_in_context(ctx, event, event->cpu);
// ... ...
/*
* File reference in group guarantees that group_leader has been
* kept alive until we place the new event on the sibling_list.
* This ensures destruction of the group leader will find
* the pointer to itself in perf_group_detach().
*/
/* 注意这个地方: 1 首先通过全局变量current获取进程对应的文件current->files。
2 通过files_struct 获取fdtable(current->files->fdt)
3 将event_file挂在到当前进程的fdt表中(类似:current->files->fdt[event_fd] = event_file)
这样,当这个fd返回给用户态的程序后,我们的程序可以通过fd来调用event_file里面的perf_fops里面定义的接口。
*/
fd_install(event_fd, event_file);
return event_fd;
// ... ...
}
总的来说,这个syscall返回了一个fd,这个fd绑定了一个struct file 对象,这个对象的private数据域就是我们初始化的perf_event *event。这个event中包含了:与pmu驱动的关联,perf采用属性,目标进程的重要结构struct task_struct *task,perf的fops(ioctl/mmap等)等。
其实这就是一个普通的驱动申请fd绑定一个私有数据、和fops的常见操作,这里绑定的就是perf_event,细节的内容见上面代码中的注释。
perf_mmap 实现基本原理
在上面的demo中,我们可以知道,当perf_open完成后,我们先在需要通过mmap来申请ringbuf空间,然后我们就直接调用mmap指定空间,经过文件系统调度后,也就是上文我们的event_file中的fops中的perf_mmap.
我们先看一下其实现的源码:
linux-7.1.6/kernel/events/core.c
c
static int perf_mmap(struct file *file, struct vm_area_struct *vma)
{
struct perf_event *event = file->private_data;
unsigned long vma_size, nr_pages;
mapped_f mapped;
int ret;
/*
* Don't allow mmap() of inherited per-task counters. This would
* create a performance issue due to all children writing to the
* same rb.
*/
if (event->cpu == -1 && event->attr.inherit)
return -EINVAL;
if (!(vma->vm_flags & VM_SHARED))
return -EINVAL;
ret = security_perf_event_read(event);
if (ret)
return ret;
vma_size = vma->vm_end - vma->vm_start;
nr_pages = vma_size / PAGE_SIZE;
if (nr_pages > INT_MAX)
return -ENOMEM;
if (vma_size != PAGE_SIZE * nr_pages)
return -EINVAL;
scoped_guard (mutex, &event->mmap_mutex) {
/*
* This relies on __pmu_detach_event() taking mmap_mutex after marking
* the event REVOKED. Either we observe the state, or __pmu_detach_event()
* will detach the rb created here.
*/
if (event->state <= PERF_EVENT_STATE_REVOKED)
return -ENODEV;
if (vma->vm_pgoff == 0)
ret = perf_mmap_rb(vma, event, nr_pages);
else
ret = perf_mmap_aux(vma, event, nr_pages);
if (ret)
return ret;
/*
* Since pinned accounting is per vm we cannot allow fork() to copy our
* vma.
*/
vm_flags_set(vma, VM_DONTCOPY | VM_DONTEXPAND | VM_DONTDUMP);
vma->vm_ops = &perf_mmap_vmops;
mapped = get_mapped(event, event_mapped);
if (mapped)
mapped(event, vma->vm_mm);
/*
* Try to map it into the page table. On fail undo the above,
* as the callsite expects full cleanup in this case and
* therefore does not invoke vmops::close().
*/
ret = map_range(event->rb, vma);
if (likely(!ret))
return 0;
/* Error path */
/*
* If this is the first mmap(), then event->mmap_count should
* be stable at 1. It is only modified by:
* perf_mmap_{open,close}() and perf_mmap().
*
* The former are not possible because this mmap() hasn't been
* successful yet, and the latter is serialized by
* event->mmap_mutex which we still hold (note that mmap_lock
* is not strictly sufficient here, because the event fd can
* be passed to another process through trivial means like
* fork(), leading to concurrent mmap() from different mm).
*
* Make sure to remove event->rb before releasing
* event->mmap_mutex, such that any concurrent mmap() will not
* attempt use this failed buffer.
*/
if (refcount_read(&event->mmap_count) == 1) {
/*
* Minimal perf_mmap_close(); there can't be AUX or
* other events on account of this being the first.
*/
mapped = get_mapped(event, event_unmapped);
if (mapped)
mapped(event, vma->vm_mm);
perf_mmap_unaccount(vma, event->rb);
ring_buffer_attach(event, NULL); /* drops last rb->refcount */
refcount_set(&event->mmap_count, 0);
return ret;
}
/*
* Otherwise this is an already existing buffer, and there is
* no race vs first exposure, so fall-through and call
* perf_mmap_close().
*/
}
perf_mmap_close(vma);
return ret;
}
上面这个的核心就是通过mmap系统调用,然后调用perf_mmap,分配一片空间给perf_event,后续会使用到这片空间,其挂在event->rb中,后续根据这个来操作。
注意,全程通过fd访问struct file *file,然后访问file->private_data来得到struct perf_event *event。
armv8_pmuv3_pmu 驱动简介
首先我们介绍一下pmu是什么,pmu是Performance Monitoring Unit的简称,是可以监测硬件芯片的一些指标的功能。本文用的监测事件就是cpu执行的周期计数器。
对应pmu硬件,有一个叫做pmu的驱动,这里我以我手里的设备为例,我们先来看看dts定义(从/sys/firmware/fdt解析出来的,可能和基线源码定义不一样):
dts
{
soc{
cpu-pmu {
compatible = "arm,armv8-pmuv3";
interrupts = <0x01 0x07 0x04>;
phandle = <0x365>;
};
}
}
然后我们来看看pmu驱动对应的初始化部分
linux-7.1.6/drivers/perf/arm_pmuv3.c
c
static struct platform_driver armv8_pmu_driver = {
.driver = {
.name = ARMV8_PMU_PDEV_NAME,
.of_match_table = armv8_pmu_of_device_ids,
.suppress_bind_attrs = true,
},
.probe = armv8_pmu_device_probe,
};
static int __init armv8_pmu_driver_init(void)
{
int ret;
if (acpi_disabled)
ret = platform_driver_register(&armv8_pmu_driver);
else
ret = arm_pmu_acpi_probe(armv8_pmuv3_pmu_init);
if (!ret)
lockup_detector_retry_init();
return ret;
}
device_initcall(armv8_pmu_driver_init)
当内核启动后,platform_driver会根据dts的定义,探测相关的设备,这里调用的就是armv8_pmu_device_probe,也就是说armv8_pmu_device_probe是pmu驱动的初始化函数。
linux-7.1.6/drivers/perf/arm_pmuv3.c
c
static const struct of_device_id armv8_pmu_of_device_ids[] = {
{.compatible = "arm,armv8-pmuv3", .data = armv8_pmuv3_pmu_init},
// ... ...
}
static int armv8_pmu_device_probe(struct platform_device *pdev)
{
return arm_pmu_device_probe(pdev, armv8_pmu_of_device_ids, NULL);
}
static int armv8_pmu_init(struct arm_pmu *cpu_pmu, char *name,
int (*map_event)(struct perf_event *event))
{
int ret = armv8pmu_probe_pmu(cpu_pmu);
if (ret)
return ret;
cpu_pmu->handle_irq = armv8pmu_handle_irq;
cpu_pmu->enable = armv8pmu_enable_event;
cpu_pmu->disable = armv8pmu_disable_event;
cpu_pmu->read_counter = armv8pmu_read_counter;
cpu_pmu->write_counter = armv8pmu_write_counter;
cpu_pmu->get_event_idx = armv8pmu_get_event_idx;
cpu_pmu->clear_event_idx = armv8pmu_clear_event_idx;
cpu_pmu->start = armv8pmu_start;
cpu_pmu->stop = armv8pmu_stop;
cpu_pmu->reset = armv8pmu_reset;
cpu_pmu->set_event_filter = armv8pmu_set_event_filter;
cpu_pmu->pmu.event_idx = armv8pmu_user_event_idx;
if (brbe_num_branch_records(cpu_pmu))
cpu_pmu->pmu.sched_task = armv8pmu_sched_task;
cpu_pmu->name = name;
cpu_pmu->map_event = map_event;
cpu_pmu->attr_groups[ARMPMU_ATTR_GROUP_EVENTS] = &armv8_pmuv3_events_attr_group;
cpu_pmu->attr_groups[ARMPMU_ATTR_GROUP_FORMATS] = &armv8_pmuv3_format_attr_group;
cpu_pmu->attr_groups[ARMPMU_ATTR_GROUP_CAPS] = &armv8_pmuv3_caps_attr_group;
armv8_pmu_register_sysctl_table();
return 0;
}
linux-7.1.6/drivers/perf/arm_pmu.c
c
struct arm_pmu *armpmu_alloc(void)
{
struct arm_pmu *pmu;
int cpu;
pmu = kzalloc_obj(*pmu);
if (!pmu)
goto out;
pmu->hw_events = alloc_percpu_gfp(struct pmu_hw_events, GFP_KERNEL);
if (!pmu->hw_events) {
pr_info("failed to allocate per-cpu PMU data.\n");
goto out_free_pmu;
}
pmu->pmu = (struct pmu) {
.pmu_enable = armpmu_enable,
.pmu_disable = armpmu_disable,
.event_init = armpmu_event_init,
.add = armpmu_add,
.del = armpmu_del,
.start = armpmu_start,
.stop = armpmu_stop,
.read = armpmu_read,
.filter = armpmu_filter,
.attr_groups = pmu->attr_groups,
/*
* This is a CPU PMU potentially in a heterogeneous
* configuration (e.g. big.LITTLE) so
* PERF_PMU_CAP_EXTENDED_HW_TYPE is required to open
* PERF_TYPE_HARDWARE and PERF_TYPE_HW_CACHE events on a
* specific PMU.
*/
.capabilities = PERF_PMU_CAP_EXTENDED_REGS |
PERF_PMU_CAP_EXTENDED_HW_TYPE,
};
pmu->attr_groups[ARMPMU_ATTR_GROUP_COMMON] =
&armpmu_common_attr_group;
for_each_possible_cpu(cpu) {
struct pmu_hw_events *events;
events = per_cpu_ptr(pmu->hw_events, cpu);
events->percpu_pmu = pmu;
}
return pmu;
out_free_pmu:
kfree(pmu);
out:
return NULL;
}
int armpmu_register(struct arm_pmu *pmu)
{
int ret;
ret = cpu_pmu_init(pmu);
if (ret)
return ret;
/*
* By this stage we know our supported CPUs on either DT/ACPI platforms,
* detect the SMT implementation.
*/
pmu->has_smt = topology_core_has_smt(cpumask_first(&pmu->supported_cpus));
if (!pmu->set_event_filter)
pmu->pmu.capabilities |= PERF_PMU_CAP_NO_EXCLUDE;
ret = perf_pmu_register(&pmu->pmu, pmu->name, -1);
if (ret)
goto out_destroy;
pr_info("enabled with %s PMU driver, %d (%*pb) counters available%s\n",
pmu->name, bitmap_weight(pmu->cntr_mask, ARMPMU_MAX_HWEVENTS),
ARMPMU_MAX_HWEVENTS, &pmu->cntr_mask,
has_nmi ? ", using NMIs" : "");
kvm_host_pmu_init(pmu);
return 0;
out_destroy:
cpu_pmu_destroy(pmu);
return ret;
}
linux-7.1.6/drivers/perf/arm_pmu_platform.c
c
int arm_pmu_device_probe(struct platform_device *pdev,
const struct of_device_id *of_table,
const struct pmu_probe_info *probe_table)
{
armpmu_init_fn init_fn;
struct device *dev = &pdev->dev;
struct arm_pmu *pmu;
int ret = -ENODEV;
pmu = armpmu_alloc();
if (!pmu)
return -ENOMEM;
pmu->pmu.parent = &pdev->dev;
pmu->plat_device = pdev;
ret = pmu_parse_irqs(pmu);
if (ret)
goto out_free;
init_fn = of_device_get_match_data(dev);
if (init_fn) {
pmu->secure_access = of_property_read_bool(dev->of_node,
"secure-reg-access");
/* arm64 systems boot only as non-secure */
if (IS_ENABLED(CONFIG_ARM64) && pmu->secure_access) {
dev_warn(dev, "ignoring \"secure-reg-access\" property for arm64\n");
pmu->secure_access = false;
}
ret = init_fn(pmu);
} else if (probe_table) {
cpumask_setall(&pmu->supported_cpus);
ret = probe_current_pmu(pmu, probe_table);
}
if (ret) {
dev_err(dev, "failed to probe PMU!\n");
goto out_free;
}
ret = armpmu_request_irqs(pmu);
if (ret)
goto out_free_irqs;
ret = armpmu_register(pmu);
if (ret) {
dev_err(dev, "failed to register PMU devices!\n");
goto out_free_irqs;
}
return 0;
out_free_irqs:
armpmu_free_irqs(pmu);
out_free:
armpmu_free(pmu);
return ret;
}
注意上面的probe函数,有几个比较重要的内容:
- 在armpmu_alloc中,内核会申请pmu结构体,并填写几个重要的接口(struct arm_pmu->struct pmu pmu;),比如上面我们在ioctl里面调用的pmu-add()/pmu-enable()等
- of_device_get_match_data 获取设备表中的初始化函数:armv8_pmu_init,里面最重要的就是armv8pmu_handle_irq(这个其实就是最终的中断服务响应程序)
- armpmu_request_irqs注册pmu中断服务程序armpmu_dispatch_irq(后面这个地方是重点,perf的采样数据就是这个地方写到mmap的空间)
- 通过armpmu_register调用perf_pmu_register注册pmu到上面的pmu_idr中。
到了这里,通过perf驱动中的pmu_idr全局变量,我们就将 armv8_pmuv3_pmu 与 perf envent驱动建立了联系。后续就可以在perf的open,ioctl等接口里面访问armv8_pmuv3_pmu。
perf_ioctl 与 PERF_EVENT_IOC_ENABLE
在上一个小节,我们已经为perf_event建立了一片空间,用于输出采样数据,先在我们启动采样。和上面的mmap同样的操作,我们直接看对应的fops的ioctl,代码如下:
linux-7.1.6/kernel/events/core.c
c
static long perf_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
struct perf_event *event = file->private_data;
struct perf_event_context *ctx;
long ret;
/* Treat ioctl like writes as it is likely a mutating operation. */
ret = security_perf_event_write(event);
if (ret)
return ret;
ctx = perf_event_ctx_lock(event);
ret = _perf_ioctl(event, cmd, arg);
perf_event_ctx_unlock(event, ctx);
return ret;
}
static long _perf_ioctl(struct perf_event *event, unsigned int cmd, unsigned long arg)
{
void (*func)(struct perf_event *);
u32 flags = arg;
if (event->state <= PERF_EVENT_STATE_REVOKED)
return -ENODEV;
switch (cmd) {
case PERF_EVENT_IOC_ENABLE:
func = _perf_event_enable;
break;
case PERF_EVENT_IOC_DISABLE:
func = _perf_event_disable;
break;
// ... ...
}
// ... ...
}
static void _perf_event_enable(struct perf_event *event)
{
struct perf_event_context *ctx = event->ctx;
// ... ...
event_function_call(event, __perf_event_enable, NULL);
}
static void __perf_event_enable(struct perf_event *event,
struct perf_cpu_context *cpuctx,
struct perf_event_context *ctx,
void *info)
{
struct perf_event *leader = event->group_leader;
struct perf_event_context *task_ctx;
// ... ...
ctx_resched(cpuctx, task_ctx, event->pmu_ctx->pmu, get_event_type(event));
}
// 注意ctx_resched经过多层调用后,到了event_sched_in,开启pmu采样
// ctx_resched->perf_event_sched_in-> ... .... -> event_sched_in
static int
event_sched_in(struct perf_event *event, struct perf_event_context *ctx)
{
struct perf_event_pmu_context *epc = event->pmu_ctx;
struct perf_cpu_pmu_context *cpc = this_cpc(epc->pmu);
int ret = 0;
WARN_ON_ONCE(event->ctx != ctx);
lockdep_assert_held(&ctx->lock);
if (event->state <= PERF_EVENT_STATE_OFF)
return 0;
WRITE_ONCE(event->oncpu, smp_processor_id());
/*
* Order event::oncpu write to happen before the ACTIVE state is
* visible. This allows perf_event_{stop,read}() to observe the correct
* ->oncpu if it sees ACTIVE.
*/
smp_wmb();
perf_event_set_state(event, PERF_EVENT_STATE_ACTIVE);
/*
* Unthrottle events, since we scheduled we might have missed several
* ticks already, also for a heavily scheduling task there is little
* guarantee it'll get a tick in a timely manner.
*/
if (unlikely(event->hw.interrupts == MAX_INTERRUPTS))
perf_event_unthrottle(event, false);
perf_pmu_disable(event->pmu);
perf_log_itrace_start(event);
if (event->pmu->add(event, PERF_EF_START)) {
perf_event_set_state(event, PERF_EVENT_STATE_INACTIVE);
event->oncpu = -1;
ret = -EAGAIN;
goto out;
}
if (!is_software_event(event))
cpc->active_oncpu++;
if (is_event_in_freq_mode(event)) {
ctx->nr_freq++;
epc->nr_freq++;
}
if (event->attr.exclusive)
cpc->exclusive = 1;
out:
perf_pmu_enable(event->pmu);
return ret;
}
这个ioctl + PERF_EVENT_IOC_ENABLE 主要是为了调用pmu驱动里面对应的add+enable回调。注意这里的struct pmu,在上面的perf open操作中提到了,在open中查询到对应的pmu驱动(通过perf_pmu_register来注册,本文以armv8 pmu驱动为例)。所以,这里的pmu->add(), pmu->enable()其实都是调用的对应pmu驱动的方法。
armv8_pmuv3_pmu 中断及采样数据写入 (本文核心)
上文,我们提到了pmu是一个硬件单元,当我们通过ioctl + PERF_EVENT_IOC_ENABLE后,pmu硬件单元就开始工作了,当采样的数据达到一定的数量是,就会触发中断。上面我们在armpmu_request_irqs中提到,中断服务程序是armpmu_dispatch_irq,我们先来看看这部分代码:
linux-7.1.6/drivers/perf/arm_pmu.c
c
static irqreturn_t armpmu_dispatch_irq(int irq, void *dev)
{
struct arm_pmu *armpmu;
int ret;
u64 start_clock, finish_clock;
/*
* we request the IRQ with a (possibly percpu) struct arm_pmu**, but
* the handlers expect a struct arm_pmu*. The percpu_irq framework will
* do any necessary shifting, we just need to perform the first
* dereference.
*/
armpmu = *(void **)dev;
if (WARN_ON_ONCE(!armpmu))
return IRQ_NONE;
start_clock = sched_clock();
ret = armpmu->handle_irq(armpmu);
finish_clock = sched_clock();
perf_sample_event_took(finish_clock - start_clock);
return ret;
}
这里的armpmu->handle_irq就是armv8pmu_handle_irq,也就是中断服务程序。
linux-7.1.6/drivers/perf/arm_pmuv3.c
c
static irqreturn_t armv8pmu_handle_irq(struct arm_pmu *cpu_pmu)
{
u64 pmovsr;
struct perf_sample_data data;
struct pmu_hw_events *cpuc = this_cpu_ptr(cpu_pmu->hw_events);
struct pt_regs *regs;
int idx;
/*
* Get and reset the IRQ flags
*/
pmovsr = armv8pmu_getreset_flags();
/*
* Did an overflow occur?
*/
if (!armv8pmu_has_overflowed(pmovsr))
return IRQ_NONE;
/*
* Handle the counter(s) overflow(s)
*/
regs = get_irq_regs();
/*
* Stop the PMU while processing the counter overflows
* to prevent skews in group events.
*/
armv8pmu_stop(cpu_pmu);
for_each_set_bit(idx, cpu_pmu->cntr_mask, ARMPMU_MAX_HWEVENTS) {
struct perf_event *event = cpuc->events[idx];
struct hw_perf_event *hwc;
/* Ignore if we don't have an event. */
if (!event)
continue;
/*
* We have a single interrupt for all counters. Check that
* each counter has overflowed before we process it.
*/
if (!armv8pmu_counter_has_overflowed(pmovsr, idx))
continue;
hwc = &event->hw;
armpmu_event_update(event);
perf_sample_data_init(&data, 0, hwc->last_period);
if (!armpmu_event_set_period(event))
continue;
if (has_branch_stack(event))
read_branch_records(cpuc, event, &data);
/*
* Perf event overflow will queue the processing of the event as
* an irq_work which will be taken care of in the handling of
* IPI_IRQ_WORK.
*/
perf_event_overflow(event, &data, regs);
}
armv8pmu_start(cpu_pmu);
return IRQ_HANDLED;
}
这个中断服务程序的核心就是,先关闭pmu硬件功能,然后根据当前的pmu的寄存器数据,初始化perf_sample_data,然后调用perf_event_overflow进行处理。
linux-7.1.6/kernel/events/core.c
c
int perf_event_overflow(struct perf_event *event,
struct perf_sample_data *data,
struct pt_regs *regs)
{
/*
* Entry point from hardware PMI, interrupts should be disabled here.
* This serializes us against perf_event_remove_from_context() in
* things like perf_event_release_kernel().
*/
lockdep_assert_irqs_disabled();
return __perf_event_overflow(event, 1, data, regs);
}
static int __perf_event_overflow(struct perf_event *event,
int throttle, struct perf_sample_data *data,
struct pt_regs *regs)
{
int events = atomic_read(&event->event_limit);
int ret = 0;
/*
* Non-sampling counters might still use the PMI to fold short
* hardware counters, ignore those.
*/
if (unlikely(!is_sampling_event(event)))
return 0;
ret = __perf_event_account_interrupt(event, throttle);
if (event->attr.aux_pause)
perf_event_aux_pause(event->aux_event, true);
if (event->prog && event->prog->type == BPF_PROG_TYPE_PERF_EVENT &&
!bpf_overflow_handler(event, data, regs))
goto out;
/*
* XXX event_limit might not quite work as expected on inherited
* events
*/
event->pending_kill = POLL_IN;
if (events && atomic_dec_and_test(&event->event_limit)) {
ret = 1;
event->pending_kill = POLL_HUP;
perf_event_disable_inatomic(event);
event->pmu->stop(event, 0);
}
if (event->attr.sigtrap) {
/*
* The desired behaviour of sigtrap vs invalid samples is a bit
* tricky; on the one hand, one should not loose the SIGTRAP if
* it is the first event, on the other hand, we should also not
* trigger the WARN or override the data address.
*/
bool valid_sample = sample_is_allowed(event, regs);
unsigned int pending_id = 1;
enum task_work_notify_mode notify_mode;
if (regs)
pending_id = hash32_ptr((void *)instruction_pointer(regs)) ?: 1;
notify_mode = in_nmi() ? TWA_NMI_CURRENT : TWA_RESUME;
if (!event->pending_work &&
!task_work_add(current, &event->pending_task, notify_mode)) {
event->pending_work = pending_id;
local_inc(&event->ctx->nr_no_switch_fast);
WARN_ON_ONCE(!atomic_long_inc_not_zero(&event->refcount));
event->pending_addr = 0;
if (valid_sample && (data->sample_flags & PERF_SAMPLE_ADDR))
event->pending_addr = data->addr;
} else if (event->attr.exclude_kernel && valid_sample) {
/*
* Should not be able to return to user space without
* consuming pending_work; with exceptions:
*
* 1. Where !exclude_kernel, events can overflow again
* in the kernel without returning to user space.
*
* 2. Events that can overflow again before the IRQ-
* work without user space progress (e.g. hrtimer).
* To approximate progress (with false negatives),
* check 32-bit hash of the current IP.
*/
WARN_ON_ONCE(event->pending_work != pending_id);
}
}
READ_ONCE(event->overflow_handler)(event, data, regs);
if (*perf_event_fasync(event) && event->pending_kill) {
event->pending_wakeup = 1;
irq_work_queue(&event->pending_irq);
}
out:
if (event->attr.aux_resume)
perf_event_aux_pause(event->aux_event, false);
return ret;
}
然后通过event->overflow_handler写入采样数据到mmap申请的内存空间(我们上面在open的时候提到了,这里的溢出处理handler就是perf_event_output_forward)。
c
void
perf_event_output_forward(struct perf_event *event,
struct perf_sample_data *data,
struct pt_regs *regs)
{
__perf_event_output(event, data, regs, perf_output_begin_forward);
}
static __always_inline int
__perf_event_output(struct perf_event *event,
struct perf_sample_data *data,
struct pt_regs *regs,
int (*output_begin)(struct perf_output_handle *,
struct perf_sample_data *,
struct perf_event *,
unsigned int))
{
struct perf_output_handle handle;
struct perf_event_header header;
int err;
/* protect the callchain buffers */
rcu_read_lock();
perf_prepare_sample(data, event, regs);
perf_prepare_header(&header, data, event, regs);
err = output_begin(&handle, data, event, header.size);
if (err)
goto exit;
perf_output_sample(&handle, &header, data, event);
perf_output_end(&handle);
exit:
rcu_read_unlock();
return err;
}
在__perf_event_output中:
- perf_prepare_sample 从寄存器中准备采样数据
- perf_prepare_header 准备采样数据的头。
- 根据上面的header,通过output_begin 在event->rb的ringbuf中申请当前这个样本的数据空间
- 然后perf_output_sample开始写入。
在perf_output_end中,我们需要通知用户态的poll可以返回了,现在有数据可以返回了。
linux-7.1.6/kernel/events/ring_buffer.c
c
void perf_output_end(struct perf_output_handle *handle)
{
perf_output_put_handle(handle);
rcu_read_unlock();
}
static void perf_output_put_handle(struct perf_output_handle *handle)
{
struct perf_buffer *rb = handle->rb;
unsigned long head;
unsigned int nest;
// ... ...
if (handle->wakeup != local_read(&rb->wakeup))
perf_output_wakeup(handle);
out:
preempt_enable();
}
static void perf_output_wakeup(struct perf_output_handle *handle)
{
atomic_set(&handle->rb->poll, EPOLLIN | EPOLLRDNORM);
handle->event->pending_wakeup = 1;
if (*perf_event_fasync(handle->event) && !handle->event->pending_kill)
handle->event->pending_kill = POLL_IN;
irq_work_queue(&handle->event->pending_irq);
}
注意,perf_output_end 还是在pmu中断服务程序内部调用的,为了异步交付数据给用户态,这里给软中断发送一个中断请求,中断服务地址是:event->pending_irq ,也就是perf_pending_irq,这个是在open的过程中, 初始化指定的。
linux-7.1.6/kernel/events/ring_buffer.c
c
static void perf_pending_irq(struct irq_work *entry)
{
struct perf_event *event = container_of(entry, struct perf_event, pending_irq);
int rctx;
/*
* If we 'fail' here, that's OK, it means recursion is already disabled
* and we won't recurse 'further'.
*/
rctx = perf_swevent_get_recursion_context();
/*
* The wakeup isn't bound to the context of the event -- it can happen
* irrespective of where the event is.
*/
if (event->pending_wakeup) {
event->pending_wakeup = 0;
perf_event_wakeup(event);
}
if (rctx >= 0)
perf_swevent_put_recursion_context(rctx);
}
void perf_event_wakeup(struct perf_event *event)
{
ring_buffer_wakeup(event);
if (event->pending_kill) {
kill_fasync(perf_event_fasync(event), SIGIO, event->pending_kill);
event->pending_kill = 0;
}
}
static void ring_buffer_wakeup(struct perf_event *event)
{
struct perf_buffer *rb;
if (event->parent)
event = event->parent;
rcu_read_lock();
rb = rcu_dereference(event->rb);
if (rb) {
list_for_each_entry_rcu(event, &rb->event_list, rb_entry)
wake_up_all(&event->waitq);
}
rcu_read_unlock();
}
static __poll_t perf_poll(struct file *file, poll_table *wait)
{
struct perf_event *event = file->private_data;
struct perf_buffer *rb;
__poll_t events = EPOLLHUP;
if (event->state <= PERF_EVENT_STATE_REVOKED)
return EPOLLERR;
poll_wait(file, &event->waitq, wait);
if (event->state <= PERF_EVENT_STATE_REVOKED)
return EPOLLERR;
if (is_event_hup(event))
return events;
if (unlikely(READ_ONCE(event->state) == PERF_EVENT_STATE_ERROR &&
event->attr.pinned))
return EPOLLERR;
/*
* Pin the event->rb by taking event->mmap_mutex; otherwise
* perf_event_set_output() can swizzle our rb and make us miss wakeups.
*/
mutex_lock(&event->mmap_mutex);
rb = event->rb;
if (rb)
events = atomic_xchg(&rb->poll, 0);
mutex_unlock(&event->mmap_mutex);
return events;
}
注意,通过perf_pending_irq,我们调用到了ring_buffer_wakeup,我们可以wake_up_all(&event->waitq)所有等待队列上面的进程,每个进程自己检查,是否有poll数据了,我们在软中断中标记了handle->rb->poll为有数据。。
后记
总的来说,perf的工作原理(以采集cpu cycle为例)大概如下:
- cpu有一个叫做pmu的硬件监测单元,因此设备平台编写了一个叫做pmu的平台驱动,本文是:armv8_pmuv3_pmu驱动。在内核启动的时候,根据dts的定义,自动探测到armv8_pmuv3_pmu驱动,并将pmu驱动注册到perf驱动中的pmu_idr全局变量中。
- 用户通过__NR_perf_event_open 系统调用创建一个struct perf_event,绑定armv8_pmuv3_pmu驱动,然后绑定给一个struct file,并给struct file装载各种fops的实现(例如:ioctl/mmap/poll等)。
- 用户通过mmap访问perf_mmap,然后将申请的vm挂在到struct perf_event->rb中
- 用户通过ioctl+PERF_EVENT_IOC_ENABLE,通过struct perf_event->pmu里面的相关ops,设置pmu,然后开启pmu中断
- pmu中断开始响应,将采样数据写入到struct perf_event->rb中,标记了struct perf_event->rb->poll有数据,然后发出一个新的irq软中断,pmu中断结束。
- 在perf_pending_irq中,唤醒struct perf_event->waitq上面的所有进程,irq软中断结束。
- 在perf_poll中,由于vfs poll的实现原理,在反复调用perf_poll,当我们通过软中断唤醒所有等待队列的进程后,再次调用perf_poll,然后检查struct perf_event->rb->poll,然后返回用户态,用户态开始处理新的采样数据,然后返回。
完结散花。
参考文献
- 无
打赏、订阅、收藏、丢香蕉、硬币,请关注公众号(攻城狮的搬砖之路)
PS: 请尊重原创,不喜勿喷。
PS: 要转载请注明出处,本人版权所有。
PS: 有问题请留言,看到后我会第一时间回复。