在开发 php 扩展的过程中,希望能创建一个独立的子进程做一些额外的处理工作,并且为子进程修改一个有意义的名称,发现还是有一些难度的。

效果预览

要实现的效果就像 nginx 启动后通过 ps 查到的名称一样,这个名称就是自定义的,如下图

nginx-process-name

方法一

prctl(PR_SET_NAME, name);

通过这个函数可以将当前进程的名称修改为 name 的内容。

我测试了一下,发现只有使用 ps -L 才能看到,达不到想要的效果。

方法二

参考了 nginx 中的源码,主要是通过修改 argv[0] 的值来实现的,但是需要注意将后面跟着的 environ 环境表中的信息移到其他地方,避免被覆盖。

void set_proctitle(char** argv, const char* new_name)
{
    int size = 0;
    int i;
    // 申请新的空间存放 environ 中内容
    for (i = 0; environ[i]; i++) {
        size += strlen(environ[i]) + 1;
    }
    char* p = (char*)malloc(size);
    char* last_argv = argv[0];
    for (i = 0; argv[i]; i++) {
        if (last_argv == argv[i]) {
            last_argv = argv[i] + strlen(argv[i]) + 1;
        }
    }  
    for (i = 0; environ[i]; i++) {
        if (last_argv == environ[i]) {
            size = strlen(environ[i]) + 1;
            last_argv = environ[i] + size;  
   
            memcpy(p, environ[i], size);
            environ[i] = (char*)p;
            p += size;
        }  
    }
    last_argv--;
    // 修改 argv[0],argv剩余的空间全部填0
    strncpy(argv[0], new_name, last_argv - argv[0]);
    p = argv[0] + strlen(argv[0]) + 1;
    if (last_argv - p > 0) {
        memset(p, 0, last_argv - p);
    }  
}

稍微解释一下,每一个 c 程序都有个 main 函数,作为程序启动入口函数。main 函数的原型是 int main(int argc , char *argv[]),其中 argc 表示命令行参数的个数,argv 是一个指针数组,保存所有命令行字符串。Linux进程名称是通过命令行参数 argv[0] 来表示的。

而进程执行时的环境变量信息的存储地址就是紧接着 argv 之后,通过 char **environ 变量来获取,类似于下图

argv-info

由于我们需要修改 argv[0] 的值,有可能新的字符串的长度超过原来 argv 中所有字符串长度的总和,又因为 environ 在内存空间上是紧跟着 argv 的,我们如果直接修改 argv[0] 的值,有可能会覆盖掉 environ 的内存空间,所以需要先将 environ 的内容 copy 到一块新的内存空间,之后再将 environ 指针指向新的空间。

php 扩展中遇到的困难

在修改 php 扩展中 fork 的子进程名称时遇到了问题,由于 php 扩展是注入的方式,提供的动态库,无法获取到从 main 函数传入过来的 argv 参数的地址。

经过测试,发现 environ 是一个全局变量,可以获取到它的地址,而 argv 中内容可以用另外一种方式取得,通过查看 /proc/10000/cmdline 中的值(10000是该进程的进程号),可以获取命令行启动参数的字符串(也就是 argv 中的内容,如果 argv 没有被其他代码修改过的话),所以用 environ 的地址减去 cmdline 中字符串的长度就可以得到 argv[0] 的地址。

注:需要注意的是 cmdline 不是一个普通文件,不能用 stat 或者 ftell 等函数来获取长度,必须用 read 等读取文件的函数去读取。

参考代码如下:

void set_proctitle_unsafe(const char* new_name)
{
    // 获取该进程的启动参数字符串
    int pid = getpid();
    char file_name[100];
    snprintf(file_name, sizeof(file_name), "/proc/%d/cmdline", pid);

    int fd = open(file_name, O_RDONLY);
    if (fd < 0)
        return;

    char tempCmd[513];
    long cmd_length = read(fd, tempCmd, sizeof(tempCmd));
    close(fd);

    // 获取 argv[0] 的地址
    char *argv = environ[0];
    argv = argv - cmd_length;

    int size = 0;
    int i;
    // 申请新的空间存放 environ 中内容
    for (i = 0; environ[i]; i++) {
        size += strlen(environ[i]) + 1;
    }
    char* p = (char*)malloc(size);

    char* last_argv = argv;
    last_argv = argv + cmd_length;

    for (i = 0; environ[i]; i++) {
        if (last_argv == environ[i]) {
            size = strlen(environ[i]) + 1;
            last_argv = environ[i] + size;

            memcpy(p, environ[i], size);
            environ[i] = (char*)p;
            p += size;
        }
    }
    last_argv--;

    // 修改 argv[0] 的内容
    strncpy(argv, new_name, last_argv - argv);
    p = (argv) + strlen(argv) + 1;
    if (last_argv - p > 0) {
        memset(p, 0, last_argv - p);
    }
}

这个函数是不安全的,需要小心使用,因为不能确定 environ 的地址是否已经被其他人修改过了,比如在 php 扩展中,有可能已经被其他程序用同样的方法修改过,这样就会造成获取到的 argv[0] 的地址是未知的,执行的程序可能就会出现内存错误。