Linux 內核(如下簡稱內核)提供了 kprobe 和 uprobe 的機制,容許用戶經過編寫本身的內核模塊,掛載特定的事件來執行本身的函數。好比咱們能夠在 accept 系統調用結束時記錄下新建立的 fd;或者在 VFS 讀寫操做先後記錄時間戳,統計它們的耗時。數據庫
直接裸寫內核模塊費時費力,其中有部分工做仍是能夠套模板的(好比從掛載 accept 系統調用改爲掛載 write 調用)。並且更大的問題是它不夠安全。要是內核態的程序崩潰了,那就是 kernel panic 的事了,準備去重啓服務器吧。因而乎像是 systemtap 這樣的項目就應運而生了。這一類項目提供了本身的 DSL,而後編譯成內核模塊。用戶不用直接跟 C 代碼和內核接口打交道,而是改用能力受限但安全得多的 DSL 編寫本身的小工具。打個比方,就像寫 SQL 查詢數據庫和直接編寫數據庫 C 插件的區別。後端
須要強調的是,systemtap DSL(如下簡稱 stp)並不是絕對地安全。首先,stp 容許咱們嵌入 C 代碼,而這部分天然是不安全的。其次,stp 容許咱們經過宏來調整編譯出來的內核模塊的行爲,而有些宏是有反作用的。舉個例子,stp 裏面的數組大小是預先分配好的,大小取決於 MAXMAPENTRIES這個宏(默認 2048)。有些時候,因爲要插入不少的數據,你須要調大這個宏。若是簡單粗暴地隨便在後面加若干個 0,可能會致使內核分配內存失敗,進而產生一系列問題(包括 kernel panic)。數組
對於 4.x 高版本的內核,咱們可使用 ebpf 機制而不是編寫或者間接編寫內核模塊。ebpf 是個在內核態運行的,解釋執行字節碼的虛擬機。它從底子上提供了更多的安全限制,要比內核模塊安全得多。並且編譯成 ebpf 字節碼要比編譯內核模塊快不少,這也是基於 ebpf 的內核 profile 的一大優點。安全
systemtap 除了能夠生成內核模塊,它還有生成 ebpf 字節碼和生成 ptrace 代碼的後端。2018 年,我曾嘗試過 systmtap 的 ebpf 後端,發現該後端因爲正在開發中,支持的 stp 語法特性較少,只有一部分 stp 文件能成功跑起來。相比較而言,BCC 支持的語言特性卻是足夠多,然而其對 debuginfo 的支持幾乎等於沒有,以至於用戶態 profile 只能基於 USDT (刻薄一點,就是幾乎不能用)。不知道它們倆如今發展得怎樣了。服務器
回到一開始的話題上來。一次完整的 stap test.stp ...
會經歷下面的階段:函數
其中最後一個階段由 stap 建立的 staprun 子進程執行。這一階段能夠單獨拆開來,即前四個階段在開發機器上生成編譯好的內核模塊,而後第五個階段在目標機器上執行。具體怎麼作參考 systemtap 的文檔。工具
若是你對其中的細節感興趣,或者須要搞明白 systemtap 那些奇奇怪怪的錯誤信息,能夠在運行時加上 -vvv
來顯示更多上下文。優化
另外在命令中加入 -p 階段序號
可讓 systemtap 在執行完特定階段後停下來。好比 stap -p3
會在生成完 C 代碼後停下來。插件
本系列會按這五個階段,一步步介紹整個流程。重點會放在第三階段生成 C 代碼的部分。咱們會講講那些 stp 語言的元素,好比全局數組、聚合函數,是怎樣編譯成 C 代碼的。debug
不過在此以前,先從第一階段開始吧。
這一階段沒什麼好說的,就是解析 stp 代碼,生成一棵 AST 樹。
若是解析過程當中出錯,會報告 parse error
:
parse error: expected literal string or number saw: operator ')' at test.stp:3:15 source: probe begin() { ^ 1 parse error. Pass 1: parse failed. [man error::pass1]
上面是 systemtap 奇奇怪怪的報錯信息的一個例子。正確的寫法是 probe begin {
,begin 後面不帶括號。
systemtap 雖然發現了語法上的問題,可是它的錯誤信息裏面沒有考慮到當前正在解析 probe 的上下文。它覺得是在解析某些帶參數的 probe,如 probe process("path").function("xxx")
,因此才聲稱 expected literal string or number
。
不過好在 stp 的語法比較簡單,即便看不懂錯誤提示也能改對。
上一階段結束後,stp 代碼已經被解析成一棵 AST 樹了。這一階段主要是對這棵樹作修剪,爲下一階段生成 C 代碼作準備。
這一階段由一系列分階段組成。每一個分階段會執行一個或多個 visitor,遍歷整棵樹,處理每一個節點。這些分階段包括 stp 語言宏的展開、查找 stp 函數定義、匹配 stp 變量類型、debuginfo 相關的一些操做、若干種優化器等等。做爲承上啓下的一個階段,該階段涉及了不少細節。因爲這些細節跟 systemtap 內部實現緊密相關,要想理解它們,最好須要閱讀其實現代碼。
這裏咱們就只提下優化器部分。在生成 C 代碼以前,systemtap 會執行一系列優化器,如常量摺疊、冗餘分支消除等,對 AST 樹進行剪枝。對於大型的 stp 腳本,花在這一階段的時間可能多達 10 秒,快遇上第四階段編譯內核模塊的耗時了。若是你對 systemtap 腳本的預備耗時比較敏感,能夠在運行時加上 -u
選項,跳過這些優化階段。
接下來,我會花上幾篇文章的篇幅,講講第三階段中 C 代碼的生成,重點是 stp 語言的一些特性。