本文詳細描述了一種利用 ptrace 系統調用,實現嵌入式系統內部進程通信的監視方法,并提供了相應的實現方案。
概述
復雜的嵌入式系統中,常常同時運行著相當多的進程。這些進程之間頻繁的進行著大量的通信動作。進程的運行狀態與這些不斷發生的通信有著直接和緊密的聯系。通過對進程間通信的監視,開發人員可以掌控系統內部運轉的狀態。發現錯誤時,利用獲取到的進程間通信的信息,調試工程師更容易發現問題之所在。
但是,嵌入式系統與開發人員的接口往往較為單一。開發人員廣泛使用通常是基于串口或是網絡接口的終端( console )方式。在這個模式下,開發人員難以細致準確的觀察進程間的通信。而且對于計算能力薄弱的嵌入式系統來說,在終端上打印出通信報文既會影響系統內部的運行,同時,也會使屏幕上充斥的過多的無用信息,使開發人員的分析工作無從下手。
為了解決這個問題,在嵌入式 Linux 的平臺上,我們開發了一整套用于監視嵌入式系統內進程間通信的軟件,用于調試我們開發的嵌入式產品。本文詳細介紹了監視嵌入式系統內進程間通信的技術原理和實現監視軟件的推薦方案。
監視方法的基本原理
Linux 中的 ptrace 系統調用是監視進程間通信的關鍵。 ptrace 為我們提供了一種觀察和控制其它進程的方法。利用 ptrace ,我們可以截獲正在運行的進程的所有的系統調用。所謂截獲是指,監視程序可以在這些系統調用發生和退出時,獲得系統調用的參數,甚至修改參數。這些系統調用包括: read , write , sendto, recv 等等。在 Linux 中,用戶可以通過“ man syscalls ”來查看當前版本的 Linux 所支持的系統調用。
在我們的 Linux 嵌入式產品中, AF_UNIX 域的 socket 被廣泛使用。它被用來完成進程間通信的工作。 AF_UNIX 域的 socket 的編程模型與通常的 socket 編程模型完全相同。我們的使用方法是:接收進程創建一個 AF_UNIX 域的 socket ,設定其模式為數據報( SOCK_DGRAM )。在這之后,為其綁定一個含路徑的文件名,例如: /var/tmp/receive.unix 。這個文件名被內核用于標識socket。發送進程創建一個相同模式的 AF_UNIX 域的 socket 。然后,調用 sendto 向接收進程發送消息。用來標識接收進程 socket 的就是前面提到的文件名,也就是 /var/tmp/receive.unix 。而接收進程使用 recvfrom 系統調用,就可以收到發送進程發出的消息。
因此,通過 ptrace ,一旦我們接管了被監視進程的 sendto 和 recvfrom 系統調用,將使我們能夠截獲到使用這兩個系統調用進行通信的數據。
ptrace 系統調用的定義如下:
#include long int ptrace(enum __ptrace_request request, pid_t pid, void * addr, void * data); |
它共有四個參數。 request 的值決定 ptrace 執行什么樣的任務。 pid 指明被追蹤的進程的 id 。 request 參數決定了是否需要一個有效的 addr 參數,還是僅用 NULL 即可。如果有必要使用有效的 addr 參數,它的含義是被追蹤的進程的進程空間的偏移量。 data 類似于 addr 參數,有時也可以使用 NULL 來代替。如果它被使用,它的含義是指向一些數據,這些數據希望被放置到被監視的進程的用戶空間中。
一個完整的示例代碼將向我們展示監視進程間通信的技術細節和關鍵點。代碼按前后順序分段說明。
為了在程序中使用 ptrace 系統調用,我們需要增加 ptrace.h 頭文件。為了能夠獲得截獲的系統調用的函數入參,我們需要使用 struct user_regs_struct 結構。它在 user.h 中被定義。由于在程序中使用了信號,因此,我們也需要 wait.h 。我們要監視通信動作, socket.h 和 un.h 則是必不可少的。
下面是程序的入口主函數:
status 用于記錄被監視進程的狀態變化; syscall_entry 記錄被監視進程當前是進入系統調用,還是從系統調用中返回; u_in 用來獲得截獲的系統調用的參數; traced_process 則是被監視進程的 PID 值。
參數為 PTRACE_ATTACH 的 ptrace 對被監視進程在內核中的進程結構進行修改。使被監視進程成為當前程序的子進程。一旦被監視進程的狀態發生變化, wait() 將返回。程序再次調用 ptrace 。這次的參數為 PTRACE_SYSCALL 。被監視進程的進程結構再次被修改,其 trace 標志被激活。內核將在被監視進程的每一次系統調用時,觸發當前程序的運行。
被監視進程的 trace 標志被激活后,它的每一次系統調用都會被內核檢查。我們程序也隨之被內核用信號通知。使用參數 PTRACE_GETREGS 的 ptrace() 將獲得截獲的系統調用的參數。最重要的參數是系統調用號。它保存在了 u_in.orig_eax 中。通過系統調用號,我們可以確定發生的是那一個系統調用。系統調用號可以在 Linux 的源代碼中查找。它的定義在 Linux-source-2.6.xx/arch/x86/kernel/syscall_table_32.S 中。它的部分代碼如下所示:
在這里,我們最關心的是 sendto 系統調用。在 Linux 的內核中, sendto 的真實入口是 socketcall 系統調用。它是 bind , sendto 等socket相關系統調用的入口。在這個系統調用中,通過一個 call number 來區分出 bind , sendto 等不同的子系統調用。在我們的程序中,這個 call number 保存在 u_in.ebx 中。 從上面的 syscall_table_32.S 示例代碼就可以看出, socketcall 的系統調用號是102(從100向下數兩行)。而 call number 則在 net.h 有定義,我們關心的 sendto 的 call number 被定義為 SYS_SENDTO ,其絕對值為11。有了這兩個重要的數據,我們的程序據此判斷當前發生的系統調用是否為 sendto 。這一點表現為代碼:
被監視進程進入系統調用和退出系統調用時,都會觸發 wait() 返回,使我們的程序有機會運行。因此,我們需要使用 syscall_entry 來記錄當前時刻是被監視進程進入系統調用,還是退出系統調用。這是一個開關量,非常容易理解。 最后,每次處理完,都需要再次調用參數為 PTRACE_SYSCALL 的 ptrace ,準備監視下一次的系統調用。
上面的程序雖然很簡單,但已經可以完整的表現出利用 ptrace 截獲被監視進程的 sendto 系統調用的過程。值得補充一點的是,利用 ptrace 也可以獲得 sendto 向外發送的數據。
sendto 系統調用的定義是:
sendto 包含了六個參數,特別是 msg 參數指出了發送的數據內容。參數 to 指出了發送的目標。利用 PTRACE_PEEKDATA 參數的 ptrace ,監視程序將可以獲得 sendto 的全部的六個參數。這樣監視程序就完全獲得了被監視進程要向外發送的數據和發送目標。具體的實現細節在此不再展開論述。請參考 man ptrace 說明手冊。監視系統的體系和應用
利用上面討論的技術,我們開發了可以運行在 mips 目標板上的監視程序,名為 ipcmsg 。它是一個命令行程序。在我們的應用環境中,它的使用方法是:
pid 是被監視進程的 pid ,可以通過 ps 命令獲得。 -l 參數后面指定 PC 主機的 IP 地址。 -b 參數指明了接收的端口號。
最初進行監視時, ipcmsg 是沒有 IP 地址和端口號參數的。所有信息是輸出到串口控制臺中。這既影響了運行的效率(大量的在串口上的輸出會影響目標板的運行速度),也不利于信息的處理。由于我們的目標板具備以太網接口,我們很容易的想到將 ipcmsg 截獲的數據包轉發到 PC 主機上。使用 PC 主機更便于對進程間通信的數據包進行分析。在 PC 主機上,我們使用 wireshark 這個非常流行的開源的網絡報文分析軟件接收來自目標板的信息。整個監視系統的架構如下圖所示:
在實際的使用過程中,我們使用以太網線將目標板與 PC 主機相連。然后,在目標板上啟動 ipcmsg ,并為其指定監視進程的 pid 。 ipcmsg運行后,我們在PC主機上啟動 wireshark 接收來自 ipcmsg 的數據包。這些數據包中包含了 mips 目標板上進程間通信的信息。利用我們為 ipcmsg 專門開發的 wireshark 插件,在 wireshark 上,我們可以詳細的分解 ipcmsg 轉發來的數據包,非常直觀的分析進程間通信的過程和可能存在的問題。下面是 wireshark 分解 ipcmsg 數據包的實際運行圖:
從圖中可以看到,我們從 ipcmsg 獲得了進程間通信的方式,參數( path 是 AF_UNIX域 socket 地址參數),方向和內容,以及進程名稱。這些信息幫助我們對嵌入式系統的運行狀態進行分析。而這一切非常直觀和便于操作。
#include
#include
#include
#include
#include
#include
#include
#include
int main (int argc, char *argv[])
{
int status;
int syscall_entry = 0;
int traced_process;
struct user_regs_struct u_in;
traced_process = atoi(argv[1]); /* 從命令行得到監視進程的PID */
ptrace(PTRACE_ATTACH, traced_process, NULL, NULL);
wait(&status); /* 等待被監視進程狀態變化 */
ptrace(PTRACE_SYSCALL, traced_process, NULL, NULL);
While (1) {
/* 等待被監視程序調用系統調用或是發生其它狀態變化 */
wait(&status);
/* 如果被監視進程退出,函數返回真。程序退出 */
if ( WIFEXITED(status) )
break;
ptrace(PTRACE_GETREGS, traced_process, 0, &u_in);
if (u_in.orig_eax == 102 && u_in.ebx == SYS_SENDTO) {
if (syscall_entry == 0) { /* syscall entry */
insyscall = 1;
printf("call sendto()n");
}
else { /* Syscall exit */
Syscall_entry = 0;
}
}
ptrace(PTRACE_SYSCALL, traced_process, NULL, NULL);
} /* while */
return 0;
} /* main */
.long sys_fstatfs /* 100 */
.long sys_ioperm
.long sys_socketcall
.long sys_syslog
if (u_in.orig_eax == 102 && u_in.ebx == SYS_SENDTO)
#include
#include
size_t sendto(int s, const void *msg, size_t len, int flags,
const struct sockaddr *to, socket len_t tolen);
root@host:~$ ipcmsg -p pid -l xxx.xxx.xxx.xxx -b 6000