获取fork+exec启动的程序的PID值
2019-11-10

问题背景

    业务中有个场景需要自动起一个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",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("", stderr); exit(EXIT_FAILURE); } while (0)intmain(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(" = ?", 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", (long)regs.rax); /*clone 系统调用号的特判 if (56 == syscall){ printf("%ld", (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_64ptrace-examplesProgramming with PTRACE, Part2 - 系统调用入门使用 Ptrace 拦截和模拟 Linux 系统调用

, 1, 0, 9);