文章目录
- 前言
- [1. 接口介绍](#1. 接口介绍)
- [2. 案例分析](#2. 案例分析)
- [3. 原理分析](#3. 原理分析)
前言
本文章,小编会和大家一起探讨关于进程替换的接口和细节。通过学习了解进程替换,我们就能够明白:命令行参数、环境变量表的传递,和bash命令行上执行命令。
1. 接口介绍
在正式进入介绍接口之前,我们先来看看,一个进程替换的现象:
c
int main()
{
printf("I am a process pid:%d\n", getpid());
execl("/usr/bin/ls", "ls", "-a", "-l", NULL); // 必须以NULL结尾
printf("我已经执行到这里了\n");
return 0;
}
-
来看下面的运行结果:
现象就是:调用了
execl
函数之后的代码并没有执行。
下面我们正式来认识进程替换的接口:

这些接口exec*
系列的库函数。
上面一共有5个接口(execvpe
未被声明),记忆比较麻烦。这里给出一点提示:
- 【
l
】:可以表示成为list,意思就是传参的时候需要像命令行参数一样传入。 - 【
p
】:可以表示执行的文件可以 不用带路径 ,进行程序替换的时候会从环境变量PATH
中找该文件。 - 【
e
】:表示成为environment ,表示在传参的时候传入一个环境变量表。 - 【
v
】:表示成为vector,表示指令和选项以指针数组的形式传入。。
注意:不管是l、v,最后必须以NULL
结尾,这是规定。
2. 案例分析
接下来,我们通过几个案例来说明这几个接口的使用
- 案例一:使用
execl
替换成为ls
指令
c
int main()
{
// 1、"/usr/bin/ls":表示我要执行谁,我执行的文件在哪里?
// 2、 "ls", "-a", "-l", "NULL":表示我要怎么执行。和命令行类似
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
return 0;
}
- 案例二:使用
execlp
替换成功ls
指令
c
int main()
{
// 1、"ls":表示我要执行谁,因为我们带了【p】所以会在环境变量下找
// 2、 "ls", "-a", "-l", "NULL":表示我要怎么执行。
execlp("ls", "ls", "-a", "-l", NULL);
return 0;
}
- 案例三:创建子进程,子进程使用
execvp
替换成为ls
指令。
c
int main()
{
char* const cmd[] = {
"ls",
"-a",
"-l",
NULL
};
pid_t pid = fork();
if(pid == 0)
{
//子进程
printf("I am a child pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
//进行进程替换
execvp("ls", cmd);
printf("I am here\n");
exit(10); //故意设置
}
//父进程
int status = 0;
pid_t ret = waitpid(pid, &status, 0);
if(ret > 0)
{
printf("I am father, I wait child's pid:%d, ret:%d\n", pid, ret);
if(WIFEXITED(status))
{
printf("child exit code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}

-
现象:
-
程序替换之后,后续的代码并没有执行。
-
程序替换之后,子进程的pid没有改变。
-
程序替换之后,父子进程的关系并没有改变。
-
-
问题及结论:
-
子进程被程序替换之后不会影响父进程吗?为什么?代码和数据是如何处理的?
子进程程序替换之后不会影响父进程。因为进程具有独立性 。父子进程指向的同一份代码,会发生"写时拷贝"。对于进程替换来说,仅仅是改变进程的代码和数据,并且将PC指针指向自己的程序启动的起始地址。
-
子进程在程序替换之后的代码为什么没有执行?
发生程序替换成功之后,子进程的数据和代码已经替换为需要执行的代码和数据了,原始想要执行的代码已经找不到了。如果程序替换失败,才会执行后续代码。
-
CPU是如何得知,进程替换之后,新进程的代码入口地址呢?
在Linux下,每一个可执行程序都会有严格的格式【ELF 】,其中表头就包含这一个程序的入口地址 。当发生进程替换之后,原来子进程的
task_struct
中的一些属性字段就会发生改变,例如页表指针、mm_struct
指针、还有维护的一些关于CPU硬件资源的字段都会发生替换。这些属性的替换,都后面的CPU执行都有关系...... -
进程替换可以替换哪些可执行文件?
我们不仅仅可以执行一些系统的指令,也可以执行我们自己所写的代码、脚本。这是具有跨语言性的,例如我们可以跑Python脚本,Shell的脚本。在Linux,程序本质都是进程,只要被执行起来了,Linux就为其分配PCB资源。
-
3. 原理分析
实际上,上面认识的接口都是对系统调用
execve
的封装
-
系统调用和库函数关系图:
-
下面我们正式来认识进程替换的原理:

进程替换的核心函数是execve()
。它的作用不是创建一个新进程,而是将当前正在调用的进程(子进程)的代码和数据完全替换为一个新的程序。
-
验证与准备:
当进程调用
execve()
时。内核首先检查该进程是否有权限执行这个操作。然后内核会找到磁盘上的目标ELF程序文件,并解析其格式,读取文件头信息(如代码段、数据段的大小、入口地址等)。 -
销毁旧的地址空间(除PCB外)
这是"替换"的本质。内核会释放当前进程虚拟内存地址空间 中除了PCB(内核部分)之外的所有资源:释放代码段、数据段、堆、栈所占用的所有物理内存页和虚拟内存映射。(文件部分 :关闭所有通过fcntl标记为CLOEXEC(执行时关闭)的文件描述符。默认情况下,所有打开的文件描述符会被保留,这是实现输入输出重定向的基础。信号部分:对于通过signal设置的handel方法会清除,因为程序替换之后原来的地址已经失效了。同时如何设置为忽略会保存)
-
建立新的地址空间并加载新程序
内核根据解析到的ELF文件信息,为新的程序重新设置虚拟内存布局 (进程地址空间),创建新的代码段、数据段、堆、栈的映射关系。代码段和数据段的映射非常巧妙:内核并不是立即将整个程序文件从磁盘读入物理内存 (惰性加载机制 )。为新的代码段和数据段建立内存映射,将它们映射到磁盘上的ELF文件本身。这意味着,在刚开始的时候,物理内存中可能只有新程序的一小部分(例如文件头)。后面程序运行触发缺页中断......
-
设置栈和堆
栈: 内核会为新程序分配新的栈空间,并将命令行参数 (argv) 和环境变量 (envp) 压入新栈的顶部。(如果在系统调用之前,我们设置了新的环境变量,那么进程替换之后的环境变量表就是使用的是新的表)
堆: 初始时堆的大小为零,随着新程序的执行(如调用 malloc),堆才会动态增长。
-
重置寄存器状态并开始执行
内核将进程的程序计数器(PC/IP)等寄存器重置为新程序的入口点(_start,通常由C运行时库定义)。
完成所有设置后,进程返回。此时,进程的上下文(代码、数据)已经完全变成了新程序的。CPU意识不到发生了替换,它只是从新的地址开始取指令执行。
完。希望这篇文章能够帮助你!