文章目录
💐专栏导读
🌸作者简介:***花想云 ***,在读本科生一枚,C/C++领域新星创作者,新星计划导师,阿里云专家博主,CSDN内容合伙人…致力于 C/C++、Linux 学习。
🌸专栏简介:本文收录于 Linux从入门到精通,本专栏主要内容为本专栏主要内容为Linux的系统性学习,专为小白打造的文章专栏。
🌸相关专栏推荐:C语言初阶系列、C语言进阶系列 、C++系列、数据结构与算法。
💐文章导读
本章我们将学习一个强大的功能——
程序替换
。之前我们创建的子进程只能完成简单的一些任务且部分代码继承自父进程。有了程序替换以后,我们可以让子进程轻松的做更多的事情。学会了程序替换,我们可以编写一个简易的
shell
玩玩了,由此也可以对前几章的内容作复习与巩固~
🐧程序进程替换
我们一直在提子进程,那么创建子进程的目的是什么呢?无非是想让它帮助我们做某件事情。
用
fork
创建子进程后,子进程执行的是和父进程相同的程序(但有可能执行不同的代码分支)。如果此时我们想要子进程执行一个
全新的程序
该怎么做呢?这就需要用到程序替换了~
🐦替换原理
当进程调用一种
exec
函数时,
该进程的用户空间代码和数据完全被新程序替换
,从新程序的启动例程开始执行。调用
exec
并不创建新进程,所以调用
exec
前后该进程的
id
并未改变。
接下来我们就认识几个程序替换相关的函数,并演示如何操作。
🐦替换函数
一共有六种以
exec
开头的函数,称
exec
函数:
#include<unistd.h>externchar**environ;intexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);intexecle(constchar*path,constchar*arg,...,char*const envp[]);intexecv(constchar*path,char*const argv[]);intexecvp(constchar*file,char*const argv[]);intexecvpe(constchar*file,char*const argv[],char*const envp[]);
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回
-1
。 - 所以
exec函数只有出错的返回值而没有成功的返回值
。
在这里,我们先以execl为例。
🔔示例1——execl
在代码中,我们尝试在子进程中进行程序替换,替换为
ls
指令。
execl
使用时:
- 第一个参数是程序所在路径;
- 剩下的参数为执行该程序时想要传递的命令行参数(简述:平时你在命令行中怎么用,就怎么传参);
- 当确定想要传递的参数都给出后,一定要以
NULL
结尾。
#include<stdio.h>#include<unistd.h>#include<sys/wait.h>intmain(){pid_t id =fork();if(id ==0){// 子进程execl("/bin/ls","ls","-a","-n","-l",NULL);printf("程序替换失败\n");}// 父进程printf("等待子进程成功,child_id:%d\n",wait(NULL));return0;}
如图所示,结果正是我们想要的。
🐔观察与结论
根据对示例1的观察,我们发现:
- 子进程中,
execl
替换后剩下的语句未执行(printf
); - 子进程发生替换并未影响父进程;
由此我们可以得出结论:
- 因为进程具有独立性,尽管父子进程刚开始用的是同一个代码和数据,但是当程序替换发生后,由于
写时拷贝
的存在,仅仅只是子进程的代码和数据被替换后的程序覆盖了,并不会影响父进程。 - 程序替换函数,一旦替换发生,原来的代码在替换的语句执行后,就已经被新程序的代码和数据覆盖了,所以
printf
并未执行;
🐔函数命名理解
其实四个函数的功能是类似的,都用于完成程序替换。只不过针对不同的场景,我们可以选择不同的函数。
这些函数根据函数名就大致可以判断如何使用:
l (list)
:表示参数采用列表 ;v (vector)
:参数用数组 ;p (path)
:有p自动搜索环境变量PATH
;e (env)
:表示自己维护环境变量;
🔔其余函数示例
#include<stdio.h>#include<unistd.h>#include<sys/wait.h>intmain(){externchar** environ;pid_t id =fork();if(id ==0){// 子进程char*const myargv[]={"ls","-a","-l",NULL};// 带l的,需要跟上路径execl("/bin/ls","ls","-a","-n","-l",NULL);// 带p的,可以使用环境变量PATH,无需写全路径execlp("ls","ls","-a","-l",NULL);// 带V的,可以使用自己的参数列表数组execvp("ls",myargv);// 带e的,需要自己组装环境变量execvpe("ls",myargv,environ);printf("程序替换失败\n");}// 父进程printf("等待子进程成功,child_id:%d\n",wait(NULL));return0;}
有了前几章所讲知识以及在、本章程序替换部分的知识,我们可以试着自己实现一个简易的
shell
。
🐧myshell编写
🔔代码展示
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>#include<sys/types.h>#include<assert.h>#defineMAX1024#defineARGC64#defineSEP" "// 将输入的字符串切割并保存到argv中intsplit(char* commandstr,char* argv[]){assert(commandstr);assert(argv);
argv[0]=strtok(commandstr,SEP);if(argv[0]==NULL)return-1;// 若为NULL,则重新输入int i =1;while(argv[i++]=strtok(NULL,SEP));return0;}intmain(){while(1){char commandstr[MAX]={0};// 用于保存用户输入的指令char* argv[ARGC]={NULL};printf("[hxy@mychaimachine]$ ");fflush(stdout);char* s =fgets(commandstr,sizeof(commandstr),stdin);// 获取指令assert(s);(void)s;
commandstr[strlen(commandstr)-1]='\0';// 去掉键盘输入的\nint n =split(commandstr,argv);// 切割输入的指令字符串if(n!=0)continue;pid_t id =fork();if(id ==0){// 子进程execvp(argv[0],argv);// 程序替换exit(1);}int status =0;waitpid(id,&status,0);// 等待子进程}return0;}
🔔效果展示
如图所示,我们用简短的50行代码写了一个简易的
shell
(命令行解释器)。其实Linux源码中的内容可远不止这些,而且我们实现的
shell
所能实现的功能非常少,非常简陋。例如,
ls
并没有对不同的文件“上色”。
接下来,我们可以完善上述的代码,继续添加一些小功能。
🐧myshell_plus
上文中的
myshell
是非常简陋的,有许多指令诸如:
cd
、
export
、
env
等指令并不能正确执行。
就用
cd
来举例,
myshell
执行指令其实是交给子进程去做的,子进程的执行结果并不会影响父进程。也就是说,
cd
指令需要
mybash
自己去执行。
- 我们把让
bash
自己执行的命令叫作内建命令
。我们之前学到过的几乎所有关于环境变量的命令都是内建命令
。
于是,我们可以在创建子进程之前用
if
做判断,若用户输入的指令为
内建命令
,则让父进程执行该指令。
🔔代码展示
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>#include<sys/types.h>#include<assert.h>#defineMAX1024#defineARGC64#defineSEP" "intsplit(char* commandstr,char* argv[]){assert(commandstr);assert(argv);
argv[0]=strtok(commandstr,SEP);if(argv[0]==NULL)return-1;int i =1;while((argv[i++]=strtok(NULL,SEP)));return0;}voidshowEnv(){externchar** environ;for(int i =0; environ[i]; i++)printf("%d:%s\n",i,environ[i]);}intmain(){externintputenv(char* string);char myenv[32][256];int env_index =0;int exitCode =0;while(1){char commandstr[MAX]={0};char* argv[ARGC]={NULL};printf("[hxy@mychaimachine]$ ");fflush(stdout);char* s =fgets(commandstr,sizeof(commandstr),stdin);assert(s);(void)s;
commandstr[strlen(commandstr)-1]='\0';// 去掉键盘输入的\nint n =split(commandstr,argv);// 切割字符串if(n !=0)continue;if(strcmp(argv[0],"cd")==0){if(argv[1]!=NULL)chdir(argv[1]);continue;}elseif(strcmp(argv[0],"export")==0){if(argv[1]!=NULL){strcpy(myenv[env_index],argv[1]);// 用户自己定义的环境变量,需要bash自己来维护putenv(myenv[env_index++]);}continue;}elseif(strcmp(argv[0],"env")==0){showEnv();// env查看环境变量时,其实看的是父进程bash的变量continue;}elseif(strcmp(argv[0],"echo")==0){constchar* target_env =NULL;if(argv[1][0]=='$'){if(argv[1][1]=='?'){printf("%d\n",exitCode);continue;}else target_env =getenv(argv[1]+1);if(target_env !=NULL)printf("%s = %s\n",argv[1]+1,target_env);}continue;}// ls设置颜色选项if(strcmp(argv[0],"ls")==0){int pos =0;while(argv[pos]!=NULL){
pos++;}
argv[pos++]=(char*)"--color=auto";
argv[pos]=NULL;}pid_t id =fork();if(id ==0){// 子进程execvp(argv[0],argv);exit(1);}int status =0;pid_t ret =waitpid(id,&status,0);if(ret >0){
exitCode =WEXITSTATUS(status);// 获取最近一次进程的退出码}}return0;}
🔔效果展示
本章的内容就到这里了,觉得对你有帮助的话就支持一下博主把~
点击下方个人名片,交流会更方便哦~
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
版权归原作者 花想云 所有, 如有侵权,请联系我们删除。