🌟 各位看官好,我是egoist2023!
🌍 Linux == Linux is not Unix !
🚀 今天来学习exec系列的进程程序替换,从"fork"的"克隆"到"exec"的"重生"。
👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享给更多人哦!
目录
[系统+自带 环境变量](#系统+自带 环境变量)
书接上文
上文介绍了Linux中进程等待的必要性和实现方法。主要内容包括:
- 子进程退出后若父进程不回收会导致僵尸进程和内存泄漏;
- wait和waitpid函数的使用方法,重点讲解了如何获取子进程退出状态和信号编号;
- 非阻塞等待的实现方式,通过WNOHANG选项让父进程在等待子进程时能同时处理其他任务;
- 解释了孤儿进程无法被Ctrl+C终止的原因。文章还通过代码示例演示了如何正确获取子进程退出码和信号,以及父进程如何通过轮询方式非阻塞等待子进程。
进程程序替换
通过上一节的学习,我们掌握了如何使用 wait
和 waitpid
来回收子进程资源,避免僵尸进程的产生,并让父进程能够知晓子进程的结束状态。无论是阻塞还是非阻塞等待,其核心目的都是管理一个已经结束或即将结束的"任务"。
然而,我们不禁要思考一个更根本的问题:创建子进程的初衷,难道仅仅是为了让它复制一份父进程的代码,然后执行一模一样的操作吗?显然不是。在绝大多数实际应用中,我们创建子进程主要有两个目的:
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
如何让子进程执行一个全新的程序呢?
子进程有自己的代码,有自己的数据 --> 成为一个真正的独立的进程!
原理
程序替换的过程有没有必要创建新的进程呢? --> 没有必要,为什么呢? --> 通过将代码和数据加载到内存里,覆盖代码和数据段。而pcb、虚拟内存空间、页表不会有太大变化,顶多修改页表的映射关系,因此没必要创建一个新的进程。(打印程序替换前后的pid即可证明)

⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种exec函数以执⾏另⼀个程序。当进程调⽤⼀种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤exec并不创建新进程,所以调⽤exec前后该进程的id并未改变。
正如上文所述,exec
函数族的核心功能是用一个新的程序替换当前进程的映像 。这个功能本身是明确的,但在实际应用时,我们调用新程序的需求却是多变的:命令有时需要带上复杂的参数列表,例如 ls -l -a -i
为了理解exec系列的函数,这里需要先提一嘴可变参数,只有理解了可变参数,才能方便引入接下来的exec系列函数.
替换操作
理解可变参数

可变参数函数是指能够接受数量不确定的参数函数。那又是如何做到接受不确定参数数量的呢?毫无疑问,肯定需要一个"路标"指引它从哪里开始走,要走多少步啊!!!
"路标":就是函数里的第一个固定参数 count
。它的最大作用就是告诉函数:"从我后面开始,一共有 count
个参数"。
va_start:根据最后一个固定参数,找到可变参数的起始位置。
va_arg
: 取出 当前位置的一个值;把位置移动到下一个参数那里。
va_end
:打扫战场,清理工作。
bash
int execl(const char *pathname, const char *arg, ... /*, (char *) NULL */);
int execlp(const char *file, const char *arg, ... /*, (char *) NULL */);
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
理解了可变参数后,讲述进程程序替换的操作:六种以exec开头的函数,统称exec函数。
- 这些函数如果调⽤成功则加载新的程序从启动代码开始执⾏,不再返回。
- 如果调⽤出错则返回-1
- 所以exec函数只有出错的返回值⽽没有成功的返回值
execl函数
int execl(const char *pathname, const char *arg, ... /*, (char *) NULL */);
第一个参数是传一个程序,程序也是一个文件,是文件就需要有路径 + 文件名;
第二个参数是要执行程序的程序名称;
第三个参数是可变参数,必须以NULL结尾。
举个梨子:
bash
int main()
{
printf("我是一个进程,pid:%d,getpid());
sleep(1);
int n = execl("/usr/bin/ls","ls","-l","-a","-n",NULL);
printf("进程结束:%d\n",n);
return 0;
}


可以看到我们的程序确实被替换了,但同时也会发现显示器没有打印 " 进程结束 : ... " ,直接结束了。为什么会没有后续的打印了呢?不就是被替换了嘛,进程已经执行另一个程序的代码,原来的代码已经没有了。因此程序替换函数一旦调用成功,后续代码不再执行。(失败返回-1)

那么,我们可不可以替换子进程的代码和数据呢?当然可以,子进程会自己执行一个全新的程序。那么会影响到父进程吗?不会,因为进程之间必须具有独立性啊 ! (可以理解成代码如果要被替换,也要进行写时拷贝,如上图所示.)
execv函数
int execv(const char *pathname, char *const argv[]);
- 第二个参数是一个指针数组,即是把程序名称+可变参数存放在该数组中,传递给子进程的命令行参数。
举个梨子:
bash
char *const myargv[] =
{
(char*)"ls",
(char*)"-l",
(char*)"-i",
(char*)"-n",
(char*)"-l",
NULL
};
char **myargv = &argv[1];
execv(myargv[0],myargv);
在前面章节中,我们清楚main函数也是可以带参数的,其中一个就包含命令行参数表。此时./myexe执行自己的执行流,后半部分传递给子进程,让子进程来执行。 (./myexe不就是我们的bash,后半部分不就是我们的命令+选项吗?)

execlp函数
int execlp(const char *file, const char *arg, ... /*, (char *) NULL */);
- 第一个参数指明要执行谁,只需要告诉名字,如指令ls。执行指令的命令,需要让execlp自己在环境变量PATH中寻找指定的程序!
execvp函数
int execvp(const char *file, char *const argv[]);

execvpe函数
int execvpe(const char *file, char *const argv[], char *const envp[]);
- 第三个参数是一个指针数组,指向环境变量表。
那么这是否意味着我们同样能通过程序替换函数获取环境变量的内容。这里通过三种操作来观察。
系统默认环境变量
bash
extern char **environ;
char *const myargv[] = {"mycmd",NULL};
execvpe("./mycmd",myargv,environ);

自带环境变量
bash
char *const myargv[] = {"mycmd",NULL};
char *const myenv[] = {"hello=1234","world=1",NULL}
execvpe("./mycmd",myargv,myenv);

系统+自带 环境变量
bash
char *my = "hello=123456789";
putenv(my);
char *const myargv[]={"mycmd",NULL};
execvpe("./mycmd",myargv,environ);

execle函数
int execle(const char *pathname, const char *arg, ... /*, (char *) NULL, char *const envp[] */);
execle的参数在上面都有谈及,就不再进行过多赘述。
总结

- l(list) : 表⽰参数采⽤列表
- v(vector) : 参数⽤数组
- p(path) : 有p⾃动搜索环境变量PATH
- e(env) : 表⽰自己维护环境变量
为什么程序替换函数要设计这么多个呢?只有传参形式的不同,满足我们未来不同的应用场景!!!
虽然在上层传参形式有所不同,但每个函数内部实际上都调用了同一个系统调用execve!
bash
int execve(const char *pathname, char *const _Nullable argv[], char *const _Nullable envp[]);

