进程控制块(PCB):
什么是进程替换?
进程替换是一种在计算机操作系统中,通过特定的系统调用(如exec系列函数)实现的进程执行内容的改变过程。进程替换的本质是将当前进程的用户空间代码和数据全部替换为另一个程序的内容,而进程的标识符(PID)保持不变。
进程替换的影响
虚拟地址空间的替换:当进程调用exec系列函数时,操作系统会为新程序分配一个全新的虚拟地址空间。这意味着旧的虚拟地址空间不再有效,需要被释放
页表的更新:新的虚拟地址空间需要新的页表来管理虚拟地址与物理内存之间的映射关系。操作系统会创建新的页表来支持这个新的虚拟地址空间
资源回收:旧的虚拟地址空间和旧的页表占用内存需要被回收,以方便系统可以重用这些资源。操作系统的内存管理单元(MMU)会处理这些资源的回收工作。
进程状态的重置:除了替换虚拟地址空间和页表外,exec调用还会重置进程状态,包括堆栈,寄存器等,确保新程序在一个干净的环境启动。
性能和安全性考虑:回收旧的虚拟地址空间和页表有助于防止内存泄漏和其他潜在的安全问题。此外,这也是操作系统高效内存管理的一部分。
从不同角度看待进程替换
从程序的角度看待进程替换
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往需要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动历程开始执行。调用exec并不创建新进程,所以调用exec前后进程的id并未改变。
从程序的角度看待进程替换
程序原本是存放于磁盘中的,当调用了exec函数时,程序的代码和数据分别会加载到当前进程对应的代码段和数据段,代码和数据一旦替换后,相当于用一个老进程的壳子去执行一个新的程序的代码和数据。程序替换就相当于程序加载器,我们平时说的程序被加载到内存中,其实就是调用了exec函数。在创建进程的时候,是先创建进程的数据结构PCB,再把代码和数据加载到内存中的。
深度理解进程替换
当调用 exec 函数时,程序的代码和数据分别加载到当前进程对应的代码段和数据段,代码和数据一旦替换之后,相当于用一个老进程的壳子,而这个老进程的壳子就是原本的子进程的task_struct,进程程序的替换不是进程替换,也就是说,进程程序的替换并没有创造一个新的PCB,而是修改了其中的mm_struct的指定内容。
当使用了exec函数之后,把只属于子进程的内存的数据和代码、旧的虚拟地址空间、旧的页表回收,而子进程和父进程共享的内存的数据和代码并没有回收。而没有修改task_struct中的 int exit_code 与 int exit_signal ,也没有丢失task_structPPID等的数据。
替换函数exec系列
进程替换的本质就是把程序的进程代码+数据加载到特定的进程的上下文中,C/C++程序要运行,必须要先使用加载器加载到内存中,这就要用到exec*系列程序的替换函数,他们充当加载器,把磁盘中的程序加载到内存中
分析:3号手册的exec()系列函数都通过封装2号手册的execve函数。其他exec系列的函数都是C语言的库函数,而execve函数都是系统调用函数。
函数int execl(const char *path, const char *arg, ...);
参数:path:程序的路径,arg为命令+命令参数。如:"ls","-a","-l",最后以NULL结尾表示选项传递结束
返回值:程序替换成功就不会出现返回值,如果替换失败就继续执行老代码,并且execl一定会返回-1
如果程序替换成功后,新程序的退出码也会返回给子程序,同样可以被父进程拿到子进程的退出状态
int main()
{
printf("I am a process! pid:%d\n",getpid());
execl("/bin/ls","ls","-a","-l",NULL);//程序替换,不再执行execl函数之后的代码了
printf("you can see me?\n");
return 0;
}
分析结果:该可执行程序它打印了printf函数的结果并且还打印出了指令“ls -la”的运行结果,但是并没有把最后一个printf函数的结果打印出来
结论:当进程执行了exec()系列函数时,原本程序的代码和数据会全部被新的程序所替换。程序的替换是整体替换而不是局部替换。
分析现象:ls找不到对应的文件,所以进程替换失败,在进程替换失败后,会返回-1,并且设置了错误码error为2,也就是进程退出状态码。而这个错误码/进程退出状态码一个进程只能设置一次,由于execl进程替换失败后,会赋值给error2,因此,父进程收到的退出码为2,而非1。
函数int execv(const char *path, char *const argv[]);
参数
- path 为程序路径
- argv 数组内存放 命令 + 命令参数
- execl 与 execv 只在传参形式上有所不同,execl用的是可变参数列表,而execv用的是指针数组,数组元素个数由我们来定
函数int execlp(const char *file, const char *arg, ...);
execlp 中的 p 表示能够自动搜索环境变量 PATH,在执行特定程序时,只要知道程序名系统就会自动在环境变量path中搜索程序位置,不需要知道这个程序在哪里。使用 execlp 替换程序更加方便,只要待替换程序路径位于 PATH
中,就不会替换失败
函数int execvp(const char *file, char *const argv[]);
execlp 中的 p 表示能够自动搜索环境变量 PATH,在执行特定程序file时,只要知道程序名系统就会自动在环境变量path中搜索程序位置,不需要知道这个程序在哪里,argv 数组内存放 命令 + 命令参数
函数int execle(const char *path, const char *arg, ..., char * const envp[]);
envp 为自定义环境变量,可以将自定义或当前程序中的环境变量表传给待替换程序
在这个可执行程序中,因为没有添加MYENV,可以发现该子进程没有环境变量 MYENV 为空 。
可以看见在test1进程中可以发现环境变量MYENV,但是环境变量PATH却没有了,这是因为函数execle传递环境变量表是覆盖式传递的,老的环境变量表会被我们传递的自定义的环境变量表所取代覆盖,而如果是想在原本的环境变量表上面添加环境变量,需要用到putenv(),在环境变量表*environ上面添加
现在可以理解为什么在 bash 中创建程序并运行,程序能继承 bash 中的环境变量表了
在 bash 下执行程序,等价于在 bash 下替换子进程为指定程序,并将 bash 中的环境变量表 environ 传递给指定程序使用
其他没有带 e 的替换函数,默认传递当前程序中的环境变量表
因此,我们称环境变量具有全局属性。
函数int execvpe(const char* file, char* const argv[], char* const envp[]);
对 execvp
的再一层封装,使用方法与 execvp
一致,不过最后一个参数可以传递环境变量表
函数int execve(const char* filename, char* const argv[], char* const envp[]);
execve
是系统真正提供的程序替换函数,其他替换函数都是在调用 execve
总结
execl
相当于将链式信息转化为argv
表,供execve
参数2使用execlp
相当于在PATH
中找到目标路径信息后,传给execve
参数1使用execle
的envp
最终也是传给execve
中的参数3
替换函数除了能替换为 C++
编写的程序外,还能替换为其他语言编写的程序,如 Java
、Python
、PHP
等等,虽然它们在语法上各不相同,但在 OS 看来都属于 可执行程序
,数据位于 代码段
和 数据段
,直接替换即可。
系统级接口是不分语言的,因为不论什么语言最终都需要调用系统级接口,比如文件流操作中的 open
、close
、write
等函数,无论什么语言的文件流操作函数都需要调用它们。