您的位置:首页 >聚焦 >

全球热门:一文看懂 Linux 性能分析|perf 源码实现

2022-10-15 06:07:58    来源:程序员客栈

我们在《一文看懂Linux性能分析|perf 原理》一文中介绍过,perf 是基于采样来对程序进行分析的。采样的步骤如下:

通过设置一个定时器,定时器的触发时间可以由用户设定。

定时器被触发后,将会调用采集函数收集当前运行环境的数据(如当前正在执行的进程和函数等)。


(资料图片仅供参考)

将采集到的数据写入到一个环形缓冲区(ring buffer)中。

应用层可以通过内存映射来读取环形缓冲区中的采样数据。

上述步骤如下图所示:

接下来,我们将会介绍 perf 在 Linux 内核中的实现。

事件

perf 是基于事件进行采样的,上面所说的定时器就是其中一种事件,被称为:CPU时钟事件。除了 CPU 时钟事件外,perf 还支持多种事件,如:

上下文切换事件:当调度器切换进程时触发。

缺页异常事件:当进程访问还没有映射到物理内存的虚拟内存地址时触发。

CPU迁移事件:当进程从一个 CPU 迁移到另一个 CPU 时触发。

...

由于 perf 支持的事件众多,所以本文只挑选CPU时钟事件进行分析。

1. perf_event 结构体

Linux 内核使用perf_event结构体来描述一个事件(如 CPU 时钟事件),其定义如下(由于 perf_event 结构体过于庞大,所以对其进行简化):

structperf_event{...structlist_headevent_entry;conststructpmu*pmu;enumperf_event_active_statestate;atomic64_tcount;//事件被触发的次数...structperf_event_attrattr;//事件的属性(由用户提供)structhw_perf_eventhw;structperf_event_context*ctx;//事件所属的上下文...};

我们现在只需关注其中的两个成员变量:count和ctx。

count:表示事件被触发的次数。

ctx:表示当前事件所属的上下文。

count成员变量容易理解,所以就不作详细介绍了。我们注意到 ctx 成员变量的类型为perf_event_context结构,那么这个结构代表什么?

2. perf_event_context 结构体

因为一个进程可以同时分析多种事件,所以就使用perf_event_context结构来记录属于进程的所有事件。我们来看看perf_event_context结构的定义,如下所示:

structperf_event_context{...structlist_headevent_list;//连接所有属于当前上下文的事件intnr_events;//属于当前上下文的所有事件的总数...structtask_struct*task;//当前上下文属于的进程...};

我们对perf_event_context结构进行了简化,下面介绍一下各个成员的作用:

event_list:连接所有属于当前上下文的事件。

nr_events:属于当前上下文的所有事件的总数。

task:当前上下文所属的进程。

perf_event_context结构通过event_list字段把所有属于本上下文的事件连接起来,如下图所示:

另外,在进程描述结构体task_struct中,有个指向perf_event_context结构的指针。如下所示:

structtask_struct{...structperf_event_context*perf_event_ctxp;...};

这样,内核就能通过进程描述结构体的perf_event_ctxp成员,来获取属于此进程的事件列表。

3. pmu 结构体

前面我们说过 perf 支持多种事件,而不同的事件应该有不同的启用和禁用动作。为了让不同的事件有不同的启用和禁用动作,所以内核定义了pmu结构。其定义如下:

structpmu{int(*enable)(structperf_event*event);void(*disable)(structperf_event*event);void(*read)(structperf_event*event);...};

下面介绍一下各个字段的作用:

enable:启用事件。disable:禁用事件。read:事件被触发时的回调。

perf_event结构的pmu成员是一个指向pmu结构的指针。如果当前事件是个 CPU 时钟事件时,pmu成员将会指向perf_ops_cpu_clock变量。

我们来看看perf_ops_cpu_clock变量的定义:

staticconststructpmuperf_ops_cpu_clock={.enable=cpu_clock_perf_event_enable,.disable=cpu_clock_perf_event_disable,.read=cpu_clock_perf_event_read,};

也就是说:

当要启用一个 CPU 时钟事件时,内核将会调用cpu_clock_perf_event_enable()函数来启用这个事件。当要禁用一个 CPU 时钟事件时,内核将会调用cpu_clock_perf_event_disable()函数来禁用这个事件。当事件被触发时,内核将会调用cpu_clock_perf_event_read()函数来进行特定的动作。启用事件

前面说过,当要启用一个 CPU 时钟事件时,内核会调用cpu_clock_perf_event_enable()函数来启用它。我们来看看cpu_clock_perf_event_enable()函数的实现,代码如下:

staticintcpu_clock_perf_event_enable(structperf_event*event){...perf_swevent_start_hrtimer(event);return0;}

从上面代码可以看出,cpu_clock_perf_event_enable()函数实际上调用了perf_swevent_start_hrtimer()函数来进行初始化工作。我们再来看看perf_swevent_start_hrtimer()函数的实现:

staticvoidperf_swevent_start_hrtimer(structperf_event*event){structhw_perf_event*hwc=&event->hw;// 1. 初始化一个定时器,定时器的回调函数为:perf_swevent_hrtimer()hrtimer_init(&hwc->hrtimer,CLOCK_MONOTONIC,HRTIMER_MODE_REL);hwc->hrtimer.function=perf_swevent_hrtimer;if(hwc->sample_period){...//2.启动定时器__hrtimer_start_range_ns(&hwc->hrtimer,ns_to_ktime(period),0,HRTIMER_MODE_REL,0);}}

从上面的代码可知,perf_swevent_start_hrtimer()函数主要完成两件事情:

初始化一个定时器,定时器的回调函数为:perf_swevent_hrtimer()。启动定时器。

这个定时器结构保存在perf_event结构的hwc成员中,我们在以后的文章中将会介绍 Linux 高精度定时器的实现。

当定时器被触发时,内核将会调用perf_swevent_hrtimer()函数来处理事件。我们再来分析一下perf_swevent_hrtimer()函数的实现:

staticenumhrtimer_restartperf_swevent_hrtimer(structhrtimer*hrtimer){enumhrtimer_restartret=HRTIMER_RESTART;structperf_sample_datadata;structpt_regs*regs;structperf_event*event;u64period;//获取当前定时器所属的事件对象event=container_of(hrtimer,structperf_event,hw.hrtimer);//前面说过,如果是CPU时钟事件,将会调用cpu_clock_perf_event_read()函数event->pmu->read(event);data.addr=0;//获取定时器被触发时所有寄存器的值regs=get_irq_regs();...if(regs){if(!(event->attr.exclude_idle&¤t->pid==0)){//最重要的地方:对数据进行采样if(perf_event_overflow(event,0,&data,regs))ret=HRTIMER_NORESTART;}}...returnret;}

perf_swevent_hrtimer()函数最重要的操作就是:调用perf_event_overflow()函数对数据进行采样与收集。perf_event_overflow()函数在后面将会介绍,我们暂时跳过。

那什么时候会启用事件呢?答案就是:进程被调度到 CPU 运行时。调用链如下:

schedule()└→ context_switch()   └→ finish_task_switch()      └→ perf_event_task_sched_in()         └→ __perf_event_sched_in()            └→ group_sched_in()               └→ event_sched_in()                  └→ event->pmu->enable()                     └→ cpu_clock_perf_event_enable()

内核通过调用schedule()函数来完成调度工作。从上面的调用链可知,当进程选中被调度到 CPU 运行时,最终会调用cpu_clock_perf_event_enable()函数来启用这个 CPU 时钟事件。

启用事件的过程如下图所示:

所以,当进程被选中并且被调度运行时,内核会启用属于此进程的 perf 事件。不难看出,当进程被调度出 CPU 时(停止运行),内核会禁用属于此进程的 perf 事件。

数据采样

最后,我们来看看 perf 是怎么进行数据采样的。

通过上面的分析,我们知道 perf 最终会调用perf_event_overflow()函数来进行数据采样。所以我们来看看perf_event_overflow()函数的实现,代码如下:

intperf_event_overflow(structperf_event*event,intnmi,structperf_sample_data*data,structpt_regs*regs){return__perf_event_overflow(event,nmi,1,data,regs);}

可以看出,perf_event_overflow()函数只是对__perf_event_overflow()函数的封装。我们接着来分析__perf_event_overflow()函数的实现:

staticint__perf_event_overflow(structperf_event*event,intnmi,intthrottle,structperf_sample_data*data,structpt_regs*regs){...perf_event_output(event,nmi,data,regs);returnret;}

从上面代码可知,__perf_event_overflow()会调用perf_event_output()函数来进行数据采样。perf_event_output()函数的实现如下:

staticvoidperf_event_output(structperf_event*event,intnmi,structperf_sample_data*data,structpt_regs*regs){structperf_output_handlehandle;structperf_event_headerheader;//进行数据采样,并且把采样到的数据保存到data变量中perf_prepare_sample(&header,data,event,regs);...//把采样到的数据保存到环形缓冲区中perf_output_sample(&handle,&header,data,event);...}

perf_event_output()函数会进行两个操作:

调用perf_prepare_sample()函数进行数据采样,并且把采样到的数据保存到 data 变量中。调用perf_output_sample()函数把采样到的数据保存到环形缓冲区中。

我们来看看 perf 是怎么把采样到的数据保存到环形缓冲区的:

voidperf_output_sample(structperf_output_handle*handle,structperf_event_header*header,structperf_sample_data*data,structperf_event*event){u64sample_type=data->type;...//1.保存当前IP寄存器地址(用于获取正在执行的函数)if(sample_type&PERF_SAMPLE_IP)perf_output_put(handle,data->ip);//2.保存当前进程IDif(sample_type&PERF_SAMPLE_TID)perf_output_put(handle,data->tid_entry);//3.保存当前时间if(sample_type&PERF_SAMPLE_TIME)perf_output_put(handle,data->time);...//n.保存函数的调用链if(sample_type&PERF_SAMPLE_CALLCHAIN){if(data->callchain){intsize=1;if(data->callchain)size+=data->callchain->nr;size*=sizeof(u64);perf_output_copy(handle,data->callchain,size);}else{u64nr=0;perf_output_put(handle,nr);}}...}

perf_output_sample()通过调用perf_output_put()函数把用户感兴趣的数据保存到环形缓冲区中。

用户感兴趣的数据是在创建事件时指定的,例如,如果我们对函数的调用链感兴趣,那么可以在创建事件时指定PERF_SAMPLE_CALLCHAIN标志位。

perf 事件可以通过pref_event_open()系统调用来创建,关于pref_event_open()系统调用的使用,读者可以自行参考相关的资料。

当 perf 把采样的数据保存到环形缓冲区后,用户就可以通过mmap()系统调用把环形缓冲区的数据映射到用户态的虚拟内存地址来进行读取。由于本文只关心数据采样部分,所以 perf 的其他实现细节可以参考 perf 的源代码。

数据采样的流程如下图所示:

总结

本文主要介绍了 perf 的 CPU 时钟事件的实现原理,另外 perf 除了需要内核支持外,还需要用户态应用程序支持,例如:把采样到的原始数据生成可视化的数据或者使用图形化表现出来。

当然,本文主要是介绍 perf 在内核中的实现,用户态的程序可以参考 Linux 源码tools/perf目录下的源代码。

当然,perf 是非常复杂的,本文也忽略了很多细节(如果把所有细节都阐明,那么篇幅将会非常长),所以读者如果有什么疑问也可以留言讨论。

关键词: 我们来看看 如下图所示 环形缓冲

相关阅读