在軟體部署中使用 strace 進行偵錯

2019-12-12 13:05:00

我的大部分工作都涉及到部署軟體系統,這意味著我需要花費很多時間來解決以下問題:

  • 這個軟體可以在原開發者的機器上工作,但是為什麼不能在我這裡執行?
  • 這個軟體昨天可以在我的機器上工作,但是為什麼今天就不行?

這是一種偵錯的型別,但是與一般的軟體偵錯有所不同。一般的偵錯通常只關心程式碼的邏輯,但是在軟體部署中的偵錯關注的是程式的程式碼和它所在的執行環境之間的相互影響。即便問題的根源是程式碼的邏輯錯誤,但軟體顯然可以在別的機器上執行的事實意味著這類問題與執行環境密切相關。

所以,在軟體部署過程中,我沒有使用傳統的偵錯工具(例如 gdb),而是選擇了其它工具進行偵錯。我最喜歡的用來解決“為什麼這個軟體無法在這台機器上執行?”這類問題的工具就是 strace

什麼是 strace?

strace 是一個用來“追蹤系統呼叫”的工具。它主要是一個 Linux 工具,但是你也可以在其它系統上使用類似的工具(例如 DTracektrace)。

它的基本用法非常簡單。只需要在 strace 後面跟上你需要執行的命令,它就會顯示出該命令觸發的所有系統呼叫(你可能需要先安裝好 strace):

$ strace echo Hello...Snip lots of stuff...write(1, "Hello\n", 6)                  = 6close(1)                                = 0close(2)                                = 0exit_group(0)                           = ?+++ exited with 0 +++

這些系統呼叫都是什麼?它們就像是作業系統核心提供的 API。很久以前,軟體擁有直接存取硬體的許可權。如果軟體需要在螢幕上顯示一些東西,它將會與視訊硬體的埠和記憶體對映暫存器糾纏不清。當多工作業系統變得流行以後,這就導致了混亂的局面,因為不同的應用程式將“爭奪”硬體,並且一個應用程式的錯誤可能致使其它應用程式崩潰,甚至導致整個系統崩潰。所以 CPU 開始支援多種不同的特權模式(或者稱為“保護環”)。它們讓作業系統核心在具有完全硬體存取許可權的最高特權模式下執行,於此同時,其它在低特權模式下執行的應用程式必須通過向核心發起系統呼叫才能夠與硬體進行互動。

在二進位制級別上,發起系統呼叫相比簡單的函數呼叫有一些區別,但是大部分程式都使用標準庫提供的封裝函數。例如,POSIX C 標準庫包含一個 write() 函數,該函數包含用於進行 write 系統呼叫的所有與硬體體系結構相關的程式碼。

簡單來說,一個應用程式與其環境(計算機系統)的互動都是通過系統呼叫來完成的。所以當軟體在一台機器上可以工作但是在另一台機器無法運作的時候,追蹤系統呼叫是一個很好的查錯方法。具體地說,你可以通過追蹤系統呼叫分析以下典型操作:

  • 控制台輸入與輸出 (IO)
  • 網路 IO
  • 檔案系統存取以及檔案 IO
  • 進程/執行緒生命週期管理
  • 原始記憶體管理
  • 存取特定的裝置驅動

什麼時候可以使用 strace?

理論上,strace 適用於任何使用者空間程式,因為所有的使用者空間程式都需要進行系統呼叫。strace 對於已編譯的低階程式最有效果,但如果你可以避免執行時環境和直譯器帶來的大量額外輸出,則仍然可以與 Python 等高階語言程式一起使用。

當軟體在一台機器上正常工作,但在另一台機器上卻不能正常工作,同時丟擲了有關檔案、許可權或者不能執行某某命令等模糊的錯誤資訊時,strace 往往能大顯身手。不幸的是,它不能診斷高等級的問題,例如數位憑證驗證錯誤等。這些問題通常需要組合使用 strace(有時候是 ltrace)和其它高階工具(例如使用 openssl 命令列工具偵錯數位憑證錯誤)。

本文中的範例基於獨立的伺服器,但是對系統呼叫的追蹤通常也可以在更複雜的部署平台上完成,僅需要找到合適的工具。

一個簡單的例子

假設你正在嘗試執行一個叫做 foo 的伺服器應用程式,但是發生了以下情況:

$ fooError opening configuration file: No such file or directory

顯然,它沒有找到你已經寫好的組態檔。之所以會發生這種情況,是因為包管理工具有時候在編譯應用程式時指定了自定義的路徑,所以你應當遵循特定發行版提供的安裝指南。如果錯誤資訊告訴你正確的組態檔應該在什麼地方,你就可以在幾秒鐘內解決這個問題,但如果沒有告訴你呢?你該如何找到正確的路徑?

如果你有權存取原始碼,則可以通過閱讀原始碼來解決問題。這是一個好的備用計劃,但不是最快的解決方案。你還可以使用類似 gdb 的單步偵錯程式來觀察程式的行為,但使用專門用於展示程式與系統環境互動作用的工具 strace 更加有效。

一開始, strace 產生的大量輸出可能會讓你不知所措,幸好你可以忽略其中大部分的無用資訊。我經常使用 -o 引數把輸出的追蹤結果儲存到單獨的檔案裡:

$ strace -o /tmp/trace fooError opening configuration file: No such file or directory$ cat /tmp/traceexecve("foo", ["foo"], 0x7ffce98dc010 /* 16 vars */) = 0brk(NULL)                               = 0x56363b3fb000access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3fstat(3, {st_mode=S_IFREG|0644, st_size=25186, ...}) = 0mmap(NULL, 25186, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f2f12cf1000close(3)                                = 0openat(AT_FDCWD, "/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0\260A\2\0\0\0\0\0"..., 832) = 832fstat(3, {st_mode=S_IFREG|0755, st_size=1824496, ...}) = 0mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f2f12cef000mmap(NULL, 1837056, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x7f2f12b2e000mprotect(0x7f2f12b50000, 1658880, PROT_NONE) = 0mmap(0x7f2f12b50000, 1343488, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x22000) = 0x7f2f12b50000mmap(0x7f2f12c98000, 311296, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x16a000) = 0x7f2f12c98000mmap(0x7f2f12ce5000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1b6000) = 0x7f2f12ce5000mmap(0x7f2f12ceb000, 14336, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f2f12ceb000close(3)                                = 0arch_prctl(ARCH_SET_FS, 0x7f2f12cf0500) = 0mprotect(0x7f2f12ce5000, 16384, PROT_READ) = 0mprotect(0x56363b08b000, 4096, PROT_READ) = 0mprotect(0x7f2f12d1f000, 4096, PROT_READ) = 0munmap(0x7f2f12cf1000, 25186)           = 0openat(AT_FDCWD, "/etc/foo/config.json", O_RDONLY) = -1 ENOENT (No such file or directory)dup(2)                                  = 3fcntl(3, F_GETFL)                       = 0x2 (flags O_RDWR)brk(NULL)                               = 0x56363b3fb000brk(0x56363b41c000)                     = 0x56363b41c000fstat(3, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x8), ...}) = 0write(3, "Error opening configuration file"..., 60) = 60close(3)                                = 0exit_group(1)                           = ?+++ exited with 1 +++

strace 輸出的第一頁通常是低階的進程啟動過程。(你可以看到很多 mmapmprotectbrk 呼叫,這是用來分配原始記憶體和對映動態連結庫的。)實際上,在查詢錯誤時,最好從下往上閱讀 strace 的輸出。你可以看到 write 呼叫在最後返回了錯誤資訊。如果你向上找,你將會看到第一個失敗的系統呼叫是 openat,它在嘗試開啟 /etc/foo/config.json 時丟擲了 ENOENT (“No such file or directory”)的錯誤。現在我們已經知道了組態檔應該放在哪裡。

這是一個簡單的例子,但我敢說在 90% 的情況下,使用 strace 進行偵錯不需要更多複雜的工作。以下是完整的偵錯步驟:

  1. 從程式中獲得含糊不清的錯誤資訊
  2. 使用 strace 執行程式
  3. 在輸出中找到錯誤資訊
  4. 往前追溯並找到第一個失敗的系統呼叫

第四步中的系統呼叫很可能向你顯示出問題所在。

小技巧

在開始更加複雜的偵錯之前,這裡有一些有用的偵錯技巧幫助你高效使用 strace

man 是你的朋友

在很多 *nix 作業系統中,你可以通過 man syscalls 檢視系統呼叫的列表。你將會看到類似於 brk(2) 之類的東西,這意味著你可以通過執行 man 2 brk 得到與此相關的更多資訊。

一個小問題:man 2 fork 會顯示出在 GNU libc 裡封裝的 fork() 手冊頁,而 fork() 現在實際上是由 clone 系統呼叫實現的。fork 的語意與 clone 相同,但是如果我寫了一個含有 fork() 的程式並使用 strace 去偵錯它,我將找不到任何關於 fork 呼叫的資訊,只能看到 clone 呼叫。如果將原始碼與 strace 的輸出進行比較的時候,像這種問題會讓人感到困惑。

使用 -o 將輸出儲存到檔案

strace 可以生成很多輸出,所以將輸出儲存到單獨的檔案是很有幫助的(就像上面的例子一樣)。它還能夠在控制台中避免程式自身的輸出與 strace 的輸出發生混淆。

使用 -s 檢視更多的引數

你可能已經注意到,錯誤資訊的第二部分沒有出現在上面的例子中。這是因為 strace 預設僅顯示字串引數的前 32 個位元組。如果你需要捕獲更多引數,請向 strace 追加類似於 -s 128 之類的引數。

-y 使得追蹤檔案或通訊端更加容易

“一切皆檔案”意味著 *nix 系統通過檔案描述符進行所有 IO 操作,不管是真實的檔案還是通過網路或者進程間管道。這對於程式設計而言是很方便的,但是在追蹤系統呼叫時,你將很難分辨出 readwrite 的真實行為。

-y 引數使 strace 在註釋中註明每個檔案描述符的具體指向。

使用 -p 附加到正在執行的進程中

正如我們將在後面的例子中看到的,有時候你想追蹤一個正在執行的程式。如果你知道這個程式的進程號為 1337 (可以通過 ps 查詢),則可以這樣操作:

$ strace -p 1337...system call trace output...

你可能需要 root 許可權才能執行。

使用 -f 追蹤子進程

strace 預設只追蹤一個進程。如果這個進程產生了一個子進程,你將會看到建立子進程的系統呼叫(一般是 clone),但是你看不到子進程內觸發的任何呼叫。

如果你認為在子進程中存在錯誤,則需要使用 -f 引數啟用子進程追蹤功能。這樣做的缺點是輸出的內容會讓人更加困惑。當追蹤一個進程時,strace 顯示的是單個呼叫事件流。當追蹤多個進程的時候,你將會看到以 <unfinished ...> 開始的初始呼叫,接著是一系列針對其它執行緒的呼叫,最後才出現以 <... foocall resumed> 結束的初始呼叫。此外,你可以使用 -ff 引數將所有的呼叫分離到不同的檔案中(檢視 strace 手冊 獲取更多資訊)。

使用 -e 進行過濾

正如你所看到的,預設的追蹤輸出是所有的系統呼叫。你可以使用 -e 引數過濾你需要追蹤的呼叫(檢視 strace 手冊)。這樣做的好處是執行過濾後的 strace 比起使用 grep 進行二次過濾要更快。老實說,我大部分時間都不會被打擾。

並非所有的錯誤都是不好的

一個簡單而常用的例子是一個程式在多個位置搜尋檔案,例如 shell 搜尋哪個 bin/ 目錄包含可執行檔案:

$ strace sh -c uname...stat("/home/user/bin/uname", 0x7ffceb817820) = -1 ENOENT (No such file or directory)stat("/usr/local/bin/uname", 0x7ffceb817820) = -1 ENOENT (No such file or directory)stat("/usr/bin/uname", {st_mode=S_IFREG|0755, st_size=39584, ...}) = 0...

“錯誤資訊之前的最後一次失敗呼叫”這種啟發式方法非常適合於查詢錯誤。無論如何,自下而上地查詢是有道理的。

C 程式設計指南非常有助於理解系統呼叫

標準 C 庫函數呼叫不屬於系統呼叫,但它們僅是系統呼叫之上的唯一一個薄層。所以如果你了解(甚至只是略知一二)如何使用 C 語言,那麼閱讀系統呼叫追蹤資訊就非常容易。例如,如果你在偵錯網路系統呼叫,你可以嘗試略讀 Beej 經典的《網路程式設計指南》

一個更複雜的偵錯例子

就像我說的那樣,簡單的偵錯例子表現了我在大部分情況下如何使用 strace。然而,有時候需要一些更加細緻的工作,所以這裡有一個稍微複雜(且真實)的例子。

bcron 是一個任務排程器,它是經典 *nix cron 守護程式的另一種實現。它已經被安裝到一台伺服器上,但是當有人嘗試編輯作業時間表時,發生了以下情況:

# crontab -e -u logsbcrontab: Fatal: Could not create temporary file

好的,現在 bcron 嘗試寫入一些檔案,但是它失敗了,也沒有告訴我們原因。以下是 strace 的輸出:

# strace -o /tmp/trace crontab -e -u logsbcrontab: Fatal: Could not create temporary file# cat /tmp/trace...openat(AT_FDCWD, "bcrontab.14779.1573691864.847933", O_RDONLY) = 3mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f82049b4000read(3, "#Ansible: logsagg\n20 14 * * * lo"..., 8192) = 150read(3, "", 8192)                       = 0munmap(0x7f82049b4000, 8192)            = 0close(3)                                = 0socket(AF_UNIX, SOCK_STREAM, 0)         = 3connect(3, {sa_family=AF_UNIX, sun_path="/var/run/bcron-spool"}, 110) = 0mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f82049b4000write(3, "156:Slogs\0#Ansible: logsagg\n20 1"..., 161) = 161read(3, "32:ZCould not create temporary f"..., 8192) = 36munmap(0x7f82049b4000, 8192)            = 0close(3)                                = 0write(2, "bcrontab: Fatal: Could not creat"..., 49) = 49unlink("bcrontab.14779.1573691864.847933") = 0exit_group(111)                         = ?+++ exited with 111 +++

在程式結束之前有一個 write 的錯誤資訊,但是這次有些不同。首先,在此之前沒有任何相關的失敗系統呼叫。其次,我們看到這個錯誤資訊是由 read 從別的地方讀取而來的。這看起來像是真正的錯誤發生在別的地方,而 bcrontab 只是在轉播這些資訊。

如果你查閱了 man 2 read,你將會看到 read 的第一個引數 (3) 是一個檔案描述符,這是 *nix 作業系統用於所有 IO 操作的控制代碼。你該如何知道檔案描述符 3 代表什麼?在這種情況下,你可以使用 -y 引數執行 strace(如上文所述),它將會在注釋裡告訴你檔案描述符的具體指向,但是了解如何從上面這種輸出中分析追蹤結果是很有用的。

一個檔案描述符可以來自於許多系統呼叫之一(這取決於它是用於控制台、網路通訊端還是真實檔案等的描述符),但不論如何,我們都可以搜尋返回值為 3 的系統呼叫(例如,在 strace 的輸出中查詢 =3)。在這次 strace 中可以看到有兩個這樣的呼叫:最上面的 openat 以及中間的 socketopenat 開啟一個檔案,但是緊接著的 close(3) 表明其已經被關閉。(注意:檔案描述符可以在開啟並關閉後重複使用。)所以 socket 呼叫才是與此相關的(它是在 read 之前的最後一個),這告訴我們 brcontab 正在與一個網路通訊端通訊。在下一行,connect 表明檔案描述符 3 是一個連線到 /var/run/bcron-spool 的 Unix 域通訊端。

因此,我們需要弄清楚 Unix 通訊端的另一側是哪個進程在監聽。有兩個巧妙的技巧適用於在伺服器部署中偵錯。一個是使用 netstat 或者較新的 ss。這兩個命令都描述了當前系統中活躍的網路通訊端,使用 -l 引數可以顯示出處於監聽狀態的通訊端,而使用 -p 引數可以得到正在使用該通訊端的程式資訊。(它們還有更多有用的選項,但是這兩個已經足夠完成工作了。)

# ss -pl | grep /var/run/bcron-spoolu_str LISTEN 0   128   /var/run/bcron-spool 1466637   * 0   users:(("unixserver",pid=20629,fd=3))

這告訴我們 /var/run/bcron-spool 通訊端的監聽程式是 unixserver 這個命令,它的進程 ID 為 20629。(巧合的是,這個程式也使用檔案描述符 3 去連線這個通訊端。)

第二個常用的工具就是使用 lsof 查詢相同的資訊。它可以列出當前系統中開啟的所有檔案(或檔案描述符)。或者,我們可以得到一個具體檔案的資訊:

# lsof /var/run/bcron-spoolCOMMAND   PID   USER  FD  TYPE  DEVICE              SIZE/OFF  NODE    NAMEunixserve 20629 cron  3u  unix  0x000000005ac4bd83  0t0       1466637 /var/run/bcron-spool type=STREAM

進程 20629 是一個常駐進程,所以我們可以使用 strace -o /tmp/trace -p 20629 去檢視該進程的系統呼叫。如果我們在另一個終端嘗試編輯 cron 的計劃任務表,就可以在錯誤發生時捕獲到以下資訊:

accept(3, NULL, NULL)                   = 4clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21181close(4)                                = 0accept(3, NULL, NULL)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21181, si_uid=998, si_status=0, si_utime=0, si_stime=0} ---wait4(0, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG|WSTOPPED, NULL) = 21181wait4(0, 0x7ffe6bc36764, WNOHANG|WSTOPPED, NULL) = -1 ECHILD (No child processes)rt_sigaction(SIGCHLD, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, 8) = 0rt_sigreturn({mask=[]})                 = 43accept(3, NULL, NULL)                   = 4clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21200close(4)                                = 0accept(3, NULL, NULL)                   = ? ERESTARTSYS (To be restarted if SA_RESTART is set)--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=21200, si_uid=998, si_status=111, si_utime=0, si_stime=0} ---wait4(0, [{WIFEXITED(s) && WEXITSTATUS(s) == 111}], WNOHANG|WSTOPPED, NULL) = 21200wait4(0, 0x7ffe6bc36764, WNOHANG|WSTOPPED, NULL) = -1 ECHILD (No child processes)rt_sigaction(SIGCHLD, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, {sa_handler=0x55d244bdb690, sa_mask=[CHLD], sa_flags=SA_RESTORER|SA_RESTART, sa_restorer=0x7faa47ab9840}, 8) = 0rt_sigreturn({mask=[]})                 = 43accept(3, NULL, NULL

(最後一個 accept 呼叫沒有在追蹤期間完成。)不幸的是,這次追蹤沒有包含我們想要的錯誤資訊。我們沒有觀察到 bcrontan 往通訊端傳送或接受的任何資訊。然而,我們看到了很多進程管理操作(clonewait4SIGCHLD,等等)。這個進程產生了子進程,我們猜測真實的工作是由子進程完成的。如果我們想捕獲子進程的追蹤資訊,就必須往 strace 追加 -f 引數。以下是我們最終使用 strace -f -o /tmp/trace -p 20629 找到的錯誤資訊:

21470 openat(AT_FDCWD, "tmp/spool.21470.1573692319.854640", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 EACCES (Permission denied)21470 write(1, "32:ZCould not create temporary f"..., 36) = 3621470 write(2, "bcron-spool[21470]: Fatal: logs:"..., 84) = 8421470 unlink("tmp/spool.21470.1573692319.854640") = -1 ENOENT (No such file or directory)21470 exit_group(111)                   = ?21470 +++ exited with 111 +++

現在我們知道了進程 ID 21470 在嘗試建立檔案 tmp/spool.21470.1573692319.854640 (相對於當前的工作目錄)時得到了一個沒有許可權的錯誤。如果我們知道當前的工作目錄,就可以得到完整路徑並能指出為什麼該進程無法在此處建立臨時檔案。不幸的是,這個進程已經退出了,所以我們不能使用 lsof -p 21470 去找出當前的工作目錄,但是我們可以往前追溯,查詢進程 ID 21470 使用哪個系統呼叫改變了它的工作目錄。這個系統呼叫是 chdir(可以在搜尋引擎很輕鬆地找到)。以下是一直往前追溯到伺服器進程 ID 20629 的結果:

20629 clone(child_stack=NULL, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7faa47c44810) = 21470...21470 execve("/usr/sbin/bcron-spool", ["bcron-spool"], 0x55d2460807e0 /* 27 vars */) = 0...21470 chdir("/var/spool/cron")          = 0...21470 openat(AT_FDCWD, "tmp/spool.21470.1573692319.854640", O_RDWR|O_CREAT|O_EXCL, 0600) = -1 EACCES (Permission denied)21470 write(1, "32:ZCould not create temporary f"..., 36) = 3621470 write(2, "bcron-spool[21470]: Fatal: logs:"..., 84) = 8421470 unlink("tmp/spool.21470.1573692319.854640") = -1 ENOENT (No such file or directory)21470 exit_group(111)                   = ?21470 +++ exited with 111 +++

(如果你在這裡迷糊了,你可能需要閱讀 我之前有關 *nix 進程管理和 shell 的文章

現在 PID 為 20629 的伺服器進程沒有許可權在 /var/spool/cron/tmp/spool.21470.1573692319.854640 建立檔案。最可能的原因就是典型的 *nix 檔案系統許可權設定。讓我們檢查一下:

# ls -ld /var/spool/cron/tmp/drwxr-xr-x 2 root root 4096 Nov  6 05:33 /var/spool/cron/tmp/# ps u -p 20629USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMANDcron     20629  0.0  0.0   2276   752 ?        Ss   Nov14   0:00 unixserver -U /var/run/bcron-spool -- bcron-spool

這就是問題所在!這個服務進程以 cron 使用者執行,但是只有 root 使用者才有向 /var/spool/cron/tmp/ 目錄寫入的許可權。一個簡單 chown cron /var/spool/cron/tmp/ 命令就能讓 bcron 正常工作。(如果不是這個問題,那麼下一個最有可能的懷疑物件是諸如 SELinux 或者 AppArmor 之類的核心安全模組,因此我將會使用 dmesg 檢查核心紀錄檔。)

總結

最初,系統呼叫追蹤可能會讓人不知所措,但是我希望我已經證明它們是偵錯一整套常見部署問題的快速方法。你可以設想一下嘗試用單步偵錯程式去偵錯多進程的 bcron 問題。

通過一連串的系統呼叫解決問題是需要練習的,但正如我說的那樣,在大多數情況下,我只需要使用 strace 從下往上追蹤並查詢錯誤。不管怎樣,strace 節省了我很多的偵錯時間。我希望這也對你有所幫助。