1. 程式人生 > >獲取fork+exec啟動的程式的PID值

獲取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, &regs) == -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, &regs) == -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位機器。

參考資料