前言
之前項目中遇到過一個bug,bug產生的原因是某個程序在兩個不同的啓動腳本中被同時啓動了兩次,系統中出現了兩個實例。這個程序在代碼內部沒有保證單實例,靠shell
腳本的pidof(1)
命令保證單實例,然而因爲兩個啓動腳本的啓動時間太過接近,pidof(1)
沒能起作用,導致程序被啓動了兩次。
出了這個bug後就想着需要梳理一下系統在啓動時都運行了哪些腳本,這個系統“傳承”了估計有七八年,啓動腳本粗略估計有幾十個了,這幾十個腳本里面必然有可以合併或者精簡的。一開始人肉分析,發現難度有點大,有些腳本是rc
腳本,有些腳本在rc
腳本中被啓動,有些腳本被網卡配置腳本啓動,有些腳本被Xserver
啓動腳本啓動,甚至有些腳本放在了窗口管理器啓動腳本中啓動,還有些腳本雖然在磁盤上躺着,但已經被另外的啓動腳本取代了,但根本沒有被啓動。
所以有沒有現成的工具能夠記錄系統啓動時都啓動了哪些進程呢,我一開始想到了用auditd
,之前我在另外一篇博客:linux下監控shell腳本或可執行程序啓動過的子進程中提到過auditd
,它在我的Ubuntu
上工作得很正常(不過會拖慢系統速度),但在項目的系統裏面無法正常工作,很容易把系統搞死,在經過幾個小時的嘗試以後,我放棄了這種方法。
經過一番思考後,我決定使用最笨最直接的方法:printk
大法。既然有內核源碼,何不直接修改內核,在內核創建進程的時候打印出來呢?
日誌生成流程和結果可視化代碼的Github倉庫:Tasktree
環境
我使用的“發行版”是之前編的一個lfs
,內核版本是5.2.8
修改內核
首先需要指出,我在文章標題寫的是“進程樹”,這個“進程”指的是Linux
內核裏面的“輕量級進程”(LWP
),而不是操作系統課本里的那個“進程”的概念。在Linux
內核中並不會區分進程或者線程,只有LWP
,使用task_struct
結構體保存。
總共有3個需要修改的源文件:kernel/fork.c、fs/exec.c和kernel/exit.c,只要能夠獲取一個每一個進程fork
/exec
/exit
的時間,我們就能夠還原出整顆進程樹。
插入的printk
主要需要記錄進程號和進程名的信息,爲了方便以後將內核線程隱藏,我還在fork.c的printk
中記錄了p->flags & PF_KTHREAD
的值。
kernel/fork.c
在 copy_process
中,在return
之前插入一個printk
...
trace_task_newtask(p, clone_flags);
uprobe_copy_process(p, clone_flags);
printk(KERN_ERR "FORK|%d|%s|=>|%d|%u", current->pid, current->comm, p->pid, p->flags & PF_KTHREAD); // Inserted here!
return p;
...
fs/exec.c
在__set_task_comm
中,在函數一開始插入一個printk
void __set_task_comm(struct task_struct *tsk, const char *buf, bool exec)
{
printk(KERN_ERR "EXEC|%d|%s|=|%s", tsk->pid, tsk->comm, buf); // Inserted here!
task_lock(tsk);
trace_task_rename(tsk, buf);
strlcpy(tsk->comm, buf, sizeof(tsk->comm));
task_unlock(tsk);
perf_event_comm(tsk, exec);
}
kernel/exit.c
在do_exit
中,在函數一開始插入一個printk
void __noreturn do_exit(long code)
{
printk(KERN_ERR "EXIT|%d|%s", current->pid, current->comm); // Inserted here!
struct task_struct *tsk = current;
int group_dead;
profile_task_exit(tsk);
...
日誌輸出
將修改後的內核編譯安裝完畢後重新啓動系統,我們就能在/var/log/kern.log或者dmesg(1)
命令的輸出中看到我們插入的日誌了:
[ 4.070211] FORK|170|S05modules|=>|172|0
[ 4.082378] EXEC|172|S05modules|=|egrep
[ 4.092978] EXEC|172|egrep|=|grep
[ 4.098429] EXIT|172|grep
[ 4.099345] EXIT|170|S05modules
[ 4.099777] FORK|130|rc|=>|173|0
[ 4.100872] EXEC|173|rc|=|S08localnet
[ 4.102791] FORK|173|S08localnet|=>|174|0
[ 4.103201] EXEC|174|S08localnet|=|stty
[ 4.104313] EXIT|174|stty
[ 4.108745] FORK|173|S08localnet|=>|175|0
[ 4.123296] EXEC|175|S08localnet|=|cat
[ 4.125846] EXIT|175|cat
[ 4.126549] FORK|173|S08localnet|=>|176|0
進程樹
使用Tasktree將日誌還原成進程樹,會得到如下輸出:
[0] idle(living)
\_ [1] swapper/0 -> [1] init(living)
| \_ [129] init
| | \_ [130] init -> [130] rc
| | | \_ [133] rc -> [133] stty
| | | \_ [134] rc -> [134] dmesg
| | | \_ [135] rc
| | | | \_ [136] rc -> [136] ls
| | | \_ [137] rc -> [137] S00mountvirtfs
| | | | \_ [138] S00mountvirtfs -> [138] stty
...
" -> "
表示一個exec
調用。
" \_ "
表示一個fork
調用。
這個輸出樣式是仿照ps(1)
做的,但是ps(1)
輸出不了已經退出的進程,也記錄不了exec
調用。
時間線
由於日誌都是有時間戳的,除了能生成進程樹外,還能畫出進程fork
/exec
/exit
的時間線:
結語
以上就是使用內核日誌來還原出進程樹的流程了。修改內核聽起來雖有些麻煩,但是比使用各種文檔殘缺可用性難以保障的工具的心智負擔要低得多。如果你有更好的方式或工具,請一定要指教一下。