news 2026/7/5 2:57:06

【Linux进程控制】从exec程序替换到手写简易Shell:fork、execvp、环境变量与内建命令

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【Linux进程控制】从exec程序替换到手写简易Shell:fork、execvp、环境变量与内建命令

🔥个人主页:爱和冰阔乐
📚专栏传送门:《数据结构与算法》 、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[]);

这几个接口不用死记,名字已经说明了用法:

字母含义理解
llist参数一个一个传
vvector参数放进数组
pPATH自动到PATH中找程序
eenv自己传环境变量表

我的记忆方式还是两句话:我要执行谁,我要怎么执行它。

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的接口与环境变量

execleexecveexecvpe中的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是系统调用

execlexeclpexecvexecvp等接口形式不同,底层最终都要完成同一件事:调用系统接口加载新程序。Linux中真正的程序替换系统调用是execve

库函数提供多种形式,只是为了让我们按列表、数组、PATH和环境变量等不同方式传参。

三、自定义Shell

3.1 Shell的执行流程

Shell执行普通命令的流程可以整理成五步:

  1. 打印提示符并读取命令;
  2. 把命令字符串拆成argv
  3. fork创建子进程;
  4. 子进程调用execvp
  5. 父进程使用waitpid等待。

Shell自己不能直接调用execvp,否则Shell也会被新程序替换,执行一条命令后就没了。

3.2 打印提示符

常见提示符由用户名、主机名和当前工作目录组成:

USERHOSTNAME可以通过getenv获取。工作目录建议使用getcwd,不要一直读取PWD。因为chdir改变的是进程真实目录,我们写的Shell不会自动更新PWD

3.3 获取并解析命令

这里使用fgets读取一整行,不使用scanf("%s"),因为%s遇到空格就结束。

fgets会把\n一起读进数组,可以这样去掉:

command[strcspn(command,"\n")]='\0';

接下来使用strtokls -a -l拆成参数数组:

argv最后必须是NULL,这样execvp才知道参数表在哪里结束。

3.4 普通命令与内建命令

普通命令交给子进程执行,父进程等待并保存退出码。cd却不能交给子进程,因为子进程修改目录后马上退出,父Shell的目录不会变化。

所以cd必须由Shell自己调用chdir。同理,export要修改Shell自己的环境变量,也必须是内建命令。

echo $?读取的是上一条外部命令的退出码。父进程通过waitpid得到status后,先用WIFEXITED判断是否正常退出,再用WEXITSTATUS提取退出码。

四、完整my_shell.cc

下面把前面的流程放到一份代码中。这个版本支持普通命令、cdcd ~cd -echo $?exportenvexit。引号、管道、重定向暂时没有处理。

#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

总结

本文主要整理了下面几件事:

  1. 程序替换不会创建新进程,调用前后PID不变;
  2. exec成功不返回,失败才返回-1
  3. l表示列表,v表示数组,p表示查找PATHe表示自己传环境变量;
  4. Shell执行外部命令的流程是fork + exec + waitpid
  5. cdexport需要修改Shell自身状态,必须做成内建命令;
  6. echo $?读取的是父进程保存的上一条命令退出码。

把这条主线理清以后,程序替换、环境变量和Shell的执行流程就能连起来了。

资源分享
【Linux系统篇】从 fork 到 WNOHANG:进程创建与等待机制详解

【Linux进程】程序地址空间详解:虚拟地址、页表、写时拷贝与mm_struct

【Linux排障实战】Docker容器启动失败怎么查:端口、日志、权限与网络

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/5 2:55:00

国家中小学智慧教育平台电子课本下载完整指南:三步获取PDF教材

国家中小学智慧教育平台电子课本下载完整指南&#xff1a;三步获取PDF教材 【免费下载链接】tchMaterial-parser 国家中小学智慧教育平台 电子课本下载工具&#xff0c;帮助您从智慧教育平台中获取电子课本的 PDF 文件网址并进行下载&#xff0c;让您更方便地获取课本内容。 …

作者头像 李华
网站建设 2026/7/5 2:54:29

开题高效撰写新解法:okbiye 一站式 AI 开题功能化解科研入门难题

okbiye-免费查重复率aigc检测/开题报告/毕业论文/智能排版/文献综述/科研绘图开题报告 - Okbiye智能写作https://www.okbiye.com/ai/ktbg 引言&#xff1a;开题报告&#xff0c;科研路上第一道棘手关卡 对于刚踏入学术研究阶段的本科生、硕博新生而言&#xff0c;开题报告是正…

作者头像 李华
网站建设 2026/7/5 2:49:47

Claude Code 大规模封号,美团免费提供 GLM-5.2

美团推出了AI编程工具 CatPaw&#xff0c;免费提供 GLM-5.2大模型&#xff08;需手动切换&#xff09;&#xff0c;当然还有DeepSeek&#xff0c;kimi&#xff0c;LongCat等大模型。新用户注册即赠 500 Credits&#xff0c;1 Credit 可进行 1 次对话&#xff0c;额度耗尽后&…

作者头像 李华
网站建设 2026/7/5 2:48:35

高精度气压表整体解决方案

在汽车胎压检测、工业气压测控、民用充气设备等诸多领域&#xff0c;气压表是保障设备稳定运行、使用安全的核心检测器件。本方案采用西城微科自主研发的核心主控芯片与传感适配架构&#xff0c;以国产化高集成硬件为基础&#xff0c;搭配精细化算法优化&#xff0c;广泛适配车…

作者头像 李华
网站建设 2026/7/5 2:47:04

OpenCode敏感信息过滤插件——Privacer

Privacer&#xff1a;LLM 请求的实时隐私过滤插件 一个问题 今年年初我注意到一个现象&#xff1a;身边越来越多的开发者在终端里使用 AI 编程助手&#xff0c;与此同时&#xff0c;他们也越来越多地在对话中粘贴包含敏感信息的内容。这不是个例&#xff0c;因为我自己就在用…

作者头像 李华