🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、C++
🐶学习方向:C++方向学习爱好者
⭐人生格言:得知坦然 ,失之淡然
🏠博主简介
文章目录
- 前言
- 一、进程程序替换
- 1.1 先看替换效果
- 1.2 替换失败会发生什么
- 1.3 为什么通常让子进程替换
- 二、exec系列接口
- 2.1 接口和命名规律
- 2.2 execl与execlp
- 2.3 execv与execvp
- 2.4 带e的接口与环境变量
- 2.5 execve是系统调用
- 三、自定义Shell
- 3.1 Shell的执行流程
- 3.2 打印提示符
- 3.3 获取并解析命令
- 3.4 普通命令与内建命令
- 四、完整my_shell.cc
- 总结
前言
前面我们已经学习了进程创建、进程终止和进程等待,本文继续看进程程序替换。
程序替换解决的问题很直接:一个进程已经被创建出来了,能不能让它去执行另外一个程序?Linux提供了exec系列接口。后面写简易Shell时,外部命令也是通过这组接口执行的。
本文还是按学习顺序来写:先看程序替换现象,再认识exec接口,最后把fork + exec + waitpid串起来。
一、进程程序替换
1.1 先看替换效果
下面先看execl的使用:
#include<stdio.h>#include<unistd.h>intmain(){printf("我的程序要运行了\n");execl("/usr/bin/ls","ls","-l","-a",NULL);printf("我的程序运行完毕了\n");return0;}运行结果如下:
我们运行的是自己的程序,最后执行的却是ls -l -a,这就是程序替换。
进程可以简单理解为“内核数据结构 + 代码和数据”。调用exec成功以后,并不会创建新进程,原来的PID也不变;变化的是当前进程用户空间中的代码和数据,它们被新程序重新建立。
所以第一个printf可以执行,第二个却不会执行。因为execl成功后,当前进程已经开始运行ls,原程序后面的代码被替换掉了。
exec成功后不会返回,只有失败才返回
-1。
1.2 替换失败会发生什么
如果路径写错,新程序无法加载,execl就会返回:
intret=execl("/usr/bn/ls","ls","-l",NULL);printf("execl failed, ret: %d\n",ret);perror("execl");只要代码还能执行到exec的下一行,就说明替换失败。子进程中一般直接处理错误并退出:
execl("/usr/bin/ls","ls","-l",NULL);perror("execl");_exit(127);1.3 为什么通常让子进程替换
如果当前进程直接调用exec,自己的程序也会被换掉。实际使用时,一般让父进程保留,让子进程执行新程序:
pid_tid=fork();if(id==0){execl("/usr/bin/ls","ls","-l","-a",NULL);perror("execl");_exit(127);}waitpid(id,NULL,0);printf("父进程继续向后执行\n");子进程的程序替换不会影响父进程,因为父子进程具有独立性。fork后父子进程最开始会通过写时拷贝共享部分物理页,子进程执行exec时,内核再为新程序重新建立地址空间。
exec也可以执行我们自己编译的程序:
在替换前后分别打印PID:
std::cout<<"My Pid Is: "<<getpid()<<std::endl;两次PID相同,说明程序换了,但进程没有重新创建。
二、exec系列接口
2.1 接口和命名规律
常见接口如下:
intexecl(constchar*path,constchar*arg,...);intexeclp(constchar*file,constchar*arg,...);intexecle(constchar*path,constchar*arg,...,char*constenvp[]);intexecv(constchar*path,char*constargv[]);intexecvp(constchar*file,char*constargv[]);intexecve(constchar*path,char*constargv[],char*constenvp[]);intexecvpe(constchar*file,char*constargv[],char*constenvp[]);这几个接口不用死记,名字已经说明了用法:
| 字母 | 含义 | 理解 |
|---|---|---|
l | list | 参数一个一个传 |
v | vector | 参数放进数组 |
p | PATH | 自动到PATH中找程序 |
e | env | 自己传环境变量表 |
我的记忆方式还是两句话:我要执行谁,我要怎么执行它。
2.2 execl与execlp
execl第一个参数必须写程序路径,后面按列表传递参数:
execl("/usr/bin/ls","ls","-l","-a",NULL);路径中的ls用来找到程序,参数中的第一个"ls"会成为新程序的argv[0],二者作用不同。
execlp多了一个p,会按PATH查找程序,因此可以只写文件名:
execlp("ls","ls","-l","-a",NULL);列表形式最后必须传NULL,否则系统不知道参数在哪里结束。
2.3 execv与execvp
v表示参数使用数组:
char*constargv[]={(char*)"ls",(char*)"-l",(char*)"-a",NULL};execv("/usr/bin/ls",argv);// 需要路径execvp(argv[0],argv);// 自动查PATH我们在Shell中输入ls -l -a,Shell会把字符串拆成argv,再调用execvp(argv[0], argv)。所以写简易Shell时,execvp最方便。
2.4 带e的接口与环境变量
execle、execve、execvpe中的e表示由调用者提供环境变量表。原稿中使用execvpe传入一个自定义环境变量:
char*constargv[]={(char*)"other",(char*)"-a",(char*)"-b",NULL};char*constenvp[]={(char*)"MYVAL=123456789",NULL};execvpe("./other",argv,envp);下面是other自己运行时的结果:
显式传入envp后,新程序拿到的是这张新表。只传MYVAL,原来从Shell继承的其他环境变量就不会自动保留。
不带e的接口为什么仍然能拿到环境变量?因为它们默认使用当前进程的全局环境变量表environ。
如果想保留原环境变量,同时增加一项,可以先修改当前进程环境,再执行新程序:
externchar**environ;charnew_env[]="MYVAL=123456789";putenv(new_env);execvpe("./other",argv,environ);putenv可能直接使用传入字符串的地址,所以字符串在使用期间必须有效。只是设置一个键值时,也可以使用setenv。
2.5 execve是系统调用
execl、execlp、execv、execvp等接口形式不同,底层最终都要完成同一件事:调用系统接口加载新程序。Linux中真正的程序替换系统调用是execve。
库函数提供多种形式,只是为了让我们按列表、数组、PATH和环境变量等不同方式传参。
三、自定义Shell
3.1 Shell的执行流程
Shell执行普通命令的流程可以整理成五步:
- 打印提示符并读取命令;
- 把命令字符串拆成
argv; fork创建子进程;- 子进程调用
execvp; - 父进程使用
waitpid等待。
Shell自己不能直接调用execvp,否则Shell也会被新程序替换,执行一条命令后就没了。
3.2 打印提示符
常见提示符由用户名、主机名和当前工作目录组成:
USER、HOSTNAME可以通过getenv获取。工作目录建议使用getcwd,不要一直读取PWD。因为chdir改变的是进程真实目录,我们写的Shell不会自动更新PWD。
3.3 获取并解析命令
这里使用fgets读取一整行,不使用scanf("%s"),因为%s遇到空格就结束。
fgets会把\n一起读进数组,可以这样去掉:
command[strcspn(command,"\n")]='\0';接下来使用strtok把ls -a -l拆成参数数组:
argv最后必须是NULL,这样execvp才知道参数表在哪里结束。
3.4 普通命令与内建命令
普通命令交给子进程执行,父进程等待并保存退出码。cd却不能交给子进程,因为子进程修改目录后马上退出,父Shell的目录不会变化。
所以cd必须由Shell自己调用chdir。同理,export要修改Shell自己的环境变量,也必须是内建命令。
echo $?读取的是上一条外部命令的退出码。父进程通过waitpid得到status后,先用WIFEXITED判断是否正常退出,再用WEXITSTATUS提取退出码。
四、完整my_shell.cc
下面把前面的流程放到一份代码中。这个版本支持普通命令、cd、cd ~、cd -、echo $?、export、env和exit。引号、管道、重定向暂时没有处理。
#include<cstdio>#include<cstdlib>#include<cstring>#include<iostream>#include<string>#include<unistd.h>#include<sys/types.h>#include<sys/wait.h>constintCOMMAND_SIZE=1024;constintARGV_SIZE=64;char*g_argv[ARGV_SIZE];intg_argc=0;intg_lastcode=0;std::string g_oldpwd;externchar**environ;constchar*GetUserName(){constchar*name=getenv("USER");returnname==nullptr?"None":name;}std::stringGetHostName(){constchar*hostname=getenv("HOSTNAME");if(hostname!=nullptr)returnhostname;charbuffer[256];if(gethostname(buffer,sizeof(buffer))==0){buffer[sizeof(buffer)-1]='\0';returnbuffer;}return"None";}std::stringGetCurrentDir(){charcwd[COMMAND_SIZE];if(getcwd(cwd,sizeof(cwd))==nullptr)return"None";returncwd;}std::stringDirName(conststd::string&path){if(path=="/"||path=="None")returnpath;size_t pos=path.rfind('/');returnpos==std::string::npos?path:path.substr(pos+1);}voidPrintCommandPrompt(){std::string host=GetHostName();std::string dir=DirName(GetCurrentDir());printf("[%s@%s %s]# ",GetUserName(),host.c_str(),dir.c_str());fflush(stdout);}boolGetCommandLine(charcommand[]){if(fgets(command,COMMAND_SIZE,stdin)==nullptr)returnfalse;command[strcspn(command,"\n")]='\0';returntrue;}boolParseCommandLine(charcommand[]){memset(g_argv,0,sizeof(g_argv));g_argc=0;char*token=strtok(command," \t");while(token!=nullptr&&g_argc<ARGV_SIZE-1){g_argv[g_argc++]=token;token=strtok(nullptr," \t");}g_argv[g_argc]=nullptr;returng_argc>0;}boolChangeDirectory(){std::string current=GetCurrentDir();std::string target;if(g_argc==1||std::string(g_argv[1])=="~"){constchar*home=getenv("HOME");if(home==nullptr){std::cerr<<"cd: HOME not set"<<std::endl;g_lastcode=1;returntrue;}target=home;}elseif(std::string(g_argv[1])=="-"){if(g_oldpwd.empty()){std::cerr<<"cd: OLDPWD not set"<<std::endl;g_lastcode=1;returntrue;}target=g_oldpwd;std::cout<<target<<std::endl;}else{target=g_argv[1];}if(chdir(target.c_str())!=0){perror("cd");g_lastcode=1;returntrue;}g_oldpwd=current;std::string pwd=GetCurrentDir();setenv("OLDPWD",g_oldpwd.c_str(),1);setenv("PWD",pwd.c_str(),1);g_lastcode=0;returntrue;}boolExportEnv(constchar*item){constchar*equal=strchr(item,'=');if(equal==nullptr||equal==item)returnfalse;std::stringname(item,equal-item);std::stringvalue(equal+1);returnsetenv(name.c_str(),value.c_str(),1)==0;}boolCheckAndExecBuiltin(){if(g_argv[0]==nullptr)returntrue;std::string cmd=g_argv[0];if(cmd=="cd")returnChangeDirectory();if(cmd=="echo"){if(g_argc==2&&std::string(g_argv[1])=="$?"){std::cout<<g_lastcode<<std::endl;}elseif(g_argc==2&&g_argv[1][0]=='$'){constchar*value=getenv(g_argv[1]+1);if(value!=nullptr)std::cout<<value;std::cout<<std::endl;}else{for(inti=1;i<g_argc;i++){if(i>1)std::cout<<' ';std::cout<<g_argv[i];}std::cout<<std::endl;}g_lastcode=0;returntrue;}if(cmd=="export"){if(g_argc!=2||!ExportEnv(g_argv[1])){std::cerr<<"export: usage: export NAME=VALUE"<<std::endl;g_lastcode=1;}elseg_lastcode=0;returntrue;}if(cmd=="env"){for(inti=0;environ[i]!=nullptr;i++){std::cout<<environ[i]<<std::endl;}g_lastcode=0;returntrue;}if(cmd=="exit"){intcode=g_argc==2?atoi(g_argv[1]):g_lastcode;exit(code);}returnfalse;}voidExecuteCommand(){pid_t id=fork();if(id<0){perror("fork");g_lastcode=1;return;}if(id==0){execvp(g_argv[0],g_argv);perror(g_argv[0]);_exit(127);}intstatus=0;if(waitpid(id,&status,0)<0){perror("waitpid");g_lastcode=1;return;}if(WIFEXITED(status))g_lastcode=WEXITSTATUS(status);elseif(WIFSIGNALED(status))g_lastcode=128+WTERMSIG(status);}intmain(){g_oldpwd=GetCurrentDir();charcommand[COMMAND_SIZE];while(true){PrintCommandPrompt();if(!GetCommandLine(command)){if(feof(stdin)){std::cout<<std::endl;break;}clearerr(stdin);continue;}if(!ParseCommandLine(command))continue;if(CheckAndExecBuiltin())continue;ExecuteCommand();}return0;}编译运行:
g++-std=c++11 my_shell.cc-omy_shell ./my_shell可以依次测试:
ls-a-lcd..cd~cd-echo$?exportMYVAL=123456789echo$MYVALenv总结
本文主要整理了下面几件事:
- 程序替换不会创建新进程,调用前后PID不变;
exec成功不返回,失败才返回-1;l表示列表,v表示数组,p表示查找PATH,e表示自己传环境变量;- Shell执行外部命令的流程是
fork + exec + waitpid; cd、export需要修改Shell自身状态,必须做成内建命令;echo $?读取的是父进程保存的上一条命令退出码。
把这条主线理清以后,程序替换、环境变量和Shell的执行流程就能连起来了。
资源分享
【Linux系统篇】从 fork 到 WNOHANG:进程创建与等待机制详解
【Linux进程】程序地址空间详解:虚拟地址、页表、写时拷贝与mm_struct
【Linux排障实战】Docker容器启动失败怎么查:端口、日志、权限与网络