性能计数器是在大多数现代cpu上可用的特殊硬件寄存器。这些寄存器统计特定类型的hw事件的数量:例如执行的指令、遭受的cachemisses或错误预测的分支,而不会减慢内核或应用程序的速度。这些寄存器还可以在事件数量达到阈值时触发中断——因此可以用于分析在该CPU上运行的代码。
错误预测的分支是指处理器的分支预测机制在条件分支指令中错误地预测了分支的结果。分支预测
是现代处理器中使用的一种技术,通过在实际解析之前猜测分支指令的结果来提高性能。
当遇到分支指令时,处理器会尝试预测分支是否会被执行(条件为真)或不会被执行(条件为假)。
基于这个预测,处理器会推测性地执行预测的分支路径中的指令,以保持流水线的正常运作并避免停顿。
然而,如果分支预测结果错误,处理器需要清空错误执行的指令并重新启动正确的分支路径,这会
导致性能下降,称为分支预测错误。
分支预测错误可能由多种原因引起,包括复杂的分支条件、有限的历史信息和程序行为的变化。
现代处理器采用复杂的分支预测算法来最小化错误预测并提高整体性能。
对于软件开发人员和编译器设计者来说,了解分支预测的行为并考虑优化代码以减少错误预测
非常重要,例如使用无分支的编程技巧或重新排列代码以提高分支预测的准确性。
Linux性能计数器子系统提供了这些硬件功能的抽象。它提供每个任务和每个CPU的计数器、计数器组,并在此基础上提供事件功能。它提供了“虚拟的”64位计数器,与底层硬件计数器的宽度无关。
Linux性能计数器子系统(Linux Performance Counter Subsystem)是Linux内核中的一个
组件,提供了对硬件性能计数器的访问和使用。
Linux性能计数器子系统为用户空间提供了一组接口,允许应用程序或工具通过编程方式访问和
配置硬件性能计数器。它包括以下主要组件:
1. perf_event:这是Linux内核的核心组件,负责与硬件性能计数器进行交互,以收集和管理
性能事件数据。它提供了一组系统调用和配置选项,允许用户空间应用程序注册事件、启动和停止计数
器,以及读取计数器的值。
2. perf tool:这是一个基于命令行的实用工具,利用perf_event接口来收集和分析性能事件
数据。它可以用于统计系统或应用程序的性能指标、检测瓶颈和分析性能问题。(kernel\tools\perf
目录下)
通过Linux性能计数器子系统,开发人员可以深入了解系统和应用程序的行为,并通过优化算法、
调整参数或改进代码来提高性能。它在系统调优、性能分析和性能测试等领域有广泛的应用。
必读:perf_event框架之:ARM PMU硬件
linux-4.19-kernel\tools\perf\perf-sys.h
static inline int sys_perf_event_open(struct perf_event_attr *attr,
pid_t pid, int cpu, int group_fd,
unsigned long flags)
{
int fd;
fd = syscall(__NR_perf_event_open, attr, pid, cpu,
group_fd, flags);
#ifdef HAVE_ATTR_TEST
if (unlikely(test_attr__enabled))
test_attr__open(attr, pid, cpu, fd, group_fd, flags);
#endif
return fd;
}
性能计数器可以通过特殊的文件描述符访问。每个虚拟计数器使用一个文件描述符。这个特殊的文件描述符是通过sys_perf_event_open()系统调用打开的:
int sys_perf_event_open(struct perf_event_attr *hw_event_uptr, pid_t pid, \
int cpu, int group_fd, unsigned long flags);
系统调用返回新的fd。fd可以通过普通的VFS系统调用来使用:read()可以用来读取计数器,fcntl()可以用来设置阻塞模式,等等。
可以同时打开多个计数器,这些计数器可以使用poll()函数调用。
当创建一个新的计数器fd时,perf_event_attr
是:
struct perf_event_attr { /*
/* config字的MSB表示剩余的位是否包含cpu特定的(原始)计数器配置数据,如果未设置,
* 则接下来的7位是事件类型,其余位是事件标识符。
*/
__u64 config;
__u64 irq_period;
__u32 record_type;
__u32 read_format;
__u64 disabled : 1, /* off by default */
inherit : 1, /* children inherit it */
pinned : 1, /* must always be on PMU */
exclusive : 1, /* only group on PMU */
exclude_user : 1, /* don't count user */
exclude_kernel : 1, /* ditto kernel */
exclude_hv : 1, /* ditto hypervisor */
exclude_idle : 1, /* don't count when idle */
mmap : 1, /* include mmap data */
munmap : 1, /* include munmap data */
comm : 1, /* include comm data */
__reserved_1 : 52;
__u32 extra_config_len;
__u32 wakeup_events; /* wakeup every n events */
__u64 __reserved_2;
__u64 __reserved_3;
};
config字段指定了计数器应该计数的内容。它分为3个位域:
Raw_type: 1位(最高位)0x8000_0000_0000_0000
type: 7位(次高位)0x7f00_0000_0000_0000
event_id: 56位(最低位)0x00ff_ffff_ffff_ffff
如果` raw_type `为1,则计数器将统计由event_config的剩余63位指定的硬件事件。编码是
某个机器特有的,不同的CPU有不同的编码。
如果` raw_type `为0,那么` type `字段表示这是什么类型的计数器,编码如下:
enum perf_type_id {
PERF_TYPE_HARDWARE = 0,
PERF_TYPE_SOFTWARE = 1,
PERF_TYPE_TRACEPOINT = 2,
};
PERF_TYPE_HARDWARE的计数器将对` event_id `指定的硬件事件进行计数:sys_perf_event_open
系统调用的Event_id参数如下:
enum perf_hw_id {
/* 常见的硬件事件,由内核概括:*/
PERF_COUNT_HW_CPU_CYCLES = 0,
PERF_COUNT_HW_INSTRUCTIONS = 1,
PERF_COUNT_HW_CACHE_REFERENCES = 2,
PERF_COUNT_HW_CACHE_MISSES = 3,
PERF_COUNT_HW_BRANCH_INSTRUCTIONS = 4,
PERF_COUNT_HW_BRANCH_MISSES = 5,
PERF_COUNT_HW_BUS_CYCLES = 6,
};
这些都是标准化的事件类型,在Linux下所有实现了性能计数器支持的cpu上的工作相对统一,尽管可能会有变化(例如,不同的cpu可能会对缓存层次结构的不同级别的缓存引用和缺失计数)。
如果CPU无法对所选事件进行计数,则系统调用将返回-EINVAL。
它还支持更多的hw_event_types,但它们是特定于某个cpu的,可以作为原始事件访问。例如,要在Intel Core cpu上统计“总线锁信号断言时外部总线周期”事件,可以传入0x4064 event_id值并设置hw_event。Raw_type为1。
PERF_TYPE_SOFTWARE类型的计数器将对可用的软件事件之一进行计数,通过` event_id `选择:
/*
* 内核提供的特殊“软件”计数器,即使是硬件不支持性能计数器。这些计数器测量内核的各种物理事件
* 和软件事件(也允许对这些事件进行剖析):
*/
enum perf_sw_ids {
PERF_COUNT_SW_CPU_CLOCK = 0,
PERF_COUNT_SW_TASK_CLOCK = 1,
PERF_COUNT_SW_PAGE_FAULTS = 2,
PERF_COUNT_SW_CONTEXT_SWITCHES = 3,
PERF_COUNT_SW_CPU_MIGRATIONS = 4,
PERF_COUNT_SW_PAGE_FAULTS_MIN = 5,
PERF_COUNT_SW_PAGE_FAULTS_MAJ = 6,
PERF_COUNT_SW_ALIGNMENT_FAULTS = 7,
PERF_COUNT_SW_EMULATION_FAULTS = 8,
};
当ftrace事件跟踪器可用时,PERF_TYPE_TRACEPOINT类型的计数器可用,event_id的值可以从/debug/tracing/events///id中获取
计数器有两种类型:计数计数器和采样计数器。“计数”计数器用于对发生的事件的数目进行计数,其特征是irq_period = 0。计数器的read()返回计数器的当前值和由read_format
指定的可能的附加值,每个值的大小为u64(8字节)。
/*
* 可以在hw_event中设置的比特位。Read_format要求读取计数器时,返回指定的数值,
* 按位值的递增顺序,在计数器值之后。
* /
enum perf_event_read_format {
PERF_FORMAT_TOTAL_TIME_ENABLED = 1,
PERF_FORMAT_TOTAL_TIME_RUNNING = 2,
};
使用这些额外的值,我们可以建立特定计数器的超额分配比率,从而使我们能够将轮询调度效果考虑在内。
一个“采样”计数器被设置为每N个事件产生一个中断,其中N由irq_period
给出。采样计数器的irq_period > 0。record_type控制在每个中断上记录的数据:
/*
* 可以在hw_event中设置的比特位
*/
enum perf_event_record_format {
PERF_RECORD_IP = 1U << 0,
Perf_record_tid = 1,
Perf_record_time = 1u << 2,
Perf_record_addr = 1u << 3,
Perf_record_group = 1u << 4,
Perf_record_callchain = 1u << 5
};
这样(和其他)的事件将被记录在一个环形缓冲区中,该缓冲区可以通过mmap()向用户空间提供。
disabled
位指定计数器一开始是禁用还是启用。如果它最初是禁用的,那么可以通过ioctl或prctl启用它。
如果设置了inherit
位,则指定此计数器应该对后代任务以及指定任务的事件进行计数。这只适用于新创建的后代节点,而不是计数器创建时已存在的后代节点(也不适用已存在后代节点的新后代节点)。
如果设置了pinning
位,则指定该计数器应尽可能始终在CPU上。它只适用于硬件计数器和计数器组领导。如果固定的计数器不能被放置到CPU上(例如,因为没有足够的硬件计数器或因为与其他事件冲突),那么计数器将进入“错误”状态,其中read返回文件结束符(即read()返回0),直到计数器随后被启用或禁用。
如果设置了exclusive
位,则指定当该计数器的组在CPU上时,它应该是唯一使用CPU计数器的组。在未来,这将允许复杂的监控程序通过extra_config_len
提供额外的配置信息,以利用CPU的性能监控单元(PMU)的高级特性,这些特性无法通过其他方式访问,并且可能会干扰其他硬件计数器。
exclude_user
、exclude_kernel
和exclude_hv
位提供了一种方法,可以要求将事件计数限制为CPU处于用户模式、内核模式 或hypervisor模式时的次数。
Hypervisor模式(又称为虚拟化模式或监控模式)是一种在计算机系统中实现虚拟化的技术。
它是一种软件或硬件层面的虚拟化技术,允许多个虚拟机(Virtual Machine,VM)共享同一台
物理主机(Host)的计算资源。
在Hypervisor模式下,虚拟机监控程序(也称为Hypervisor或VMM,Virtual Machine
Monitor)作为一个中间层,运行在物理主机的操作系统或硬件之上。它负责管理和调度虚拟机
的创建、运行和访问计算资源。
Hypervisor模式有两种类型:
Type 1 :Hypervisor(裸机Hypervisor,也称为本地Hypervisor或裸金属Hypervisor):该类型
的Hypervisor直接运行在物理主机的硬件上,独立于宿主操作系统。它提供了更高的性能和直接的硬
件访问能力,因为它绕过了宿主操作系统的层。常见的Type 1 Hypervisor包括VMware ESXi、
Microsoft Hyper-V和Xen。
Type 2 : Hypervisor(主机Hypervisor):该类型的Hypervisor运行在宿主操作系统上作为一个
应用程序,与宿主操作系统共享硬件资源。它通过宿主操作系统提供的接口来访问硬件,并对虚拟机
进行管理。常见的Type 2 Hypervisor包括Oracle VirtualBox和VMware Workstation。
Hypervisor模式使得在一台物理主机上同时运行多个虚拟机成为可能,每个虚拟机可以独立运行
不同的操作系统和应用程序。这为资源利用率的提高、系统隔离和应用程序测试等场景提供了灵活性和
便利性。
mmap
和munmap
位允许记录PROT_EXEC的mmap/munmap操作,这些可以用于将用户空间IP地址与实际代码关联,即使映射(甚至整个过程)消失后,这些事件也被记录在环缓冲区中。
`PROT_EXEC`是一个用于内存保护的常量,在Linux中与mmap(内存映射)系统调用以及内存页的
保护属性有关。
`PROT_EXEC`表示页是可执行的。当创建或修改内存映射时,可以使用这个标志来指示内核将相关
的内存页标记为可执行,使得该页中的指令可以被处理器执行。
当一个内存页被设置为`PROT_EXEC`时,可以将其视为一个包含可执行代码的区域,允许程序在该
内存页中运行代码。
这个标志通常在加载可执行文件到内存时使用,确保代码区域被标记为可执行以供程序执行。同时
它也用于共享库(动态链接库),以便允许多个进程共享同一库的可执行代码部分。
总结来说,`PROT_EXEC`常量用于指示内核将内存页标记为可执行,以便程序可以在这些页中运行代码。
comm
位允许在进程创建时跟踪进程的comm数据。这也记录在环缓冲区中。
在Linux系统中,每个进程都有一个名为`comm`的属性,它代表进程的命令名或进程名。
它是进程在进程表中的一个字段,用于标识进程所属的可执行文件或命令。
`comm`属性通常通过读取进程的`/proc/<pid>/comm`文件来获取,其中`<pid>`是进程
的ID。这个文件中只包含一个单行文本,表示进程的命令名。
例如,假设进程ID为1234,要获取它的`comm`数据,可以使用以下命令:
cat /proc/1234/comm
这将输出进程1234的命令名。
`comm`属性在系统监控、进程管理和调试等方面非常有用。它可以帮助用户了解系统中
运行的进程以及它们所关联的可执行文件或命令。
sys_perf_event_open()系统调用的pid
参数允许计数器特定于一个任务:
Pid == 0:如果Pid参数为0,则将计数器附加到当前进程。
Pid > 0:将计数器附加到特定进程(如果当前进程有足够的权限)
Pid < 0:对所有进程计数(每个CPU计数器)
cpu
参数允许特定于cpu的计数器:
cpu >= 0:该计数器限制到特定的cpu
cpu == -1:计数器对所有cpu计数
(注意:pid == -1
和cpu == -1
的组合是无效的。)
pid > 0
和cpu == -1
计数器是一个针对任务的计数器,它对该任务的事件进行计数,并跟随
该任务到调度到的任何cpu。任何用户都可以为自己的任务创建每个任务的计数器。
pid == -1
和cpu == x
计数器是一个针对cpu的计数器,对cpu -x上的所有事件计数。每个CPU计数器需要CAP_SYS_ADMIN权限。
group_fd
参数允许设置计数器“组”。一个计数器组有一个计数器是组的“领导”。首先创建leader,在sys_perf_event_open调用中使用group_fd = -1创建leader。接下来创建该组其他成员,其中group_fd给出了该组领导的fd。接下来创建其他组成员,其中group_fd给出了组长的fd。(用group_fd = -1创建一个计数器,它本身被认为是一个只有一个成员的组。)
计数器组是作为一个单位调度到CPU上的,也就是说,只有当组中的所有计数器都可以放置到CPU上时,它才会被放置到CPU上。这意味着成员计数器的值可以相互比较、相加、除以(以获得比率)等,因为它们对执行的同一组指令的事件进行了计数。
flags
参数当前未被使用,并且必须为0。
如前所述,异步事件(如计数器溢出或PROT_EXEC mmap跟踪)被记录到环形缓冲区中。这个环形缓冲区是通过mmap()创建和访问的。
mmap的大小应该是1+2^n页,其中第一页是元数据页(struct perf_event_mmap_page),包含了各种信息,例如环缓冲区头的位置。
/*
* 可以通过mmap映射的页的结构
*/
struct perf_event_mmap_page {
__u32 version; /* version number of this structure */
__u32 compat_version; /* 这是兼容的最低版本 */
/*
* 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 */
__u32 index; /* 硬件计数器标识符 */
__s64 offset; /* 添加到硬件计数器值 */
/*
* 控制mmap()数据缓冲区中的数据。
*
* 在支持SMP的平台上,读取这个值后,用户空间应该发出一个rmb()——
* 参见perf_event_wakeup()。
*/
__u32 data_head; /* 数据部分的头 */
};
注意:hw-counter用户空间位是特定于arch的,目前只在powerpc上实现。
以下2^n页是环形缓冲区,其中包含了这种形式的事件:
#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 {
/*
* MMAP事件记录了PROT_EXEC映射,这样我们就可以将用户空间的ip与代码关联起来。它们
* 的结构如下:
*
* 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
* };
* 当 header 的 type 为 PERF_RECORD_IP 时有效。
* 当 header 的 type 为 PERF_RECORD_TID 时有效。
* ....
*/
};
注意:PERF_RECORD_CALLCHAIN是特定于arch的,目前只在x86上实现。
可以通过poll()/select()/epoll()和fcntl()
管理信号来通知新事件。通常为每个页面生成通知,但是可以另外设置 perf_event_attr.wakeup_events
生成一个每个计数器溢出事件。
未来的工作将包括一个到环形缓冲区的splice()接口。
计数器可以启用和禁用在两个方面:通过ioctl和通过通过prctl。当计数器被禁用时,它不会计数或生成事件,但会继续存在并保持其count值。
可以启用单个计数器
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0);
可以禁用单个计数器
ioctl(fd, PERF_EVENT_IOC_DISABLE, 0);
对于计数器组,传递PERF_IOC_FLAG_GROUP作为第三个参数。启用或禁用一个组的leader使整个组启用或禁用;也就是说,当组长被禁用时,组中的任何计数器都不会计数。启用或禁用除leader以外的其他成员只会影响该计数器——禁用非leader会停止该计数器的计数,但不会影响其他计数器。
此外,还可以使用非继承的溢出计数器
ioctl(fd, PERF_EVENT_IOC_REFRESH, nr);
为nr
事件启用一个计数器,之后它会再次被禁用。进程可以使用prctl启用或禁用所有附加到它的计数器组:
prctl(PR_TASK_PERF_EVENTS_ENABLE);
prctl(PR_TASK_PERF_EVENTS_DISABLE);
这适用于当前进程上的所有计数器,无论是由这个进程还是由另一个进程创建的,并不影响该进程在其他进程上创建的任何计数器。它只启用或禁用组长,而不是组中的任何其他成员。
架构要求
如果您的架构没有硬件性能指标,您仍然可以使用基于hrtimers的通用软件计数器进行采样。
Hrtimers(High Resolution Timers)是Linux内核中提供的一种机制,用于实现对高精度时间
的处理和管理。它提供了比传统定时器更高的精度和灵活性。
Hrtimers主要用于以下几个方面:
1. 定时器精度:Hrtimers允许用户程序和内核能够以纳秒级(nanosecond)的精度设置和处理定时任
务,相较于传统定时器(jiffies),提供了更高的分辨率和精度。
2. 定时器类型:Hrtimers支持多种定时器类型,包括相对定时器(相对于当前时间的延时触发)、绝
对定时器(相对于特定时间点的延时触发)以及周期性定时器(以固定间隔触发)。
3. 定时器管理:Hrtimers提供了管理定时器的API和回调函数,可以创建、修改、取消和查询定时器,
以及处理定时器触发时的回调函数。
4. 用户空间接口:作为系统调用的一部分,Hrtimers还为用户空间程序提供了相关的API以便于用户
程序创建和管理定时器。
Hrtimers在实时系统、高性能计算和嵌入式系统中广泛应用,能够提供更精确的定时任务和事件
处理。通过使用Hrtimers,开发者可以更好地控制和利用系统中的时间资源。
因此,为了将HAVE_PERF_EVENTS添加到Kconfig中,我们至少需要这样做:
HAVE_PERF_EVENTS是一个宏定义,在Linux内核中用于判断系统是否支持性能事件子系统。该宏
定义用于编译时确定系统是否支持性能事件子系统。如果系统支持该子系统,宏定义值为1,表示开发者
可以使用perf_event_open()等相关函数来创建和管理性能事件。如果系统不支持该子系统,宏定义值
通常为0,表示不能使用性能事件相关的功能。
在软件开发中,存根(Stub)通常用于代替真正的实现或外部依赖,以便进行测试、集成或模块间
的开发。存根是一个轻量级的替代品,用于模拟或模仿某个组件的行为,以便进行开发和测试,而不必
依赖完整的实现或外部系统。
存根通常简化了组件之间的依赖关系,并且可以具有一些预定的行为或输出,以便用于测试特定的
场景。它们可以通过编程手段直接创建,也可以通过使用存根生成工具自动生成。
在上下文中,当提到一个基本的存根就足够时,意味着在这个特定的场景中只需一个简单的替代
实现来模拟所需的行为。这个存根通常只需要提供所需的最小功能,以满足测试或开发过程中的特定
需求,而无需实现完整的功能或依赖于外部系统。
如果您的架构确实具有硬件功能,则可以覆盖弱存根hw_perf_event_init()以注册硬件计数器。
具有d-cache混迭问题的体系结构,如Sparc和ARM,内核配置应该选择PERF_USE_VMALLOC,以避免使用perf mmap()时出现这些问题。
下面是一个使用 perf_event_open 函数与 Linux内核perf event子系统 进行交互的基本的 Linux 应用程序示例的 C 代码:
#include
#include
#include
#include
#include
#include
#include
// 定义 perf event 变量
struct perf_event_attr pe;
int fd;
int main() {
memset(&pe, 0, sizeof(struct perf_event_attr));
pe.type = PERF_TYPE_HARDWARE; // 选择硬件性能计数器类型
pe.size = sizeof(struct perf_event_attr);
pe.config = PERF_COUNT_HW_INSTRUCTIONS; // 统计指令数
pe.disabled = 1; // 初始化时禁用计数器
pe.exclude_kernel = 1; // 排除内核态的计数
fd = syscall(__NR_perf_event_open, &pe, 0, -1, -1, 0);
if (fd == -1) {
printf("Failed to open perf event!\n");
exit(1);
}
ioctl(fd, PERF_EVENT_IOC_RESET, 0); // 重置计数器
ioctl(fd, PERF_EVENT_IOC_ENABLE, 0); // 启用计数器
// 执行需要统计的程序或代码
ioctl(fd, PERF_EVENT_IOC_DISABLE, 0); // 禁用计数器
// 读取计数器的结果
uint64_t count;
read(fd, &count, sizeof(uint64_t));
printf("Instructions Executed: %" PRIu64 "\n", count);
close(fd); // 关闭 perf event 文件描述符
return 0;
}
--------------------本文主要内容来自:linux-4.19-kernel/tools/perf/design.txt--------------------
欢迎大家指导和交流!如果我有任何错误或遗漏,请立即指正,我愿意学习改进。期待与大家一起进步!