【Linux进程控制】从exec程序替换到手写简易Shell:fork、execvp、环境变量与内建命令
个人主页爱和冰阔乐专栏传送门《数据结构与算法》 、C学习方向C方向学习爱好者⭐人生格言得知坦然 失之淡然博主简介文章目录前言一、进程程序替换1.1 先看替换效果1.2 替换失败会发生什么1.3 为什么通常让子进程替换二、exec系列接口2.1 接口和命名规律2.2 execl与execlp2.3 execv与execvp2.4 带e的接口与环境变量2.5 execve是系统调用三、自定义Shell3.1 Shell的执行流程3.2 打印提示符3.3 获取并解析命令3.4 普通命令与内建命令四、完整my_shell.cc总结前言前面我们已经学习了进程创建、进程终止和进程等待本文继续看进程程序替换。程序替换解决的问题很直接一个进程已经被创建出来了能不能让它去执行另外一个程序Linux提供了exec系列接口。后面写简易Shell时外部命令也是通过这组接口执行的。本文还是按学习顺序来写先看程序替换现象再认识exec接口最后把fork exec waitpid串起来。一、进程程序替换1.1 先看替换效果下面先看execl的使用#includestdio.h#includeunistd.hintmain(){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就会返回intretexecl(/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_tidfork();if(id0){execl(/usr/bin/ls,ls,-l,-a,NULL);perror(execl);_exit(127);}waitpid(id,NULL,0);printf(父进程继续向后执行\n);子进程的程序替换不会影响父进程因为父子进程具有独立性。fork后父子进程最开始会通过写时拷贝共享部分物理页子进程执行exec时内核再为新程序重新建立地址空间。exec也可以执行我们自己编译的程序在替换前后分别打印PIDstd::coutMy 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[]);这几个接口不用死记名字已经说明了用法字母含义理解llist参数一个一个传vvector参数放进数组pPATH自动到PATH中找程序eenv自己传环境变量表我的记忆方式还是两句话我要执行谁我要怎么执行它。2.2 execl与execlpexecl第一个参数必须写程序路径后面按列表传递参数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与execvpv表示参数使用数组char*constargv[]{(char*)ls,(char*)-l,(char*)-a,NULL};execv(/usr/bin/ls,argv);// 需要路径execvp(argv[0],argv);// 自动查PATH我们在Shell中输入ls -l -aShell会把字符串拆成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*)MYVAL123456789,NULL};execvpe(./other,argv,envp);下面是other自己运行时的结果显式传入envp后新程序拿到的是这张新表。只传MYVAL原来从Shell继承的其他环境变量就不会自动保留。不带e的接口为什么仍然能拿到环境变量因为它们默认使用当前进程的全局环境变量表environ。如果想保留原环境变量同时增加一项可以先修改当前进程环境再执行新程序externchar**environ;charnew_env[]MYVAL123456789;putenv(new_env);execvpe(./other,argv,environ);putenv可能直接使用传入字符串的地址所以字符串在使用期间必须有效。只是设置一个键值时也可以使用setenv。2.5 execve是系统调用execl、execlp、execv、execvp等接口形式不同底层最终都要完成同一件事调用系统接口加载新程序。Linux中真正的程序替换系统调用是execve。库函数提供多种形式只是为了让我们按列表、数组、PATH和环境变量等不同方式传参。三、自定义Shell3.1 Shell的执行流程Shell执行普通命令的流程可以整理成五步打印提示符并读取命令把命令字符串拆成argvfork创建子进程子进程调用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。引号、管道、重定向暂时没有处理。#includecstdio#includecstdlib#includecstring#includeiostream#includestring#includeunistd.h#includesys/types.h#includesys/wait.hconstintCOMMAND_SIZE1024;constintARGV_SIZE64;char*g_argv[ARGV_SIZE];intg_argc0;intg_lastcode0;std::string g_oldpwd;externchar**environ;constchar*GetUserName(){constchar*namegetenv(USER);returnnamenullptr?None:name;}std::stringGetHostName(){constchar*hostnamegetenv(HOSTNAME);if(hostname!nullptr)returnhostname;charbuffer[256];if(gethostname(buffer,sizeof(buffer))0){buffer[sizeof(buffer)-1]\0;returnbuffer;}returnNone;}std::stringGetCurrentDir(){charcwd[COMMAND_SIZE];if(getcwd(cwd,sizeof(cwd))nullptr)returnNone;returncwd;}std::stringDirName(conststd::stringpath){if(path/||pathNone)returnpath;size_t pospath.rfind(/);returnposstd::string::npos?path:path.substr(pos1);}voidPrintCommandPrompt(){std::string hostGetHostName();std::string dirDirName(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_argc0;char*tokenstrtok(command, \t);while(token!nullptrg_argcARGV_SIZE-1){g_argv[g_argc]token;tokenstrtok(nullptr, \t);}g_argv[g_argc]nullptr;returng_argc0;}boolChangeDirectory(){std::string currentGetCurrentDir();std::string target;if(g_argc1||std::string(g_argv[1])~){constchar*homegetenv(HOME);if(homenullptr){std::cerrcd: HOME not setstd::endl;g_lastcode1;returntrue;}targethome;}elseif(std::string(g_argv[1])-){if(g_oldpwd.empty()){std::cerrcd: OLDPWD not setstd::endl;g_lastcode1;returntrue;}targetg_oldpwd;std::couttargetstd::endl;}else{targetg_argv[1];}if(chdir(target.c_str())!0){perror(cd);g_lastcode1;returntrue;}g_oldpwdcurrent;std::string pwdGetCurrentDir();setenv(OLDPWD,g_oldpwd.c_str(),1);setenv(PWD,pwd.c_str(),1);g_lastcode0;returntrue;}boolExportEnv(constchar*item){constchar*equalstrchr(item,);if(equalnullptr||equalitem)returnfalse;std::stringname(item,equal-item);std::stringvalue(equal1);returnsetenv(name.c_str(),value.c_str(),1)0;}boolCheckAndExecBuiltin(){if(g_argv[0]nullptr)returntrue;std::string cmdg_argv[0];if(cmdcd)returnChangeDirectory();if(cmdecho){if(g_argc2std::string(g_argv[1])$?){std::coutg_lastcodestd::endl;}elseif(g_argc2g_argv[1][0]$){constchar*valuegetenv(g_argv[1]1);if(value!nullptr)std::coutvalue;std::coutstd::endl;}else{for(inti1;ig_argc;i){if(i1)std::cout ;std::coutg_argv[i];}std::coutstd::endl;}g_lastcode0;returntrue;}if(cmdexport){if(g_argc!2||!ExportEnv(g_argv[1])){std::cerrexport: usage: export NAMEVALUEstd::endl;g_lastcode1;}elseg_lastcode0;returntrue;}if(cmdenv){for(inti0;environ[i]!nullptr;i){std::coutenviron[i]std::endl;}g_lastcode0;returntrue;}if(cmdexit){intcodeg_argc2?atoi(g_argv[1]):g_lastcode;exit(code);}returnfalse;}voidExecuteCommand(){pid_t idfork();if(id0){perror(fork);g_lastcode1;return;}if(id0){execvp(g_argv[0],g_argv);perror(g_argv[0]);_exit(127);}intstatus0;if(waitpid(id,status,0)0){perror(waitpid);g_lastcode1;return;}if(WIFEXITED(status))g_lastcodeWEXITSTATUS(status);elseif(WIFSIGNALED(status))g_lastcode128WTERMSIG(status);}intmain(){g_oldpwdGetCurrentDir();charcommand[COMMAND_SIZE];while(true){PrintCommandPrompt();if(!GetCommandLine(command)){if(feof(stdin)){std::coutstd::endl;break;}clearerr(stdin);continue;}if(!ParseCommandLine(command))continue;if(CheckAndExecBuiltin())continue;ExecuteCommand();}return0;}编译运行g-stdc11 my_shell.cc-omy_shell ./my_shell可以依次测试ls-a-lcd..cd~cd-echo$?exportMYVAL123456789echo$MYVALenv总结本文主要整理了下面几件事程序替换不会创建新进程调用前后PID不变exec成功不返回失败才返回-1l表示列表v表示数组p表示查找PATHe表示自己传环境变量Shell执行外部命令的流程是fork exec waitpidcd、export需要修改Shell自身状态必须做成内建命令echo $?读取的是父进程保存的上一条命令退出码。把这条主线理清以后程序替换、环境变量和Shell的执行流程就能连起来了。资源分享【Linux系统篇】从 fork 到 WNOHANG进程创建与等待机制详解【Linux进程】程序地址空间详解虚拟地址、页表、写时拷贝与mm_struct【Linux排障实战】Docker容器启动失败怎么查端口、日志、权限与网络