1. 程式人生 > >[monitor] 10. Linux oprofile(硬體取樣效能分析)

[monitor] 10. Linux oprofile(硬體取樣效能分析)

1、oprofile概念

前面我們通過tick timer增加“/proc/stat”計數的方式來計算cpu佔用率,更精確的計算方法是採用performance counter。cpu硬體提供了一系列的performance counter用於分析程式效能。

performance counter的原理很簡單就是取樣。

比如把performance counter配置成“取樣cpu週期”型的,取樣count為1000。那麼在cpu執行1000個時鐘週期後發生counter中斷,在中斷中記錄當前的pc、task、is kernel、backtrace等資訊,在取樣多次以後,使用者根據取樣的資料來統計cpu的佔用率。

也可以把performance counter配置成“取樣cache miss”型的,取樣count為10.那麼cpu在發生10次cache 未命中事件後發生counter中斷,在中斷中記錄當前的pc、task、is kernel、backtrace等資訊,在取樣多次以後,使用者根據取樣的資料來分析程式碼中哪些地方容易發生cache miss,以此來優化程式碼。

Oprofile就是利用performance counter機制來實現程式效能分析的軟體,他的實現包括兩部分:核心模組和使用者態程式。核心負責使能硬體performance counter取樣資料記錄到buffer中,使用者態的守護程序定時重新整理核心buffer到使用者取樣檔案中,使用者態還提供取樣檔案分析工具用來把原始取樣資料轉化成使用者容易分析的資料。

1

2、oprofile核心態實現

oprofile在核心態以驅動的形式存在,程式碼位於drivers/oprofile路徑。

2.1、oprofile fs

2
3
4

可以看到profile驅動註冊檔案系統oprofilefs_type,在mount的時候會呼叫get_sb函式oprofilefs_get_sb()。

5
6
7

2.1.1、“/dev/oprofile/enable”

“/dev/oprofile/enable”啟動/停止核心的效能取樣,寫入0停止取樣,寫入非0啟動取樣。

8
9
10
11

2.1.1.1、fops->read

12

2.1.1.2、fops->write

13
14
15

2.1.2、“/dev/oprofile/dump”

“/dev/oprofile/dump”dump event buffer中的內容,喚醒阻塞在讀“/dev/oprofile/buffer”上的使用者程序。

16
17
18

2.1.2.1、fops->write

19
20
21

2.1.3、“/dev/oprofile/buffer”

“/dev/oprofile/buffer”用來提供介面給使用者態讀取event buffer。對檔案的第一次open操作會觸發分配event buffer、cpu buffer,並啟動buffer sync工作佇列;對檔案的read操作讀出event buffer中的內容。

22
23

2.1.3.1、fops->open

24
25
26
27

2.1.3.2、fops->read

28

2.1.3.3、fops->release

29
30

2.1.4、“/dev/oprofile/buffer_size”

“/dev/oprofile/buffer_size”用來修改fs_buffer_size變數,即event buffer的大小,預設值為131072。

31
32
33
34

2.1.5、“/dev/oprofile/buffer_watershed”

“/dev/oprofile/buffer_watershed”用來修改fs_buffer_watershed變數,即event buffer的水位大小,預設值為32768。

35

2.1.6、“/dev/oprofile/cpu_buffer_size”

“/dev/oprofile/cpu_buffer_size”用來修改fs_cpu_buffer_size變數,即cpu buffer的大小,預設值為8192。

36

2.1.7、“/dev/oprofile/cpu_type”

“/dev/oprofile/cpu_type”用來讀取cpu型別。

37
38

2.1.8、“/dev/oprofile/backtrace_depth”

“/dev/oprofile/backtrace_depth”用來設定堆疊回溯的深度。

39
40
41

2.1.9、“/dev/oprofile/pointer_size”

“/dev/oprofile/pointer_size”用來讀取系統指標的長度。

42
43

2.1.10、“/dev/oprofile/stats/”

“/dev/oprofile/stats/cpu%d/”是一個子資料夾,其中包含了3個檔案”sample_received”、”sample_lost_overflow”、”backtrace_aborted”。

“/dev/oprofile/stats/資料夾中還包含”sample_lost_no_mm” 、”sample_lost_no_mapping”、”event_lost_overflow”、”bt_lost_no_mapping”幾個計數檔案。

44

2.1.11、oprofile_ops.create_files

效能計數器會在“/dev/oprofile/”資料夾下面建立自己的檔案,具體的可以見各種取樣模式。

2.2、performance counter取樣(CONFIG_X86_LOCAL_APIC模式)

45
46
47
48
49
50
51

2.2.1、oprofile_ops.create_files

效能計數器會在“/dev/oprofile/”資料夾下面建立自己的檔案。每個performance counter都建立一個子資料夾“/dev/oprofile/%d/”,每個子資料夾下再建立計數器對應的屬性檔案。

  • event The event type e.g. DATA_MEM_REFS
  • unit mask The sub-events to count (more detailed specification)
  • counter The hardware counter(s) that can count this event
  • count The reset value (how many events before an interrupt)
  • kernel Whether the counter should increment when in kernel space
  • user Whether the counter should increment when in user space

52

2.2.2、oprofile_ops. setup

53
54
55

2.2.2.1、model->fill_in_addresses

56
57
58
59

2.2.2.2、model->setup_ctrs

60
61
62
63
64
65
66
67
68
69
70

2.2.2.3、model->check_ctrs

71
72
73
74
75
76
77
78
79

2.2.3、oprofile_add_sample

在上一節的oprofile_ops. Setup()函式中配置完performance counter以後,在發生counter的nmi中斷以後會呼叫oprofile_add_sample()函式向cpu buffer中增加一條記錄。記錄當時的pc指標、當前任務、當前是核心還是使用者態等資訊。

80
81
82
83
84
85

2.2.3.1、log_sample

86

2.2.4、oprofile_ops. backtrace

在oprofile_add_sample()中如果啟用了堆疊回溯會呼叫oprofile_ops. backtrace。

87
88
89
90

2.2.5、oprofile_ops. shutdown

91

2.2.6、oprofile_ops. start

92
93

2.2.6.1、model-> start

94

2.2.7、oprofile_ops. stop

95
96

2.2.7.1、model-> stop

97

2.3、nmi timer取樣(CONFIG_X86_IO_APIC模式)

在註冊performance counter失敗的情況下,註冊nmi timer來取樣,相當於只有一個 “cpu執行“型別的counter。

98
99
100

2.3.1、oprofile_ops. start

101
102

2.3.2、oprofile_ops. stop

103

2.4、tick timer取樣

在註冊performance counter和nmi timer都失敗的情況下,註冊tick timer來取樣,相當於只有一個 “cpu執行“型別的counter。

104-0
104

2.4.1、oprofile_ops. start

105
106
107
108

系統的tick timer中斷中最終會呼叫我們註冊的鉤子函式timer_notify()。

109

2.4.2、oprofile_ops. stop

110

2.5、buffer sync

performance counter在取樣條件滿足時發生中斷,取樣記錄當時的pc、task、is kernel、backtrace等資訊到cpu buffer中,系統會起一個獨立的核心任務來定時重新整理cpu buffer到event buffer,同時系統還會註冊事件的鉤子來非同步重新整理。

Event buffer通過“/dev/oprofile/buffer”把取樣記錄傳遞給使用者態。

111
112
113

2.5.1、wq_sync_buffer

114
115
116
117
118

這裡要描述兩個極其重的概念,app_cookiecookie

Cookie這個東西是可以在檔案系統中用來惟一標識一個檔案,也就是說每一個檔案都有一個獨一無二的cookie,通過cookie可以找到這個檔案。

我們cpu_buffer得到的只是task_struct,我們不能把task_struct傳給oprofiled,因為它解析不了;我們不能把task的名字傳給oprofiled,因為這樣太佔空間。所以我們最好傳cookie,oprofiled通過系統呼叫可以通過cookie得到這個檔案所有的東西。

那app_cookie和cookie是啥東西呢?

通過get_exec_dcookie和lookup_dcookie,我們可以知道兩者的區別。

通過get_exec_dcookie可以找到app_cookie,它是找EXEC的VMA,所以我們可以認為它是這個可執行檔案。

通過lookup_dcookie可以找到cookie,它是找所有的vma,看EIP落在哪個區間,然後找出該vma, 然後找出cookie。

我們知道一個程式,它的動態庫是通過mmap對映到某個庫檔案,但虛存vma還是在這個程式的地址空間裡。

所以app_cookie只是一個大概的,說明了這個可執行檔案。

而cookie卻是精確的,它找到具體是哪個對映,可能cookie等於app_cookie,那這個就直接是可執行檔案的對映,有可能它是哪個庫。

我們為什麼需要app_cookie呢?這是為了separate_kernel, separate_lib, separate_thread做考慮。

一般的,我們profile都是以某個cookie為單位,也就是以某個可執行檔案,某個庫,kernel為單位,取樣屬於它們的,則歸到一類,這樣最後opreport時,我們得到的結果是某個庫的符號的比例,某個可執行檔案的符號比例。

但如果使用separate_kernel,這就要求,對kernel和lib的取樣要歸到程式上來。也就是說執行程式a, kernel的某個符號在a執行中佔了多少比例,某個lib在a執行中佔用了多少比例。

這兩者是顯然有區別的,前者庫和kernel的符號都在整個kernel和庫中做對比,而後者庫和kernel的符號需要歸到程式中。

要達到這樣的效果,我們必須加入app_cookie。

  • 1、通過app_cookie,我們可以知道某個庫符號它當前屬於哪個程序
  • 2、通過app_cookie,  我們可以知道kernel當前是屬於哪個程序上下文,因為在kernel中cookie為NO_COOKIE。
static void add_kernel_ctx_switch(unsigned int in_kernel)
{

                add_event_entry(ESCAPE_CODE);

                if (in_kernel)

                                add_event_entry(KERNEL_ENTER_SWITCH_CODE);

                else

                                add_event_entry(KERNEL_EXIT_SWITCH_CODE);

}

對於kernel user的互換,我們沒有做比較多的處理

static void
add_user_ctx_switch(struct task_struct const * task, unsigned long cookie)

{
                add_event_entry(ESCAPE_CODE);

                add_event_entry(CTX_SWITCH_CODE);

                add_event_entry(task->pid);

                add_event_entry(cookie);

                /* Another code for daemon back-compat */

                add_event_entry(ESCAPE_CODE);

                add_event_entry(CTX_TGID_CODE);

                add_event_entry(task->tgid);

}

這個處理就比較大了,因為從cpu_buffer傳過來的是task_struct的變化,因此有可能是執行緒的變化,有可能是子程序的變化。所以我們需要加上TGID來區分這樣的情況。

對於執行緒,tgid是一樣的,都是父程序的,但pid各不相同。

對於子程序,tgid,pid都不一樣了。

對於context switch來說,這裡的cookie就是app_cookie.

static int
add_sample(struct mm_struct * mm, struct op_sample * s, int in_kernel)
{

                if (in_kernel) {

                                add_sample_entry(s->eip, s->event);

                                return 1;

                } else if (mm) {

                                return add_us_sample(mm, s);

                } else {

                                atomic_inc(&oprofile_stats.sample_lost_no_mm);

                }

                return 0;

}

static int add_us_sample(struct mm_struct * mm, struct op_sample * s)
{

                unsigned long cookie;

                off_t offset;



                cookie = lookup_dcookie(mm, s->eip, &offset);



                if (cookie == INVALID_COOKIE) {

                                atomic_inc(&oprofile_stats.sample_lost_no_mapping);

                                return 0;

                }



                if (cookie != last_cookie) {

                                add_cookie_switch(cookie);

                                last_cookie = cookie;

                }



                add_sample_entry(offset, s->event);



                return 1;

}

如果cpu_buffer傳過來的是“資料”資訊,

如果是在kernel裡面,則我們直接寫到event_buffer裡面。

如果是user的,我們使用add_us_sample,這個函式又做了比較多的事,它先是去查詢真正的cookie, 找到後看cookie是否變化了,如果變化了,還要通知oprofiled,cookie變化了。

可以想像,在核心和oprofiled裡面,都一直維護著kernel, cookie, app_cookie這三個量,所有的“資料”取樣,都是在這三個範圍內。因為一個數據取樣,它是沒有意義的,它必須附在這三個資訊裡面,才能找到相應的sample。

所以,一旦這三個量變化,kernel和oprofiled都會同時做出修改,以表示後面的取樣都是在新的環境中。

舉幾個例子

  • 1、系統呼叫從user—kernel

假設啥都不變,則kernel=1, cookie變成了NO_COOKEI和app_cookie還是原來的task,這就很自然了,因為在kernel中cookie是沒有意義的,但app_cookie還是有意義的,因為系統呼叫還是在這個task的上下文中。

  • 2、從使用者程序排程到核心執行緒

首先這會有一個context switch的切換,app_cookie會變成NO_COOKIE。
然後cookie也會變成NO_COOKIE。

  • 3、從使用者程式到動態庫

首先app_cookie不會變,但cooike會變成動態庫的

  • 4、程序到程序的排程

Context switch當然會有,app_cookie會變,然後cookie也會變。

  • 5、程序中的執行緒的排程

Context switch會有,因為task_struct變化了,但app_cookie是一樣的,不過針對這點,我們會把pid, tgpid記錄下來,以便上層區分。

理解上面很重要,因為oprofiled中把這些取樣歸檔,寫檔案,正是需要用到上面的資訊。

2.5.2、非同步事件同步

除了起核心任務來同步意外,系統還註冊了一些鉤子函式在一些非同步事件時進行同步。

119
120
121
122
123
124

3、oprofile使用者態實現

oprofile的使用者態程式碼可以下載oprofile-0.9.8.tar.gz軟體包。使用者態的程式碼就是提供了一個守護程序來接收核心態的取樣資料,並提供了一些列工具來把取樣資料轉換成使用者可用的資料。

4、Oprofile的使用

4.1、oprofile使用步驟

首先要把核心態的oprofile程式碼編譯成模組,在使用者態安裝oprofile軟體包。現在就可以利用oprofile提供的工作來開始進行效能分析了。

  • Step 1、載入oprofile驅動:

“opcontrol –init”載入oprofile驅動並掛載oprofile檔案系統。

125

可以看到我們的cpu支援0-3一共4個performance counter。

  • Step 2、配置oprofile的核心檔案:

如果不分析核心配置“opcontrol –no-vmlinux”,如果分析核心使用“opcontrol –vmlinux=/boot/vmlinux-uname -r”制定oprofile使用的核心檔案。

126

  • Step 3、配置取樣的counter:

首先使用“opcontrol -l”查詢本cpu支援哪些型別的performance counter。

127

可以看到我們的cpu支援“CPU_CLK_UNHALTED”、“ INST_RETIRED”、“ LLC_MISSES”等一系列performance counter。
我們配置例項取樣使用“CPU_CLK_UNHALTED”和“ LLC_MISSES”兩個counter,使用“opcontrol –event=”命令來配置。

128

  • Step 4、得到取樣資料:

我們分析一個例項簡單程式,首先編譯一個帶除錯資訊的應用程式:

129

啟動程式執行:

130

“opcontrol –start”啟動oprofile取樣,取樣一段時間後,“opcontrol –stop”oprofile取樣。

131

可以看到“/var/lib/oprofile/samples/current/”路徑下已經有采樣生成的資料。“opcontrol –reset”清除掉上一次的取樣資料。如果使用“opcontrol –event=”命令重新配置過counter,則需使用“opcontrol –shutdown”先kill掉取樣程序,再使用“opcontrol –start”重新啟動取樣。

  • Step 5、使用opreport來分析取樣資料:

“opreport –l ./hello”來分析hello程式內部的效能:

132

也可以不指定程式看整個系統的效能情況:

133

  • Step6、使用opannotate來分析取樣資料:

使用opannotate來檢視取樣資料和原始碼的對應關係:

134

  • Step7、使用opgprof來分析取樣資料:

opgprof可以把oprofile生成的資料轉換成gprof使用的資料,可以使用gprof工具來分析。

135

4.2、gprof工具

在 編譯或連結源程式的時候在編譯器的命令列引數中加入“-pg”選項,編譯時編譯器會自動在目的碼中插入用於效能測試的程式碼片斷,這些程式碼在程式在執行時 採集並記錄函式的呼叫關係和呼叫次數,以及採集並記錄函式自身執行時間和子函式的呼叫時間,程式執行結束後,會在程式退出的路徑下生成一個 gmon.out檔案。這個檔案就是記錄並儲存下來的監控資料。

可以通過命令列方式的gprof或圖形化的Kprof來解讀這些資料並對程式的效能進行分 析。另外,如果想檢視庫函式的profiling,需要在編譯是再加入“-lc_p”編譯引數代替“-lc”編譯引數,這樣程式會連結libc_p.a 庫,才可以產生庫函式的profiling資訊。如果想執行一行一行的profiling,還需要加入“-g”編譯引數。

136
137