[授權發表]原始碼分析:動態分析 Linux 核心函式呼叫關係
By Falcon of TinyLab.org
2015/04/18
緣由
原始碼分析是程式設計師離不開的話題。
無論是研究開源專案,還是平時做各類移植、開發,都避免不了對原始碼的深入解讀。
工欲善其事,必先利其器。
前兩篇介紹了靜態分析和應用程式部分的動態分析。這裡開始討論如何動態分析 Linux 核心部分。
準備工作
Ftrace
類似於使用者態的 gprof
,在跟蹤核心函式之前,需要對核心做額外的一些配置,在核心相關函式插入一些程式碼,以便獲取必要資訊,比如呼叫時間,呼叫次數,父函式等。
早期的核心函式跟蹤支援有 KFT,它基於 -finstrument-functions
Ftrace
,為什麼呢?雖然是類似的思路,但是 Ftrace
有重大的創新:
- Ftrace 只需要在函式入口插入一個外部呼叫:mcount,而 KFT 在入口和出口都要加
- Ftrace 巧妙的攔截了函式返回的地址,從而可以在執行時先跳到一個事先準備好的統一出口,記錄各類資訊,然後再返回原來的地址
- Ftrace 在連結完成以後,把所有插入點地址都記錄到一張表中,然後預設把所有插入點都替換成為空指令(nop),因此預設情況下 Ftrace 的開銷幾乎是 0
- Ftrace 可以在執行時根據需要通過 Sysfs 介面使能和使用,即使在沒有第三方工具的情況下也可以方便使用
所以,本文只介紹 Ftrace
,關於其詳細用法,推薦看 Ftrace
作者 Steven 在 LWN 寫的序列文章,例如:
對於本文要介紹的內容,大家只要使能 Ftrace
核心配置就可以,我們不會直接使用它的底層介面:
CONFIG_FUNCTION_TRACER
CONFIG_DYNAMIC_FTRACE
CONFIG_FUNCTION_GRAPH_TRACER
除此之外,還需要把核心函式的符號表編譯進去:
CONFIG_KALLSYMS=y CONFIG_KALLSYMS_ALL=y
如果要直接使用 Ftrace
的話,可以安裝下述工具,不過本文不做進一步介紹:
$ sudo apt-get install trace-cmd kernelshark pytimerchart
Perf
Perf
最早是為取代 Oprofile
而生,從 2009 年開始只是增加了一個新的系統呼叫,如今強大到幾乎把 Oprofile
逼退歷史舞臺。因為它不僅支援硬體效能計數器,還支援各種軟體計數器,為 Linux 世界提供了一套完美的效能 Profiling 工具,當然,核心底層部分的函式 Profiling 離不開 Ftrace
支援。
關於 Perf
的詳細用法,可以參考:Perf Wiki。
Ok,同樣需要使能如下核心配置:
CONFIG_HAVE_PERF_EVENTS=y
CONFIG_PERF_EVENTS=y
客戶端安裝:
$ sudo apt-get install linux-tools-`uname -r`
FlameGraph
FlameGraph 是 Profiling 資料展示領域的一大創新,傳統的樹狀結構佔用的視覺面積很大,而且無法精準地找到熱點,而 FlameGraph
通過火焰狀的資料展示,採用層疊結構,佔用頁面空間小,可以快速清晰地展示出每條路徑的佔比,而且基於 SVG 可以自由縮放,基於 Javascript 可以動態地展示每個函式的具體樣本和佔比。
Ok,把 FlameGraph
準備好:
$ git clone https://github.com/brendangregg/FlameGraph.git
$ sudo cp FlameGraph/flamegraph.pl /usr/local/bin/
$ sudo cp FlameGraph/stackcollapse-perf.pl /usr/local/bin/
在使用 FlameGraph
前,我們簡單介紹一個例子以便更好地理解它的獨到之處。
a;b;c;d 90
e; 10
這個資料有三個資訊:
- 函式呼叫關係:a 依次呼叫 b, c, d
- 呼叫次數佔比:a 分支 90 次,e 分支 10 次
- 主要有兩個大的分支:a 和 e
要渲染這個資料,如果用之前的 dot
描述語言,相對比較複雜一些,特別是當函式節點特別多的時候,幾乎會沒法檢視,但是 FlameGraph
處理得很好,把上面的資訊儲存為 calls.log 並處理如下:
$ cd FlameGraph
$ cat calls.log | flamegraph.pl > calls-flame.svg
效果如下:
更多準備
日常程式開發時我們基本都只是關心使用者態的情況,在系統級的優化中,則會兼顧系統庫甚至核心部分,因為日常應用執行時的蠻多工作除了應用本身的各類操作外,還有蠻大一部分會訪問到各類系統庫,然後通過庫訪問到各類底層系統呼叫,進而訪問到 Linux 核心空間。
我們回到上篇文章的例子:fib.c
,可以通過 ltrace
和 strace
檢視庫函式和系統呼叫的情況:
$ ltrace -f -T -ttt -c ./fib 2>&1 > /dev/null
% time seconds usecs/call calls function
------ ----------- ----------- --------- --------------------
100.00 0.006063 141 43 printf
------ ----------- ----------- --------- --------------------
100.00 0.006063 43 total
$ strace -f -T -ttt -c ./fib 2>&1 > /dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
22.77 0.000051 6 8 mmap
15.18 0.000034 9 4 mprotect
11.61 0.000026 9 3 3 access
9.82 0.000022 22 1 munmap
9.38 0.000021 21 1 execve
8.93 0.000020 10 2 open
7.14 0.000016 5 3 fstat
4.46 0.000010 5 2 close
3.12 0.000007 7 1 read
3.12 0.000007 7 1 brk
2.68 0.000006 6 1 1 ioctl
1.79 0.000004 4 1 arch_prctl
0.00 0.000000 0 1 write
------ ----------- ----------- --------- --------- ----------------
100.00 0.000224 29 4 total
上面文章可以看到應用本身的 fibonnaci()
佔用了幾乎 100%
的時間開銷,但實際上在一個應用程式執行時,庫函式和核心都有開銷。上述 ltrace
反應了庫函式的呼叫情況,strace
則反應了系統呼叫的情況,核心開銷則是通過系統呼叫觸發的,當然,還有一部分是核心本身排程,正文切換,記憶體分配等開銷。大概的時間佔比可以通過 time
命令檢視:
$ time ./fib 2>&1 > /dev/null
real 0m5.887s
user 0m5.881s
sys 0m0.004s
接下來,咱們切入正題。通過基於 Ftrace
的 Perf
來綜合看看一個應用程式執行時使用者空間和核心空間兩部分的呼叫情況並通過 FlameGraph
繪製出來。
核心函式呼叫
在使用 Perf
之前,除了上述核心配置外,還需要使能一個符號獲取許可權,否則結果會是一大堆 16 進位制數字,看不到函式符號:
$ echo 0 > /proc/sys/kernel/kptr_restrict
咱們先分開來看看使用者空間,庫函式和系統呼叫的情況,以該命令為例:
find /proc/ -maxdepth 2 -name "vm" 2>&1 >/dev/null
使用者空間
$ valgrind --tool=callgrind find /proc/ -maxdepth 2 -name "vm" 2>&1 >/dev/null
$ gprof2dot -f callgrind ./callgrind.out.24273 | dot -Tsvg -o find-callgrind.svg
效果如下:
庫函式
$ ltrace -f -ttt -c find /proc/ -maxdepth 2 -name "vm" 2>&1 >/dev/null
% time seconds usecs/call calls function
------ ----------- ----------- --------- --------------------
30.75 2.939452 62 47175 strlen
16.71 1.597174 62 25560 free
15.38 1.469654 62 23589 memmove
9.18 0.877211 63 13773 malloc
8.55 0.817158 65 12542 readdir
7.65 0.731476 62 11796 fnmatch
7.56 0.722771 61 11793 __strndup
1.73 0.165002 83 1966 __fxstatat
0.41 0.039644 78 503 fchdir
0.23 0.022348 54 408 memcmp
0.23 0.022276 89 250 closedir
0.23 0.021551 86 250 opendir
0.22 0.021419 85 250 close
0.22 0.021144 84 251 open
0.21 0.019795 79 249 __fxstat
0.21 0.019790 19790 1 qsort
0.17 0.016417 65 250 dirfd
0.16 0.015680 98 159 strcmp
0.16 0.015218 60 252 __errno_location
0.00 0.000417 417 1 dcgettext
0.00 0.000404 404 1 setlocale
0.00 0.000266 133 2 isatty
0.00 0.000213 106 2 getenv
0.00 0.000158 158 1 __fprintf_chk
0.00 0.000158 79 2 fclose
0.00 0.000135 135 1 uname
0.00 0.000120 120 1 strtod
0.00 0.000113 56 2 __fpending
0.00 0.000110 110 1 bindtextdomain
0.00 0.000107 107 1 gettimeofday
0.00 0.000107 107 1 textdomain
0.00 0.000107 107 1 fileno
0.00 0.000106 106 1 strchr
0.00 0.000106 106 1 memcpy
0.00 0.000105 105 1 __cxa_atexit
0.00 0.000102 51 2 ferror
0.00 0.000092 92 1 fflush
0.00 0.000079 79 1 realloc
0.00 0.000076 76 1 strspn
0.00 0.000072 72 1 strtol
0.00 0.000052 52 1 calloc
0.00 0.000051 51 1 strrchr
------ ----------- ----------- --------- --------------------
100.00 9.558436 151045 total
系統呼叫
$ strace -f -ttt -c find /proc/ -maxdepth 2 -name "vm" 2>&1 >/dev/null
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
39.93 0.007072 4 1966 newfstatat
22.44 0.003974 8 500 getdents
10.53 0.001865 4 508 close
8.27 0.001464 3 503 fchdir
6.09 0.001079 4 261 4 open
5.72 0.001013 4 250 openat
4.68 0.000829 3 256 fstat
0.68 0.000120 9 13 mmap
0.36 0.000064 11 6 mprotect
0.33 0.000058 12 5 brk
0.27 0.000048 16 3 munmap
0.20 0.000036 9 4 4 access
0.19 0.000034 9 4 read
0.11 0.000020 7 3 2 ioctl
0.07 0.000012 12 1 write
0.05 0.000009 9 1 execve
0.03 0.000006 6 1 uname
0.03 0.000006 6 1 arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.017709 4286 10 total
接下來,通過 perf
來看看核心部分:
核心空間
$ perf record -g find /proc -maxdepth 2 -name "vm" 2>&1 >/dev/null
$ perf report -g --stdio
$ perf script | stackcollapse-perf.pl > find.perf-outfolded
$ flamegraph.pl find.perf-outfolded > find-flame.svg
上述幾條命令大體意思如下:
perf record -g
記錄後面跟著命令當次執行時的函式呼叫關係perf report -g --stdio
在控制檯打印出獲取到的函式關係資料(輸出結果有點類似於樹狀圖)perf script | stackcollapse-perf.pl > find.perf-outfolded
轉換為 FlameGraph 支援的格式flamegraph.pl find.perf-outfolded > find-flame.svg
生成火焰圖
效果如下:
小結
通過上述過程,咱們演示瞭如何分析一個應用程式執行時的核心空間部分函式呼叫情況,進而對前面兩篇文章進行了較好的補充。
整個序列到目前為止主要都是函式呼叫關係的分析。對於原始碼的分析也好,對於效能的優化也好,都是完全不夠的:
- 一方面,這個只能輔助理解到函式級別,無法理解到程式碼級別。要做進一步,得
gcov
和kgcov
的支援。 - 如果要做效能分析,除了函式呼叫關係跟蹤熱點區域外,其實還缺少一些資訊,比如整個呼叫時序,當前的處理器頻率,核心排程情況等,並不能在這個序列體現。
接下來,針對該原始碼分析系列,我們會再補充三篇文章:
- 函式呼叫關係(流程圖)繪圖方法介紹,將在現有的基礎上再介紹幾種新方法並分析優點和缺點。
- 程式碼級別的原始碼分析,通過
gcov
和kgcov
進行分析。
除此之外,我們會新開另外一個性能優化系列,來介紹各種效能優化的例項,包括應用程式與核心兩個方面。
倡議
最後,筆者想對那些開源工具的開發者和貢獻者們致敬!
Linux 領域聚攏了太多的天才,創意如泉湧般不斷滋潤 IT 世界,本文用到的三大工具的原創作者都是這類天才的代表,敬仰之情無以言表。
跟 Steven 有過一面之緣,而且在筆者 2009 年往官方社群提交 Ftrace For MIPS 時,他提供了諸多指導和幫助,感激之情化作無限專研的動力。
在這裡,誠邀更多的一線工程師們匯聚到泰曉科技,一起協作,分享學習的心得,交流研發的經驗,協同開發開源工具,一起致力於促進業界的交流與繁榮。目前已經有 15 個一線工程師參與進來,我們一同通過 worktile
協作,一起探討,一起創作。如果樂意加入,可通過聯絡我們獲得邀請。