Linux:进程替换
- 创业
- 2025-09-15 23:39:01

目录
进程程序替换
替换原理
进程替换相关函数
环境变量与进程替换函数
命令行解释器(my_xshell)
进程程序替换
上一篇进程控制讲到,父进程创建子进程就是为了让子进程去做一些另外的事情,但是不管怎么说,子进程的部分代码也还是父进程的一部分,那么想要子进程去执行一个新的程序呢?也就是去执行一个与父进程毫无相关的程序,一个全新的代码和访问全新的数据,那么如何进行的呢?也就是我们现在要讲的进程程序替换!所以现在我们可以理清思路回答以下问题:
1.为什么要有程序替换?
创建子进程的目的一般是这两个:①执行父进程的部分代码,完成特定功能。②执行其它新的程序。——> 需要进行「进程替换」,用新程序的代码和数据替换父进程的代码和数据,让子进程执行。
2.在程序替换中OS有没有创建新的进程?
没有。进程的程序替换,不改变内核相关的数据结构,只修改部分的页表数据,将新程序的代码和数据加载带内存,重新构建映射关系,和父进程彻底脱离。
3.OS是如何做到创新构建映射关系的呢?
操作系统可以对父进程的全部代码和数据进行写入,子进程会自动触发写时拷贝,开辟新的空间,再把磁盘中第三方程序的代码和数据写入到其中,子进程页表重新建立映射关系。最终结果是:父进程指向自己的代码和数据,而子进程指向第三方程序的代码和数据。
那么现在又有问题了,子进程指向第三方程序的代码和数据?是如何实现的?代码中也没有呀!那么我们接下分析它是如何做的。
替换原理一直说父子进程,这是两个进程,那么我们先看单进程进行程序替换是如何进行的。首先要调用一个函数execl( )。该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
单进程演示:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { printf("execl begin:\n"); execl("/usr/bin/ls", "ls", "-a", "-l", "-n", NULL); printf("execl end:\n"); return 0; }运行结果:
我们首先发现,execl begin之后,打印的内容和我们在解释器输入指令ls -a -l -n 打印的内容一样,可以很好的证明了,这个单进程通过调用execl函数,帮我们执行了ls -a -l -n这条指令。
我们又发现,程序中有两个printf函数,但是只打印了一个?这又是为什么呢?
我们可以退出打印了第一个printf函数之后,在单进程中就发生了进程替换,去执行另外的程序了,第二个printf没有执行的原因是执行到进程替换函数的时候,如果成功,整个进程的代码和数据都会被替换为所需替换的目标代码和数据,这样在后续执行的时候都会使用这份新的代码和数据,因此不会调用后续出现的代码。
多进程演示:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { // child printf("pid:%d,begin to exec!\n",getpid()); sleep(3); execl("/usr/bin/ls","ls","-a","-l",NULL); printf("pid:%d,end to exec!\n",getpid()); } else { // father printf("wait child\n"); pid_t rid = waitpid(-1,NULL,0); if(rid > 0) { printf("wait success\n"); } } return 0; }运行结果:
和单进程相差并不是很大,只是多进程替换中增加了父进程对子进程的等待和回收的部分功能。
那在多进程下应该如何理解进程替换呢?用下面图示的过程来演示:
子进程原先和父进程共用代码和数据,但是子进程发生改变,就出发了写时拷贝, 构建新的映射关系,页表也就指向新的代码和数据。从这里的进程替换中可以发掘出一些东西,替换的是进程,而不是代码,所以这里可以替换的内容有很多,甚至可以是Java写的程序运行起来的进程等等,看下面的实验:
myprocess.c:
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { // child printf("pid:%d,begin to exec!\n",getpid()); sleep(3); execl("./cpptest","./cpptest",NULL); //execl("/usr/bin/ls","ls","-a","-l",NULL); printf("pid:%d,end to exec!\n",getpid()); } else { // father printf("wait child\n"); pid_t rid = waitpid(-1,NULL,0); if(rid > 0) { printf("wait success\n"); } } return 0; }cpptest.cc:
#include <iostream> int main() { std::cout<<"this is a cpp program"<<std::endl; return 0; }makefile:
.PHONY:all all:myprocess cpptest cpptest:cpptest.cc g++ -o $@ $^ myprocess:myprocess.c gcc -o $@ $^ .PHONY:clean clean: rm -rf myprocess cpptest运行结果:
此两个程序分析,运行的是myprocess.c但是调用execl程序后,它帮我们执行了./cpptest进而运行了一个cc文件,也就是进程替换了,并且替换的还是其它进程。这也就解释了在不同的公司中是可以存在分块进行构建模块功能的,最后都可以通过进程的形式链接起来。
从某种意义来说,进程的替换已经可以被看成是一种系统调用了,站在系统的视角看内存中的所谓进程,实际上是一样的,系统高于一切,它可以对进程进行调度和分配。
进程替换相关函数系统调用 execve 函数,功能:执行文件名 filename 指向的程序,文件名必须是一个二进制的 exe 可执行文件。
#include <unistd.h> int execve(const char *filename, char *const argv[], char *const envp[]);有6种exec 系列的库函数,统称为 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 execvpe(const char *file, char *const argv[], char *const envp[]);只有 execve 是真正的系统调用,其它 6 个函数都是库函数,最终都是调用的 execve,execve 在 man 手册的第 2 节,其它函数在 man 手册第 3 节。
l (list):表示参数采用列表(可变参数列表)v (vector):参数采用数组p (path):自动在环境变量 PATH 中搜索可执行程序(不需要带可执行程序的路径)e (env):可以传入默认的或者自定义的环境变量给目标可执行程序 环境变量与进程替换函数细心的我们发现,上面程序execl中,都是传了环境变量路径的, 那么当进行进程替换的过程中,对于环境变量的角度来讲,是以什么样的情况进行的传递呢?我们在环境变量中讲过的,直接得出结论是:子进程对应的环境变量,是可以直接从父进程来的。
对这个结论进行验证:
1.execl函数,需要找到命令所在的文件目录,使用方法如下:
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { // child // 进行进程替换 execl("/usr/bin/ls", "ls", "-a", "-l", "-d", NULL); } else { // parent // 对子进程回收 pid_t rid = waitpid(-1, NULL, 0); if(rid > 0) { printf("wait success\n"); } } return 0; }2.execlp函数:会到系统默认的路径下寻找命令:
execlp("ls", "ls", "-a", "-l", "-d", NULL);3.execle函数:用一个程序调用另外一个程序,但环境变量是自己的环境变量,不是系统的,通过获取环境变量查看。
如何在进程中添加一个环境变量?用到的是putenv函数:
void *putenv(char *name)程序演示:
//cpptest.c #include <iostream> int main(int argc, char* argv[], char* env[]) { // 输出命令行参数 for(int i = 0; argv[i]; i++) { std::cout << i << "->" << argv[i] << std::endl; } std::cout << "##############" << std::endl; // 输出环境变量 for(int i = 0; env[i]; i++) { std::cout << i << "->" << env[i] << std::endl; } return 0; } //myprocess.c #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { // 在程序中新增环境变量 char* myenv = "MYVAL1 = 11111111"; putenv(myenv); pid_t id = fork(); if(id == 0) { // child // 进行进程替换 execl("./cpptest", "cpptest", NULL); } else { // parent // 对子进程回收 pid_t rid = waitpid(-1, NULL, 0); if(rid > 0) { printf("wait success\n"); } } }
从中看出,在子进程中是出现了新增的这个环境变量的,由此可以基本验证,在父进程中添加的环境变量会继承到子进程中。那么父进程的父进程是谁呢?答案是bash,那么是不是在bash中添加的环境变量也会继承到子进程中?
程序演示(对上面程序进行修改):
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char* argv[], char* env[]) { // 输出环境变量 for(int i = 0; env[i]; i++) { printf("%d -> %s\n", i, env[i]); } // 在程序中新增环境变量 char* myenv = { "MYVAL1 = 11111111", "MYVAL2 = 22222222", NULL }; putenv(myenv); pid_t id = fork(); if(id == 0) { // child // 进行进程替换 execl("./cpptest", "cpptest", NULL); } else { // parent // 对子进程回收 pid_t rid = waitpid(-1, NULL, 0); if(rid > 0) { printf("wait success\n"); } } }运行结果:
由此可以得出这样的一条线索化的示意图:
环境变量的传递方式:
前面的例子证明,子进程的环境变量是由父进程传递的,而execle函数就是一个显示传递环境变量的函数,它的第三个参数是envp[],实际上就是环境变量。
程序演示:
//myprocess.cc #include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main(int argc, char* argv[], char* env[]) { // 在程序中新增环境变量 char* const myenv[] = { "MYVAL1 = 11111111", "MYVAL2 = 22222222", NULL }; pid_t id = fork(); if(id == 0) { // child // 进行进程替换 execle("./cpptest", "cpptest", NULL, myenv); } else { // parent // 对子进程回收 pid_t rid = waitpid(-1, NULL, 0); if(rid > 0) { printf("wait success\n"); } } return 0; } //cpptest.cc #include <iostream> int main(int argc, char* argv[], char* env[]) { // 输出命令行参数 for(int i = 0; argv[i]; i++) { std::cout << i << "->" << argv[i] << std::endl; } std::cout << "##############" << std::endl; // 输出环境变量 for(int i = 0; env[i]; i++) { std::cout << i << "->" << env[i] << std::endl; } return 0; }运行结果:
从中看出,通过这个函数可以把环境变量进行显示传递给子进程,并且是一种覆盖式传递
到此,有关进程替换的基本逻辑已经结束,那进程替换可以做什么实际的东西呢?比如我们用的xshell,我们可以自主实现一个简易版的xshell。
命令行解释器(my_xshell)在前面的认知中,命令行解释器,也就是bash,可以把用户在命令行中敲的命令转换成命令再输出,而实际上,这是一个逻辑很简单的过程:
bash程序相当于是一个一直在后台运行的程序,而当用户敲了一些命令行后,bash创建子进程,就将这些命令行转换为一个字符串数组,采用进程替换的方式就可以把要找的命令和选项替换到前台,那依据这个原理,其实我们自己也能实现一个命令行解释器:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #define NUM 1024 #define SIZE 64 #define SEP " " char cwd[1024]; char enval[1024]; int lastcode = 0; const char *getUsername() { const char *name = getenv("USER"); if(name) return name; else return "none"; } const char *getHostname() { const char *hostname = getenv("HOSTNAME"); if(hostname) return hostname; else return "none"; } const char *getCwd() { const char *cwd = getenv("PWD"); if(cwd) return cwd; else return "none"; } int getUserCommand(char *command, int num) { printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd()); char *r = fgets(command, num, stdin); if(r == NULL) return -1; command[strlen(command) - 1] = '\0'; return strlen(command); } void commandSplit(char *in, char *out[]) { int argc = 0; out[argc++] = strtok(in, SEP); while(out[argc++] = strtok(NULL, SEP)); } int execute(char *argv[]) { pid_t id = fork(); if(id < 0) { return -1; } else if(id == 0) { execvp(argv[0], argv); exit(1); } else { int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { lastcode = WEXITSTATUS(status); } } return 0; } void cd(const char *path) { chdir(path); char tmp[1024]; getcwd(tmp, sizeof(tmp)); sprintf(cwd, "PWD=%s", tmp); putenv(cwd); } int doBuildin(char *argv[]) { if(strcmp(argv[0], "cd") == 0) { char *path = NULL; if(argv[1] == NULL) path = "."; else path = argv[1]; cd(path); return 1; } else if(strcmp(argv[0], "export") == 0) { if(argv[1] == NULL) return 1; strcpy(enval, argv[1]); putenv(enval); // ??? return 1; } else if(strcmp(argv[0], "echo") == 0) { char *val = argv[1] + 1; if(strcmp(val, "?") == 0) { printf("%d\n", lastcode); lastcode = 0; } else { printf("%s\n", getenv(val)); } return 1; } else if(0) {} return 0; } int main() { while(1) { char usercommand[NUM]; char *argv[SIZE]; // 1. 打印提示符&&获取用户命令字符串获取成功 int n = getUserCommand(usercommand, sizeof(usercommand)); if(n <= 0) continue; // 2. 分割字符串 // "ls -a -l" -> "ls" "-a" "-l" commandSplit(usercommand, argv); // 3. check build-in command n = doBuildin(argv); if(n) continue; // 4. 执行对应的命令 execute(argv); } }【补充】
一次性编译两个目标程序的makefile文件编写:
.PHONY:all #定义为目标 all:myprocess cpptest #依赖项,all依赖于myprocess cpptest 这两个目标程序 cpptest:cpptest.cc #依赖关系,形成目标程序 g++ -o $@ $^ myprocess:myprocess.cc gcc -o $@ $^ .PHONY:clean #定义为目标,clean总是可以被执行的 clean: #依赖项为空 rm -rf myprocess cpptest #依赖方法执行 make 命令,可以看到,形成了两个目标程序:
Linux:进程替换由讯客互联创业栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“Linux:进程替换”
 
               
               
               
               
               
               
               
   
   
   
   
   
   
   
   
   
   
   
  