问题背景

    业务中有个场景需要自动起一个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位机器。

参考资料

Searchable Linux Syscall Table for x86 and x86_64
ptrace-examples
Programming with PTRACE, Part2 – 系统调用入门
使用 Ptrace 拦截和模拟 Linux 系统调用

版权声明:本文为ZhaoxiCheung原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/ZhaoxiCheung/p/9742718.html