目录
1.进程替换原理
- 用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支)
- 子进程通过调用一种exec函数可以执行另一个程序:当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换**,操作系统修正进程地址空间、页表等数据结构之后** ,该进程开始执行新程序。
- 调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
- 多子进程的程序替换实行写时拷贝,不影响父进程,单进直接进行替换
- 所以得出一个结论,数据段是可写的,通过写时拷贝机制实现进程间隔离;代码段是只读的,exec()时直接被新程序替换
- 编译器形成的可执行程序是有格式的,如ELF,可执行程序的表头存储了程序的入口

注意,代码和数据是从磁盘文件加载到内存的,进程替换时,操作系统将新程序的代码和数据替换到原代码和数据处,空间不够就扩容
2.替换函数


2.1execl
int execl(const char *path, const char *arg, ...);
execl 函数用于执行指定路径的程序(参数1)
并将后续一系列独立的字符串作为命令行参数传递给该程序 (后续的可变参数)
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程执行ls -a -l命令
printf("子进程执行任务:\n");
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
exit(errno);
}
else if(id > 0)
{
printf("父进程等待子进程:\n");
int status = 0;
pid_t ret = waitpid(id, &status, 0);//阻塞等待
if(ret < 0)
{
printf("等待失败\n");
exit(errno);
}
else
{
printf("等待成功,子进程退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}

如上图结果所示,子进程的代码和数据替换成了ls程序,并成功执行了ls -a -l 命令
所以参数1传入的是所替换程序的完整路径
参数二传入的是命令行参数,注意:最后传入 NULL
执行一个程序,首先需要找到该程序,然后决定如何执行该程序,需要涵盖哪些选项所以参数1:定位要执行的程序
参数2及后续:告诉程序做什么
值得注意的是,如果execl函数成功执行,那么这意味着进程被完全替换,原代码不再执行;如果执行失败时才返回-1并继续执行原进程代码。
2.2execlp
int execlp(const char *file, const char *arg, ...);
execlp 函数用于执行PATH路径下的程序(参数1)
并将后续一系列独立的字符串作为命令行参数传递给该程序 (后续的可变参数)
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程执行ls -a -l命令
printf("子进程执行任务:\n");
execlp("ls", "ls", "-a");
exit(errno);
}
else if(id > 0)
{
printf("父进程等待子进程:\n");
int status = 0;
pid_t ret = waitpid(id, &status, 0);//阻塞等待
if(ret < 0)
{
printf("等待失败\n");
exit(errno);
}
else
{
printf("等待成功,子进程退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}

如上图结果所示,子进程的代码和数据替换成了ls程序,并成功执行了ls -a 命令
所以参数1传入的是所替换程序的路径,参数二传入的是命令行参数,注意:最后传入 NULL
值得注意的是,
- 如果execlp函数成功执行,那么这意味着进程被完全替换,原代码不再执行;如果执行失败时才返回-1并继续执行原进程代码。
- 参数1可以只传入程序名,系统自动到PATH路径下寻找
- 参数1也可以传入完整的路径,此时execl与execlp功能一致
2.3execle
int execle(const char *path, const char *arg,..., char * const envp[]);
execle 函数用于执行指定路径下的程序(参数1)
并将后续一系列独立的字符串作为命令行参数传递给该程序 (后续的可变参数)
最后将envp(环境变量表)传递给该程序 (最后一个参数)
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程执行ls -a -l命令
printf("子进程执行任务:\n");
char* const env[] = {"MY=11111111111", "X=22222222222", NULL};
execle("/usr/bin/env", "env", NULL, env);
exit(errno);
}
else if(id > 0)
{
printf("父进程等待子进程:\n");
int status = 0;
pid_t ret = waitpid(id, &status, 0);//阻塞等待
if(ret < 0)
{
printf("等待失败\n");
exit(errno);
}
else
{
printf("等待成功,子进程退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}

如上图结果所示,子进程的代码和数据替换成了env程序,并成功执行了env命令
所以参数1传入的是所替换程序的路径,参数二传入的是命令行参数,注意:最后传入 NULL
参数三传入了自定义环境变量表env
值得注意的是,
- 如果execle函数成功执行,那么这意味着进程被完全替换,原代码不再执行;如果执行失败时才返回-1并继续执行原进程代码。
- execle函数参数1与参数2的规则与execl一致
- 参数三意义不是传递全局变量environ(即父进程的环境变量表),因为子进程一旦被创建就会自动继承父进程的环境变量,进程替换时,如果不传入自定义环境变量表,替换后的子进程的环境变量表仍然与父进程保持一致,如果传入了自定义环境变量表,原环境变量表将被覆盖
2.4execv
int execv(const char *path, char *const argv[]);
execv 函数用于执行指定路径下的程序(参数1)
并将用数组保存的命令行参数传递给该程序
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程执行ls -a -l命令
printf("子进程执行任务:\n");
char* const _argv[] = {"ls", "-a", "-l", NULL};
execv("/usr/bin/ls", _argv);
exit(errno);
}
else if(id > 0)
{
printf("父进程等待子进程:\n");
int status = 0;
pid_t ret = waitpid(id, &status, 0);//阻塞等待
if(ret < 0)
{
printf("等待失败\n");
exit(errno);
}
else
{
printf("等待成功,子进程退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}

如上图结果所示,子进程的代码和数据替换成了ls程序,并成功执行了ls -a -l 命令
所以参数1传入的是所替换程序的路径,参数二传入的是数组名
注意:数组最后一个元素是NULL
值得注意的是,如果execv函数成功执行,那么这意味着进程被完全替换,原代码不再执行;如果执行失败时才返回-1并继续执行原进程代码。
execv 的本质就是 "数组版的 execl"
2.5execvp
int execvp(const char *file, char *const argv[]);
execvp 函数用于执行PATH路径下的程序(参数1)
并将用数组保存的命令行参数传递给该程序 (参数2)
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程执行ls -a -l命令
printf("子进程执行任务:\n");
char* const _argv[] = {"ls", "-a", "-l", NULL};
execvp("ls", _argv);
exit(errno);
}
else if(id > 0)
{
printf("父进程等待子进程:\n");
int status = 0;
pid_t ret = waitpid(id, &status, 0);//阻塞等待
if(ret < 0)
{
printf("等待失败\n");
exit(errno);
}
else
{
printf("等待成功,子进程退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}

如上图结果所示,子进程的代码和数据替换成了ls程序,并成功执行了ls -a 命令
所以参数1传入的是所替换程序的路径,参数二传入的是数组名
注意:数组最后一个参数是 NULL
值得注意的是,
- 如果execvp函数成功执行,那么这意味着进程被完全替换,原代码不再执行;如果执行失败时才返回-1并继续执行原进程代码。
- 参数1可以只传入程序名,系统自动到PATH路径下寻找
- 参数1也可以传入完整的路径,此时execvp与execv功能一致
- 参数2使用数组保存的命令行参数,注意最后一个参数为NULL
execv 的本质就是 数组版的 execlp
2.6execve
int execvpe(const char *file, char *const argv[],char *const envp[]);
execve 函数用于执行指定路径下的程序(参数1)
并将用数组保存的命令行参数传递给该程序 (参数2)
最后将envp(环境变量表)传递给该程序 (最后一个参数)
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
//子进程执行ls -a -l命令
printf("子进程执行任务:\n");
char* const env[] = {"MY=11111111111", "X=22222222222", NULL};
char* const _argv[] = {"env", NULL};
execve("/usr/bin/env", _argv, env);
exit(errno);
}
else if(id > 0)
{
printf("父进程等待子进程:\n");
int status = 0;
pid_t ret = waitpid(id, &status, 0);//阻塞等待
if(ret < 0)
{
printf("等待失败\n");
exit(errno);
}
else
{
printf("等待成功,子进程退出码:%d\n", WEXITSTATUS(status));
}
}
return 0;
}

如上图结果所示,子进程的代码和数据替换成了env程序,并成功执行了env命令
所以参数1传入的是所替换程序的路径,参数二传入的是命令行参数,注意:最后传入 NULL
参数三传入了自定义环境变量表env
值得注意的是,
- 如果execve函数成功执行,那么这意味着进程被完全替换,原代码不再执行;如果执行失败时才返回-1并继续执行原进程代码。
- execve函数参数1与参数2的规则与execv一致
- 参数三意义不是传递全局变量environ(即父进程的环境变量表),因为子进程一旦被创建就会自动继承父进程的环境变量,进程替换时,如果不传入自定义环境变量表,替换后的子进程的环境变量表仍然与父进程保持一致,如果传入了自定义环境变量表,原环境变量表将被覆盖
2.7总结
- char* const p 的特点是 p 的指向无法改变,exec系列的部分函数中设计该类指针的数组,这就能够保证函数内部无法修改数组元素的指针指向
- exec系列函数通过替换把可执行程序导入了内存,起到了代码级别的加载器效果
- 不同语言形成的可执行程序最终会成为进程,所以一旦指明了路径与执行命令,exec系列函数就能替换并运行不同语言形成的可执行程序
- 显然,exec系列函数中l、c不能同时存在
| 函数名 | 参数传递形式 | 程序文件查找方式 | 环境变量传递 | 核心特点与适用场景 |
|---|---|---|---|---|
| execl | 列表 (可变参数) | 需提供完整路径 | 继承当前环境 | 最基础形式,需指定全路径。 |
| execlp | 列表 (可变参数) | 可在PATH环境变量中搜索文件名 | 继承当前环境 | 只需提供文件名,方便调用系统命令。 |
| execle | 列表 (可变参数) | 需提供完整路径 | 自定义环境变量数组 | 可完全控制新进程的环境。 |
| execv | 数组 (指针数组) | 需提供完整路径 | 继承当前环境 | 适合命令行参数已构建在数组中的情况。 |
| execvp | 数组 (指针数组) | 可在PATH环境变量中搜索文件名 | 继承当前环境 | execv 的便捷版,无需完整路径。 |
| execve | 数组 (指针数组) | 需提供完整路径 | 自定义环境变量数组 | 唯一的系统调用,功能最基础,其他函数均基于此实现。 |
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
命令理解
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
