• 获取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
    ",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)
    
    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(" = ?
    ", 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_64
    ptrace-examples
    Programming with PTRACE, Part2 - 系统调用入门
    使用 Ptrace 拦截和模拟 Linux 系统调用

  • 相关阅读:
    ASP.NET 无提示关闭窗口
    C# Winform程序获取外网IP地址
    使用System.Drawing.Printing 画报表。
    C# Winform调用IP地址、手机归属和身份证查询接口
    C# 获取文中文字符首字母
    C# WinForm给Button或其它控件添加快捷键响应
    C# 获取农历日期
    TSQL之插入的内容是查询出的值
    C# 获取中文星期的两种方法
    C#批量操作控件
  • 原文地址:https://www.cnblogs.com/ZhaoxiCheung/p/9742718.html
Copyright © 2020-2023  润新知