【linux进程控制(三)】进程程序替换&自己实现一个bash解释器
本文介绍了进程程序替换的概念及其在Linux系统中的应用。通过exec系列函数,一个进程可以完全替换自身执行的程序,保留原进程ID但运行新程序代码。文章详细解析了各函数的参数传递方式、环境变量处理及PATH查找特性,并通过ls命令案例演示了实际用法。特别指出父子进程场景下程序替换的写时拷贝机制,以及bash解释器的实现原理:父进程解析命令后创建子进程执行,但内建命令需由父进程直接处理。最后强调ex

🎬 个人主页:HABuo
📖 个人专栏:《C++系列》《Linux系列》《数据结构》《C语言系列》《Python系列》《YOLO系列》
⛰️ 如果再也不能见到你,祝你早安,午安,晚安

目录
前言:
我们在进程控制的第一篇博客中就知道,子进程被创建出来是可以通过if else语句和父进程互相执行各自的代码,因此,本篇博客就是利用这一特性,让子进程搞点事情,即进程程序替换!
本章重点:
本篇文章着重讲解进程程序替换的exec系列函数的用法(一共七个),并且自主实现一个bash解释器。最后拓展如何使用C调用其他语言的程序
📚一、进程程序替换
📖1.1 概念
进程程序替换:一个进程完全替换自己正在执行的程序,转而执行另一个全新的程序。这个过程会替换当前进程的代码段、数据段、堆栈等,但保留进程ID(PID)和其他一些属性(如文件描述符、环境变量等,除非显式更改)。
核心特点
-
"替换"而非"创建":不创建新进程,只是将当前进程的代码和数据替换为新的
-
进程ID不变:还是原来的进程,只是运行的程序变了
-
继承性:保留原进程的某些属性(如文件描述符、环境变量等)
-
原程序消失:替换成功后,原程序的代码不再执行
程序替换的系统调用:exec系列函数
Linux提供了6个exec函数,用于程序替换:
#include <unistd.h>
int execl(const char *path, const char *arg, ... /* (char *) NULL */);
int execlp(const char *file, const char *arg, ... /* (char *) NULL */);
int execle(const char *path, const char *arg, ... /*, (char *) NULL*/, char *const envp[] );
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
函数名规律
-
l:参数以列表(list)形式传递,逐个列出,以NULL结束
-
v:参数以向量(vector/数组)形式传递
-
p:使用PATH环境变量查找可执行文件
-
e:可以传递自定义环境变量
接下来,我们来一一了解这些系统接口函数
📖1.2 execl系列函数
execl() - 参数列表(命令行上怎么输这里就怎么输),需要指定完整路径
直接上案例:
#include <unistd.h>
#include <stdio.h>
int main() {
printf("准备执行ls命令...\n");
// 替换当前进程为ls命令
execl("/usr/bin/ls", "ls", "-al", NULL);
// 如果exec成功,下面的代码不会执行
printf("这行永远不会执行!\n");
return 0;
}

可以发现在打印完:准备执行ls命令后,就去执行了ls程序了,并且执行完后并没有打印"这行永远不会执行"!
execlp() - 参数列表(命令行上怎么输这里就怎么输),自动PATH(环境变量)查找
直接上案例:
#include <unistd.h>
#include <stdio.h>
int main() {
printf("准备执行ls命令...\n");
// 系统会在PATH环境变量中查找"ls"
execlp("ls", "ls", "-al", NULL);//p:使用PATH环境变量查找可执行文件
// 如果exec成功,下面的代码不会执行
printf("这行永远不会执行!\n");
return 0;
}

execle() - 参数列表(命令行上怎么输这里就怎么输),自定义环境变量
直接上案例:
int main()
{
char *env[] = {
"MYVAR=value",
"PATH=/usr/local/bin:/usr/bin",
NULL
};
printf("使用自定义环境执行程序...\n");
// 执行env命令查看环境变量
execle("/usr/bin/env", "env", NULL, env);
perror("execle失败");
return 1;
}

这里呢,相信你很懵懂,为啥只打印了我们数组里的内容,env不是把所有环境变量都打印吗?
事实上execle函数的最后一个参数是环境变量数组,这个数组会被传递给新程序作为其环境变量。
当我们执行execle("/usr/bin/env", "env", NULL, env)时,就启动了一个新的程序(这里是env命令),并且将env数组作为环境变量传递给了这个新程序。
env命令的作用是打印出当前进程的环境变量。因此,它会打印出我们传递给它的环境变量数组env中的内容,即MYVAR=value和PATH=/usr/local/bin:/usr/bin。
注意:这里的环境变量数组是我们自定义的,它完全替换了从父进程继承的环境变量。也就是说,新进程不会继承父进程的环境变量,而是使用我们提供的env数组。
所以,即使MYVAR=value不是shell环境变量里的,而是代码中定义的数组里的数据,它也会被传递给新进程,并被env命令打印出来。我们的程序进程 ├── 环境变量(继承自shell) → 被execle丢弃 │ ├── HOME=/home/user │ ├── PATH=/bin:/usr/bin │ └── ... ├── 代码中定义:char *env[] = {...} → 传递给新进程 │ ├── "MYVAR=value" │ ├── "PATH=/usr/local/bin:/usr/bin" │ └── NULL └── 调用execle("/usr/bin/env", ...) → 创建新进程env └── 新进程env的环境变量只有env数组的内容
📖1.3 execv系列函数
execv() - 参数数组,需要指定完整路径
直接上案例:
int main() {
char *args[] = {
"echo",
"Hello,",
"World!",
"This is execv",
NULL
};
printf("准备执行echo...\n");
execv("/bin/echo", args);
perror("execv失败");
return 0;
}
![]()
execvp() - 参数数组,自动PATH查找
int main()
{
char *argv[] = {"ls", "-al", "/home", NULL};
execvp("ls", argv);
return 0;
}

execvpe() - 参数数组,PATH查找,自定义环境变量
int main()
{
char *argv[] = {"env", NULL};
char *envp[] = {"MYVAR=test", NULL};
execvpe("env", argv, envp);
return 0;
}
![]()
总结如下:
| 函数 | 环境变量行为 | 是否使用PATH查找程序 | 是否数组传参 |
|---|---|---|---|
execl() |
继承父进程所有环境 | ❌ 需提供完整路径 | ❌v:vector缩写 |
execv() |
继承父进程所有环境 | ❌ 需提供完整路径 | ✅ |
execlp() |
继承父进程所有环境 | ✅ 使用PATH查找 | ❌ |
execvp() |
继承父进程所有环境 | ✅ 使用PATH查找 | ✅ |
execle() |
完全替换为指定环境 | ❌ 需提供完整路径 | ❌ |
execvpe() |
完全替换为指定环境 | ✅ 使用PATH查找 | ✅ |
注:实际上还有一个execve,这一个是真正的系统调用的接口,上面六个是它的封装,为什么要封装,就是为了让我们适应更多的使用场景,更方便一些,知道上述六个execve也就顺理成章的清楚了!
📖1.4 main函数参数从何而来
环境变量那篇博客我们清楚的知道main函数也是有参数的,那你是不是有这样的一个疑惑,main函数是在我的代码中放着的,我加载到内存cpu去调用,那这个参数是谁传给它的呢?我既然放到这个章节,聪明的你一定会想到和exec系列函数有关系!没错!exec就是充当一个加载器的功能!
int execle(const char *path, const char *arg,.../*, (char *) NULL*/, char *const envp[] );
请看上述execle函数,它既有获取命令行参数的参数,又有获取环境变量的参数,上面的使用时我们也清楚了envp参数传什么新程序里就有什么环境变量。
因此,exec系列函数本质上是一个程序加载器,它将一个新的程序加载到当前进程的内存空间,并开始执行。
📚二、程序替换的使用场景
其实一般情况下,程序替换都不是将自己替换掉,而是创建子进程去替换,让子进程去干活,而父进程当"监工"
接下来我们就用一个父子进程分别运行的代码来演示:
int main()
{
printf("-----------程序正在运行------------\n");
int id = fork();
if (id == 0)//子进程执行的代码
{
sleep(1);
execlp("ls", "ls", "-al", NULL);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0) printf("wait process success: exit code: %d, sig: %d \n ", (status >> 8) & 0xFF, status & 0x7F);
return 0;
}

可以发现子进程发生替换后,父进程依然执行了相应的代码,又一次验证了进程具有独立性!可是原理是什么呢?难道说和前面写时拷贝一样?没错,聪明的你真聪明,写时拷贝不光发生在变量中,在代码和数据中也会发生,也就是说父进程创建子进程按照自己的那套机制进行创建(PCB、虚拟地址空间、页表等),当子进程发生进程程序替换的时候,操作系统会重新开辟一块空间,将新的代码和数据放到这个新的空间中
📚三、高级应用场景(实现一个bash解释器)
在这种场景下,我们可以很自然的想到bash解释器的工作原理,可能就是创建子进程去执行任务,而bash父进程本身就需要获取指令,并传达命令即可!
所以让我们动手实现一个bash解释器
我们先把bash的整体结构分析一下,
然后在一步一步的实现它:
- 首先我们需要定义两个数组A和B,A用来存放用户输入的所有字符串,B用来存放以空格打散后的字符串
- 第二步,获取用户输入的字符串后,将字符串以空格为分割打散
- 第三步,创建子进程使用exec系列,函数去执行用户输入的指令,而bash本身充当监工的角色等待子进程死亡
首先先创建两个数组备用,然后再接收用户的输入
#define NUM 1000
#define SIZE 16
char cmd_line[NUM];//保存完整的命令行字符串
char* my_argv[SIZE];//保存打散后的字符串
if(fgets(cmd_line,sizeof cmd_line,stdin)==NULL)//用fgets将标准输入输入到数组中
continue;
cmd_line[strlen(cmd_line)-1] = '\0';//将输入的换行符给清除掉
接下来就是将字符串以空格为分割打散了,在C语言的学习时有strtok函数可以帮助我们解决这个问题,它的功能如下:

//命令行字符串解析:以空格为分割打散
my_argv[0]=strtok(cmd_line," ");//提出第一部分
int index=1;
while(my_argv[index++] = strtok(NULL," "));//第二次调用strtok时若还想解析第一次调用的字符串,则传NULL
这段代码写完后,字符串就已经被我们分割成了几个小字符串了,比如用户输入“ls -a -l"就转换成了"ls”,“-a”,"-l"了,接下来只需创建子进程完成任务即可!
//shell运行原理:通过子进程执行命令,父进程等待&&解析命令
//命令行解释器是一个常驻程序
#define NUM 1000
#define SIZE 16
char cmd_line[NUM];//保存完整的命令行字符串
char* my_argv[SIZE];//保存打散后的字符串
int main()
{
while(1)
{
//打印提示信息
printf("[hab@localhost myshell]# ");
fflush(stdout);
memset(cmd_line,'\0',sizeof cmd_line);
//获取用户的键盘输入
if(fgets(cmd_line,sizeof (cmd_line) - 1, stdin)==NULL)
continue;
cmd_line[strlen(cmd_line)-1] = '\0';//将输入的换行符给清除掉
//命令行字符串解析:以空格为分割打散
my_argv[0]=strtok(cmd_line," ");//提出第一部分
int index=1;
while(my_argv[index++] = strtok(NULL," "));//第二次调用strtok时若还想解析第一次调用的字符串,则传NULL
//fork后子进程去完成任务
pid_t id=fork();
if(id == 0)//子进程
{
printf("下面的功能让子进程执行\n");
//当执行cd等命令时,改变的是子进程的路径,而父进程的路径没变
execvp(my_argv[0],my_argv);
exit(1);//执行失败就返回1
}
//父进程的代码,当监工
int status = 0;
pid_t ret = waitpid(-1,&status,0);
if(ret>0) printf("exit code: %d\n",WEXITSTATUS(status));
}
return 0;
}
📖3.1 内建命令
在实现bash时,可能会遇见一个问题:就是cd指令进入某个文件夹似乎没用
这一点其实很好理解,因为指令cd是进入某个文件夹,而进入此文件夹当然是当前进程进入了,如果创建了子进程去进去文件夹,由于写时拷贝的原因,父进程并不会进去,所以对于像cd这样的指令我们称为内建命令,也就是不能让子进程来完成的命令,只能父进程亲自动手!
if (strcmp(my_argv[0], "cd") == 0) {
if (my_argv[1]) {
if (chdir(my_argv[1]) != 0) {
perror("cd失败");
}
}
}
chdir即为切换当前的工作目录
内建命令不止cd,像export,kill和history等等也是内建命令!
📚四、总结
今天这篇博客我们了解了进程程序替换的相关知识。
小结一下:
进程程序替换:
是什么:
- 一个进程完全替换自己正在执行的程序,转而执行另一个全新的程序。
为什么:
- 单进程发生进程程序替换时是把新进程的代码和数据直接进行替换,因此原程序的代码将不会再执行但保留进程ID(PID)和其他一些属性(如文件描述符、环境变量等,除非显式更改)。
- 多进程,如果子进程发生程序替换,通过写时拷贝的方式,在内存新的空间存储新程序的代码和数据从而达到父子进程互不干扰!
怎么办:
- exec系列函数:l表示通过列表传参,v表示通过数组进行传参,p表示通过环境变量PATH来找对应的文件,e表示可以获取对应的环境变量(数组的形式)
补充知识:
- 内建命令,怎么理解exec系列函数一种加载器

更多推荐

所有评论(0)