Linux程序替换
- 创建子进程的目的?
- 程序替换
- 如何实现程序替换?
- 什么是程序替换?
- 先见一见单进程版本的程序替换
- 程序替换原理
- 多进程版本的程序替换
- execl函数组
- 简易版Shell
创建子进程的目的?
目的:为了帮助父进程完成一些特定的任务;
子进程帮助父进程完成任务的方式有那些?
1、执行一段父进程的代码;(这是我们初学者经常使用子进程的方式):
2、让子进程执行一段与父进程完全不一样、全新的代码;
那么如何做到让子进程执行一段全新的代码呢?
对子进程实现程序替换;
程序替换
如何实现程序替换?
Linux给我们提供了7个接口:
#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[]);
int execve(const char *path, char *const argv[], char *const envp[]);
这些函数叫做exec函数组,我们暂且先不详细讲解着些函数的具体用法,我们后文在重点讲解;
我们现在只需知道通过调用这7个函数中的任意一个就可以完成程序替换;
什么是程序替换?
先见一见单进程版本的程序替换
为此我们先从最简单的ececl()函数讲解着走:
execl函数第一个参数path,表示我们要替换的程序在哪里,第二个参数arg表示我们想用怎么样的方式运行我们的程序!写完记得在传个参数NULL结尾!
test代码:
运行结果如下:
现象:我们观察到了我们的进程执行了我们begin的打印,然后又立马执行了ls -a -l
命令,随之整个进程就运行结束了,我们发现并没有像我们想象的那样接着运行我们所写的printf(“end…\n”);语句;
这是为什么?
想要弄清楚这些现象产生的原因我们就必须清楚execl()接口进行程序替换的原理;
程序替换原理
首先根据上面的代码,我们自己写的代码(比如:打印begin的语句)在运行起来的时候就已经是一个进程,那么这时候该进程就已经有了自己的内核数据结构了,比如:pcb、进程地址空间、页表等!现在当我们的进程运行到ececl语句的时候,会发生程序替换:
当我们自己写的程序运行到execl()语句时,就会根据ececl()的path参数,将ls命令从磁盘加载进物理内存!加载到物理内存的那个地方呢?加载到我们自己写的程序对应的物理内存上的位置!
也就是说用ls命令的数据和代码,替换原来老的程序的代码和数据,物理空间还是原来的物理空间,页表的映射关系也基本不变,如果ls命令数据和代码太多了,os会在页表增加一些映射关系!然后该进程开始重新运行一段新的程序!
明白了上面的原理,我们就能明白为什么我们的老程序在运行到execl过后,会执行一段新的程序!同时由于新程序的代码和数据替换了老程序的代码和数据,当新程序运行结束过后,并不会再去运行原来老的程序的剩下的语句,比如上面代码中的打印end语句,因为打印end语句属于老程序的代码,而老程序的代码在execl接口中就被替换了,新程序运行完毕,也就表示这个进程终止了!新程序的退出码也就是该进程的退出码了!
换而言之,就是当我们的程序利用execl完成程序替换过后,当前进程的退出码就由新程序决定了!
为此我们可以查看一下在程序替换过后,进程的退出码!
测试代码:
当然我们也可以让我们的程序执行一个错误的ls命令,再次来观察当前进程的退出码:
站在进程的角度的角度来看等待程序替换:
现在我是一个进程,我的代码段和数据段都在物理内存上有一份映射,现在我调用了execl接口,execl会将我在物理内存上的数据和代码用一个新的程序的数据和代码来替换!然后我(当前进程)开始重新运行这段新程序!并且我(当前进程)的退出码由这个新程序的main函数返回值来确定!在整个程序替换期间,我(当前进程)并没由被销毁,我依旧存在!在完成程序替换过后,依旧是我(当前进程)来运行这段新程序!并不会创建一个新的进程来运行新的代码!
站在新程序的角度来看待程序替换:
我是一个程序,我安安静静的躺在磁盘看“电视”,突然某一天我被execl加载进内存,让后某个进程就要求我帮他办件事;那么我(新程序)被加载进内存这个动作是由谁完成的? execl!!!
execl就充当着这个加载器的角色!
既然我自己写的程序都能加载新的程序,那么OS?
当我们想要运行某段程序的时候,OS会首先为我们的程序建立pcb、进程地址空间、页表等内核数据结构,也就是说这时候进程已经创建好了,然后在让当前进程调用execl()接口将我们的程序加载进内存,然后再开始运行我们程序!而我们的程序是从main函数开始的,但是我们的进程是先调用的execl过后我们的程序才开始运行起来的,那么换而言之在execl内部,帮助我们完成程序替换过后,execl会调用该程序的main函数,然后让该程序成功运行起来!
多进程版本的程序替换
上面我们讲解了单进程版本的程序替换和程序替换的原理,接下来我们来尝试一下多进程版本的程序替换:
也就是说我们让我们的子进程去执行一段与父进程完全不一样的代码:
测试代码:
当然,我们也可以让子进程去运行我们自己写的程序,无论我们的程序是用什么语言写的!
比如,现在我用C语言写一个程序,去运行一个C++写的程序:
测试:
被子进程运行的程序:
主程序:
运行结果:
接下来我们来讲解一下多进程进行程序替换的原理:
首先我们的父进程,也就是mytest利用fork函数创建了一个子进程对吧!
那么刚开使的时候,子进程会继承父进程的大多数信息,包括子进程会共享着父进程的代码和数据,通过前面的学习我们知道,当我们的子进程想要修改与父进程共享的数据时,会发生写时拷贝!在物理内存中重新找一块新空间,让后将将需要修改的数据拷贝到新空间中去,然后修改子进程页表映射到该物理内存的映射关系,然后再让子进程去修改数据!以此达到进程之间的相互独立!
那么现在也是这样:刚开始的时候父子进程都共享着同一块物理内存的数据和代码:
当我们的子进程调用execl函数进行程序替换时,是会用程序的代码和数据来替换子进程原来数据段和代码段存的信息的!如果我们直接在“数据”和“代码”这块空间进行替换的话,我们就会将父进程的代码和数据也替换掉!从而影响到了进程的独立性!我们现在的目的是不想影想父进程,而让子进程执行一段全新的代码,为此os也会也会触发写时拷贝,当我们的子进程尝试修改代码段和数据段的信息时,os也会去重新找一物理内存中重新找一块空间来存储子进程的代码和数据,同时修改子进程代码段和数据段映射关系!
重新理解Shell运行原理
明白了上面的过程我们就能更好的理解Shell的运行原理了,首先shell从命令行接受到我们的命令后会创建一个子进程来执行我们的命令,然后在让该子进程调用execl函数来进程程序替换,替换掉子进程从Shell哪里继承下来的代码和数据!然后让子进程开始运行这段程序!
execl函数组
下面我们来正式介绍一下execl函数组:
int execl(const char *path, const char *arg, …)
参数: path//用于指定我们执行的命令在哪里
arg: 可变参数,可以传任意个参数,该参数的作用主要是告诉execl()你想怎样执行这段程序,你在命令行是怎么写的,在arg参数就怎么写,注意分割;比如:我们需要让execl按照ls -a -l
的格式执行ls命令,那么我们喂给execl的参数就是(从第二个参数起):“ls”、“-a”、“-l”,NULL;一个选项一个字符串,注意当我们确定完程序运行的格式过后,必须以再传递一个NULL结尾!表示我们已经传递完当前程序的执行的格式;
比如:
返回值:该函数只会返回-1;由于execl是进行程序替换,当execl完成程序替换那一刻开始,execl后续的代码都被替换成了新程序的代码和数据,根本就运行不到后续的代码和数据,因此也就无法返回程序替换成功的返回值;当我们的程序替换失败的时候,我们进程的老数据和代码并没由被替换掉,当前进程依旧按照顺序执行剩下的代码,同时才能向上面返回-1来表示程序替换失败!
也就是说,execl程序替换成功是没有返回值的!如果execl有返回值那么说明程序替换失败,当前进程就会执行execl后续的代码!
int execv(const char *path, char *const argv[]);
我们可以发现execv接口与execl接口十分相似,但是在参数上却并不是一样的!
execl的参数可以是任意个,而execv的参数只有2个;
同时execv的功能与execl的功能一样,只是在使用上有点区别!
我们可以看一看exec+l就表示execl,这个l(list)就代表着列表的意思!表示execl的程序运行格式以列表的形式传递!
exec+v表示execv,这个v(vector)表示数组的意思,就表示程序运行的格式以数组的形式传递;
具体演示:
程序运行结果:
程序依旧正常运行;
有了前面的理解后面我们在认识其他exec函数就轻松了:
int execlp(const char *file, const char *arg, …);
exec+l+p:l表示以列表的形式传递程序如何运行这个程序!
p:表示path,表示我们只需告诉execlp我们要运行的程序的名称也就是传递file参数,execlp会自动去PATH环境变量下搜索!
具体演示:
程序运行结果:
int execle(const char *path, const char *arg, …,char *const envp[]);
l:如何运行程序的参数以列表的形式传递;
e:env表示自己维护环境变量
比如:我们可以将当前进程的环境变量表传递给我们的新程序!
我们的新程序就可以使用这张环境变量表:
子进程去替换的程序:
主程序:
程序运行结果:
我们也可以向环境变量里面加一点东西进去:
这里我们就需要使用putenv()这个函数了,putenv()功能是向环境变量表中导入一个环境变量!
讲解到这里,其他的execl函数也就依此类推了;
只不过我们需要注意一下,在exec函数组中
只有int execve(const char *filename, char *const argv[])是真正的系统调用!
其他的exec函数是基于该系统调用进行的封装!
简易版Shell
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<unistd.h>
#include<string.h>
#include<stdbool.h>
#define COMMOD_NUM 256
#define ARGV_NUM 64
bool Strtok(char*commod,char**argv)
{
//先跳过空格
size_t i=0;
size_t len=strlen(commod);
size_t k=0;
while(i<len&&commod[i]==' ')
i++;
if(i>=len)
return false;
size_t begin=i;
size_t end=begin;
while(begin<len)
{
while(commod[end]!=' '&&commod[end]!='\0')
end++;
commod[end]='\0';
argv[k++]=commod+begin;
begin=end+1;
end=begin;
}
argv[k]=NULL;
return true;
}
extern char**environ;
int main()
{
while(1)
{
printf("[cxk@VM-12-16-centos myshell]$ ");
char commod[COMMOD_NUM]={0};//用于接受从命令行输入的命令
char*argv[ARGV_NUM]={NULL};//用于存储将commod切割成一个一个字符串的指针
fgets(commod,COMMOD_NUM,stdin);
commod[strlen(commod)-1]='\0';
//分割字符串
if(Strtok(commod,argv)==false)
continue;
//创建子进程
pid_t id=fork();
if(id==0)
{
int n= execvp(argv[0],argv);
if(n==-1)
{
printf("-bash: %s: command not found\n",argv[0]);
exit(1);
}
}
//父进程
waitpid(-1,NULL,0);
}
return 0;
}