獲取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 是 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位機器。