1. 程式人生 > >[授權發表]原始碼分析:動態分析 Linux 核心函式呼叫關係

[授權發表]原始碼分析:動態分析 Linux 核心函式呼叫關係

By Falcon of TinyLab.org
2015/04/18

緣由

原始碼分析是程式設計師離不開的話題。

無論是研究開源專案,還是平時做各類移植、開發,都避免不了對原始碼的深入解讀。

工欲善其事,必先利其器。

前兩篇介紹了靜態分析和應用程式部分的動態分析。這裡開始討論如何動態分析 Linux 核心部分。

準備工作

Ftrace

類似於使用者態的 gprof,在跟蹤核心函式之前,需要對核心做額外的一些配置,在核心相關函式插入一些程式碼,以便獲取必要資訊,比如呼叫時間,呼叫次數,父函式等。

早期的核心函式跟蹤支援有 KFT,它基於 -finstrument-functions

,在每個函式的出口、入口插入特定呼叫以便截獲上面提到的各類資訊。早期筆者就曾經維護過 KFT,並且成功移植到了 Loongson/MIPS 平臺,相關郵件記錄見:kernel function tracing support for linux-mips。不過 Linux 官方社群最終採用的卻是 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

效果如下:

Simple Flame Graph For Calls

更多準備

日常程式開發時我們基本都只是關心使用者態的情況,在系統級的優化中,則會兼顧系統庫甚至核心部分,因為日常應用執行時的蠻多工作除了應用本身的各類操作外,還有蠻大一部分會訪問到各類系統庫,然後通過庫訪問到各類底層系統呼叫,進而訪問到 Linux 核心空間。

我們回到上篇文章的例子:fib.c,可以通過 ltracestrace 檢視庫函式和系統呼叫的情況:

$ 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

接下來,咱們切入正題。通過基於 FtracePerf 來綜合看看一個應用程式執行時使用者空間和核心空間兩部分的呼叫情況並通過 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

效果如下:

The Callgraph of the find command

庫函式

$ 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 生成火焰圖

效果如下:
The FlameGraph of the Profiling result of the find command

小結

通過上述過程,咱們演示瞭如何分析一個應用程式執行時的核心空間部分函式呼叫情況,進而對前面兩篇文章進行了較好的補充。

整個序列到目前為止主要都是函式呼叫關係的分析。對於原始碼的分析也好,對於效能的優化也好,都是完全不夠的:

  • 一方面,這個只能輔助理解到函式級別,無法理解到程式碼級別。要做進一步,得 gcovkgcov 的支援。
  • 如果要做效能分析,除了函式呼叫關係跟蹤熱點區域外,其實還缺少一些資訊,比如整個呼叫時序,當前的處理器頻率,核心排程情況等,並不能在這個序列體現。

接下來,針對該原始碼分析系列,我們會再補充三篇文章:

  • 函式呼叫關係(流程圖)繪圖方法介紹,將在現有的基礎上再介紹幾種新方法並分析優點和缺點。
  • 程式碼級別的原始碼分析,通過 gcovkgcov 進行分析。

除此之外,我們會新開另外一個性能優化系列,來介紹各種效能優化的例項,包括應用程式與核心兩個方面。

倡議

最後,筆者想對那些開源工具的開發者和貢獻者們致敬!

Linux 領域聚攏了太多的天才,創意如泉湧般不斷滋潤 IT 世界,本文用到的三大工具的原創作者都是這類天才的代表,敬仰之情無以言表。

跟 Steven 有過一面之緣,而且在筆者 2009 年往官方社群提交 Ftrace For MIPS 時,他提供了諸多指導和幫助,感激之情化作無限專研的動力。

在這裡,誠邀更多的一線工程師們匯聚到泰曉科技,一起協作,分享學習的心得,交流研發的經驗,協同開發開源工具,一起致力於促進業界的交流與繁榮。目前已經有 15 個一線工程師參與進來,我們一同通過 worktile 協作,一起探討,一起創作。如果樂意加入,可通過聯絡我們獲得邀請。