前言
本文主要记录小编学习程序替换中遇到的一些问题,并分享记录下来,希望可以给大家带来帮助;
一、初始程序替换
所谓程序替换,就是将本进程的代码和数据进行替换,运行新程序的代码;我们之前在讲解进程地址空间的时候,子进程会复制父进程的PCB控制块等内核信息,其中也包括页表的映射,也就是说父进程和子进程共享同一块代码和数据,可是,当子进程对数据进行修改时,发生写时拷贝,这里一般清空下,我们进行写时拷贝的都是数据,并不会对代码进行更改,而我们今天的程序替换恰恰会对代码进行更改;
我们程序替换的系统调用主要为以下几个;
看着似乎好像很多,实际上,我们学会其中的几个,剩下几个都可以迎刃而解;接下来我就带着大家一起初步认识上面的几个系统调用;
二、如何进行程序替换
1、execl函数
函数定义如下;
int execl(const char *path, const char *arg, ...);
参数一:path
该参数为我们要替换的程序的地址,如我们的ls指令,我们通过which或whereis来查询该指令所在路径;
我们便可以把这个地址填到这个参数内;参数一是为了找到要替换程序的位置;
参数二:
这个第二个参数实际上是可变参数列表,对应着函数中的l,意味list,我们可以列出我们如何调用这条指令,以NULL结尾。还是比如ls指令,我们可以加上各种各样的选项,使 ls 个性化出不同的功能;比如,这里我们可以填 "ls", "-a", "-l", NULL;这个参数是让我们告诉OS如何运行这个程序;
返回值:
这个函数的返回值非常奇怪,若失败则返回-1,而一旦成功,则没有返回值;
问题:如何看待没有返回值?
首先思考,我们如果调用成功,我们应该有返回值吗?我们首先理解程序替换的本质,程序替换是将我们的代码和数据重新映射,并不是重新创建一个新PCB控制块、进程地址空间、页表等内核数据结构;而一旦程序替换成功,原来的代码和数据我们都无法访问了,因为页表的映射改变了,因此我们也无法保存下来这个返回值,因为代码都改变了;
注意:后面所有这种程序替换的返回值都一样
百闻不如一见:
编译运行结果如下;
结果如我们所料,我们打印了 main begin,却没有main end,因为程序替换成功后,整个代码都被替换掉了;
注意:后面的函数也与这个函数类似,就不会讲解这么详细了;
2、execv函数
这个函数与之前函数不同的是将 l 变成了 v ,这里的v可以理解称vector,就像数组一样,因此这个函数使用的不同就是第二个参数并不像列表一样列出来,而是直接传入一个数组即可,这个数组依然以NULL结尾,有的环境下可能不需要,具体函数声明如下;
int execv(const char *path, char *const argv[]);
编译运行结果如下;
3、execlp函数
这个函数在我们execl函数基础上加了一个p,函数声明如下;
int execlp(const char *file, const char *arg, ...);
此时仅仅第一个参数发生了变化,第一个参数变成了file,也就是文件名,此时我们直接输入可执行程序名即可,这时,操作系统就会在环境变量中PATH中,搜索每个路径是否存在该文件,如果有则直接执行,没有则报错返回;这里新增的p正是环境变量中的 PATH;还不了解环境变量的同学可以看看下面这篇博客;
Linux | 进程-CSDN博客
有的小伙伴可能会疑惑,为什么要些两个ls呢?实际上我们应该将这个函数的参数分为两个部分来去理解;
前面也有强调这个,希望大家能有所理解;
4、execvp函数
会使用前面execlp函数,这个函数照葫芦画瓢就行了,唯一不同就是一个是以列表的形式列出来,一个是以字符指针数组的形式传参;具体使用如下;
5、execle函数
这个函数与execl函数唯一不同的是这个函数可以传环境变量,如下声明所示;
int execle(const char *path, const char *arg,..., char * const envp[]);
6、execvpe函数
想必有了前面的学习,这个函数看一眼应该都会使用了吧,这里的v是字符指针数组,p是会默认从环境变量PATH中找程序,e代表可传入环境变量,声明如下;
int execvpe(const char *file, char *const argv[],char *const envp[]);
7、execve函数
下图为man手册查询结果,细心的小伙伴已经发现了,最开始我们只说了有6个程序替换函数呀,不信的可以去初始程序替换那张图中查找是否有该函数,这个函数实际上才是真正的系统调用,上面的6个函数实际上就是对这个系统调用的封装;这个函数的使用规则也如上面几个函数,这里也就不做过多演示;
三、深刻理解程序替换
1、程序替换代码形式
实际上,程序替换的代码分为两种,一种是直接调用程序替换函数进行替换,另一种则是先调用fork函数,再让子进程调用程序替换函数;也就是说,上面的代码可以改成如下形式;
这样写代码的好处是什么呢?我们让子进程去执行程序替换的代码,即使程序替换的程序有问题,执行后会崩溃,也只是子进程崩溃,并不会影响我们的父进程;因为我们要清楚,我们的程序替换不仅可以执行系统指令,可以执行我们自己写的可执行程序,如下所示,我们写了一个sub.c的程序;
我们在改以下原来main函数的代码,我们调用我们刚才自己写的程序sub;
编译运行,结果果然如我们所料,我们也可以使用程序替换调用我们自己写的程序;
2、灵魂提问
问题:我们的环境变量是否会被替换?
并不会,程序替换仅仅只是替换代码和数据,并不会替换我们的环境变量;
问题:程序替换和创建子进程
创建子进程首先会为我们创建PCB、进程地址空间、页表等内核数据,然后将代码和数据加载进内存,此时内核数据+代码数据我们称这个为进程,这是进程的创建才完成,而程序替换指的是将当前程序的代码和数据换成别的程序的代码和数据,此时不并不会创建新的内核数据,仅仅将改变内核数据,如页表的映射,映射到新的物理地址;
四、模拟实现shell命令行
有了如上的知识基础,我们可以模拟实现一个shell命令行小程序;首先我们要清楚一个命令行要具备什么?
1、一定是一个死循环,不断读取我们指令进行解析执行;
2、首先要有命令行提示;
3、获取用户输入指令
4、解析用户输入指令
5、判断内置命令,也就是有父进程自己完成的指令
6、创建子进程,进行程序替换
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
// 命令字符数组大小
#define NUM 128
// 解析后命令字符指针数组大小
#define SIZE 32
// 命令分隔符
#define SEP " "
// 是否调试
//#define DEBUG
// 保存用户输入命令
char cmd_line[NUM];
// 解析命令后保存的数组
char* args[SIZE];
char Myenv[64];
int main()
{
while(1)
{
// 清空命令字符数组
memset(cmd_line, 0, sizeof(cmd_line));
// 1、显示提示符
printf("[root@localhost]# ");
fflush(stdout); // 必须刷新缓冲区
// 2、获取用户输入
fgets(cmd_line, sizeof(cmd_line), stdin);
// 处理最后的换行符
cmd_line[strlen(cmd_line) - 1] = '\0';
#ifdef DEUBG
printf("%s\n", cmd_line);
#endif
// 3、对用户输入命令解析
args[0] = strtok(cmd_line, SEP);
int index = 1;
while(args[index++] = strtok(NULL, SEP));
#ifdef DEBUG
for(int i = 0; args[i]; i++)
{
printf("args[%d]: %s\n", i, args[i]);
}
#endif
// 4、内建命令处理与特殊命令处理
if(strcmp(args[0], "cd") == 0)
{
if(args[1] != NULL)
{
// 更改当前目录
int ret = chdir(args[1]);
if(ret < 0)
{
printf("更改失败\n");
}
continue;
}
else
{
printf("命令格式有误\n");
continue;
}
}
if(strcmp(args[0], "ll") == 0)
{
args[0] = (char*)"ls";
args[1] = (char*)"-l";
}
if(strcmp(args[0], "export") == 0)
{
if(args[1] != NULL)
{
strcpy(Myenv, args[1]);
putenv(Myenv);
}
}
// 5、创建子进程,并让子进程执行命令
pid_t id = fork();
if(id < 0)
{
// fork 函数调用失败
printf("fork fail\n");
continue;
}
else if(id == 0)
{
printf("MYVAL:%s\n", getenv("MYVAL"));
// 子进程
execvp(args[0], args);
exit(-1);
}
else
{
// 父进程
int status = 0;
waitpid(id, &status, 0);
if(WIFEXITED(status))
{
// 正常退出
printf("正常退出,退出码: %d\n", WEXITSTATUS(status));
}
else
{
// 异常退出,获取退出信号
printf("崩溃了,推出信号: %d\n", status & 0x7F);
}
}
}
return 0;
}