进程替换
1-1定义进程替换
通过 exec* 函数,把磁盘中的其它程序(代码+数据)加载到内存中,替换当前进程的代码和数据,让页表重新构建映射关系,这期间不会创建新的进程。
进程替换就是把一个命令变成“会消失的临时文件”,让那些原本只读文件的命令也能直接处理命令的输出结果。在处理多输入对比、避免子 shell 变量赋值失败、或需要将数据同时喂给多个命令时非常有用。
1-2替换原理
⽤fork创建⼦进程后执⾏的是和⽗进程相同的程序(但有可能执⾏不同的代码分⽀),⼦进程往往要调⽤⼀种 exec 函数以执⾏另⼀个程序。当进程调⽤⼀种 exec 函数时,该进程的⽤⼾空间代码和数据完全被新程序替换,从新程序的启动例程开始执⾏。调⽤ exec 并不创建新进程,所以调⽤ exec 前后该进程的 id 并未改变。
【问题补充】
1.进程替换的原因?
- 执行父进程的部分代码,完成特定功能。
- 执行其它新的程序,用新程序的代码和数据替换父进程的代码和数据,让子进程执行。
2.操作系统是如何做到重新建立映射的呢?
操作系统可以对父进程的全部代码和数据进行写入,fork() 阶段子进程通过复制页表共享父进程物理内存, exec() 阶段子进程申请新物理内存、加载磁盘第三方程序并重建页表映射,最终子进程指向第三方程序的物理内存,父进程保持原有映射。
而在进程替换中,“重新建立映射”主要不是指页表,而是指 Shell 通过
pipe()和dup2()把子进程的标准输出重新映射到管道的内核对象,并把管道的读端伪装成路径(如/dev/fd/63)交给另一个命令使用exec。也就是说exec重建的是虚拟内存到物理内存的页表映射;而进程替换重建的是文件描述符到内核管道对象的映射。
3.在进行程序替换的时候,有没有新的进程的创建?
没有。进程的程序替换,不改变内核相关的数据结构,只修改部分的页表数据,将新程序的1代码和数据加载带内存,重新构建映射关系,和父进程彻底脱离。
1-3如何替换(exec系列函数)
库函数
#include <unistd.h> 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 execve(const char *path, char *const argv[], char *const envp[]);系统调用execve 函数,功能:执行文件名 filename 指向的程序,文件名必须是一个二进制的 exe 可执行文件。
#include <unistd.h> int execve(const char *filename, char *const argv[], char *const envp[]);1-3-1 函数解释
(1)execl 函数
exec 函数解释:
- 这些函数如果调用成功,则加载新的程序从启动代码开始执行,不再返回。
- 如果调用出错则返回 -1。
- 所以 exec 函数只有出错的返回值而没有成功的返回值。
#include <unistd.h> int execl(const char *path, const char *arg, ...); //path: 要执行程序的路径,路径中要包括程序名,比如:usr/bin/ls //arg: 要执行的程序名/命令名 //...: 可变参数列表,必须以NULL结尾,表示参数传入完毕(就相当于列表)实例演示:
#include <stdio.h> #include <unistd.h> // exec int main() { printf("程序运行开始\n"); execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 进程的程序替换 printf("程序运行结束\n"); return 0; }运行的结果:没有执行后面的程序运行结束
注意:上述程序,因为只有一个进程,所以发生进程替换后,该进程自己就被替换了,不能去做自己的事情了。
- 因此我们一般是让父进程创建子进程,让子进程通过进程替换,去执行其它程序,而父进程去检测执行结果和等待回收子进程
#include <stdio.h> #include <stdlib.h> // exit #include <sys/types.h> // getpid, getppid, waitpid #include <sys/wait.h> // waitpid #include <unistd.h> // exec, fork, getpid, getppid int main() { pid_t id = fork(); if (id == 0) { // 子进程 printf("我是子进程, pid: %d\n", getpid()); execl("/usr/bin/pwd", "pwd", NULL); // 进程替换 exit(1); } else if (id > 0) { // 父进程 printf("我是父进程, pid: %d\n", getpid()); int status = 0; // 进程退出信息 pid_t ret = waitpid(id, &status, 0); // 进程等待 if (ret > 0) { // 等待成功,打印子进程的ID、退出码、终止信号 printf(" 父进程waits for success, ret: %d, code: %d, sig: %d\n", ret, (status >> 8) & 0xff, status & 0x7f); } else { // wait failure } } else { // fork failure } return 0; }运行结果:
- 调用 exec 函数,不用考虑当前进程的返回值,因为 exec 函数下面的代码不会被执行(因为当前进程的代码和数据已经被替换了)。所以如果当前进程返回了,则说明 exec 函数调用失败了。
- exec 函数有点像特殊的加载器,把程序的代码数据加载到内存中,然后执行。
(2)execv 函数
在功能上和 execl 没有任何区别,只有传参的区别。
int main() { pid_t id = fork(); if (id == 0) { // 子进程 printf("我是子进程, pid: %d\n", getpid()); // 字符指针数组 char* const my_argv[] = { "ls", "-l", "-a", NULL }; execv("/usr/bin/ls", my_argv); // 进程替换 exit(1); } else if (id > 0) { // 父进程 } else { // fork failure } return 0;运行结果:
(3)execlp 函数
功能上和 execl 没有任何区别,区别是,只需要给出要执行程序的名称即可,自动去 PATH 中寻找,不需要给出绝对路径。
注意:只有系统的命令,或者自己的命令(前提导入到 PATH中了),才能够找到。
int main() { pid_t id = fork(); if (id == 0) { // 子进程 printf("我是子进程pid: %d\n", getpid()); execlp("ls", "ls", "-l", "-a", NULL); // 进程替换 exit(1); } else if (id > 0) { // 父进程 } else { // fork failure } return 0; }运行结果:
(4)execle 函数
// 调用 execle 或 execve 函数进行进程替换时,可以把在当前程序中定义的环境变量 //递给要替换的程序 ,此时在程序中通过 getenv 就可以获取到这些环境变量 int execle(const char *path, const char *arg, ..., char * const envp[]); int execve(const char *filename, char *const argv[], char *const envp[]);int main() { // 获取环境变量 printf("my_cmd process is running, getenv --> MYENV: %s\n", getenv("MYENV")) return 0; }运行结果:
int main() { pid_t id = fork(); if (id == 0) { // 子进程 printf("我是子进程, pid: %d\n", getpid()); // 定义环境变量MYENV char* const my_env[] = { "MYENV=hello world!", NULL }; /* * 通过进程替换,执行proc程序,同时把定义的环境变量传递给test程序 * 这样我们执行test程序,就可以获取到环境变量MYENV了 */ execle("./test", "test", NULL, my_env); // 进程替换 exit(1); } else if (id > 0) { // 父进程 } else { // fork failure } return 0; }运行结果:
如上:如果在 proc 程序中,调用 execle 函数进行进程替换(执行test程序)时,可以把在 proc程序中定义的环境变量通过传递给要替换的test程序,运行proc 程序,进行进程替换(执行 test 程序),发现在test 中获取到了环境变量
环境变量具有全局属性,可以被子进程继承,那么它是如何做到的呢?
答:进程在运行的时候,自动会通过execle 函数执行新程序的时候,把系统的环境变量传给了新程序。
这里的小技巧,我定义了两个文件的目标编写,后面就可以定义多个按照这个格式
【补充】
多个文件的makefile通用写法
# ============================================ # 最简单通用 Makefile # ============================================ # 编译器 CC = gcc # 编译选项:-Wall 显示所有警告,-g 加入调试信息 CFLAGS = -Wall -g # 目标可执行文件名字 TARGET = myapp # 自动查找当前目录下所有 .c 文件 SRCS = $(wildcard *.c) # 将 .c 文件列表变成 .o 文件列表 OBJS = $(SRCS:.c=.o) # 默认目标:编译整个程序 all: $(TARGET) # 链接:把所有的 .o 文件链接成可执行文件 $(TARGET): $(OBJS) $(CC) $^ -o $@ # 编译:把每个 .c 文件编译成 .o 文件 %.o: %.c $(CC) $(CFLAGS) -c $< -o $@ # 清理:删除编译产生的文件 clean: rm -f $(OBJS) $(TARGET) # 声明伪目标(不是真正的文件) .PHONY: all clean 使用方法: bash make # 编译 make clean # 清理1-3-2 命名理解
• l(list) : 表⽰参数采⽤列表
• v(vector) : 参数⽤数组
• p(path) : 有 p ⾃动搜索环境变量 PATH
• e(env) : 表⽰⾃⼰维护环境变量
exec调⽤举例如下: #include <unistd.h> int main() { char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-ef", NULL); // 带p的,可以使⽤环境变量PATH,⽆需写全路径 execlp("ps", "ps", "-ef", NULL); // 带e的,需要⾃⼰组装环境变量 execle("ps", "ps", "-ef", NULL, envp); execv("/bin/ps", argv); // 带p的,可以使⽤环境变量PATH,⽆需写全路径 execvp("ps", argv); // 带e的,需要⾃⼰组装环境变量 execve("/bin/ps", argv, envp); exit(0); }事实上,只有 execve 是真正的系统调⽤,其它五个函数最终都调⽤ execve,所以 execve 在 man⼿册 第2节,其它函数在 man ⼿册第3节。这些函数之间的关系如下图所⽰。
下图exec函数簇 ⼀个完整的例⼦: