Linux内核 eBPF基础:perf基础perf_event_open系统调用内核源码分析

Posted rtoax

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux内核 eBPF基础:perf基础perf_event_open系统调用内核源码分析相关的知识,希望对你有一定的参考价值。

Linux内核 eBPF基础
perf(5)perf_event_open系统调用内核源码分析


荣涛
2021年5月19日

1. perf_event_open系统调用

详解见Linux内核 eBPF基础:perf(4)perf_event_open系统调用与用户手册详解

#include <linux/perf_event.h>
#include <linux/hw_breakpoint.h>

int perf_event_open(struct perf_event_attr *attr,
                   pid_t pid, int cpu, int group_fd,
                   unsigned long flags);

1.1. pid

参数pid允许事件以各种方式附加到进程。

  • 如果pid为0,则在当前线程上进行测量;
  • 如果pid大于0,则对pid指示的进程进行测量;
  • 如果pid为-1,则对所有进程进行计数。

1.2. cpu

cpu参数允许测量特定于CPU。

  • 如果cpu>=0,则将测量限制为指定的CPU;否则,将限制为0。
  • 如果cpu=-1,则在所有CPU上测量事件。

请注意,pid == -1cpu == -1的组合无效。

  • pid> 0cpu == -1设置会测量每个进程,并将该进程跟随该进程计划调度到的任何CPU。 每个用户都可以创建每个进程的事件。
  • 每个CPU的pid == -1cpu>= 0设置是针对每个CPU的,并测量指定CPU上的所有进程。 每CPU事件需要CAP_SYS_ADMIN功能或小于/ 1的/proc/sys/kernel/perf_event_paranoid值。见《perf_event相关的配置文件》章节。

1.3. group_fd

group_fd参数允许创建事件组。 一个事件组有一个事件,即组长。 首先创建领导者,group_fd = -1。 其余的组成员是通过随后的perf_event_open()调用创建的,其中group_fd设置为组长的fd。 (使用group_fd = -1单独创建一个事件,并且该事件被认为是只有1个成员的组。)将事件组作为一个单元调度到CPU:仅当所有 可以将组中的事件放到CPU上。 这意味着成员事件的值可以彼此有意义地进行比较,相加,除法(以获得比率)等,因为它们已经为同一组已执行指令计数了事件。

1.4. flags

#define PERF_FLAG_FD_NO_GROUP		(1UL << 0)
#define PERF_FLAG_FD_OUTPUT		(1UL << 1)
#define PERF_FLAG_PID_CGROUP		(1UL << 2) /* pid=cgroup id, per-cpu mode only */
#define PERF_FLAG_FD_CLOEXEC		(1UL << 3) /* O_CLOEXEC */

在系统调用man手册中是这样讲解的:

  • PERF_FLAG_FD_NO_GROUP:此标志允许将事件创建为事件组的一部分,但没有组长。 尚不清楚这为什么有用。
  • PERF_FLAG_FD_OUTPUT:该标志将输出从事件重新路由到组长。
  • PERF_FLAG_PID_CGROUP:该标志激活每个容器的系统范围内的监视。 容器是一种抽象,它隔离一组资源以进行更精细的控制(CPU,内存等)。 在这种模式下,仅当在受监视的CPU上运行的线程属于指定的容器(cgroup)时,才测量该事件。 通过传递在cgroupfs文件系统中其目录上打开的文件描述符来标识cgroup。 例如,如果要监视的cgroup称为test,则必须将在/ dev / cgroup / test(假定cgroupfs安装在/ dev / cgroup上)上打开的文件描述程序作为pid参数传递。 cgroup监视仅适用于系统范围的事件,因此可能需要额外的权限。(本文不讨论容器相关内容)
  • PERF_FLAG_FD_CLOEXEC:O_CLOEXEC:在linux系统中,open一个文件可以带上O_CLOEXEC标志位,这个表示位和用fcntl设置的FD_CLOEXEC有同样的作用,都是在fork的子进程中用exec系列系统调用加载新的可执行程序之前,关闭子进程中fork得到的fd。(在使用strace perf stat ls调用perf_event_open传入的即为PERF_FLAG_FD_CLOEXEC

2. 测试例

https://github.com/Rtoax/test/tree/master/c/glibc/linux/perf_event

/*
https://stackoverflow.com/questions/42088515/perf-event-open-how-to-monitoring-multiple-events

perf stat -e cycles,faults ls

*/
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <string.h>
#include <sys/ioctl.h>
#include <linux/perf_event.h>
#include <linux/hw_breakpoint.h>
#include <asm/unistd.h>
#include <errno.h>
#include <stdint.h>
#include <inttypes.h>

struct read_format {
    uint64_t nr;
    struct {
        uint64_t value;
        uint64_t id;
    } values[];
};

void do_malloc() {

    int i;
    char* ptr;
    int len = 2*1024*1024;
    ptr = malloc(len);
    
    mlock(ptr, len);
    
    for (i = 0; i < len; i++) {
        ptr[i] = (char) (i & 0xff); // pagefault
    }
    
    free(ptr);
}

void do_ls() {
    system("/bin/ls");
}

void do_something(int something) {
    
    switch(something) {
    case 1:
        do_ls();
        break;
    case 0:
    default:
        do_malloc();
        break;
    }
}

int create_hardware_perf(int grp_fd, enum perf_hw_id hw_ids, uint64_t *ioc_id)
{
    if(PERF_COUNT_HW_MAX <= hw_ids || hw_ids < 0) {
        printf("Unsupport enum perf_hw_id.\\n");
        return -1;
    }
    
    struct perf_event_attr pea;
    
    memset(&pea, 0, sizeof(struct perf_event_attr));
    pea.type = PERF_TYPE_HARDWARE;
    pea.size = sizeof(struct perf_event_attr);
    pea.config = hw_ids;
    pea.disabled = 1;
    pea.exclude_kernel = 1;
    pea.exclude_hv = 1;
    pea.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID;
    int fd = syscall(__NR_perf_event_open, &pea, 0, -1, grp_fd>2?grp_fd:-1, 0);
    ioctl(fd, PERF_EVENT_IOC_ID, ioc_id);

    return fd;
}

int create_software_perf(int grp_fd, enum perf_sw_ids sw_ids, uint64_t *ioc_id)
{
    if(PERF_COUNT_SW_MAX <= sw_ids || sw_ids < 0) {
        printf("Unsupport enum perf_sw_ids.\\n");
        return -1;
    }

    struct perf_event_attr pea;
    
    memset(&pea, 0, sizeof(struct perf_event_attr));
    pea.type = PERF_TYPE_SOFTWARE;
    pea.size = sizeof(struct perf_event_attr);
    pea.config = sw_ids;
    pea.disabled = 1;
    pea.exclude_kernel = 1;
    pea.exclude_hv = 1;
    pea.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID;
    int fd = syscall(__NR_perf_event_open, &pea, 0, -1, grp_fd>2?grp_fd:-1 /*!!!*/, 0);
    ioctl(fd, PERF_EVENT_IOC_ID, ioc_id);

    return fd;
}


int main(int argc, char* argv[]) 
{
    struct perf_event_attr pea;
    
    int group_fd, fd2, fd3, fd4, fd5;
    uint64_t id1, id2, id3, id4, id5;
    uint64_t val1, val2, val3, val4, val5;
    char buf[4096];
    struct read_format* rf = (struct read_format*) buf;
    int i;

    group_fd = create_hardware_perf(-1, PERF_COUNT_HW_CPU_CYCLES, &id1);
    
    fd2 = create_hardware_perf(group_fd, PERF_COUNT_HW_CACHE_MISSES, &id2);
    fd3 = create_software_perf(group_fd, PERF_COUNT_SW_PAGE_FAULTS, &id3);
    fd4 = create_software_perf(group_fd, PERF_COUNT_SW_CPU_CLOCK, &id4);
    fd5 = create_software_perf(group_fd, PERF_COUNT_SW_CPU_CLOCK, &id5);

    printf("ioctl %ld, %ld, %ld, %ld, %ld\\n", id1, id2, id3, id4, id5);

    ioctl(group_fd, PERF_EVENT_IOC_RESET, PERF_IOC_FLAG_GROUP);
    ioctl(group_fd, PERF_EVENT_IOC_ENABLE, PERF_IOC_FLAG_GROUP);

    do_something(-1);
    
    ioctl(group_fd, PERF_EVENT_IOC_DISABLE, PERF_IOC_FLAG_GROUP);


    read(group_fd, buf, sizeof(buf));
    for (i = 0; i < rf->nr; i++) {
        if (rf->values[i].id == id1) {
            val1 = rf->values[i].value;
        } else if (rf->values[i].id == id2) {
            val2 = rf->values[i].value;
        } else if (rf->values[i].id == id3) {
            val3 = rf->values[i].value;
        } else if (rf->values[i].id == id4) {
            val4 = rf->values[i].value;
        } else if (rf->values[i].id == id5) {
            val5 = rf->values[i].value;
        }
    }

    printf("cpu cycles:     %"PRIu64"\\n", val1);
    printf("cache misses:   %"PRIu64"\\n", val2);
    printf("page faults:    %"PRIu64"\\n", val3);
    printf(" cpu clock:     %"PRIu64"\\n", val4);
    printf("task clock:     %"PRIu64"\\n", val5);

    close(group_fd);
    close(fd2);
    close(fd3);
    close(fd4);
    close(fd5);

    return 0;
}

程序运行结果:

[rongtao@localhost perf_event]$ ./a.out 
ioctl 2640, 2641, 2642, 2643, 2644
cpu cycles:     12996145
cache misses:   1135
page faults:    518
 cpu clock:     5417726
task clock:     5393251

如程序所示,建立五个fd,并将后四个添加到第一个fd中组成一个组,分别监控以上信息,在下章源码接收中,我们将介绍以上信息是如何从内核中获取的。需要注意的是,代码中的内存锁定对缺页中断没有影响:

    mlock(ptr, len);
    
    for (i = 0; i < len; i++) {
        ptr[i] = (char) (i & 0xff); // pagefault
    }

3. perf_event_open源码分析

3.1. 如何调用perf_event_open

代码如下:

    struct perf_event_attr pea;
    uint64_t id1;
    
    memset(&pea, 0, sizeof(struct perf_event_attr));
    pea.type = PERF_TYPE_HARDWARE;
    pea.size = sizeof(struct perf_event_attr);
    pea.config = PERF_COUNT_HW_CPU_CYCLES;
    pea.disabled = 1;
    pea.exclude_kernel = 1;
    pea.exclude_hv = 1;
    pea.read_format = PERF_FORMAT_GROUP | PERF_FORMAT_ID;
    int fd = syscall(__NR_perf_event_open, &pea, getpid(), -1, -1, 0);
    ioctl(fd, PERF_EVENT_IOC_ID, &id1);

此处的PID为当前进程PID(也可以填写0),CPU为-1,group_fd为-1,flags填0,标识位flags填写为0,flags在perf命令中填写的PERF_FLAG_FD_CLOEXEC,在源码中为:

	if (flags & PERF_FLAG_FD_CLOEXEC)
		f_flags |= O_CLOEXEC;

所以以上的入参表明测量当前进程并将该进程跟随该进程计划调度到的任何CPU(pid> 0cpu == -1)。在syscall过程,将陷入内核态。

3.2. 如何处理入参

3.2.1. struct perf_event_attr

首先是从用户空间拷贝到内核空间:

err = perf_copy_attr(attr_uptr, &attr);

其内部考虑到了不同版本的struct perf_event_attr结构的长度:

	if (!size)
		size = PERF_ATTR_SIZE_VER0;
	if (size < PERF_ATTR_SIZE_VER0 || size > PAGE_SIZE)
		goto err_size;

然后使用copy_struct_from_user拷贝。接着进行参数检查:

	if (attr->__reserved_1 || attr->__reserved_2 || attr->__reserved_3)
		return -EINVAL;

	if (attr->sample_type & ~(PERF_SAMPLE_MAX-1))
		return -EINVAL;

	if (attr->read_format & ~(PERF_FORMAT_MAX-1))
		return -EINVAL;

接下来是对sample_type字段的检查(见《Linux内核 eBPF基础:perf(4)perf_event_open系统调用与用户手册详解》),分为两部分:

  • PERF_SAMPLE_BRANCH_STACK:(从Linux 3.4开始),它提供了最近分支的记录,该记录由CPU分支采样硬件(例如Intel Last Branch Record)提供。并非所有硬件都支持此功能。有关如何过滤报告的分支的信息,请参见branch_sample_type字段。
  • PERF_SAMPLE_REGS_USER:(从Linux 3.7开始)记录当前用户级别的CPU寄存器状态(调用内核之前的进程中的值)。
  • PERF_SAMPLE_STACK_USER:(从Linux 3.7开始)记录用户级堆栈,从而允许展开堆栈。
  • PERF_SAMPLE_REGS_INTR
  • PERF_SAMPLE_CGROUP

3.2.2. pidcpu

如果标志位PERF_FLAG_PID_CGROUP(该标志激活每个容器的系统范围内的监视),那么pidcpu都不可以为-1。

	/*
	 * In cgroup mode, the pid argument is used to pass the fd
	 * opened to the cgroup directory in cgroupfs. The cpu argument
	 * designates the cpu on which to monitor threads from that
	 * cgroup.
	 */
	if ((flags & PERF_FLAG_PID_CGROUP) && (pid == -1 || cpu == -1))
		return -EINVAL;

如果标志位PERF_FLAG_PID_CGROUP未设置并且pid != -1,查找task_struct结构。

static struct task_struct *
find_lively_task_by_vpid(pid_t vpid)    /* 获取task_struct */
{
	struct task_struct *task;

	rcu_read_lock();
	if (!vpid)
		task = current; /* 当前进程 */
	else
		task = find_task_by_vpid(vpid); /* 查找 */
	if (task)
		get_task_struct(task);  /* 引用计数 */
	rcu_read_unlock();

	if (!task)
		return ERR_PTR(-ESRCH);

	return task;
}

3.2.3. group_fd

如果设置了group_fd != -1,将获取struct fd结构

static inline int perf_fget_light(int fd, struct fd *p)
{
	struct fd f = fdget(fd);
	if (!f.file)
		return -EBADF;

	if (f.file->f_op != &perf_fops) {
		fdput(f);
		return -EBADF;
	}
	*p = f;
	return 0;
}

这里需要注意文件操作符为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,
};

3.2.4. flags

一系列的鉴权工作,此处不做详细讨论,可参见《Linux内核 eBPF基础:perf(4)perf_event_open系统调用与用户手册详解

#define PERF_FLAG_FD_NO_GROUP		(1UL << 0)
#define PERF_FLAG_FD_OUTPUT		(1UL << 1)
#define PERF_FLAG_PID_CGROUP		(1UL << 2) /* pid=cgroup id, per-cpu mode only */
#define PERF_FLAG_FD_CLOEXEC		(1UL << 3) /* O_CLOEXEC */

在系统调用man手册中是这样讲解的:

  • PERF_FLAG_FD_NO_GROUP:此标志允许将事件创建为事件组的一部分,但没有组长。 尚不清楚这为什么有用。
  • PERF_FLAG_FD_OUTPUT:该标志将输出从事件重新路由到组长。
  • PERF_FLAG_PID_CGROUP:该标志激活每个容器的系统范围内的监视。 容器是一种抽象,它隔离一组资源以进行更精细的控制(CPU,内存等)。 在这种模式下,仅当在受监视的CPU上运行的线程属于指定的容器(cgroup)时,才测量该事件。 通过传递在cgroupfs文件系统中其目录上打开的文件描述符来标识cgroup。 例如,如果要监视的cgroup称为test,则必须将在/ dev / cgroup / test(假定cgroupfs安装在/ dev / cgroup上)上打开的文件描述程序作为pid参数传递。 cgroup监视仅适用于系统范围的事件,因此可能需要额外的权限。(本文不讨论容器相关内容)
  • PERF_FLAG_FD_CLOEXEC:O_CLOEXEC:在linux系统中,open一个文件可以带上O_CLOEXEC标志位,这个表示位和用fcntl设置的FD_CLOEXEC有同样的作用,都是在fork的子进程中用exec系列系统调用加载新的可执行程序之前,关闭子进程中fork得到的fd。(在使用strace perf stat ls调用perf_event_open传入的即为PERF_FLAG_FD_CLOEXEC

3.3. perf_event_alloc

	event = perf_event_alloc(&attr, cpu, task, group_leader, NULL,
				 NULL, NULL, cgroup_fd);

使用kzalloc申请struct perf_event结构,然后是一系列的初始化工作。需要注意的几点如下:

  • overflow_handler为NULL,决定了环形队列的写入方向。
	if (overflow_handler) {
		event->overflow_handler	= overflow_handler;
		event->overflow_handler_context = context;
	} else if (is_write_backward(event)){/* Write ring buffer from end to beginning */
		event->overflow_handler = perf_event_output_backward;
		event->overflow_handler_context = NULL;
	} else {
		event->overflow_handler = perf_event_output_forward;
		event->overflow_handler_context = NULL;
	}
  • 状态event->state
/*
 * Initialize event state based on the perf_event_attr::disabled.
 */
static inline void perf_event__state_init(struct perf_event *event)
{
	event->state = event->attr.disabled ? PERF_EVENT_STATE_OFF :
					      PERF_EVENT_STATE_INACTIVE;
}
  • more

3.3.1. perf_init_event

3.3.1.1. perf_try_init_event

该函数将调用PMU的相关操作符pmu->event_init(event);(参见《Linux内核 eBPF基础:perf(2):perf性能管理单元PMU的注册》),如:

//kernel/events/core.c
static struct pmu/* 性能监控单元 */ perf_swevent = {
	.task_ctx_nr	= perf_sw_context,

	.capabilities	= PERF_PMU_CAP_NO_NMI,

	.event_init	= perf_swevent_init,
	.add		= perf_swevent_add,
	.del		= perf_swevent_del,
	.start		= perf_swevent_start,
	.stop		= perf_swevent_stop,
	.read		= perf_swevent_read,
};

perf_pmu_register(&perf_swevent, "software", PERF_TYPE_SOFTWARE);
//kernel/events/core.c
static struct pmu perf_cpu_clock = {
	.task_ctx_nr	= perf_sw_context,

	.capabilities	= PERF_PMU_CAP_NO_NMI,

	.event_init	= cpu_clock_event_init,
	.add		= cpu_clock_event_add,
	.del		= cpu_clock_event_del,
	.start		= cpu_clock_event_start,
	.stop		= cpu_clock_event_stop,
	.read		以上是关于Linux内核 eBPF基础:perf基础perf_event_open系统调用内核源码分析的主要内容,如果未能解决你的问题,请参考以下文章

Linux内核 eBPF基础:perf用户态指令分析

Linux内核 eBPF基础:perf:perf_event在内核中的初始化

Linux内核 eBPF基础:perfperf_event_open系统调用与用户手册详解

Linux内核 eBPF基础: 探索USDT探针

Linux内核 eBPF基础:BCC (BPF Compiler Collection)

perf命令