目录
一. 进程替换的原理
二. 进程替换的方法
2.1 进程替换的相关函数
2.2 进程替换为其它的C/C++程序或其它语言编写的程序
三. 自主实现简单地命令行解释器
四. 总结
一. 进程替换的原理
进程替换,就是对进程所执行的代码进行替换,让正在运行的一个进程,终止运行其当前的代码,转而执行其他的代码。
在我之前的博文Linux系统编程:详解进程地址空间_【Shine】光芒的博客-CSDN博客中提到,OS需要为每个进程创建一个进程控制块(PCB)、一份地址空间、一张页表,CPU拿到虚拟地址,通过页表映射,找到实际的物理内存,从而访问相应的数据和执行对应的程序。
通过系统调用接口(exec系列),可以实现对进程的替换。如果一个正在运行的进程要去执行其他的可执行程序,那么OS会将那份即将被执行的可执行程序的代码和数据加载到内存中去,并改变页表的映射关系,并释放原来程序代码和数据占用的物理内存空间,从而让PCB可以通过当前进程的地址空间和页表,映射找到替换后的可执行程序的代码和数据。
进程替换原理总结:新程序/数据载入内存 + 释放原来程序数据占用的物理内存空间 + 重新建立页表映射关系。
一般而言,采用创建子进程的方法来进行进程替换,子进程调用exec系列函数替换进程,父进程通过wait/waitpid函数,来监视子进程的状态。
如果不创建子进程,直接在对父进程进行进程替换操作,那么父进程在在于exec系列函数替换进程之后的代码将不再执行。
二. 进程替换的方法
2.1 进程替换的相关函数
有下面六个函数,可以实现进程的替换:
- int execl(const char* path, const char* argv, ...)
- int execv(const char* path, char* const argv[])
- int execlp(const char* file, const char* argv, ... )
- int execvp(const char* file, char* const argv[] )
- int execle(const char* path, const char* argv, ... , char* const env[])
- int execve(const char* file, char* const argv[], char* const env[])
在上面的函数中1~5为C语言封装后的进程替换函数,6为Linux提供的系统接口函数。函数1~5的底层都是通过封装execve来实现的。
一般来说,我们不会直接调用6号函数execve来替换进程,函数1~5有些需要传递完整的可执行程序/指令路径,有些可以直接给指令名称,有些应当以const char* 格式的数据,依次传入每个命令行参数,有些应当采用指针数组的形式传递命令行参数。表2.1为具体的分类方法,代码2.1分别展示了如何采用1~4号函数,替换进程执行运行Linux内置的指令ls -a -l。
- 函数名中p代表环境变量PATH,即:会去PATH中指定的路径查找指令,如果运行自己编写的代码,还需要指定路径。
- l表示需要以列表的形式逐个传入命令行参数,v表示以指针数组的形式传入,无论以那种方式传入命令行参数,都需要以NULL结尾。
- e表示需要用户自己组装环境变量。
函数名 | 命令行选项格式 | 是否需要带路径 | 是否需要用户组装环境变量 |
---|---|---|---|
execl | 列表 | 是 | 否 |
execv | 指针数组 | 是 | 否 |
execlp | 列表 | 否 | 否 |
execvp | 指针数组 | 否 | 否 |
execle | 列表 | 否 | 是 |
execve | 指针数组 | 是 | 是 |
代码2.1:使用进程替换函数运行Linux指令
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define NUM 10
int main()
{
pid_t id = fork();
if(id == 0)
{
char* const _argv[NUM] = {
(char* const)"ls",
(char* const)"-a",
(char* const)"-l",
NULL
};
//四种方法执行进程替换
//execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
//execv("/usr/bin/ls", _argv);
//execlp("ls", "ls", "-a", "-l", NULL);
//execvp("ls", _argv);
exit(1);
}
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
return 0;
}
2.2 进程替换为其它的C/C++程序或其它语言编写的程序
- 进程替换为其它的C/C++程序
这里使用execle函数来进行替换,被替换的可执行程序为mycmd.exe,在父进程中定义环境变量VAL_1=1234 和 VAL_2=5678,作为execle的最后一个参数传入,mycmd的源文件mycmd.cpp中,依次输出每个环境变量的值。
代码2.2:test.cpp文件(父进程源文件)和mycmd.cpp文件
//1.test.cpp文件
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define NUM 10
int main()
{
pid_t id = fork();
if(id == 0)
{
char* const env[NUM] = {
(char* const)"VAL_1=1234",
(char* const)"VAL_2=5678",
NULL
};
execle("./mycmd.exe", "mycmd.exe", NULL, env);
}
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
return 0;
}
//2. mycmd.cpp文件
#include<stdio.h>
int main(int argc, char* argv[], char* env[])
{
for(int i = 0; env[i]; ++i)
{
printf("env[%d]:%s\n", i, env[i]);
}
return 0;
}
- 替换为其它语言生成的可执行程序(python为例)
我们编写test.py文件和test.cpp文件,在test.cpp文件中,使用execl指令,让进程替换为运行test.py的代码,在命令行中,可以使用python test.py运行程序,如果test.py文件具有可执行权限,那么可直接在命令行中输入./test.py运行程序。
代码2.3:test.cpp文件和test.py文件
// test.cpp 文件
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t id = fork();
if(id == 0)
{
execl("/usr/bin/python", "python", "test.py", NULL);
exit(1);
}
int status = 0;
pid_t res = waitpid(id, &status, 0);
if(res > 0)
{
printf("exit code:%d\n", WEXITSTATUS(status));
}
return 0;
}
// test.py 文件
#! /usr/bin/python3.6
print("hello Python")
print("hello Python")
print("hello Python")
print("hello Python")
三. 自主实现简单地命令行解释器
命令行解释器是常驻系统运行的,需要循环执行下面的操作:
- 打印提示信息,假设为[root@local-address-CentOS]#
- 读入用户指令,可以使用fgets函数。
- 从用户的指令中分离出命令行的每个选项(以' '为间隔),通过strtok函数分割。
- 判断是否为内置指令,内置指令应当在父进程中执行,否则在子进程运行。(这里简化为只考虑cd指令为内置指令)
- 创建子进程,在子进程中调用execv函数运行指令。
- 父进程阻塞等待子进程退出码,进入下一层循环等待用户输入下一条指令。
这里对命令行解释器中涉及到的系统接口和函数进行简单解读:
- 系统接口chdir:int chdir(const char* path),将当前进程的路径变为path,如果成功改变路径返回0,否则返回-1。
- 以文本方式读取字符串函数fgets:char* fgets(char* str, size_t size, FILE* stream),从stream流中去读size_t个字符到str指向的空间中去,函数返回值就是str。
- strtok函数,char* strtok(char* str, const char* delim),通过指定分割符的方式,将一个字符串分割为若干个。如果调用时第一个参数为NULL,那么str就等价于上次找到的分隔符的后面那一个字符的位置,因为strtok具有记忆功能,能够记录下来每次进行分割的位置。
代码3.1为命令行解释器的简易实现程序,由于Linux系统是用C语言编写的,所以这里也采用C语言模拟实现命令行解释器。
代码3.1:命令行解释器的简单模拟实现 -- C语言代码
//所以需要死循环
while(1)
{
//1.打印提示信息
printf("[root@local-address-CentOS]# ");
fflush(stdout); //强制刷新缓冲区
//2.获取用户输入的指令
fgets(g_cmd, NUM * sizeof(char), stdin);
//fgets会引入换行符/0,因此要将/n改为'/0'
g_cmd[strlen(g_cmd) - 1] = '\0';
//3.将指令的每个选项读入到g_argv中去
int index = 0;
g_argv[index++] = strtok(g_cmd, g_seq);
while(g_argv[index++] = strtok(NULL, g_seq));
//for(int i = 0; g_argv[i]; ++i)
//{
// printf("%s\n", g_argv[i]);
//}
//4.处理内置命令(直接在父进程中运行,不切换子进程)
if(strcmp(g_argv[0], "cd") == 0)
{
if(g_argv[1] != NULL)
{
chdir(g_argv[1]);
}
continue;
}
//5.创建子进程,运行指令
pid_t id = fork();
if(id == 0) //子进程代码
{
execvp(g_argv[0], g_argv);
exit(1); //如果进程替换成功,就不会运行exit(1)
}
else if(id < 0) //如果子进程创建失败
{
perror("fork");
exit(1);
}
//6.阻塞等待指令运行的结果
int status = 0; //接收子进程运行结果
pid_t ans = waitpid(id, &status, 0);
if(ans > 0)
{
printf("exit code: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
四. 总结
- 进程替换的底层实现原理是改变页表的映射关系,一般在子进程中进行进程替换操作。
- 通过exec系列函数,可以实现进程的替换,可以是替换为Linux系统内置的指令,可以替换为其他的C/C++程序,也可以替换为其它语言的程序。