0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

perf 在内核中的实现原理

Linux阅码场 来源:Linux阅码场 作者:Linux阅码场 2022-10-17 09:24 次阅读

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

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

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

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

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

上述步骤如下图所示:

bd863970-4db2-11ed-a3b6-dac502259ad0.png

接下来,我们将会介绍 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字段把所有属于本上下文的事件连接起来,如下图所示:

bdbe49fa-4db2-11ed-a3b6-dac502259ad0.png

另外,在进程描述结构体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()函数的实现,代码如下:

staticint
cpu_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()函数的实现:

staticvoid
perf_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_restart
perf_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 时钟事件。

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

bdd390b2-4db2-11ed-a3b6-dac502259ad0.png

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

数据采样

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

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

int
perf_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()函数的实现如下:

staticvoid
perf_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 是怎么把采样到的数据保存到环形缓冲区的:

void
perf_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.保存当前进程ID
if(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 的源代码。

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

be04b110-4db2-11ed-a3b6-dac502259ad0.png

总结

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

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

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

审核编辑:彭静
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 内核
    +关注

    关注

    3

    文章

    1316

    浏览量

    39945
  • 数据
    +关注

    关注

    8

    文章

    6563

    浏览量

    87947
  • 时钟
    +关注

    关注

    10

    文章

    1498

    浏览量

    130556
  • 代码
    +关注

    关注

    30

    文章

    4596

    浏览量

    67330

原文标题:一文看懂 Linux 性能分析|perf 源码实现

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    一款随Linux内核代码维护的性能诊断工具

    Perf Event 是一款随 Linux 内核代码一同发布和维护的性能诊断工具,由内核社区维护和发展。Perf 不仅可以用于应用程序的性能统计分析,也可以应用于
    的头像 发表于 04-06 09:23 7466次阅读
    一款随Linux<b class='flag-5'>内核</b>代码维护的性能诊断工具

    全球最高性能RISC-V处理器的Perf性能分析工具发布

    探测的性能监控。通过Perf分析工具,用户可以使用可编程的硬件性能监控计数器监测预定义的硬件事件、预定义的硬件缓存事件和硬件原始事件的性能数据。Perf能针对硬件事件的每个任务、每个内核和每个工作负载的计数器进行采样。   赛昉
    的头像 发表于 04-24 14:53 1675次阅读
    全球最高性能RISC-V处理器的<b class='flag-5'>Perf</b>性能分析工具发布

    如何在内核中去读取文件的大小呢

    如何在内核中去读取文件的大小呢?有什么方法吗?
    发表于 11-02 06:57

    全志Tina中使用perf分析CPU使用率

    perf简介Perf是是内置于Linux内核源码树的性能剖析(profiling)工具。不仅可以用于应用程序的性能统计分析,还可以用于内核
    发表于 05-20 14:25

    I.MX8MM开发板Linux如何在内核添加驱动呢

    迅为I.MX8MM开发板编译驱动到内核,在平时的驱动开发,经常需要在内核配置某种功能,为了方便大家开发和学习,本小节讲解如何在内核
    发表于 08-29 17:46

    X-CUBE-PERF-H7扩展包数据手册

    X-CUBE-PERF-H7扩展包旨在展示STM32H74x和STM32H75x的性能,其Arm® Cortex®-M7单核能够以高达480 MHz的速度运行。内核的指令和数据缓存释放了它的性能,并且其性能与来自不同存储器的0等待状态的执行有关。
    发表于 11-29 07:50

    你知道perf学习-linux自带性能分析工具怎么用?

    Linux性能调优工具,32内核以上自带的工具,软件性能分析。在2.6.31及后续版本的linux内核里,安装perf非常的容易。
    发表于 05-16 14:54 2486次阅读

    米尔科技改内核调整GPIO在内核启动阶段方案

    米尔用户在使用i.MX6UL/i.MX6ULL系列产品开发时,需要调整GPIO在内核启动阶段的状态,这怎么操作呢?
    的头像 发表于 11-26 16:31 2662次阅读
    米尔科技改<b class='flag-5'>内核</b>调整GPIO<b class='flag-5'>在内核</b>启动阶段方案

    赛昉科技发布Perf性能分析工具

    日前,为配合高性能RISC-V处理器昉·天枢Dubhe应用,赛昉科技发布了“赛昉科技Perf性能分析工具”。  
    的头像 发表于 04-24 15:48 2115次阅读

    Coolbpf 在perf 事件中的增强

    Perf 是内置于 Linux 内核源码树中的性能剖析(profiling)工具。它基于事件采样的原理,以性能事件为基础,支持针对处理器相关性能指标与操作系统相关性能指标的性能剖析。
    的头像 发表于 10-25 09:00 882次阅读

    解构内核 perf 框架的实现讲解

    perf 框架,前端承接用户态的各种事件(event)的属性配置,后端将 event 嫁接到内核的调度、文件系统等框架中,底层对接各种 PMU 硬件,所以其必然要建立一个复杂、严谨的模型(抽象)系统。
    发表于 01-16 09:49 1015次阅读

    万字长文解读Linux内核追踪机制

    Linux 存在众多 tracing tools,比如 ftrace、perf,他们可用于内核的调试、提高内核的可观测性。
    的头像 发表于 06-11 11:05 556次阅读
    万字长文解读Linux<b class='flag-5'>内核</b>追踪机制

    Linux perf性能、实际应用与案例

    Linux perf(性能分析工具)是一个功能强大且灵活的性能剩余工具,它可以在Linux系统上检测和调试各种性能问题。Linux内核集成了perf工具,可用于探测内核性能事件、硬件性
    发表于 07-03 10:22 477次阅读

    如何使用perf性能分析工具

    在功能上,perf很强大,可以对众多的软硬件事件采样,还能采集出跟踪点(trace points)的信息(比如系统调用、TCP/IP事件和文件系统操作。perf的代码和Linux内核代码
    的头像 发表于 11-08 15:36 610次阅读
    如何使用<b class='flag-5'>perf</b>性能分析工具

    Linux perf 简要介绍

    的性能剩余工具,它可以在Linux系统上检测和调试各种性能问题。Linux内核集成了perf工具,可用于探测内核性能事件、硬件性能计数器以及用户级应用程序性能事件。 perf工具可以用
    的头像 发表于 11-09 17:06 489次阅读