app开发和网站开发的区别,最吉祥的公司名字大全,自助微信小程序开发教程,网络技术工程师文章目录进程程序替换与shell实现#xff1a;从fork到exec的完整闭环一、进程程序替换1.1 为什么需要程序替换1.1.1 shell如何执行命令1.2 程序替换的原理1.3 exec函数族详解1.3.1 命名规律1.3.2 六个函数的对比1.4 exec函数使用示例1.4.1 基本使用#xff1a;execl1.4.2 使用…文章目录进程程序替换与shell实现从fork到exec的完整闭环一、进程程序替换1.1 为什么需要程序替换1.1.1 shell如何执行命令1.2 程序替换的原理1.3 exec函数族详解1.3.1 命名规律1.3.2 六个函数的对比1.4 exec函数使用示例1.4.1 基本使用execl1.4.2 使用execlp简化路径1.4.3 使用execv传递参数数组1.4.4 fork exec让子进程执行新程序1.4.5 使用execle传递自定义环境变量1.5 exec调用关系图二、实现mini-shell2.1 shell的工作原理2.2 内建命令 vs 外部命令2.2.1 什么是内建命令2.2.2 为什么cd必须是内建命令2.2.3 为什么export必须是内建命令2.2.4 常见的内建命令2.3 命令行解析2.4 完整的mini-shell实现2.5 代码详解2.5.1 命令提示符的生成2.5.2 命令行解析2.5.3 内建命令cd的实现2.5.4 内建命令export的实现2.5.5 外部命令的执行2.6 编译和运行2.7 与真实shell的差距三、总结进程与函数的类比3.1 call/return vs fork/exec/wait3.2 进程通信的思想3.3 Unix哲学的体现四、总结与展望进程程序替换与shell实现从fork到exec的完整闭环欢迎讨论这是Linux系统编程系列的第六篇文章。在前五篇中我们学习了进程的创建fork、状态管理和资源回收wait/waitpid。但fork出的子进程只能执行父进程的代码副本如果我们想让子进程执行一个全新的程序该怎么办这就是本篇要深入讲解的进程程序替换技术。更重要的是我们将把fork、exec、wait三大核心技术结合起来实现一个真正的命令行解释器点赞、收藏与分享这篇文章包含了大量原理分析和一个完整的shell实现如果对你有帮助请点赞、收藏并分享循序渐进建议先学习前五篇文章理解fork、进程状态和wait机制这样学习本篇会更轻松。一、进程程序替换1.1 为什么需要程序替换在学习程序替换之前我们先思考一个问题fork创建的子进程有什么局限性让我们回顾一下fork的行为intmain(){printf(父进程开始\n);pid_t idfork();if(id0){// 子进程执行的还是父进程的代码printf(我是子进程\n);}else{printf(我是父进程\n);}return0;}fork后子进程获得了父进程的代码副本它执行的仍然是父进程程序的代码。虽然我们可以通过if-else让父子执行不同的代码分支但本质上它们运行的是同一个程序的代码。那么问题来了如果我想让子进程执行一个完全不同的程序比如执行ls命令该怎么办这时就需要**程序替换(Program Replacement)**技术。1.1.1 shell如何执行命令让我们看一个日常操作$ls-l total64-rwxr-xr-x1user user8960Dec1010:30 a.out -rw-r--r--1user user256Dec1010:25 test.c当你在shell中输入ls -l时发生了什么shell(bash)是一个进程它读取你输入的命令shell调用fork()创建子进程子进程调用exec加载ls程序子进程开始执行ls的代码而不是bash的代码父进程(shell)调用wait()等待子进程完成这就是程序替换的典型应用场景。1.2 程序替换的原理程序替换的本质是将磁盘上的一个程序加载到当前进程的地址空间替换掉原有的代码和数据。让我们从内存的角度来理解这个过程替换前子进程刚fork出来子进程地址空间 ┌──────────────┐ │ 命令行参数 │ ├──────────────┤ │ 环境变量 │ ├──────────────┤ │ 栈 │ ← 父进程代码的栈 │ ↓ │ │ │ │ ↑ │ │ 堆 │ ← 父进程的堆数据 ├──────────────┤ │ 未初始化数据 │ ← 父进程的BSS段 ├──────────────┤ │ 初始化数据 │ ← 父进程的数据段 ├──────────────┤ │ 代码段 │ ← 父进程的代码 └──────────────┘调用exec后子进程地址空间 ┌──────────────┐ │ 命令行参数 │ ← 新程序的参数 ├──────────────┤ │ 环境变量 │ ← 可以继承或重新设置 ├──────────────┤ │ 栈 │ ← 新程序的栈 │ ↓ │ │ │ │ ↑ │ │ 堆 │ ← 新程序的堆 ├──────────────┤ │ 未初始化数据 │ ← 新程序的BSS段 ├──────────────┤ │ 初始化数据 │ ← 新程序的数据段 ├──────────────┤ │ 代码段 │ ← 新程序的代码 └──────────────┘关键点进程ID不变还是同一个进程代码和数据被完全替换原来父进程的代码不见了文件描述符表继承打开的文件仍然有效除非设置了FD_CLOEXEC从新程序的main函数开始执行1.3 exec函数族详解Linux提供了6个exec系列函数它们都用于程序替换但参数形式不同#includeunistd.hintexecl(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[]);返回值成功不返回因为当前进程的代码已经被替换了失败返回-1并设置errno这6个函数看起来很复杂但只要掌握了命名规律就很好记。1.3.1 命名规律函数名由exec加上1-2个字母后缀组成每个字母都有特定含义l (list)参数列表参数以列表形式逐个传递必须以NULL结尾execl(/bin/ls,ls,-l,-a,NULL);// 程序路径 arg0 arg1 arg2 结束标记v (vector)参数数组参数放在一个字符指针数组中char*argv[]{ls,-l,-a,NULL};execv(/bin/ls,argv);p (path)搜索PATH环境变量不需要写完整路径会在PATH中搜索// 不用写/bin/ls只写ls即可execlp(ls,ls,-l,NULL);e (environment)自定义环境变量可以传递自定义的环境变量表char*envp[]{PATH/bin:/usr/bin,HOME/home/user,NULL};execle(/bin/ls,ls,-l,NULL,envp);注意这里子进程会完全替换父进程的环境变量只会使用你传入的envp里面的环境变量记忆技巧exec l/v p e ↓ ↓ ↓ 参数形式 路径 环境1.3.2 六个函数的对比函数路径参数形式环境变量execl完整路径列表继承execlp搜索PATH列表继承execle完整路径列表自定义execv完整路径数组继承execvp搜索PATH数组继承execve完整路径数组自定义注意只有execve是真正的系统调用其他5个都是库函数最终都会调用execve。1.4 exec函数使用示例让我们通过实际例子来学习如何使用这些函数。1.4.1 基本使用execl#includestdio.h#includeunistd.hintmain(){printf(程序开始PID%d\n,getpid());printf(即将执行ls命令\n);// 替换当前进程为ls程序execl(/bin/ls,ls,-l,-h,NULL);// 如果exec成功下面的代码不会执行printf(如果你看到这句话说明exec失败了\n);perror(execl);return1;}运行结果$ gcc test.c -otest$ ./test 程序开始PID15000即将执行ls命令 total 64K -rwxr-xr-x1user user8.8K Dec1010:30test-rw-r--r--1user user256Dec1010:25 test.c关键点printf(如果你看到...)没有执行因为进程已经被替换PID没变还是15000执行的是ls的代码不是test的代码了1.4.2 使用execlp简化路径#includestdio.h#includeunistd.hintmain(){printf(使用execlp执行命令\n);// 不需要写/bin/ls系统会在PATH中查找execlp(ls,ls,-l,NULL);perror(execlp);return1;}1.4.3 使用execv传递参数数组#includestdio.h#includeunistd.hintmain(){printf(使用execv执行命令\n);// 参数放在数组中char*argv[]{ls,-l,-a,-h,NULL};execv(/bin/ls,argv);perror(execv);return1;}1.4.4 fork exec让子进程执行新程序这是最常见的用法#includestdio.h#includestdlib.h#includeunistd.h#includesys/wait.hintmain(){printf(父进程[%d]开始\n,getpid());pid_t idfork();if(id0){perror(fork);return1;}elseif(id0){// 子进程执行ls命令printf(子进程[%d]即将执行ls\n,getpid());execlp(ls,ls,-l,NULL);// 如果exec失败才会执行到这里perror(execlp);exit(1);}else{// 父进程等待子进程printf(父进程[%d]等待子进程[%d]\n,getpid(),id);intstatus0;waitpid(id,status,0);if(WIFEXITED(status)){printf(子进程退出退出码%d\n,WEXITSTATUS(status));}printf(父进程继续运行\n);}return0;}运行结果父进程[15100]开始 父进程[15100]等待子进程[15101]子进程[15101]即将执行ls total64-rwxr-xr-x1user user9216Dec1011:00test-rw-r--r--1user user512Dec1011:00 test.c 子进程退出退出码0父进程继续运行这就是shell执行命令的基本模型fork exec wait1.4.5 使用execle传递自定义环境变量#includestdio.h#includestdlib.h#includeunistd.hintmain(){printf(演示execle传递环境变量\n);// 自定义环境变量char*envp[]{PATH/bin:/usr/bin,MY_VARhello,USERtestuser,NULL};// 创建一个简单的测试程序来接收环境变量pid_t idfork();if(id0){execle(./printenv,printenv,NULL,envp);perror(execle);exit(1);}else{wait(NULL);}return0;}创建接收程序printenv.c#includestdio.h#includestdlib.hintmain(){printf(MY_VAR %s\n,getenv(MY_VAR));printf(USER %s\n,getenv(USER));printf(PATH %s\n,getenv(PATH));return0;}编译运行$ gcc printenv.c -oprintenv$ gcc test.c -otest$ ./test 演示execle传递环境变量 MY_VARhelloUSERtestuserPATH/bin:/usr/bin1.5 exec调用关系图让我们通过一张图来理解这6个函数之间的关系用户调用 ↓ ┌──────┬──────┬──────┬──────┬───────┐ │execl │execlp│execle│execv │execvp │ └──┬───┴──┬───┴──┬───┴──┬───┴───┬───┘ │ │ │ │ │ └──────┴──────┴──────┴───────┘ ↓ 参数处理/路径搜索 ↓ ┌────────┐ │ execve │ ← 唯一的系统调用 └────┬───┘ ↓ 内核加载程序核心要点execve是唯一的系统调用其他5个都是库函数最终调用execve库函数做的工作参数格式转换、PATH搜索、环境变量处理二、实现mini-shell现在我们已经掌握了fork、exec、wait三大技术是时候把它们组合起来实现一个真正的命令行解释器了2.1 shell的工作原理shell的核心工作流程非常简单while(true){1. 显示命令提示符2. 读取用户输入的命令3. 解析命令分割成程序名和参数4. fork创建子进程5. 子进程exec执行命令6. 父进程wait等待子进程}让我们用伪代码表示while(1){// 1. 显示提示符printf([userhost dir]$ );// 2. 读取命令fgets(command,sizeof(command),stdin);// 3. 解析命令parse(command,argv);// 4. 创建子进程pid_t idfork();if(id0){// 5. 子进程执行命令execvp(argv[0],argv);exit(1);}else{// 6. 父进程等待waitpid(id,status,0);}}但实际实现要考虑更多细节比如如何显示美观的命令提示符如何处理内建命令(cd、export等)如何维护环境变量2.2 内建命令 vs 外部命令在实现shell之前我们需要理解一个重要概念内建命令(Built-in Command)。2.2.1 什么是内建命令Linux命令分为两类外部命令是独立的可执行文件如ls对应/bin/lsps对应/bin/psshell通过forkexec执行内建命令是shell程序内部的函数如cd、export、exitshell直接调用自己的函数执行2.2.2 为什么cd必须是内建命令让我们思考一个问题为什么cd不能做成外部命令假设cd是一个外部程序/bin/cd// shell执行cd命令的流程pid_t idfork();// 创建子进程if(id0){// 子进程execl(/bin/cd,cd,/home/user,NULL);// cd程序调用chdir()改变工作目录// 但这只改变了子进程的工作目录}waitpid(id,NULL,0);// 父进程(shell)的工作目录没有改变问题在于子进程调用chdir()只改变自己的工作目录父进程(shell)的工作目录不受影响子进程退出后shell还在原来的目录因此cd必须由shell自己执行// shell内部直接调用if(strcmp(argv[0],cd)0){chdir(argv[1]);// shell进程自己改变目录}2.2.3 为什么export必须是内建命令同样的道理// 如果export是外部命令pid_t idfork();if(id0){// 子进程设置环境变量setenv(MY_VAR,value,1);// 只影响子进程的环境变量表}// 父进程(shell)的环境变量表没有改变环境变量属于进程的私有数据子进程无法修改父进程的环境变量表。因此export也必须由shell自己执行if(strcmp(argv[0],export)0){// shell自己添加环境变量putenv(argv[1]);}2.2.4 常见的内建命令命令原因cd必须改变shell自己的工作目录export必须修改shell自己的环境变量exit必须终止shell自己alias修改shell的命令别名表source在shell进程中执行脚本jobs查看shell的作业控制表2.3 命令行解析shell需要将用户输入的字符串解析成程序名和参数数组。输入ls -l -a /home/user输出argv[0]lsargv[1]-largv[2]-aargv[3]/home/userargv[4]NULL使用strtok()函数可以轻松实现voidParseCommandLine(char*cmdline,char**argv){intargc0;constchar*sep \t\n;// 分隔符argv[argc]strtok(cmdline,sep);while((argv[argc]strtok(NULL,sep))!NULL);argc--;// 最后一个NULL不计入}2.4 完整的mini-shell实现现在让我们来实现一个功能完整的mini-shell#includeiostream#includecstdio#includecstdlib#includecstring#includestring#includeunistd.h#includesys/types.h#includesys/wait.h#includectype.husingnamespacestd;constintCMD_SIZE1024;constintARGC_MAX64;constintENV_MAX64;// 全局变量char*g_argv[ARGC_MAX];// 命令参数数组intg_argc0;// 参数个数char*g_env[ENV_MAX];// 环境变量表intg_last_code0;// 上一个命令的退出码charg_pwd[CMD_SIZE];// 当前工作目录charg_pwd_env[CMD_SIZE];// PWD环境变量// 获取用户名stringGetUserName(){constchar*namegetenv(USER);returnname?name:unknown;}// 获取主机名stringGetHostName(){constchar*hostnamegetenv(HOSTNAME);returnhostname?hostname:localhost;}// 获取当前工作目录stringGetPwd(){if(getcwd(g_pwd,sizeof(g_pwd))!NULL){// 更新PWD环境变量snprintf(g_pwd_env,sizeof(g_pwd_env),PWD%s,g_pwd);putenv(g_pwd_env);returng_pwd;}return/;}// 获取当前目录的最后一级stringLastDir(){string pwdGetPwd();if(pwd/)return/;size_t pospwd.rfind(/);if(posstring::npos)returnpwd;returnpwd.substr(pos1);}// 生成命令提示符stringMakePrompt(){charprompt[CMD_SIZE];snprintf(prompt,sizeof(prompt),[%s%s %s]$ ,GetUserName().c_str(),GetHostName().c_str(),LastDir().c_str());returnprompt;}// 显示命令提示符voidPrintPrompt(){printf(%s,MakePrompt().c_str());fflush(stdout);}// 读取命令行boolGetCommandLine(char*cmdline,intsize){char*retfgets(cmdline,size,stdin);if(retNULL){returnfalse;}// 去掉换行符cmdline[strlen(cmdline)-1]\0;// 空命令if(strlen(cmdline)0){returnfalse;}returntrue;}// 解析命令行voidParseCommandLine(char*cmdline){memset(g_argv,0,sizeof(g_argv));g_argc0;constchar*sep \t;g_argv[g_argc]strtok(cmdline,sep);while((g_argv[g_argc]strtok(NULL,sep))!NULL);g_argc--;}// 添加环境变量voidAddEnv(constchar*item){inti0;while(g_env[i]!NULL)i;g_env[i](char*)malloc(strlen(item)1);strcpy(g_env[i],item);g_env[i]NULL;}// 内建命令cdboolBuiltinCd(){if(strcmp(g_argv[0],cd)!0){returnfalse;}if(g_argc1){// cd without argument, go to homeconstchar*homegetenv(HOME);if(home)chdir(home);}elseif(g_argc2){if(chdir(g_argv[1])!0){perror(cd);g_last_code1;}else{g_last_code0;}}else{printf(cd: too many arguments\n);g_last_code1;}returntrue;}// 内建命令exportboolBuiltinExport(){if(strcmp(g_argv[0],export)!0){returnfalse;}if(g_argc2){AddEnv(g_argv[1]);g_last_code0;}else{printf(Usage: export VARVALUE\n);g_last_code1;}returntrue;}// 内建命令echoboolBuiltinEcho(){if(strcmp(g_argv[0],echo)!0){returnfalse;}if(g_argc2){if(strcmp(g_argv[1],$?)0){printf(%d\n,g_last_code);}elseif(g_argv[1][0]$){// echo $VARconstchar*valgetenv(g_argv[1]1);if(val){printf(%s\n,val);}}else{printf(%s\n,g_argv[1]);}g_last_code0;}else{printf(Usage: echo STRING or echo $VAR\n);g_last_code1;}returntrue;}// 内建命令envboolBuiltinEnv(){if(strcmp(g_argv[0],env)!0){returnfalse;}for(inti0;g_env[i]!NULL;i){printf(%s\n,g_env[i]);}g_last_code0;returntrue;}// 检查并执行内建命令boolCheckAndExecBuiltin(){returnBuiltinCd()||BuiltinExport()||BuiltinEcho()||BuiltinEnv();}// 执行外部命令boolExecuteCommand(){pid_t idfork();if(id0){perror(fork);returnfalse;}elseif(id0){// 子进程执行命令execvpe(g_argv[0],g_argv,g_env);// 如果execvpe返回说明执行失败perror(g_argv[0]);exit(127);// 命令未找到}else{// 父进程等待子进程intstatus0;pid_t retwaitpid(id,status,0);if(ret0){if(WIFEXITED(status)){g_last_codeWEXITSTATUS(status);}else{g_last_code128WTERMSIG(status);}returntrue;}}returnfalse;}// 初始化环境变量从父shell继承voidInitEnv(){externchar**environ;inti0;while(environ[i]!NULL){g_env[i](char*)malloc(strlen(environ[i])1);strcpy(g_env[i],environ[i]);i;}g_env[i]NULL;}// 主函数intmain(){// 初始化环境变量InitEnv();charcmdline[CMD_SIZE];while(true){// 1. 显示命令提示符PrintPrompt();// 2. 读取命令行if(!GetCommandLine(cmdline,CMD_SIZE)){continue;}// 3. 解析命令ParseCommandLine(cmdline);// 4. 检查是否是内建命令if(CheckAndExecBuiltin()){continue;}// 5. 执行外部命令ExecuteCommand();}return0;}2.5 代码详解让我们逐个模块分析这个shell的实现。2.5.1 命令提示符的生成stringMakePrompt(){charprompt[CMD_SIZE];snprintf(prompt,sizeof(prompt),[%s%s %s]$ ,GetUserName().c_str(),GetHostName().c_str(),LastDir().c_str());returnprompt;}生成类似bash的提示符[userhostname dir]$关键技术GetUserName()从环境变量USER获取用户名GetHostName()从环境变量HOSTNAME获取主机名LastDir()提取当前路径的最后一级目录名2.5.2 命令行解析voidParseCommandLine(char*cmdline){memset(g_argv,0,sizeof(g_argv));g_argc0;constchar*sep \t;g_argv[g_argc]strtok(cmdline,sep);while((g_argv[g_argc]strtok(NULL,sep))!NULL);g_argc--;}工作流程使用strtok()按空格和制表符分割字符串将分割结果存入g_argv数组最后一个元素设为NULLexec要求示例输入ls -l -a输出g_argv{ls,-l,-a, NULL}2.5.3 内建命令cd的实现boolBuiltinCd(){if(strcmp(g_argv[0],cd)!0){returnfalse;// 不是cd命令}if(g_argc1){// cd without argument, go to homeconstchar*homegetenv(HOME);if(home)chdir(home);}elseif(g_argc2){if(chdir(g_argv[1])!0){perror(cd);g_last_code1;}else{g_last_code0;}}else{printf(cd: too many arguments\n);g_last_code1;}returntrue;}实现要点shell进程自己调用chdir()改变工作目录支持cd回到HOME和cd 目录两种用法更新退出码g_last_code2.5.4 内建命令export的实现boolBuiltinExport(){if(strcmp(g_argv[0],export)!0){returnfalse;}if(g_argc2){AddEnv(g_argv[1]);// 添加到环境变量表g_last_code0;}else{printf(Usage: export VARVALUE\n);g_last_code1;}returntrue;}voidAddEnv(constchar*item){inti0;while(g_env[i]!NULL)i;g_env[i](char*)malloc(strlen(item)1);strcpy(g_env[i],item);g_env[i]NULL;}实现要点将新环境变量添加到g_env数组子进程通过execvpe()的第三个参数获得这些环境变量2.5.5 外部命令的执行boolExecuteCommand(){pid_t idfork();if(id0){perror(fork);returnfalse;}elseif(id0){// 子进程执行命令execvpe(g_argv[0],g_argv,g_env);// 如果execvpe返回说明执行失败perror(g_argv[0]);exit(127);// 命令未找到}else{// 父进程等待子进程intstatus0;pid_t retwaitpid(id,status,0);if(ret0){if(WIFEXITED(status)){g_last_codeWEXITSTATUS(status);}else{g_last_code128WTERMSIG(status);}returntrue;}}returnfalse;}这是fork exec wait的完美结合fork()创建子进程execvpe()子进程加载新程序自动搜索PATH传递命令参数传递环境变量waitpid()父进程等待子进程获取退出码2.6 编译和运行编译mini-shellg -o myshell myshell.cpp -stdc11运行./myshell测试示例[userlocalhost test]$ls-l total16-rwxr-xr-x1user user13824Dec1015:30 myshell -rw-r--r--1user user4096Dec1015:25 myshell.cpp[userlocalhost test]$pwd/home/user/test[userlocalhost test]$cd/tmp[userlocalhost tmp]$pwd/tmp[userlocalhost tmp]$exportMY_VARhello[userlocalhost tmp]$echo$MY_VARhello[userlocalhost tmp]$echo$?0[userlocalhost tmp]$ls/nonexist ls: cannot access/nonexist:No suchfileor directory[userlocalhost tmp]$echo$?2[userlocalhost tmp]$psPID TTY TIME CMD15500pts/0 00:00:00bash15600pts/0 00:00:00 myshell15601pts/0 00:00:00ps可以看到我们的mini-shell已经能够✅ 显示漂亮的命令提示符✅ 执行外部命令ls、pwd、ps等✅ 实现内建命令cd、export、echo✅ 维护环境变量✅ 记录命令退出码2.7 与真实shell的差距我们的mini-shell虽然功能完备但与真实的bash相比还有很多不足缺少的功能重定向ls file.txt、cat input.txt管道ps aux | grep myshell后台执行sleep 100 信号处理CtrlC不应该终止shell命令历史上下箭头翻历史命令Tab补全按Tab自动补全命令通配符ls *.txt条件执行ls pwd、ls || echo failed脚本执行source script.sh这些功能的实现会涉及到更多的系统编程知识如文件描述符重定向dup2管道pipe信号处理signal终端控制termios这些知识我们都会在后续文章中逐渐讲解三、总结进程与函数的类比通过本篇文章的学习我们完成了从fork到exec再到shell的完整闭环。现在让我们站在更高的层次来理解这些技术。3.1 call/return vs fork/exec/wait我们在编程时经常使用函数intadd(inta,intb){returnab;}intmain(){intresultadd(3,5);// 调用函数printf(result %d\n,result);return0;}函数调用的特点call调用函数传递参数执行函数执行自己的代码return返回结果给调用者这个模式与进程的使用非常相似intmain(){pid_t idfork();// 创建进程if(id0){execl(/bin/ls,ls,-l,NULL);// 执行程序传递参数exit(1);// 返回退出码}else{intstatus;waitpid(id,status,0);// 等待结果intcodeWEXITSTATUS(status);printf(exit code %d\n,code);}return0;}进程使用的特点fork exec创建进程加载程序传递参数执行子进程执行自己的代码exit wait子进程返回退出码父进程获取结果3.2 进程通信的思想函数之间通过参数和返回值通信函数A → 调用函数B(参数)→ 函数B → 返回结果 → 函数A进程之间也通过类似的方式通信进程A → forkexec(参数)→ 进程B → exit(退出码)→ wait(获取结果)→ 进程A这种模式的优势模块化每个程序专注于一个任务复用性程序可以被多个父进程调用隔离性子进程崩溃不影响父进程并发性多个子进程可以并发执行3.3 Unix哲学的体现我们实现的mini-shell体现了Unix的设计哲学“Write programs that do one thing and do it well. Write programs to work together.”“编写只做一件事并做好的程序。编写能协同工作的程序。”具体体现ls专注于列出文件grep专注于搜索文本sort专注于排序shell负责组合它们ls | grep test | sort这种设计让系统变得灵活可以任意组合命令强大简单命令组合出复杂功能可维护每个程序职责单一易于理解和修改四、总结与展望通过本篇文章我们系统地学习了进程程序替换和shell的实现原理进程程序替换理解了exec的作用将磁盘程序加载到进程地址空间掌握了exec函数族的使用和命名规律理解了fork exec wait的完整流程mini-shell实现掌握了shell的基本工作原理理解了内建命令与外部命令的区别实现了一个功能完整的命令行解释器理解了为什么cd、export必须是内建命令核心知识点exec替换当前进程不创建新进程execve是唯一的系统调用其他都是库函数内建命令修改shell自身状态外部命令在子进程执行fork exec wait是进程协作的经典模式至此我们已经完整学习了Linux进程控制的核心技术。从第一篇的进程概念到第二篇的进程状态再到第三篇的调度算法第四篇的虚拟内存第五篇的进程等待以及本篇的程序替换——我们构建了一个完整的进程管理知识体系。在后续的文章中我们将学习更高级的主题进程间通信IPC管道、共享内存、消息队列信号机制进程如何响应异步事件守护进程后台服务的实现原理线程编程多线程与多进程的选择思考题为什么exec函数成功时不返回只有失败时才返回如果在exec之前打开了一个文件exec之后文件描述符还有效吗如何在mini-shell中实现管道功能ls | grep test如果shell执行一个很慢的命令如何让shell在等待期间响应CtrlC以上就是关于进程程序替换与shell实现的全部内容至此我们已经掌握了进程控制的完整技术栈可以开始更高级的系统编程之旅了