一. 进程程序替换原理
用 fork()
创建子进程后, 子进程执行的是和父进程相同的程序 (但有可能执行不同的代码分支), 子进程往往要调用一种 exec 函数以执行另一个程序. 当进程调用一种 exec 函数时, 该进程执行的程序完全被新程序替换, 从新程序的启动例程开始执行. 调用 exec 并不创建新进程, 所以调用 exec 前后该进程的 PID 并未改变, exec
只是用磁盘上的一个新程序替换了当前进程的正文段, 数据段, 堆区和栈区.
注意: 子进程刚被创建时, 与父进程共享地址空间, 但当子进程需要进行进程程序替换时, 需要对其地址空间进行修改, 这时会发生写时复制, 此后父子进程就不再对地址空间进行共享.
二. 进程程序替换方法
进行进程程序替换的函数有六种, 它们都以 exec 开头, 统称为 exec 函数.
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
, 之后子进程正常终止, 并被父进程读取退出信息.
exec 函数返回值
- exec 函数如果调用成功, 则加载指定的程序并从启动例程开始执行, 不再返回.
- 如果调用出错, 则返回 -1.
也就是说, exec 系列函数只要返回了, 就意味着其调用失败.
命名理解
- l(list): 表示命令行参数字符串采用列表的形式, 一一列出.
- v(vector): 表示命令行参数字符串采用数组的形式组织.
- p(path): 表示能通过环境变量 PATH 在它所指定的各目录中搜寻可执行文件.
- e(env): 表示可以传入自己设置的环境变量 (也可以传入系统默认的环境变量).
事实上, 只有 execve()
才是系统调用, 其它五个函数最终都是调用的 execve()
, 也就是说其他五个函数实际上是对系统调用 execve()
进行了封装, 以满足不同的使用场景.
下图为 exec 系列函数之间的关系: