1. 采样事件
可以把采样事件按照需求类型做定义
//这些的定义得看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;
}
}
2. 采样初始化
申请空间、填充文件句柄
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);
}
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 (__perf_event_init(mem_event[cpu][event], BUFFER_SIZE)) //这一步将每个事件和ring buffer对应
return -1;
}
}
return 0;
}
这里重写了一个名为__perf_event_open的perf_event_open,但是核心操作是差不多的。
static int __perf_event_open(__u64 config, __u64 config1, __u64 cpu,
__u64 type, __u32 pid)
{
……
event_fd = htmm__perf_event_open(&attr, __pid, cpu, -1, 0);
if (event_fd <= 0) {
return -1;
}
……
}
分析自己封装的htmm__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;
……
}
这个函数不用从用户态拿到文件句柄的配置,整个采样线程都是运行在内核态的,省去了复制这一步。同理初始化ring buffer也需要重写一个内存全是内核态的函数:
int __perf_event_init(struct perf_event *event, unsigned long nr_pages)
{
struct perf_buffer *rb = NULL;
int ret = 0, flags = 0; // flags = 0 → 普通页,4KB;flags = PERF_BUFFER_HUGETLB → 尝试 2MB 大页
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;
}
3. 采样简易流程(异步性、一致性)
ksamplingd 只是消费者,生产者是硬件 PMU + 内核中断上下文。ksamplingd 是后台消费者;PMU 中断是前台生产者。ring buffer 是异步队列,解耦了硬件采样速度和软件处理速度。
危险场景:
编译器优化:
head = up->data_head; // 只读一次,寄存器缓存
while (…) {// 循环内复用旧 head,看不到中断新写的数据 process();}
CPU 乱序:
head = up->data_head; // 指令1
tail = up->data_tail; // 指令2(可能先执行!)
// 结果:head < tail,负数溢出,崩溃缓存不一致:
CPU 0 的 ksamplingd 读 head = 0(缓存值)
CPU 1 的 PMI 写 head = 40(写回内存)
// ksamplingd 继续用 0,重复处理旧数据
采样值的读取需要使用mmap()直接在用户空间操作,少一次复制,因为采样有异步性(异步写入ring buffer吗),需要一个环形队列,这是一个共享内存,一个多生产者单消费者(MPSC)队列。
static bool handle_buffer_data(int cpu, int event)
{
struct perf_buffer *rb;
struct perf_event_mmap_page *up;
struct perf_event_header *ph;
struct artmem_event *ae;
unsigned long pg_index, offset;
int page_shift;
__u64 cur_head, cur_tail, cur_size;
bool batch_mode = false;
if (!mem_event[cpu][event]) {
return false;
}
/* 内存屏障:确保之前的读写完成,再读ring buffer的内容 */
smp_mb();
rb = mem_event[cpu][event]->rb;
if (!rb) {
printk_once("[artmem] event->rb is NULL for cpu%d/event%d\n", cpu, event);
return false;
}
up = READ_ONCE(rb->user_page);
cur_head = READ_ONCE(up->data_head);
cur_tail = READ_ONCE(up->data_tail);
/* 检查 head 合法性(防止内核 bug 导致越界)*/
if (cur_head > (rb->nr_pages << PAGE_SHIFT)) {
printk("[artmem] invalid head: %llu > %lu\n",
cur_head, (unsigned long)rb->nr_pages << PAGE_SHIFT);
WRITE_ONCE(up->data_tail, 0); /* 强制重置 */
return false;
}
cur_size = cur_head - cur_tail;
if (cur_size == 0 || cur_size > (rb->nr_pages << PAGE_SHIFT))
return false;
if (cur_size > (BUFFER_SIZE_BYTES * ksampled_max_sample_ratio / 100)) {
batch_mode = true;
} else if (cur_size < (BUFFER_SIZE_BYTES * ksampled_min_sample_ratio / 100)) {
batch_mode = false;
}
smp_rmb();
/* 处理数据页的数据 */
page_shift = PAGE_SHIFT + page_order(rb);
offset = READ_ONCE(up->data_tail);
pg_index = (offset >> page_shift) & (rb->nr_pages - 1);
offset &= (1 << page_shift) - 1;
/* 检查页号合法性 */
if (pg_index >= rb->nr_pages) {
printk("[artmem] invalid pg_index: %lu\n", pg_index);
WRITE_ONCE(up->data_tail, 0);
return false;
}
/* 找到在ring buffer第几页、页内偏移多少;ph 是 perf 环形缓冲区中一条采样记录的起点struct perf_event_header *,指向一个采样记录的头部; 采样数据(紧接头后,大小 = size - 8),具体内容由 attr.sample_type 决定 */
ph = (void*)(rb->data_pages[pg_index] + offset);
switch (ph->type) {
case PERF_RECORD_SAMPLE:
/* 把 ph 强制转为自定义结构 */
ae = (struct artmem_event *)ph;
// 在x86-64架构中,虚拟地址的有效位宽由四级页表和9位共同决定。为防止传入内核空间地址或空指针,必须检查虚拟地址是否落在用户空间的有效范围内
if (!valid_va(ae->addr)) {
break;
}
update_pginfo(ae->pid, ae->addr, event);
break;
case PERF_RECORD_THROTTLE:
case PERF_RECORD_UNTHROTTLE:
case PERF_RECORD_LOST_SAMPLES:
break;
default:
break;
}
/* 写屏障:确保数据处理完,再更新 tail */
smp_mb();
WRITE_ONCE(up->data_tail, up->data_tail + ph->size);
return batch_mode;
}
static int ksamplingd(void *data)
{
while (!kthread_should_stop()) {
int cpu, event;
for (cpu = 0; cpu < CPUS_PER_SOCKET; cpu++) {
for (event = 0; event < N_ARTMEMEVENTS; event++) {
while (handle_buffer_data(cpu, event)){
/* whether to continue batch processing of samples */
}
}
}
}
}
4. 通过系统调用启动和关闭后台线程
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;
}
5. 采样频率上限
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
自定义算法用于采样频率控制,请参考文章“perf采样频率动态调整”。