🌟hello,各位读者大大们你们好呀🌟
🍭🍭系列专栏:【Linux初阶】
✒️✒️本篇内容:替换初识,替换原理,替换函数理解和使用,makefile工具的多文件编译,进程替换应用(简易命令行实现)
🚢🚢作者简介:计算机海洋的新进船长一枚,请多多指教( •̀֊•́ ) ̖́-
一、前言
在之前的学习中,我们学习了 fork子进程创建的知识,那么我们创建一个子进程的目的是什么呢?
- 想让子进程执行父进程代码的一部分,换句话说,执行父进程磁盘代码的一部分;
- 想让子进程执行一个全新的程序,换句话说,就是让子进程想办法加载磁盘上指定的程序,执行新程序的代码和数据;
- 而子进程加载磁盘上指定的程序,执行新程序的代码和数据的行为,我们称之为
进程程序替换
。
二、替换初识
1.引入
在进程程序替换中,有许多对应的替换函数,它们大多以exec为开头。在深入学习替换的知识之前,我们先对其中一个较为简单的函数进行讲解,让大家先对进程程序替换的实现有直观感受,以便后续理解。
execl - 执行,下面这条代码的含义为将指定的程序加载到内存中,让指定进程执行。
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
我们要知道,实现进程替换操作,首先需要找到对应的程序,其次还需要提供具体的执行方法。
- const char *path ——
找到对应的程序
; - const char *arg, ... ——
提供具体的执行方法
,此处的三个点为可变参数列表,我们可以暂时把它理解为省略号,代表不同的执行选项(选项最后必须以 NULL结尾);
这里我们以 ls指令,举一个简单的例子
ls -a -l
要执行上述指令,需要找到 ls的位置,还需要提供具体的执行方法,也就是 "ls", "-a", "-l", NULL。
2.代码示例
#include <stdio.h>
#include <unistd.h>
int main()
{
//.c->exe->load->process->运行->执行我们现在所写的代码
printf("process is running...\n");
// load->exe
execl("/usr/bin/ls"/*要执行哪一个程序*/, NULL/*你想怎么执行*/); // all exec* end of NULL
printf("process running done...\n");
}
通过运行上述代码,我们发现,它的结果相当于 ls指令(只是有颜色差异)
只要添加对应的选项,就可以实现功能
#include <stdio.h>
#include <unistd.h>
int main()
{
//.c->exe->load->process->运行->执行我们现在所写的代码
printf("process is running...\n");
// load->exe
execl("/usr/bin/ls"/*要执行哪一个程序*/, "ls", "--color=auto", "-a", "-l", NULL/*你想怎么执行*/); // all exec* end of NULL
printf("process running done...\n");
}
运行上述代码,我们发现,它的结果相当于ls -a -l
指令
我们通过运行自己的可执行程序,实现了Linux指令的功能,这就是程序替换体现。
细心的朋友可能发现了,我们自己写的代码为什么第二个 printf("process running done...\n"); 不执行了呢?这里主要涉及到替换原理的知识,我们在下一节再和大家讲解。
三、替换原理
1.原理讲解
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变
。
程序替换的本质,就是将指定程序的代码加载到指定的位置
。这里的加载到指定的位置,通常会覆盖原有的代码和数据。
2.为什么我exec函数后的代码都不执行了呢?
到了这里我们就不难理解,为什么第二节中我们自己写的代码第二个 printf("process running done...\n"); 不执行呢?答案就是,进程调用exec函数后,原来的代码和数据都被覆盖了!
#include <stdio.h>
#include <unistd.h>
int main()
{
//.c->exe->load->process->运行->执行我们现在所写的代码
printf("process is running...\n");
// load->exe
execl("/usr/bin/ls"/*要执行哪一个程序*/, "ls", "--color=auto", "-a", "-l", NULL/*你想怎么执行*/); // all exec* end of NULL
// 为什么这里的printf没有在执行了???printf也是代码,是在execl之后的,
// execl执行完毕的时候,代码已经全部被覆盖,开始执行新的程序的代码了,所以printf就无法执行了!
printf("process running done...\n");
}
———— 我是一条知识分割线 ————
3.exec函数调用失败
通过上面的学习,我们知道,exec函数后面的代码不执行,是建立在函数调用成功的基础上的,只有调用失败,才会执行接下来的代码。
通过查询手册,我们不难得知,exec*函数只有在调用失败的时候才会有返回值,为什么它不存在调用成功的返回值呢?答案是不需要,因为成功了,原来的代码和数据就被覆盖了,就和接下来的代码无关了,判断毫无意义。
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值。
代码示例,下面的 lsabcskd根本不存在,运行后会通过 perror输出对应的错误
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
//.c->exe->load->process->运行->执行我们现在所写的代码
printf("process is running...\n");
// load->exe
// 只要是一个函数,调用就有可能失败,如果没有替换成功,就是没有替换
// exec*为什么没有成功返回值呢?因为成功了,就和接下来的代码无关了,判断毫无意义
// execl 只要返回了,一定是错误了
execl("/usr/bin/lsabcskd"/*要执行哪一个程序*/, "ls", "--color=auto", "-a", "-l", NULL/*你想怎么执行*/); // all exec* end of NULL
perror("ececl"); //打印错误原因
// 为什么这里的printf没有在执行了???printf也是代码,是在execl之后的,
// execl执行完毕的时候,代码已经全部被覆盖,开始执行新的程序的代码了,所以printf就无法执行了!
printf("process running done...\n");
exit(1);
}
4.子进程的替换会影响父进程吗?
答案是,不会的!因为进程具有独立性,在操作系统内部,有虚拟地址空间+页表保证进程独立性,一旦有执行流想替换代码或数据,就会发生写时拷贝。
也就是说,一旦子进程想进行程序替换,那么就会发生写时拷贝,操作系统会给子进程在物理内存中开辟一块新空间,将原来的数据拷贝到新空间中,再对新空间中的数据做修改和替换。
代码示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
// 这里的替换,会影响父进程吗?? 进程具有独立性
// 类比:命令行怎么写,这里就怎么传
sleep(1);
execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
}
四、不同函数调用的对应使用方式
1.常见的替换函数
其实有七种以exec开头的函数,统称exec函数
#include <unistd.h>`
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
(1)execl
我们在之前章节的学习中已经大致了解了 execl的基础使用方法,其中 execl中的 l 我们可以把它理解为 list,标识将参数一个一个的传入 exec*中。
(2)execlp
execlp中的 p实际上代表的是 path,带 p的替换函数,不需要告诉他具体的程序路径,只需要告诉它程序是谁,就可以自动调用环境变量 PATH,进行可执行程序的查找。
代码示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
// 这里的替换,会影响父进程吗?? 进程具有独立性
// 类比:命令行怎么写,这里就怎么传
sleep(1);
//execl("/usr/bin/ls", "ls", "-a", "-l", "--color=auto", NULL);
// 这里有两个ls, 重复吗?不重复,一个是告诉系统我要执行谁?一个是告诉系统,我想怎么执行
execlp("ls", "ls", "-a", "-l", "--color=auto", NULL);
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret>0) printf("wait success: exit code: %d, sig: %d\n", (status>>8)&0xFF, status & 0x7F);
}
(3)execv
int execv(const char *path, char *const argv[]);
v:vector,可以将所有的执行参数,放入数组中,统一传递,而不需要使用可变参数方案。
代码示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
sleep(1);
char* const argv_[] = {
"ls",
"-a",
"-l",
"--color=auto",
NULL
};
execv("/usr/bin/ls", argv_);
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
}
(4)execvp
int execvp(const char *file, char *const argv[]);
execvp和上面两个函数的 v,p的含义相同。也就是说,只要传入程序名和执行参数的数组即可运行。
代码示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
sleep(1);
char* const argv_[] = {
"ls",
"-a",
"-l",
"--color=auto",
NULL
};
execvp("ls", argv_);
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
}
通过上面的学习,我们已经了解如何替换操作系统的指令,那么如何将一个进程程序替换成我们自己的可执行程序呢?别急,我们会在接下来的讲解中找到答案。
2.makefile工具的多文件编译
我们知道,常规情况下,makefile只能一次性编译一个文件。
mybin : mybin.c
gcc - o $@ $^ -std = c99
.PHONY:clean
clean :
rm - f mybin
在接下来的学习中,我们需要一次性编译多个文件,那么 makefile工具能不能支持呢?答案是可以的,我们可以定义一个为目标all
,all依赖于我们自己的代码文件,将 all跟在 .PHONY后面,最终实现多个文件的同时编译和清除。
.PHONY:all
all : mybin myexec
mybin : mybin.c
gcc - o $@ $^ -std = c99
myexec : myexec.c
gcc - o $@ $^ -std = c99
.PHONY:clean
clean :
rm - f myexec mybin
3.替换成自己的可执行程序
创建代码文件mybin
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
return 0;
}
创建代码文件myexec,同时在myexec中调用mybin(这里是在子进程中调用的)
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0) //子进程
{
sleep(1);
char* const argv_[] = {
"ls",
"-a",
"-l",
"--color=auto",
NULL
};
execl("./mybin", "mybin", NULL);
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
}
总结:我们可以使用程序替换,调用任何后端语言(C/C++、python等)对应的可执行程序
。
4.execle & execve
函数中的 e代表的是自定义环境变量的意思
(1)execle and execvpe
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
修改代码文件 mybin
#include <stdio.h>
#include <stdlib.h>
int main()
{
// 系统就有
printf("PATH:%s\n", getenv("PATH"));
printf("PWD:%s\n", getenv("PWD"));
// 自定义
printf("MYENV:%s\n", getenv("MYENV"));
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
printf("我是另一个C程序\n");
return 0;
}
代码示例(修改代码文件 myexe)
- 只显示自定义环境变量
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
sleep(1);
char* const envp_[] = {
(char*)"MYENV=11112222233334444",
NULL
};
execle("./mybin", "mybin", NULL, envp_); //自定义环境变量
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
}
- 只显示系统环境变量
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
sleep(1);
char* const envp_[] = {
(char*)"MYENV=11112222233334444",
NULL
};
extern char** environ;
//execle("./mybin", "mybin", NULL, envp_); //自定义环境变量
execle("./mybin", "mybin", NULL, environ); //实际上,默认环境变量你不传,子进程也能获取
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
}
)
- 既显示自定义环境变量,也显示系统环境变量
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
sleep(1);
char* const envp_[] = {
(char*)"MYENV=11112222233334444",
NULL
};
extern char** environ;
//execle("./mybin", "mybin", NULL, envp_); //自定义环境变量
putenv((char*)"MYENV=4443332211"); //将指定环境变量导入到系统中 environ指向的环境变量表
execle("./mybin", "mybin", NULL, environ); //实际上,默认环境变量你不传,子进程也能获取
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
}
———— 我是一条知识分割线 ————
通过观察,我们可以发现execle对应的参数和我们的 main函数参数参数极为相似,实际上,它们的确有对应的关系,我们可以直接通过给 main传参,赋予 exec*替换函数其他的使用方法,这里的知识我们放到 5. execvp的另一应用方式
来进行演示。
int execle(const char *path, const char *arg, ...,char *const envp[]);
int main(int argc, char* argv[],char* env[])
(2)execve
execve为程序替换中唯一的系统调用接口
,我们之前见到的所有 exec*函数,都是基于 execve系统调用接口做的封装,让我们具有更多选择性。
int execve(const char *filename, char *const argv[], char *const envp[]);
5. execvp的另一应用方式
代码示例
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char* argv[])
{
printf("process is running...\n");
pid_t id = fork();
assert(id != -1);
if (id == 0)
{
sleep(1);
// execvp传递方法如下
// ./exec ls -a -l -> "./exec" "ls" "-a" "-l"
execvp(argv[1], &argv[1]);
exit(1); //must failed
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait success: exit code: %d, sig: %d\n", (status >> 8) & 0xFF, status & 0x7F);
}
6.函数命名理解总结
上面讲述的函数原型看起来很容易混,但只要掌握了规律就很好记。
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量
#include <unistd.h>
int main()
{
char* const argv[] = {
"ps", "-ef", NULL };
char* const envp[] = {
"PATH=/bin:/usr/bin", "TERM=console", NULL };
execl("/bin/ps", "ps", "-ef", NULL);
// 带p的,可以使用环境变量PATH,无需写全路径
execlp("ps", "ps", "-ef", NULL);
// 带e的,需要自己组装环境变量
execle("ps", "ps", "-ef", NULL, envp);
// 带v的,可以直接传指令选项的数组,这里是argv
execv("/bin/ps", argv);
// 带p的,可以使用环境变量PATH,无需写全路径
execvp("ps", argv);
// 带e的,需要自己组装环境变量
execve("/bin/ps", argv, envp);//只有这一个是系统调用
exit(0);
}
五、应用场景(简易命令行解释器的实现)
我们之前在 第四章节5小节execvp的另一应用方式
中,有这样一个例子,它实现了用我们自己的程序替换shell的指令,那么如果我们可以将运行的方式进一步简化,那么不就可以变成一个简易的 shell了吗?
下面,我将会带着大家结合相关知识,去制作一个简易的命令行解释器。
由于文章篇幅原因,我额外制作了一篇文章对简易命令行解释器的实现进行了讲解:【Linux初阶】进程替换的应用 - 简易命令行解释器的实现
结语
🌹🌹 Linux进程替换 的知识大概就讲到这里啦,博主后续会继续更新更多C++的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪