在之前的一篇文章中,我介紹了一種調試手段:利用LD_PRELOAD機制,攔截動態鏈接器對動態庫的符號解析,達到監控程序IO的目的。事實證明我還是太naive了,我們大可利用現成的工具——strace,來更好地完成這一項工作。
strace不只能跟蹤程序IO,它能跟蹤程序的所有系統調用,實現的基本手段是ptrace系統調用,不過實現細節還沒研究過,今天只總結一下它的用法。
首先用strace來跟蹤一下喜聞樂見的hello world:
#include <stdio.h>
int main()
{
printf("Hello, world!\n");
return 0;
}
$ gcc main.c -o main
$ strace ./main
輸出是這樣的(由於太長,省略了大部分內容):
execve("./main", ["./main"], 0x7ffcea3db620 /* 33 vars */) = 0
......(太長省略)
write(1, "Hello, world!\n", 14Hello, world!
) = 14
exit_group(0) = ?
+++ exited with 0 +++
所有系統調用被一一記錄了下來,我們可以看出來,printf是通過write系統調用將字符串“Hello, world!\n”寫到文件描述符爲1的文件(即標準輸出)中,從而輸出到屏幕上的。
這就是strace最基本的用法,它還提供一些非常實用的參數,我們可以來看一下:
-e
這個參數可以用來過濾輸出,它功能很多,具體可以查看strace的手冊,我們只用其最基本的功能:跟蹤特定的系統調用。
$ strace -e write ./main
輸出瞬間清爽了:
write(1, "Hello, world!\n", 14Hello, world!
) = 14
+++ exited with 0 +++
-k
這個參數可以用來輸出堆棧,不過strace的幫助將其標記爲了實驗性功能。
$ strace -e write -k ./main
輸出爲:
write(1, "Hello, world!\n", 14Hello, world!
) = 14
> /lib/x86_64-linux-gnu/libc-2.27.so(__write+0x14) [0x110154]
> /lib/x86_64-linux-gnu/libc-2.27.so(_IO_file_write+0x2d) [0x8b1bd]
> /lib/x86_64-linux-gnu/libc-2.27.so(_IO_do_write+0xb1) [0x8cf51]
> /lib/x86_64-linux-gnu/libc-2.27.so(_IO_file_overflow+0x103) [0x8d403]
> /lib/x86_64-linux-gnu/libc-2.27.so(_IO_puts+0x1a2) [0x80b62]
> /home/imred/Documents/Workspace/playground/strace_blog/main(main+0x10) [0x64a]
> /lib/x86_64-linux-gnu/libc-2.27.so(__libc_start_main+0xe7) [0x21b97]
> /home/imred/Documents/Workspace/playground/strace_blog/main(_start+0x2a) [0x55a]
+++ exited with 0 +++
-t/-tt
這組參數可以用來打印時間戳
-y
這個參數可以在打印文件描述符相關參數時同時把文件描述符對應的文件路徑打印出來:
$ strace -e write -y ./main
輸出爲:
write(1</dev/pts/0>, "Hello, world!\n", 14Hello, world!
) = 14
+++ exited with 0 +++
可以看出來當前內容被輸出到了序號爲0的僞終端。
由於下面的參數與時間統計有關,所以最好調用耗時較長的系統調用來分析其作用,我使用的是usleep函數,它調用了nanosleep系統調用,代碼爲:
#include <unistd.h>
int main()
{
for (int i = 0; i < 10; i++)
{
usleep(1000);
}
return 0;
}
-T
這個參數可以用來輸出每個系統耗費的時間,這個時間是系統調用開始時間和結束時間之差。
$ strace -e nanosleep -T ./main
輸出爲:
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001089>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001078>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001972>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001108>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001139>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001091>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001093>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001326>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001029>
nanosleep({tv_sec=0, tv_nsec=1000000}, NULL) = 0 <0.001297>
+++ exited with 0 +++
看出來這時間統計的算不上十分準確,最大的誤差甚至接近100%,不過這是strace的問題還是系統調用本身的問題並不確定,也還有可能是我使用了虛擬機的緣故。
-c
這個參數用來彙總系統調用的數據。
$ strace -c ./main
輸出爲:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
0.00 0.000000 0 1 read
0.00 0.000000 0 2 close
0.00 0.000000 0 8 7 stat
0.00 0.000000 0 2 fstat
0.00 0.000000 0 5 mmap
0.00 0.000000 0 4 mprotect
0.00 0.000000 0 1 munmap
0.00 0.000000 0 1 brk
0.00 0.000000 0 3 3 access
0.00 0.000000 0 10 nanosleep
0.00 0.000000 0 1 execve
0.00 0.000000 0 1 arch_prctl
0.00 0.000000 0 10 8 openat
------ ----------- ----------- --------- --------- ----------------
100.00 0.000000 49 18 total
能夠分析出每種系統調用總的相對時間、總的絕對時間、平均開銷、調用次數、出錯次數信息。
你可能會比較奇怪爲什麼上面彙總出來的時間消耗都是0,至少nanosleep該消耗了時間。這是因爲在默認情況下統計的都是系統時間,即進程在內核空間執行代碼消耗的cpu時間,而不是牆上時間。調用了nanosleep雖然消耗了用戶的時間,但並沒有佔用多少cpu時間,所以統計出來就是0了,如果要統計牆上時間,需要下面的這個選項。
-w
這個參數指定-c參數統計牆上時間。
$ strace -wc ./main
輸出爲:
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
97.95 0.015065 1507 10 nanosleep
0.64 0.000098 98 1 execve
0.38 0.000058 6 10 8 openat
0.24 0.000037 5 8 7 stat
0.20 0.000031 6 5 mmap
0.18 0.000028 7 4 mprotect
0.11 0.000017 6 3 3 access
0.10 0.000016 16 1 munmap
0.06 0.000009 5 2 fstat
0.05 0.000008 4 2 close
0.03 0.000005 5 1 read
0.03 0.000005 5 1 brk
0.03 0.000004 4 1 arch_prctl
------ ----------- ----------- --------- --------- ----------------
100.00 0.015381 49 18 total
這回輸出結果大致符合我們的預期了(雖然誤差很大)。
-f
這個參數指定strace要跟蹤從主線程衍生出來的其他線程和進程。
對於下面的程序:
#include <thread>
#include <unistd.h>
void loopSleep()
{
for (int i = 0; i < 10; i++)
{
usleep(1000);
}
}
int main()
{
std::thread t1([](){ loopSleep(); });
std::thread t2([](){ loopSleep(); });
t1.join();
t2.join();
return 0;
}
nanosleep都沒有在主線程調用,如果我們直接這樣調用strace的話:
$ strace -e nanosleep ./main
其輸出爲:
+++ exited with 0 +++
並沒有追蹤到nanosleep的調用。這時就需要指定-f參數了:
$ strace -e nanosleep -f ./main
其輸出爲:
strace: Process 4623 attached
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, strace: Process 4622 attached
<unfinished ...>
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] nanosleep({tv_sec=0, tv_nsec=1000000}, <unfinished ...>
[pid 4623] <... nanosleep resumed> NULL) = 0
[pid 4623] +++ exited with 0 +++
[pid 4622] <... nanosleep resumed> NULL) = 0
[pid 4622] +++ exited with 0 +++
+++ exited with 0 +++
追蹤到了我們指定的nanosleep系統調用。
-p
這個參數指定strace連接到指定進程上。
以上就是strace這把“錘子”的大致使用手冊,但是如何把我們當前遇到的問題歸約成“釘子”就沒有手冊可以參考了,需要各位各顯神通了。