Linux - 进程 - 进程程序替换

498 阅读5分钟

一. 进程程序替换原理

fork() 创建子进程后, 子进程执行的是和父进程相同的程序 (但有可能执行不同的代码分支), 子进程往往要调用一种 exec 函数以执行另一个程序. 当进程调用一种 exec 函数时, 该进程执行的程序完全被新程序替换, 从新程序的启动例程开始执行. 调用 exec 并不创建新进程, 所以调用 exec 前后该进程的 PID 并未改变, exec 只是用磁盘上的一个新程序替换了当前进程的正文段, 数据段, 堆区和栈区.

image.png

注意: 子进程刚被创建时, 与父进程共享地址空间, 但当子进程需要进行进程程序替换时, 需要对其地址空间进行修改, 这时会发生写时复制, 此后父子进程就不再对地址空间进行共享.

二. 进程程序替换方法

进行进程程序替换的函数有六种, 它们都以 exec 开头, 统称为 exec 函数.

image.png

execl

函数原型: int execl(const char *path, const char *arg, ...);

第一个参数是存放可执行程序的路径名, 第二个参数是可变参数列表, 传递命令行参数字符串, 并以 NULL 结尾.

例如, 要执行指令 ls -l.

execl("/usr/bin/ls", "ls", "-l", NULL);

execlp

函数原型: int execlp(const char *file, const char *arg, ...);

第一个参数是可执行程序的文件名, 第二个参数是可变参数列表, 传递命令行参数字符串, 并以 NULL 结尾.

例如, 要执行指令 ls -l.

execlp("ls", "ls", "-l", NULL);

execle

函数原型: int execle(const char *path, const char *arg, ..., char *const envp[]);

第一个参数是存放可执行程序的路径名, 第二个参数是可变参数列表, 传递命令行参数字符串, 并以 NULL 结尾, 第三个参数是一个指向环境字符串指针数组的指针. (系统默认的环境变量和自己组装的环境变量都可以)

例如, 要执行指令 ls -l, 且使用系统默认的环境变量.

execle("/usr/bin/ls", "ls", "-l", NULL, environ);

execv

函数原型: int execv(const char *path, char *const argv[]);

第一个参数是存放可执行程序的路径名, 第二个参数是指向命令行参数字符串指针数组 (指针数组的最后一个元素必须设置为 NULL) 的指针.

例如, 要执行指令 ls -l.

char* argv[] = { "ls", "-l", NULL };
execv("/usr/bin/ls", argv);

execvp

函数原型: int execvp(const char *file, char *const argv[]);

第一个参数是可执行程序的文件名, 第二个参数是指向命令行参数字符串指针数组 (指针数组的最后一个元素必须设置为 NULL) 的指针.

例如, 要执行指令 ls -l.

char* argv[] = { "ls", "-l", NULL };
execvp("ls", argv);

execve

函数原型: int execve(const char *path, char *const argv[], char *const envp[]);

第一个参数是存放可执行程序的路径名, 第二个参数是指向命令行参数字符串指针数组 (指针数组的最后一个元素必须设置为 NULL) 的指针, 第三个参数是一个指向环境字符串指针数组的指针. (系统默认的环境变量和自己组装的环境变量都可以)

例如, 要执行指令 ls -l --color, 且使用系统默认的环境变量.

char* argv[] = { "ls", "-l", "--color", NULL };
execve("/usr/bin/ls", argv, environ);

编写如下代码: fork() 创建子进程后, 父进程进行非阻塞轮询, 子进程打印 5 次信息后, 使用 execve() 进行进程程序替换, 执行完替换的程序后子进程退出, 父进程使用 waitpid() 读取子进程退出信息.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    extern char **environ;
    char* argv[] = { "ls", "-l", "--color", NULL };
    pid_t id = fork();
    if (id < 0) { // fork error
        perror("fork");
        return 1;
    }
    else if (id == 0) { // child process
        int count = 5;
        while (count) {
            printf("I am child process.....:PID:%d, PPID:%d, count:%d\n", getpid(), getppid(),count);
            sleep(1);
            count--;
        }
        execve("/usr/bin/ls", argv, environ);
    }
    else { // father process
        while (1) {
            int status = 0;
            pid_t ret = waitpid(id, &status, WNOHANG);
            if (ret > 0) {
                printf("wait child process success\n");
                printf("child process exit code:%d\n", WEXITSTATUS(status));
                break;
            }
            else if (ret == 0) {
                printf("father process do his things.....\n");
                sleep(1);
            }
            else {
                printf("waitpid error...\n");
                break;
            }
        }
    }
    return 0;
}

运行生成的可执行程序后, 可以通过以下监控脚本, 每隔一秒对父子进程的信息进行检测.

while :; do ps ajx | head -1 && ps ajx | grep test | grep -v grep; echo "#######################"; sleep 1; done

这时我们可以看到, fork() 创建子进程后, 父进程进行非阻塞轮询, 子进程打印 5 次信息后进行进程程序替换, 执行指令 ls -l --color, 之后子进程正常终止, 并被父进程读取退出信息.

image.png

exec 函数返回值

  • exec 函数如果调用成功, 则加载指定的程序并从启动例程开始执行, 不再返回.
  • 如果调用出错, 则返回 -1.

也就是说, exec 系列函数只要返回了, 就意味着其调用失败.

命名理解

  • l(list): 表示命令行参数字符串采用列表的形式, 一一列出.
  • v(vector): 表示命令行参数字符串采用数组的形式组织.
  • p(path): 表示能通过环境变量 PATH 在它所指定的各目录中搜寻可执行文件.
  • e(env): 表示可以传入自己设置的环境变量 (也可以传入系统默认的环境变量).

image.png

事实上, 只有 execve() 才是系统调用, 其它五个函数最终都是调用的 execve(), 也就是说其他五个函数实际上是对系统调用 execve() 进行了封装, 以满足不同的使用场景.

下图为 exec 系列函数之间的关系:

image.png