目录
进程程序替换
上一篇进程控制讲到,父进程创建子进程就是为了让子进程去做一些另外的事情,但是不管怎么说,子进程的部分代码也还是父进程的一部分,那么想要子进程去执行一个新的程序呢?也就是去执行一个与父进程毫无相关的程序,一个全新的代码和访问全新的数据,那么如何进行的呢?也就是我们现在要讲的进程程序替换!所以现在我们可以理清思路回答以下问题:
1.为什么要有程序替换?
创建子进程的目的一般是这两个:① 执行父进程的部分代码,完成特定功能。②执行其它新的程序。------> 需要进行「进程替换」,用新程序的代码和数据替换父进程的代码和数据,让子进程执行。
2.在程序替换中OS有没有创建新的进程?
没有。进程的程序替换,不改变内核相关的数据结构,只修改部分的页表数据,将新程序的代码和数据加载带内存,重新构建映射关系,和父进程彻底脱离。
3.OS是如何做到创新构建映射关系的呢?
操作系统可以对父进程的全部代码和数据进行写入,子进程会自动触发写时拷贝,开辟新的空间,再把磁盘中第三方程序的代码和数据写入到其中,子进程页表重新建立映射关系。**最终结果是:**父进程指向自己的代码和数据,而子进程指向第三方程序的代码和数据。
那么现在又有问题了,子进程指向第三方程序的代码和数据?是如何实现的?代码中也没有呀!那么我们接下分析它是如何做的。
替换原理
一直说父子进程,这是两个进程,那么我们先看单进程进行程序替换是如何进行的。首先要调用一个函数execl( ) 。该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
单进程演示:
cpp
#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没有执行的原因是执行到进程替换函数的时候,如果成功,整个进程的代码和数据都会被替换为所需替换的目标代码和数据,这样在后续执行的时候都会使用这份新的代码和数据,因此不会调用后续出现的代码。
多进程演示:
cpp
#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:
cpp
#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;
}
cpp
#include <iostream>
int main()
{
std::cout<<"this is a cpp program"<<std::endl;
return 0;
}
makefile:
cpp
.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 可执行文件。
cpp
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
有6种exec 系列的库函数,统称为 exec 函数,功能:执行文件。
cpp
#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函数,需要找到命令所在的文件目录,使用方法如下:
cpp
#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函数:会到系统默认的路径下寻找命令:
cpp
execlp("ls", "ls", "-a", "-l", "-d", NULL);
3.execle函数:用一个程序调用另外一个程序,但环境变量是自己的环境变量,不是系统的,通过获取环境变量查看。
如何在进程中添加一个环境变量?用到的是putenv函数:
cpp
void *putenv(char *name)
程序演示:
cpp
//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;
}
cpp
//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中添加的环境变量也会继承到子进程中?
程序演示(对上面程序进行修改):
cpp
#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[],实际上就是环境变量。
程序演示:
cpp
//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;
}
cpp
//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创建子进程,就将这些命令行转换为一个字符串数组,采用进程替换的方式就可以把要找的命令和选项替换到前台,那依据这个原理,其实我们自己也能实现一个命令行解释器:
cpp
#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文件编写:
cpp
.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 命令,可以看到,形成了两个目标程序:
