1.1 内核perf架构
写硬件计数器采样程序,涉及这幅图的软件、硬件事件,以及用户空间中的ring buffer.
唯一的用户态系统调用会返回一个perf事件的句柄,这样这个perf_event
结构可以通过read/write/ioctl/mmap
通用文件接口来操作。perf_event_open(2)
的调用也是挺复杂的,详细操作可以看perf_event_open(2) Linux manual page,后面写采样后台线程用到了会详细说。
perf_event
也是内核中比较核心的结构体,性能事件有多种类型,例如跟踪点、软件、硬件。这些又具体表现为PMU结构体,每一个事件都有一个,比如software pmu:
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,
};
如果这个事件是硬件相关,那么这个PMU结构体还会有一个和架构相关的结构体,如下图的struct x86_pmu
, 这个硬件相关结构体的作用就是读或者写MSR性能监视器。
perf event的组织方式是cpu维度或者task维度,这样采样才不是只有整个系统的。在manual page有写,perf_event_open()
系统调用使用cpu、pid两个参数来指定perf_event
的cpu、task维度。两种维度的关联是靠perf_event_context
如下图:
每个perf_event
由event_list
连接,而group的连接方式便于perf count功能一次性读出。
由于cpu维度的perf_event
只要cpu online就会一直运行,task维度只有task被调度才会运行,这涉及perf驱动开关和任务调度。一个概括的函数调用图如下:
Every PMU is registerd by calling ‘perf_pmu_register’.
每个pmu拥有一个per_cpu的链表,perf_event需要在哪个cpu上获取数据就加入到哪个cpu的链表上。如果event被触发,它会根据当前的运行cpu给对应链表上的所有perf_event推送数据。
cpu维度的context:this_cpu_ptr
(pmu->pmu_cpu_context->ctx
)上链接的所有perf_event会根据绑定的pmu,链接到pmu对应的per_cpu的->perf_events链表上。
task维度的context:this_cpu_ptr
(pmu->pmu_cpu_context->task_ctx
)上链接的所有perf_event会根据绑定的pmu,链接到pmu对应的per_cpu的->perf_events链表上。perf_event还需要做cpu匹配,符合event->cpu == -1 || event->cpu == smp_processor_id()
条件的event才能链接到pmu上。
参考Linux kernel perf architecture
参考Linux perf 1.1、perf_event内核框架
1.2 perf计数器模式
perf_event_open()有两个使用模式,一个叫做计数,一个叫做采样。计数事件会统计发生的总数,采样事件会定期写入缓冲区。下面来看一个非常简单的计数的代码段,每一秒获取刚刚过去的那一秒内的指令数:
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/perf_event.h>
//目前perf_event_open在glibc中没有封装,需要手工封装一下
int perf_event_open(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);
}
int main()
{
struct perf_event_attr attr;
memset(&attr,0,sizeof(struct perf_event_attr));
attr.size=sizeof(struct perf_event_attr);
//监测硬件
attr.type=PERF_TYPE_HARDWARE;
//监测指令数
attr.config=PERF_COUNT_HW_INSTRUCTIONS;
//初始状态为禁用
attr.disabled=1;
//创建perf文件描述符,其中pid=0,cpu=-1表示监测当前进程,不论运行在那个cpu上
int fd=perf_event_open(&attr,0,-1,-1,0);
if(fd<0)
{
perror("Cannot open perf fd!");
return 1;
}
//启用(开始计数)
ioctl(fd,PERF_EVENT_IOC_ENABLE,0);
while(1)
{
uint64_t instructions;
//读取最新的计数值
read(fd,&instructions,sizeof(instructions));
//读取后清零,这样就不用手动去减了,否则会显示累计值
ioctl(fd,PERF_EVENT_IOC_RESET,0);
printf("instructions=%ld\n",instructions);
sleep(1);
}
}
不需要任何的编译选项,直接gcc,然后运行(从上个图我们知道这是用户态的函数):
gcc single.c -o single
sudo ./single
对于多个计数器不能说搞多个文件句柄去读取,这样read()函数调用开销还是有点大的,重复利用一个句柄,这样就成了前面提到的组的关系。主要有以下6点不同。
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <linux/perf_event.h>
//目前perf_event_open在glibc中没有封装,需要手工封装一下
int perf_event_open(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);
}
//1. 每次read()得到的结构体
struct read_format
{
//计数器数量(为2)
uint64_t nr;
//两个计数器的值
uint64_t values[2];
};
int main()
{
struct perf_event_attr attr;
// perf_event_attr structure provides detailed configuration information for the event being created.
//————————————————————第一个计数器—————————————————
memset(&attr,0,sizeof(struct perf_event_attr));
attr.size=sizeof(struct perf_event_attr);
//监测硬件
attr.type=PERF_TYPE_HARDWARE;
//监测指令数
attr.config=PERF_COUNT_HW_INSTRUCTIONS;
//初始状态为禁用
attr.disabled=1;
//2. 每次读取一个组
attr.read_format=PERF_FORMAT_GROUP;
//创建perf文件描述符,其中pid=0,cpu=-1表示监测当前进程,不论运行在那个cpu上
int fd=perf_event_open(&attr,0,-1,-1,0);
if(fd<0)
{
perror("Cannot open perf fd!");
return 1;
}
//————————————————————第二个计数器—————————————————
memset(&attr,0,sizeof(struct perf_event_attr));
attr.size=sizeof(struct perf_event_attr);
//监测类型
attr.type=PERF_TYPE_HARDWARE;
//监测时钟周期数
attr.config=PERF_COUNT_HW_CPU_CYCLES;
//初始状态为禁用
attr.disabled=1;
//3. 创建perf文件描述符,但是不同的是要传入上次的句柄
int fd2=perf_event_open(&attr,0,-1,fd,0);
if(fd2<0)
{
perror("Cannot open perf fd2!");
return 1;
}
//4. 启用(开始计数),注意PERF_IOC_FLAG_GROUP标志
ioctl(fd,PERF_EVENT_IOC_ENABLE,PERF_IOC_FLAG_GROUP);
while(1)
{
struct read_format aread;
//5.读取最新的计数值,每次读取一个结构体,每个计数器的读取和加入组的顺序是一致的。
read(fd,&aread,sizeof(struct read_format));
printf("instructions=%ld,cycles=%ld\n",aread.values[0],aread.values[1]);
//6. 清空组内计数器
ioctl(fd,PERF_EVENT_IOC_RESET,PERF_IOC_FLAG_GROUP);
sleep(1);
}
}
1.3 perf采样
如果机器只运行一个程序,那么使用计数的方式也是可以的吧。但是如果要在多个里面追踪一个进程或特定的core那就得采样了,而且采样的好处在于可以获得更多的信息。
采样的模板主要在于:1、采样需要设置触发源,也就是告诉kernel何时进行一次采样;2、采样需要设置信号,也就是告诉kernnel,采样完成后通知谁;3、采样值的读取需要使用mmap,因为采样有异步性,需要一个环形队列,另外也是出于性能的考虑。通过轮询或者响应信号判断是否采样完这一轮。
采样事件准备工作:
//这些的定义得看cpu的架构手册
#define DRAM_LLC_LOAD_MISS 0x1d3
#define REMOTE_DRAM_LLC_LOAD_MISS 0x2d3
#define NVM_LLC_LOAD_MISS 0x80d1
#define ALL_STORES 0x82d0
#define ALL_LOADS 0x81d0
#define STLB_MISS_STORES 0x12d0
#define STLB_MISS_LOADS 0x11d0
static __u64 get_pebs_event(enum events e)
{
switch (e) {
case DRAMREAD:
return DRAM_LLC_LOAD_MISS;
case NVMREAD:
if (!htmm_cxl_mode)
return NVM_LLC_LOAD_MISS;
else
return N_HTMMEVENTS;
case MEMWRITE:
return ALL_STORES;
case CXLREAD:
if (htmm_cxl_mode)
return REMOTE_DRAM_LLC_LOAD_MISS;
else
return N_HTMMEVENTS;
default:
return N_HTMMEVENTS;
}
}
struct perf_event ***mem_event; //perf_event结构体数组,*mem_event是数组指针,因为是二维数组所以3*
static int pebs_init(pid_t pid, int node)
{
int cpu, event; //要采样的CPU数和事件数
//存放perf事件的空间被申请,是一个二维数组,每个core有一个专门存放的地方。
//kzalloc是内核空间内存申请函数,可以保证是连续的物理地址,但不能超过128KB,与kmalloc非常相似,只是这个会将申请到的内存清零。malloc是用户空间的。
mem_event = kzalloc(sizeof(struct perf_event **) * CPUS_PER_SOCKET, GFP_KERNEL);
for (cpu = 0; cpu < CPUS_PER_SOCKET; cpu++) {
mem_event[cpu] = kzalloc(sizeof(struct perf_event *) * N_HTMMEVENTS, GFP_KERNEL);
}
printk("pebs_init\n");
for (cpu = 0; cpu < CPUS_PER_SOCKET; cpu++) {
for (event = 0; event < N_HTMMEVENTS; event++) {
if (get_pebs_event(event) == N_HTMMEVENTS) { //这里是在枚举有多少个要收集的数据,根据系统不同有的采样用不到,所以需要将这个指针变为NULL
mem_event[cpu][event] = NULL;
continue;
}
if (__perf_event_open(get_pebs_event(event), 0, cpu, event, pid)) //这一步获得每个perf事件对应的文件描述符
return -1;
if (htmm__perf_event_init(mem_event[cpu][event], BUFFER_SIZE)) //这一步将每个事件和ring buffer对应
return -1;
}
}
return 0;
}
这一步就是要创建perf文件描述符,这里重写了一个名为htmm__perf_event_open
的perf_event_open
,但是核心操作是差不多的。从调用入口来看,传入的参数依次是要采样事件的宏定义
,0
这里值config2不用,第几个cpu
,第几个perf event
,进程的pid
/********************/
/*传入要采样事件的宏定义,config1=0,cpu个数,事件个数,pid(为0就监控所有);因为这里是在循环时open,每个cpu都有一个文件操作符在做这件事*/
/*******************/
static int __perf_event_open(__u64 config, __u64 config1, __u64 cpu,
__u64 type, __u32 pid)
{
struct perf_event_attr attr; // 函数需要的结构体,告诉这个文件描述符该怎么创建,因为采样不同的事件最后传回的perf_event结构体也不一样。
struct file *file; // 已打开的文件在内核中用file结构体表示,文件描述符表中的指针指向file结构体。
int event_fd, __pid; // 我们要接收的文件句柄
memset(&attr, 0, sizeof(struct perf_event_attr));
attr.type = PERF_TYPE_RAW; // 要检测的类型有硬件、软件等等咯。This indicates a "raw" implementation-specific event in the config field.
attr.size = sizeof(struct perf_event_attr);
attr.config = config; //要监测的采样事件
/* 但是我们可以发现,这个事件传入的宏定义是自己定义的,不是系统有的默认的宏定义。If type is PERF_TYPE_RAW, then a custom "raw" config value is needed. Most CPUs support events that are not covered by the "generalized" events. These are implementation defined; see your CPU manual (for example the Intel Volume 3B documentation or the AMD BIOS and Kernel Developer Guide). The libpfm4 library can be used to translate from the name in the architectural manuals to the raw hex value perf_event_open() expects in this field.*/
attr.config1 = config1; // 这是用作扩展用的
if (config == ALL_STORES)
attr.sample_period = htmm_inst_sample_period; //采样事件间隔
else
attr.sample_period = get_sample_period(0);
attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_TID | PERF_SAMPLE_ADDR; //采样目标IP寄存器、TID实际上是内核(线程)中可调度对象的标识符(当一个进程只有一个线程时,pid和tid总是相同的)、地址
attr.disabled = 0; // 初始状态为启用
attr.exclude_kernel = 1; /* don't count kernel */
attr.exclude_hv = 1; /* don't count hypervisor */
attr.exclude_callchain_kernel = 1; /* exclude kernel callchains */
attr.exclude_callchain_user = 1; /* exclude user callchains */
attr.precise_ip = 1; /* skid constraint,默认是2咦 */
attr.enable_on_exec = 1; /* next exec enables */
if (pid == 0)
__pid = -1;
else
__pid = pid;
event_fd = htmm__perf_event_open(&attr, __pid, cpu, -1, 0); //创建文件描述符
//htmm__perf_event_open在\kernel\events\core.c ,A call to perf_event_open() creates a file descriptor that allows measuring performance information. Each file descriptor corresponds to one event that is measured; these can be grouped together to measure multiple events simultaneously.只不过这里的是修改版的,那么两个文件有些什么区别主要添加了什么呢?
if (event_fd <= 0) {
printk("[error htmm__perf_event_open failure] event_fd: %d\n", event_fd);
return -1;
}
// 这里的读句柄的方式和上面计数用到的不一样。判断是不是写入到了文件,然后保留这个文件的private_data成员指针。private_data指针的指向会根据驱动不同而不同,这里可以获得perf_event指针。
file = fget(event_fd);
if (!file) {
printk("invalid file\n");
return -1;
}
mem_event[cpu][type] = fget(event_fd)->private_data;
return 0;
}
分析自己封装的htmm__perf_event_open
函数,到底有什么区别,因为封装前的__NR_perf_event_open
这个系统调用才是主角。但是这里封装后的代码并没有这个主角。所以笔者尝试对比一下这个系统调用。
int htmm__perf_event_open(struct perf_event_attr *attr_ptr, pid_t pid,
int cpu, int group_fd, unsigned long flags)
{
……
/*err = perf_copy_attr(attr_ptr, &attr);
if (err)
return err;*/
attr = *attr_ptr;
……
}
从结果来看,这里只有一个区别,指针赋值方式。重点在于这个函数没有被系统调用了,少了软中断,由原来的内核态切换到用户态执行。但是系统调用不是说拿到用户态就可以的,接着分析一下整个后台线程中其他的操作。还有添加新的系统调用:
/* CONFIG_HTMM */
asmlinkage long sys_htmm_start(pid_t pid, int node);
asmlinkage long sys_htmm_end(pid_t pid);
/***************/
#else
SYSCALL_DEFINE2(htmm_start,
pid_t, pid, int, node)
{
ksamplingd_init(pid, node);
return 0;
}
SYSCALL_DEFINE1(htmm_end,
pid_t, pid)
{
ksamplingd_exit();
return 0;
}
也就是说虽然这里perf采样不是单独被系统调用的,但是整个后台的采样线程的启动都是被系统调用的,都是运行在内核态的。
到目前为止,后台采样线程的文件描述符已经实现了。采样值的读取需要使用mmap()
直接在用户空间操作,少一次复制,因为采样有异步性,需要一个环形队列,这是一个共享内存,一个多生产者单消费者(MPSC)队列。环形缓冲区的头是struct perf_event_mmap_page
,记录共享环形缓冲区的特点,这个头大小是一个页面大小。
/*
* Structure of the page that can be mapped via mmap
*/
struct perf_event_mmap_page {
__u32 version; /* version number of this structure */
__u32 compat_version; /* lowest version this is compat with */
/*
* Bits needed to read the hw counters in user-space.
*
* u32 seq;
* s64 count;
*
* do {
* seq = pc->lock;
*
* barrier()
* if (pc->index) {
* count = pmc_read(pc->index - 1);
* count += pc->offset;
* } else
* goto regular_read;
*
* barrier();
* } while (pc->lock != seq);
*
* NOTE: for obvious reason this only works on self-monitoring
* processes.
*/
__u32 lock; /* seqlock for synchronization */
__u32 index; /* hardware counter identifier */
__s64 offset; /* add to hardware counter value */
/*
* Control data for the mmap() data buffer.
*
* User-space reading this value should issue an rmb(), on SMP capable
* platforms, after reading this value -- see perf_event_wakeup().
*/
__u32 data_head; /* head in the data section */
};
然后来看ring buffer结构体的信息:
#define PERF_RECORD_MISC_KERNEL (1 << 0)
#define PERF_RECORD_MISC_USER (1 << 1)
#define PERF_RECORD_MISC_OVERFLOW (1 << 2)
struct perf_event_header {
__u32 type;
__u16 misc;
__u16 size;
};
enum perf_event_type {
/*
* The MMAP events record the PROT_EXEC mappings so that we can
* correlate userspace IPs to code. They have the following structure:
*
* struct {
* struct perf_event_header header;
*
* u32 pid, tid;
* u64 addr;
* u64 len;
* u64 pgoff;
* char filename[];
* };
*/
PERF_RECORD_MMAP = 1,
PERF_RECORD_MUNMAP = 2,
/*
* struct {
* struct perf_event_header header;
*
* u32 pid, tid;
* char comm[];
* };
*/
PERF_RECORD_COMM = 3,
/*
* When header.misc & PERF_RECORD_MISC_OVERFLOW the event_type field
* will be PERF_RECORD_*
*
* struct {
* struct perf_event_header header;
*
* { u64 ip; } && PERF_RECORD_IP
* { u32 pid, tid; } && PERF_RECORD_TID
* { u64 time; } && PERF_RECORD_TIME
* { u64 addr; } && PERF_RECORD_ADDR
*
* { u64 nr;
* { u64 event, val; } cnt[nr]; } && PERF_RECORD_GROUP
*
* { u16 nr,
* hv,
* kernel,
* user;
* u64 ips[nr]; } && PERF_RECORD_CALLCHAIN
* };
*/
};
接下来将文件描述符和ring buffer相关联。
int htmm__perf_event_init(struct perf_event *event, unsigned long nr_pages)
{
struct perf_buffer *rb = NULL;
int ret = 0, flags = 0;
if (event->cpu == -1 && event->attr.inherit)
return -EINVAL;
ret = security_perf_event_read(event);
if (ret)
return ret;
// 采用伙伴系统分配的话得是2的幂次方
if (nr_pages != 0 && !is_power_of_2(nr_pages))
return -EINVAL;
WARN_ON_ONCE(event->ctx->parent_ctx);
// 加上互斥锁,避免被并发访问
mutex_lock(&event->mmap_mutex);
WARN_ON(event->rb);
// 分配一个环形缓冲区
rb = rb_alloc(nr_pages,
event->attr.watermark ? event->attr.wakeup_watermark : 0,
event->cpu, flags);
if (!rb) {
ret = -ENOMEM;
goto unlock;
}
// 这三个是perf子系统自带的函数,用于实现既定的功能
// 将分配的环形缓冲区与 event 结构体关联起来。主要实现是rcu_assign_pointer(event->rb, rb);
ring_buffer_attach(event, rb);
// 用于初始化环形缓冲区的头就是初始化perf_event_mmap_page。
perf_event_init_userpage(event);
// 更新环形缓冲区头的信息,其中包括对性能事件的统计信息的更新。
perf_event_update_userpage(event);
unlock:
if (!ret) {
// 增加 event->mmap_count 的计数。
atomic_inc(&event->mmap_count);
}
// 解锁
mutex_unlock(&event->mmap_mutex);
return ret;
}
1.4 采样频率上限
perf_event_max_sample_rate
参数控制系统中性能事件采样的最大频率。简单来说,它定义了系统在多大程度上监控和记录性能事件(如 CPU 使用率、上下文切换等)。降低这个频率可以减少系统开销,特别是在高负载时,帮助系统更有效地分配资源。
在采样时查看系统输出dmesg
会遇到提示:
Lowering default frequency rate from 4000 to 1. Please consider tweaking /proc/sys/kernel/perf_event_max_sample_rate.
如果希望加大开销,临时更改:
sudo sysctl -w kernel.perf_event_max_sample_rate=100000
永久性更改:将更改写入 /etc/sysctl.conf
文件中,添加以下行kernel.perf_event_max_sample_rate = 100000
, 应用更改sudo sysctl -p
检查设置是否生效:sysctl kernel.perf_event_max_sample_rate