獲取fork+exec啟動的程式的PID值
問題背景
業務中有個場景需要自動起一個A程式(由於A程式與 sublime_text 啟動後遇到的問題有相似之處,後文就用 sublime_text 來替代A程式,當A程式與 sublime_text 的現象有所差異的時候,恢復使用 A 程式),並在適當的場景下殺死它,自然而然想到 fork + exec 的方式來啟動它。但是啟動後,在獲取程式 pid 的時候卻遇到了一點問題。以下是啟動的程式碼:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int create_process(char *name, char *argv[]) { int pid = fork(); if (0 == pid) { execv(name, argv); exit(127); } else if (0 < pid) { return pid; }else { return -1; } } int main() { char *name = "/opt/sublime_text/sublime_text"; char *argv[] = {"/opt/sublime_text/sublime_text", (char *)0}; int pid = create_process(name, argv); printf("pid = %d\n",pid); return 0; }
程式執行結果如下,從下圖我們可以清晰的看到通過 fork + exec 啟動的程式的 pid 與最後通過 ps程序檢視器查詢得到的 pid 是不一致的。

儘管它們的 pid 值只差了1,但是這個結果還是讓我感到非常疑惑。
問題分析
一般的,在子程序中使用 exec 函式並不會改變子程序的 pid 值,而得到的結果確確實實改變了。一開始懷疑是與 pid 的分配方式有關,因為多次得到的結果其 pid 都只差1(有興趣的可以自行了解 pid 點陣圖分配策略),但沒有太多的資訊進行佐證,最後懷疑是要啟動的程式的問題。
通過 strace
來跟蹤 sublime_text 程序中的系統呼叫:

從上面的結果我們可以看出,sublime_text 的真實 pid 與 strace得到的結果中 clone 一行的結果相對應。從這個資訊中,我們可以發現 sublime_text 內部通過 clone 自己建立了一個子程序來啟動程式。因此推測通過 fork 得到的子程序在完成自己的任務後就退出了,啟動程式的事情交給了 sublime_text 內部通過 clone 起的子程序去做。
問題解決
從上面的問題分析得知,sublime_text 真實的 pid 是 clone 建立的子程序的 pid,而這個 clone 建立的子程序是 sublime_text 內部啟動的。那麼如何獲取啟動的程式的 pid 呢。一開始想到方法如下:在啟動程式A之前,記錄下環境中已啟動的程式A的 pid,然後啟動 count 個A程式,扣除掉之前記錄的就是現在啟動的(sublime_text 啟動多次只有一個程式例項,而 A 程式啟動多次有多個程式例項,因此此處恢復為A程式的描述);但是這種方法存在極小概率會出錯,環境並不是只有一個使用者,也就是我在記錄完環境中已有的程式A的 pid 後,啟動 n 個程式A,此時如果有另一個使用者也起了 m 個程式A,那麼我就會認為這 n + m 個A程式都是我起的,後期殺死的時候破壞了他人啟動的程式。因此這種方式並不適用,在論壇與人討論後查詢資論發現可以使用 ptrace
來解決,其實也就是模擬 strace
來跟蹤程序中的系統呼叫。
#define _POSIX_C_SOURCE 200112L /* C standard library */ #include <errno.h> #include <stdio.h> #include <stddef.h> #include <stdlib.h> #include <string.h> /* POSIX */ #include <unistd.h> #include <sys/user.h> #include <sys/wait.h> /* Linux */ #include <syscall.h> #include <sys/ptrace.h> #define FATAL(...) \ do { \ fprintf(stderr, "strace: " __VA_ARGS__); \ fputc('\n', stderr); \ exit(EXIT_FAILURE); \ } while (0) int main(int argc, char **argv) { if (argc <= 1) FATAL("too few arguments: %d", argc); pid_t pid = fork(); switch (pid) { case -1: /* error */ FATAL("%s", strerror(errno)); case 0:/* child */ ptrace(PTRACE_TRACEME, 0, 0, 0); execvp(argv[1], argv + 1); FATAL("%s", strerror(errno)); } /* parent */ waitpid(pid, 0, 0); // sync with PTRACE_TRACEME ptrace(PTRACE_SETOPTIONS, pid, 0, PTRACE_O_EXITKILL); for (;;) { /* Enter next system call */ if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) FATAL("%s", strerror(errno)); if (waitpid(pid, 0, 0) == -1) FATAL("%s", strerror(errno)); /* Gather system call arguments */ struct user_regs_struct regs; if (ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1) FATAL("%s", strerror(errno)); long syscall = regs.orig_rax; /* Print a representation of the system call */ fprintf(stderr, "%ld(%ld, %ld, %ld, %ld, %ld, %ld)", syscall, (long)regs.rdi, (long)regs.rsi, (long)regs.rdx, (long)regs.r10, (long)regs.r8,(long)regs.r9); /* Run system call and stop on exit */ if (ptrace(PTRACE_SYSCALL, pid, 0, 0) == -1) FATAL("%s", strerror(errno)); if (waitpid(pid, 0, 0) == -1) FATAL("%s", strerror(errno)); /* Get system call result */ if (ptrace(PTRACE_GETREGS, pid, 0, ®s) == -1) { fputs(" = ?\n", stderr); if (errno == ESRCH) exit(regs.rdi); // system call was _exit(2) or similar FATAL("%s", strerror(errno)); } /* Print system call result */ fprintf(stderr, " = %ld\n", (long)regs.rax); /*clone 系統呼叫號的特判 if (56 == syscall){ printf("%ld\n", (long)regs.rax); } */ } }
程式的主體主要是關於 ptrace
的用法,本文不對 ptrace
的用法進行詳細闡述,具體可參見文末資料。上述程式是一個小型的 strace
,它將攔截所有的系統呼叫,並輸出相應的資訊,如果取消程式碼尾處對於 clone 系統呼叫號的特判的註釋,那麼其打印出來的資訊,就是 sublime_text 的 pid,此時我們的問題也得到了解決。對於系統呼叫號,可在 /usr/include/x86_64-linux-gnu/asm/unistd_64.h
查詢,也可檢視文末資料,此處針對64位機器。

參考資料
ofollow,noindex" target="_blank">Searchable Linux Syscall Table for x86 and x86_64
Programming with PTRACE, Part2 - 系統呼叫入門
使用 Ptrace 攔截和模擬 Linux 系統呼叫