我們?cè)凇兑晃目炊?a href="http://ttokpm.com/v/tag/538/" target="_blank">Linux性能分析|perf 原理》一文中介紹過(guò),perf 是基于采樣來(lái)對(duì)程序進(jìn)行分析的。采樣的步驟如下:
通過(guò)設(shè)置一個(gè)定時(shí)器,定時(shí)器的觸發(fā)時(shí)間可以由用戶設(shè)定。
定時(shí)器被觸發(fā)后,將會(huì)調(diào)用采集函數(shù)收集當(dāng)前運(yùn)行環(huán)境的數(shù)據(jù)(如當(dāng)前正在執(zhí)行的進(jìn)程和函數(shù)等)。
將采集到的數(shù)據(jù)寫入到一個(gè)環(huán)形緩沖區(qū)(ring buffer)中。
應(yīng)用層可以通過(guò)內(nèi)存映射來(lái)讀取環(huán)形緩沖區(qū)中的采樣數(shù)據(jù)。
上述步驟如下圖所示:
接下來(lái),我們將會(huì)介紹 perf 在 Linux 內(nèi)核中的實(shí)現(xiàn)。
事件
perf 是基于事件進(jìn)行采樣的,上面所說(shuō)的定時(shí)器就是其中一種事件,被稱為:CPU時(shí)鐘事件。除了 CPU 時(shí)鐘事件外,perf 還支持多種事件,如:
上下文切換事件:當(dāng)調(diào)度器切換進(jìn)程時(shí)觸發(fā)。
缺頁(yè)異常事件:當(dāng)進(jìn)程訪問(wèn)還沒(méi)有映射到物理內(nèi)存的虛擬內(nèi)存地址時(shí)觸發(fā)。
CPU遷移事件:當(dāng)進(jìn)程從一個(gè) CPU 遷移到另一個(gè) CPU 時(shí)觸發(fā)。
...
由于 perf 支持的事件眾多,所以本文只挑選CPU時(shí)鐘事件進(jìn)行分析。
1. perf_event 結(jié)構(gòu)體
Linux 內(nèi)核使用perf_event結(jié)構(gòu)體來(lái)描述一個(gè)事件(如 CPU 時(shí)鐘事件),其定義如下(由于 perf_event 結(jié)構(gòu)體過(guò)于龐大,所以對(duì)其進(jìn)行簡(jiǎn)化):
structperf_event{ ... structlist_headevent_entry; conststructpmu*pmu; enumperf_event_active_statestate; atomic64_tcount;//事件被觸發(fā)的次數(shù) ... structperf_event_attrattr;//事件的屬性(由用戶提供) structhw_perf_eventhw; structperf_event_context*ctx;//事件所屬的上下文 ... };
我們現(xiàn)在只需關(guān)注其中的兩個(gè)成員變量:count和ctx。
count:表示事件被觸發(fā)的次數(shù)。
ctx:表示當(dāng)前事件所屬的上下文。
count成員變量容易理解,所以就不作詳細(xì)介紹了。我們注意到 ctx 成員變量的類型為perf_event_context結(jié)構(gòu),那么這個(gè)結(jié)構(gòu)代表什么?
2. perf_event_context 結(jié)構(gòu)體
因?yàn)橐粋€(gè)進(jìn)程可以同時(shí)分析多種事件,所以就使用perf_event_context結(jié)構(gòu)來(lái)記錄屬于進(jìn)程的所有事件。我們來(lái)看看perf_event_context結(jié)構(gòu)的定義,如下所示:
structperf_event_context{ ... structlist_headevent_list;//連接所有屬于當(dāng)前上下文的事件 intnr_events;//屬于當(dāng)前上下文的所有事件的總數(shù) ... structtask_struct*task;//當(dāng)前上下文屬于的進(jìn)程 ... };
我們對(duì)perf_event_context結(jié)構(gòu)進(jìn)行了簡(jiǎn)化,下面介紹一下各個(gè)成員的作用:
event_list:連接所有屬于當(dāng)前上下文的事件。
nr_events:屬于當(dāng)前上下文的所有事件的總數(shù)。
task:當(dāng)前上下文所屬的進(jìn)程。
perf_event_context結(jié)構(gòu)通過(guò)event_list字段把所有屬于本上下文的事件連接起來(lái),如下圖所示:
另外,在進(jìn)程描述結(jié)構(gòu)體task_struct中,有個(gè)指向perf_event_context結(jié)構(gòu)的指針。如下所示:
structtask_struct{ ... structperf_event_context*perf_event_ctxp; ... };
這樣,內(nèi)核就能通過(guò)進(jìn)程描述結(jié)構(gòu)體的perf_event_ctxp成員,來(lái)獲取屬于此進(jìn)程的事件列表。
3. pmu 結(jié)構(gòu)體
前面我們說(shuō)過(guò) perf 支持多種事件,而不同的事件應(yīng)該有不同的啟用和禁用動(dòng)作。為了讓不同的事件有不同的啟用和禁用動(dòng)作,所以內(nèi)核定義了pmu結(jié)構(gòu)。其定義如下:
structpmu{ int(*enable)(structperf_event*event); void(*disable)(structperf_event*event); void(*read)(structperf_event*event); ... };
下面介紹一下各個(gè)字段的作用:
enable:?jiǎn)⒂檬录?/p>
disable:禁用事件。
read:事件被觸發(fā)時(shí)的回調(diào)。
perf_event結(jié)構(gòu)的pmu成員是一個(gè)指向pmu結(jié)構(gòu)的指針。如果當(dāng)前事件是個(gè) CPU 時(shí)鐘事件時(shí),pmu成員將會(huì)指向perf_ops_cpu_clock變量。
我們來(lái)看看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, };
也就是說(shuō):
當(dāng)要啟用一個(gè) CPU 時(shí)鐘事件時(shí),內(nèi)核將會(huì)調(diào)用cpu_clock_perf_event_enable()函數(shù)來(lái)啟用這個(gè)事件。
當(dāng)要禁用一個(gè) CPU 時(shí)鐘事件時(shí),內(nèi)核將會(huì)調(diào)用cpu_clock_perf_event_disable()函數(shù)來(lái)禁用這個(gè)事件。
當(dāng)事件被觸發(fā)時(shí),內(nèi)核將會(huì)調(diào)用cpu_clock_perf_event_read()函數(shù)來(lái)進(jìn)行特定的動(dòng)作。
啟用事件
前面說(shuō)過(guò),當(dāng)要啟用一個(gè) CPU 時(shí)鐘事件時(shí),內(nèi)核會(huì)調(diào)用cpu_clock_perf_event_enable()函數(shù)來(lái)啟用它。我們來(lái)看看cpu_clock_perf_event_enable()函數(shù)的實(shí)現(xiàn),代碼如下:
staticint cpu_clock_perf_event_enable(structperf_event*event) { ... perf_swevent_start_hrtimer(event); return0; }
從上面代碼可以看出,cpu_clock_perf_event_enable()函數(shù)實(shí)際上調(diào)用了perf_swevent_start_hrtimer()函數(shù)來(lái)進(jìn)行初始化工作。我們?cè)賮?lái)看看perf_swevent_start_hrtimer()函數(shù)的實(shí)現(xiàn):
staticvoid perf_swevent_start_hrtimer(structperf_event*event) { structhw_perf_event*hwc=&event->hw; // 1. 初始化一個(gè)定時(shí)器,定時(shí)器的回調(diào)函數(shù)為:perf_swevent_hrtimer() hrtimer_init(&hwc->hrtimer,CLOCK_MONOTONIC,HRTIMER_MODE_REL); hwc->hrtimer.function=perf_swevent_hrtimer; if(hwc->sample_period){ ... //2.啟動(dòng)定時(shí)器 __hrtimer_start_range_ns(&hwc->hrtimer,ns_to_ktime(period),0, HRTIMER_MODE_REL,0); } }
從上面的代碼可知,perf_swevent_start_hrtimer()函數(shù)主要完成兩件事情:
初始化一個(gè)定時(shí)器,定時(shí)器的回調(diào)函數(shù)為:perf_swevent_hrtimer()。
啟動(dòng)定時(shí)器。
這個(gè)定時(shí)器結(jié)構(gòu)保存在perf_event結(jié)構(gòu)的hwc成員中,我們?cè)谝院蟮奈恼轮袑?huì)介紹 Linux 高精度定時(shí)器的實(shí)現(xiàn)。
當(dāng)定時(shí)器被觸發(fā)時(shí),內(nèi)核將會(huì)調(diào)用perf_swevent_hrtimer()函數(shù)來(lái)處理事件。我們?cè)賮?lái)分析一下perf_swevent_hrtimer()函數(shù)的實(shí)現(xiàn):
staticenumhrtimer_restart perf_swevent_hrtimer(structhrtimer*hrtimer) { enumhrtimer_restartret=HRTIMER_RESTART; structperf_sample_datadata; structpt_regs*regs; structperf_event*event; u64period; //獲取當(dāng)前定時(shí)器所屬的事件對(duì)象 event=container_of(hrtimer,structperf_event,hw.hrtimer); //前面說(shuō)過(guò),如果是CPU時(shí)鐘事件,將會(huì)調(diào)用cpu_clock_perf_event_read()函數(shù) event->pmu->read(event); data.addr=0; //獲取定時(shí)器被觸發(fā)時(shí)所有寄存器的值 regs=get_irq_regs(); ... if(regs){ if(!(event->attr.exclude_idle&¤t->pid==0)){ //最重要的地方:對(duì)數(shù)據(jù)進(jìn)行采樣 if(perf_event_overflow(event,0,&data,regs)) ret=HRTIMER_NORESTART; } } ... returnret; }
perf_swevent_hrtimer()函數(shù)最重要的操作就是:調(diào)用perf_event_overflow()函數(shù)對(duì)數(shù)據(jù)進(jìn)行采樣與收集。perf_event_overflow()函數(shù)在后面將會(huì)介紹,我們暫時(shí)跳過(guò)。
那什么時(shí)候會(huì)啟用事件呢?答案就是:進(jìn)程被調(diào)度到 CPU 運(yùn)行時(shí)。調(diào)用鏈如下:
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()
內(nèi)核通過(guò)調(diào)用schedule()函數(shù)來(lái)完成調(diào)度工作。從上面的調(diào)用鏈可知,當(dāng)進(jìn)程選中被調(diào)度到 CPU 運(yùn)行時(shí),最終會(huì)調(diào)用cpu_clock_perf_event_enable()函數(shù)來(lái)啟用這個(gè) CPU 時(shí)鐘事件。
啟用事件的過(guò)程如下圖所示:
所以,當(dāng)進(jìn)程被選中并且被調(diào)度運(yùn)行時(shí),內(nèi)核會(huì)啟用屬于此進(jìn)程的 perf 事件。不難看出,當(dāng)進(jìn)程被調(diào)度出 CPU 時(shí)(停止運(yùn)行),內(nèi)核會(huì)禁用屬于此進(jìn)程的 perf 事件。
數(shù)據(jù)采樣
最后,我們來(lái)看看 perf 是怎么進(jìn)行數(shù)據(jù)采樣的。
通過(guò)上面的分析,我們知道 perf 最終會(huì)調(diào)用perf_event_overflow()函數(shù)來(lái)進(jìn)行數(shù)據(jù)采樣。所以我們來(lái)看看perf_event_overflow()函數(shù)的實(shí)現(xiàn),代碼如下:
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()函數(shù)只是對(duì)__perf_event_overflow()函數(shù)的封裝。我們接著來(lái)分析__perf_event_overflow()函數(shù)的實(shí)現(xiàn):
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()會(huì)調(diào)用perf_event_output()函數(shù)來(lái)進(jìn)行數(shù)據(jù)采樣。perf_event_output()函數(shù)的實(shí)現(xiàn)如下:
staticvoid perf_event_output(structperf_event*event,intnmi, structperf_sample_data*data, structpt_regs*regs) { structperf_output_handlehandle; structperf_event_headerheader; //進(jìn)行數(shù)據(jù)采樣,并且把采樣到的數(shù)據(jù)保存到data變量中 perf_prepare_sample(&header,data,event,regs); ... //把采樣到的數(shù)據(jù)保存到環(huán)形緩沖區(qū)中 perf_output_sample(&handle,&header,data,event); ... }
perf_event_output()函數(shù)會(huì)進(jìn)行兩個(gè)操作:
調(diào)用perf_prepare_sample()函數(shù)進(jìn)行數(shù)據(jù)采樣,并且把采樣到的數(shù)據(jù)保存到 data 變量中。
調(diào)用perf_output_sample()函數(shù)把采樣到的數(shù)據(jù)保存到環(huán)形緩沖區(qū)中。
我們來(lái)看看 perf 是怎么把采樣到的數(shù)據(jù)保存到環(huán)形緩沖區(qū)的:
void perf_output_sample(structperf_output_handle*handle, structperf_event_header*header, structperf_sample_data*data, structperf_event*event) { u64sample_type=data->type; ... //1.保存當(dāng)前IP寄存器地址(用于獲取正在執(zhí)行的函數(shù)) if(sample_type&PERF_SAMPLE_IP) perf_output_put(handle,data->ip); //2.保存當(dāng)前進(jìn)程ID if(sample_type&PERF_SAMPLE_TID) perf_output_put(handle,data->tid_entry); //3.保存當(dāng)前時(shí)間 if(sample_type&PERF_SAMPLE_TIME) perf_output_put(handle,data->time); ... //n.保存函數(shù)的調(diào)用鏈 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()通過(guò)調(diào)用perf_output_put()函數(shù)把用戶感興趣的數(shù)據(jù)保存到環(huán)形緩沖區(qū)中。
用戶感興趣的數(shù)據(jù)是在創(chuàng)建事件時(shí)指定的,例如,如果我們對(duì)函數(shù)的調(diào)用鏈感興趣,那么可以在創(chuàng)建事件時(shí)指定PERF_SAMPLE_CALLCHAIN標(biāo)志位。
perf 事件可以通過(guò)pref_event_open()系統(tǒng)調(diào)用來(lái)創(chuàng)建,關(guān)于pref_event_open()系統(tǒng)調(diào)用的使用,讀者可以自行參考相關(guān)的資料。
當(dāng) perf 把采樣的數(shù)據(jù)保存到環(huán)形緩沖區(qū)后,用戶就可以通過(guò)mmap()系統(tǒng)調(diào)用把環(huán)形緩沖區(qū)的數(shù)據(jù)映射到用戶態(tài)的虛擬內(nèi)存地址來(lái)進(jìn)行讀取。由于本文只關(guān)心數(shù)據(jù)采樣部分,所以 perf 的其他實(shí)現(xiàn)細(xì)節(jié)可以參考 perf 的源代碼。
數(shù)據(jù)采樣的流程如下圖所示:
總結(jié)
本文主要介紹了 perf 的 CPU 時(shí)鐘事件的實(shí)現(xiàn)原理,另外 perf 除了需要內(nèi)核支持外,還需要用戶態(tài)應(yīng)用程序支持,例如:把采樣到的原始數(shù)據(jù)生成可視化的數(shù)據(jù)或者使用圖形化表現(xiàn)出來(lái)。
當(dāng)然,本文主要是介紹 perf 在內(nèi)核中的實(shí)現(xiàn),用戶態(tài)的程序可以參考 Linux 源碼tools/perf目錄下的源代碼。
當(dāng)然,perf 是非常復(fù)雜的,本文也忽略了很多細(xì)節(jié)(如果把所有細(xì)節(jié)都闡明,那么篇幅將會(huì)非常長(zhǎng)),所以讀者如果有什么疑問(wèn)也可以留言討論。
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1361瀏覽量
40185 -
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
6819瀏覽量
88746 -
時(shí)鐘
+關(guān)注
關(guān)注
10文章
1714瀏覽量
131277 -
代碼
+關(guān)注
關(guān)注
30文章
4723瀏覽量
68237
原文標(biāo)題:一文看懂 Linux 性能分析|perf 源碼實(shí)現(xiàn)
文章出處:【微信號(hào):LinuxDev,微信公眾號(hào):Linux閱碼場(chǎng)】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論