好了,上篇我们已经讲完了进程等待的部分,那么这篇我们接着来讲剩下的进程替换部分以及一些关于进程的补充知识。
进程替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
首先,我们先来看看进程替换有哪些接口:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,
..., 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[]);
先看execl的现象:


由上面,我们可以看出来几个现象和结论:
- 基本原理:程序替换不会创建新进程,仅替换进程的程序代码和数据。
- exec函数特性:程序替换成功后, exec* 后续的代码不会被执行;只有替换失败时才会执行后续代码,且 exec* 函数只有失败返回值-1,无成功返回值。
- 环境变量:环境变量在创建子进程时继承(环境变量也是数据,在创建子进程的时候,环境变量就已经被子进程继承下去了),所有,程序替换中,环境变量信息不会被替换( extern char**environ 可体现)。
(ps:CPU获取程序入口地址:依赖Linux可执行程序的ELF格式,其可执行程序的入口地址在表头中)
看完现象和结论后,现在,我们正式来解释一下关于进程替换的接口:

现在,我们用代码来看一下使用它们之间的区别:
int main()
{
const char* const argv[]={"ls","-l",NULL};
const char* const envp[]={"PATH=/usr/bin",NULL};
execl("/usr/bin/ls","ls","-a","-l", NULL);
execlp("ls","ls","-a","-l",NULL);
execle("/bin/ls","ls","-a","-l", NULL,envp);
execv("/bin/ls",argv);
execvp("/bin/ls",argv);
execve("/bin/ls",argv,envp);
return 0;
}
上面只有execve是真正的系统调用,其它五个函数最终都调用 execve,

理解exec系列接口:
你要执行一个程序的第一件事是什么??先找到这个程序(即有路径"/bin/ls")
决定如何找到改这个程序。接着,找到这个程序之后,接下来怎么办?如何执行这个程序,主要是要不要覆盖选项,覆盖哪些?(命令行怎么写,你就怎么传eg:ls -a -l)
除此之外,我们上面exec*是执行系统命令行,那么他能不能执行我们自己的命令呢?
答案是肯定可以的,所以,我如果想给子进程传递环境变量,该怎么传递呢?
1.新增环境变量,即在父进程地址空间中之间加putenv.
2.彻底替换
补充知识:
我们知道,所有子进程都会继承父进程列表,因为环境变量具有全局属性。
Linux中,所有的进程都一定是别人的子进程,怎么理解他?
1.所有进程都由父进程创建。任何进程的启动都依赖另一个进程的创建动作。
eg:要执行ls指令,是由当前shell进程(bash)通过fork+exec的方式创建的,bash是父进程,ls是它的子进程。
2.每个进程都有唯一的父进程。
3为了Linux的进程管理,资源回收,权限控制提供基础。
理解加载器:
加载器是一段“可执行的代码逻辑”,属于软件层面的功能模块:
- 它的核心作用是解析可执行文件(如ELF格式),将文件中的代码段、数据段加载到内存的指定区域,并完成符号解析、重定位等操作。- 从代码形态上,加载器可以是操作系统内核的一部分(如Linux内核中的ELF加载器),也可以是用户态的程序(如动态链接器 ld.so 负责加载动态库)——本质上都是“用代码实现的逻辑模块”。
加载器的所有操作都发生在内存中:它的执行过程:
- 当你运行一个程序(如 ./a.out )时,操作系统的加载器会先将 a.out 文件从磁盘读入内存,然后在内存中完成“代码段映射、数据初始化、栈堆分配”等操作,最终让程序在内存中“活起来”并开始执行。
- 动态加载的场景更直观:比如程序运行时需要加载 libc.so 动态库, ld.so 会在内存中解析库文件,将其代码和数据与主程序的内存空间关联,这个过程完全是“内存内的操作”。
以exec为例:先通过加载器(exec)在内存里,我们获得命令行参数,execv在调用我们的函数时,把argv参数传递给我们的程序,由上面了解知道,所有函数的本质上是压栈,所以它无非就是在调用main之前,先形成一个简单的栈帧结构,把argv地址push进去,给你构造出一个main函数会调用的上下文。
另外补充:shell脚本了解,以.sh结尾的文件
赋予执行权限:
chmod +x test.sh
./test.sh(运行)
结论:shell脚本是一行一行拿出来去执行的,这也证明了语言之间是可以相互调用的。
知道有这东西(概念)就行,到时候需要再另外去学习。
好了,本次分享就到处结束了,希望大家有所进步!
最后,到了本次鸡汤环节:
希望,大家坚持的东西都有所回报!



被折叠的 条评论
为什么被折叠?



