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_openperf_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 是异步队列,解耦了硬件采样速度和软件处理速度。
危险场景:

  1. 编译器优化:
    head = up->data_head; // 只读一次,寄存器缓存
    while (…) {

    // 循环内复用旧 head,看不到中断新写的数据
    process();
    

    }

  2. CPU 乱序:
    head = up->data_head; // 指令1
    tail = up->data_tail; // 指令2(可能先执行!)
    // 结果:head < tail,负数溢出,崩溃

  3. 缓存不一致:
    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采样频率动态调整”。


文章作者: 易百分
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 易百分 !
  目录