进程程序替换
fork创建子进程后一般会有两种行为:
- 想让子进程执行父进程的一部分代码(可以理解为子承父业)
- 想让子进程执行和父进程完全不同的代码,也就是程序替换(可以理解为儿子创业)
原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当程序调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
问题:
1.程序替换的本质是什么?
把磁盘中的程序的代码和数据用加载器加载进特定的进程的上下文,底层用到了exec系列的程序替换了函数。
2.程序替换后,有没有新进程被创建?
没有。因为进程替换前后,没有创建新的PCB,虚拟内存和页表等数据结构,也就是进程的这些数据结构没有发生变化,进程替换只是对物理内存中的数据和代码进行了修改,前后进程的ID没有发生改变,所以程序替换不创建新进程.
3. 子进程发生程序替换后,代码和数据都发生写时拷贝嘛?
由于进程替换会把新程序的代码和数据加载到特定的进程,为了让父子进程之间具有独立性,修改的代码和数据都要发生写时拷贝,这样才不会影响父进程的数据和代码。
替换函数
其中有六种以exec开头的函数,统称exec函数:操作系统其实只提供了第六个系统调用接口,其他五个都是由第六个系统调用接口封装出来的。
#include <unistd.h> extern char **environ; 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 execve(const char *path, char *const argv[], char *const envp[]);
函数返回值:只要exec*返回,就一定调用失败了,调用成功不需要有返回值检测。
函数参数:
- path:用来替换的程序所在的路径
- file:程序名
- arg,...:列表的形式传参
- arg[]:数组的形式传参
- envp[]:自己维护的环境变量
函数名解释:
- l(list):表示参数采用列表
- v(vector):参数用数组
- p(path):有p自动搜索环境变量PATH
- e(env):表示自己维护环境变量
void myfun(char *arg1,char *arg2,char *arg3);//列表式传参 myfun(a1,a2,a3);//list void myfun(char *arg[]);//非列表式的传参 char *arg[] = {a1,a2,a3}; myfun(arg);
参数命名中有I的需要一个一个进行传参,有v的需要将参数放入数组,通过数组传参,有p的第一个参数是file,而不带p的第一个参数是path,有p自动去环境变量PATH中搜索;传参时可以直接传想要使用的命令,不需要传路径,会自动搜索,只需要告诉执行的命令是谁
函数的使用方法
函数名 | 参数格式 | 是否带路径 | 是否使用当前环境变量 |
execl | 列表 | 是 | 是 |
execlp | 列表 | 否 | 是 |
execle | 列表 | 是 | 否,自己组装环境变量 |
execv | 数组 | 是 | 是 |
execvp | 数组 | 否 | 是 |
execve | 数组 | 是 | 否,自己组装环境变量 |
execl
第一个参数是你要执行哪个程序(需要带路径),因为执行程序需要知道你在哪,你是谁,第二个是要执行的程序名,命令行怎么执行,传入什么选项,你就可以在这里直接按照顺序填写参数,命令行上怎么写,这里就怎么写,这种传参方式叫做list方式,最后必须以NULL结尾,告知execl传参结束。
#include<stdio.h> #include<unistd.h> int main() { execl("/usr/bin/ps","ps","-e","-l","-f",NULL); return 0; }
运行结果
第一个参数代表你要执行谁,第二个参数是你在命令行怎么调用执行,在后面的参数中你就怎么传递。
再看一组程序
#include<stdio.h> #include<unistd.h> int main() { printf("begin............................\n"); execl("/usr/bin/ls","ls","-a","-l","-i",NULL); printf("hello world\n"); printf("hello world\n"); printf("hello world\n"); printf("hello world\n"); return 0; }
运行结果
./test是自己写的可执行程序,./test变成了进程,代码执行到execl,进行程序替换,用ls进程的代码和数据替换test进程的代码和数据,执行ls进程。
注意:这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值,所以exec系列函数是没有返回值的,如果返回了,或者执行了后续的代码,一定是程序替换错了。
举个例子,加深理解
#include<stdio.h> #include<unistd.h> int main() { printf("begin...........\n"); execl("/usr/bi/ps","ls","-a","-l","-i",NULL); printf("hello world!\n"); printf("hello world!\n); printf("hello world!\n"); printf("hello world!\n"); return 0; }
运行结果
可以让子进程去干程序替换这件事情:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { //execl("/usr/bin/ls", "ls",_"-a","-l","-i",NULL); //execl("/usr/bin/top" , "top" ,NULL); pid_t id = fork(); if(id<0) { perror("fork"); return 1; } else if(id == 0) { //child printf("i am a child , pid:%d, ppid: %d\n",getpid(),getppid()); execlp("ls","ls","-al",NULL); printf("hello world!\n); exit(1); } //father int status = 0; pid_t ret = waitpid(id,&status,0); if(ret > 0) { printf("wait success!\n"); // printf("exit code: %d,exit status: %d\n",(status>>8)&0xFF,status & 0x7F); } else{ printf("wait failed!\n"); } return 0; }
这里子进程进行了程序替换,退出的进程其实就是ls程序
exec系列函数的理解
软件被加载进内存,需要加载器,一个软件加载到内存就成了进程,首先软件先运行起来变成进程,然后进程调用exec系列函数,就可以完成加载到内存的过程,exec可以理解成一种特殊的加载器.
execv
传参以数组进行传参
#include<stdio.h> #include<unistd.h> int main() { printf("begin............................\n"); char *arg[] = {"ls","-a","-l","-i",NULL}; execv("/usr/bin/ls",arg); printf("you should running here\n"); return 0; }
execvp
带v以及带p,参数用数组传,带p说明第一个参数不需要传路径,它会自动的去环境变量PATH里面去找可执行程序,所以传命令名字就行。
#include<stdio.h> #include<unistd.h> int main() { printf("begin............................\n"); char *arg[] = {"ls","-a","-l","-i",NULL}; execvp("ls",arg); printf("you should running here\n"); return 0; }
execlp
带l以及带p,参数用列表形式传,带p说明第一个参数不需要传路径,传命令名字就行。
#include<stdio.h> #include<unistd.h> int main() { printf("begin............................\n"); execlp("ls","ls","-a","-l","-i",NULL); printf("you should running here\n"); return 0; }
elecle
带l和带e的,带e表示自己维护环境变量,传入默认的或者自定义的环境变量给目标可执行程序。
在说明elecle函数之前,我门先想一个问题:
exec系列函数能调用系统程序,那么他能调用自己的程序嘛?答案是可以的。
#include<stdio.h> int main() { int i = 0; int sum = 0; for(;i<=100;i++) { sum+=i; } printf("result[1~100] sum is:%d\n",sum); return 0; }
Makefile的编写:
makefile默认只生成一个可执行程序,默认是自顶向下扫描makefile文件遇到的第一个目标
输入make bin命令 ,默认就生成bin
.PHONY:all all:mytest mycmd mytest:Test.c gcc -o $@ $^ mycmd:mycmd.c gcc -o $@ $^ .PHONY:clean clean: rm mytest mycmd
我们在Test.c中使用程序替换execl函数去执行我们写的程序:
#include<stdio.h> #include<unistd.h> int main() { printf("begin............................\n"); execl("./mycmd","./mycmd",NULL); return 0; }
可以看到我们成功的通过exec系列函数调用自己写的程序:
上面说的都是为了说明execle系统调用所做的铺垫,下面我们再来看execle:
我们在mycmd.c中获取一个环境变量myenv,而这个程序它本身是没有myenv这个环境变量的,所以我们就可以通过execle函数给我们写的程序将环境变量传过去:
#include<stdio.h> #include<stdlib.h> #include<unistd.h> int main() { char *env[] = {"myenv = you_can_see_me!",NULL};//自定义环境变量 printf("begin............................\n"); execle("./mycmd","./mycmd",NULL,env);//调用该函数并将自定义的环境变量数据传给目标程序 return 0; }
#include<stdio.h> #include<stdlib.h> int main() { int i = 0; int sum = 0; for(;i<=100;i++) { sum+=i; } printf("result[1~100] sum is:%d\n",sum); printf("myenv: %s\n",getenv("myenv")); return 0; }
我们运行mytest,发现完成的程序替换,而且将环境变量也传过去了。
man查看exec系列函数:
我们发现execve是和上面的函数分开的,本质上是因为,execve是最底层的系统调用,其他都是去调用它去完成的:
实际中,我们可以fork出子进程,让子进程去进行程序替换,替父进程完成事情:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { pid_t id = fork(); if(id<0) { perror("fork error\n"); return 1; } if(id == 0) { //child execl("usr/bin/ls","ls","-a","-l","-i",NULL); //如果返回则替换失败 exit(-1); } pid_t ret = waitpid(id,NULL,0); if(ret > 0) { printf("wait success,cmd exit\n"); } return 0; }
父进程正常执行自己要干的事情,因为替换的是子进程,进程是有独立性的,所以,父进程是不受影响的!
简易shell的实现
要写一个shell,需要循环以下过程:
1.获取命令行
2.解析命令行
3.建立一个子进程(fork)
4.替换子进程(execvp)
5.父进程等待子进程退出(waitpid)
我们发现shell运行原理就是用户执行命令,shell解释器创建子进程去执行命令,子进程将执行结构告诉shell,最后再反馈给用户,其实就是给上面的程序套上一层循环去创建子进程去执行命令:
#include<stdio.h> #include<unistd.h> #include<stdlib.h> int main() { while(1) { pid_t id = fork(); if(id<0) { perror("fork error\n"); return 1; } if(id == 0) { //child execl("usr/bin/ls","ls","-a","-l","-i",NULL); //如果返回则替换失败 exit(-1); } pid_t ret = waitpid(id,NULL,0); if(ret > 0) { printf("wait success,cmd exit\n"); } } return 0; }
下面我们来实现我们的myshell.c,首先我们登录主机后,会打印命令提示符(用户名@主机名 当前目录)提示符,这里我们为了简单,就直接打印一个主机名:
const char* cmd_line = "[temp@VM-0-3-centos myshell]#";
然后我们需要做的就是数据读取,C语言有一个fgets函数,我们可以这样读取数据:
fgets(cmd,SIZE,stdin);
cmd是保存输入命令的一个数组,大小自己决定即可,size是读取的字符个数,stream是从哪里读,这里需要注意的是,我们读取结束后最后一个字符是\n,所以需要将它置为\0
cmd[strlen(cmd)-1] = '\0';//标准输入会输入\n,将\n改为\0
接下来就要进行字符串数据分析,怎么分析呢?我们首先要把输入的字符串以空格为标志进行分割,然后放进一个字符指针数组,不了解strtok函数的可以去了解一下:
//字符串(命令行数据分析) char* args[NUM]; args[0] = strtok(cmd," ");//字符串分割 int i = 1; do{ rgs[i] = strtok(NULL," "); if(args[i] == NULL) { break; } ++i; }while(1);
然后就是创建子进程进行执行命令,子进程通过调用程序替换函数去执行命令,那么我们想一下我们用哪个函数呢?我们的命令是用数组存起来的,所以需要带v,那么就用execvp,并且不用传路径,传命令名就好,会自动去环境变量PATH里找:
pid_t id = fork(); if(id < 0) { perror("fork error!\n"); continue; } //4.执行非内置命令 if(id == 0) { //child execvp(args[0],args); exit(1);//替换失败了就直接退出 } int status = 0; pid_t ret = waitpid(id,&status,0); if(ret>0) { printf("status code :%d\n",(status>>8)&0xff); }
代码实现如下:
#include<stdio.h> #include<string.h> #include<unistd.h> #include<stdlib.h> #define SIZE 256 #define NUM 16 int main() { char cmd[SIZE];//保存命令 const char* cmd_line = "[temp@VM-0-3-centos ~]#"; while(1) { cmd[0] = 0;//清空数据 //memset(cmd,'\0',sizeof(cmd)); printf("%s",cmd_line); //数据读取 fgets(cmd,SIZE,stdin); //printf("%s",cmd); cmd[strlen(cmd)-1] = '\0';//标准输入会输入\n,将\n改为\0 //字符串(命令行数据分析) char* args[NUM]; args[0] = strtok(cmd," ");//字符串分割 int i = 1; do{ args[i] = strtok(NULL," "); if(args[i] == NULL) { break; } ++i; }while(1); //shell内的函数调用,内置命令 pid_t id = fork(); if(id < 0) { perror("fork error!\n"); continue; } //4.执行非内置命令 if(id == 0) { //child execvp(args[0],args); exit(1);//替换失败了就直接退出 } int status = 0; pid_t ret = waitpid(id,&status,0); if(ret>0) { printf("status code :%d\n",(status>>8)&0xff); } } return 0; }
上面的代码只是支持非内置命令,内置命令不可以,比如cd:
我们期望改的是父进程shell的当前路径,这里则是修改的是子进程的当前路径,子进程干完事就退出了,所以不能创建子进程执行cd,也不能让父进程通过程序替换去执行cd,因为执行了父进程会影响,所以需要系统接口来完成命令的执行。
如果想要支持cd命令,就需要在创建子进程前判断命令:
if(strcmp( args[0],"cd" ) == 0 && chdir(args[1])== 0)//chdir修改当前路径 { continue; }
执行结果
最终代码
#include<stdio.h> #include<string.h> #include<unistd.h> #include<stdlib.h> #define SIZE 256 #define NUM 16 int main() { char cmd[SIZE];//保存命令 const char* cmd_line = "[temp@VM-0-3-centos ~]#"; while(1) { cmd[0] = 0;//清空数据 //memset(cmd,'\0',sizeof(cmd)); printf("%s",cmd_line); //数据读取 fgets(cmd,SIZE,stdin); //printf("%s",cmd); cmd[strlen(cmd)-1] = '\0';//标准输入会输入\n,将\n改为\0 //字符串(命令行数据分析) char* args[NUM]; args[0] = strtok(cmd," ");//字符串分割 int i = 1; do{ args[i] = strtok(NULL," "); if(args[i] == NULL) { break; } ++i; }while(1); //3.判断命令 if(strcmp( args[0],"cd" ) == 0 && chdir(args[1])== 0)//chdir修改当前路径 { continue; } //shell内的函数调用,内置命令 pid_t id = fork(); if(id < 0) { perror("fork error!\n"); continue; } //4.执行非内置命令 if(id == 0) { //child execvp(args[0],args); exit(1);//替换失败了就直接退出 } int status = 0; pid_t ret = waitpid(id,&status,0); if(ret>0) { printf("status code :%d\n",(status>>8)&0xff); } } return 0; }