进程替换
进程替换的基本概念
父进程调用fork函数创建子进程,一般而言,父子进程代码共享,数据写时拷贝。即子进程只能执行父进程代码的一部分,这种情况下,一般使用if/else来控制父子进程各自能够执行的代码。现在想让子进程与父进程不是代码共享的,想让子进程执行一个全新的程序,有自己的代码和数据,就要使用进程替换来完成这个任务。
进程替换的概念:进程替换是指通过使用特定的系统调用接口,加载磁盘上一个程序的代码和数据到内存中,让进程的页表重新映射物理内存。
一般情况下父子进程的代码共享,数据写时拷贝:
现在想要达到的效果是加载磁盘中一个程序的代码和数据到内存中,然后子进程的页表重新建立映射,不在使用父进程的代码和数据,达到进程替换的目的。
进程替换不是创建一个新的进程,是把已有进程的页表重新建立映射关系。让子进程使用A的代码和数据,操作系统并没有给A建立它的内核数据结构。程序A要加载到内存中,需要借助加载器,对于一个c/c++文件,要想编译需要使用编译器。编辑文件需要使用编辑器,链接文件要链接器,把一个程序加载到内存中需要使用加载器。
进程替换需要使用到exec系列的函数,exec系列的函数可以把程序从磁盘加载到内存,并且完成进程替换的工作,exec系列函数的功能就是加载器,可以加载任何程序,包括系统的可执行二进制文件,例如可执行命令,也能加载我们自己写的执行程序。
exec系列函数
exec系列的函数有execl, execlp, execle, execv, execvp, execvpe
它们的头文件都是unistd.h
EXEC(3)
NAME
execl, execlp, execle, execv, execvp, execvpe - execute a file
SYNOPSIS
#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[]);
exec系列的函数会把命令行参数以...(可变参数)
或指针数组
的方式传递给main函数,这里的main函数指的是进程在调用exec系列函数后页表重新建立映射以后对应的代码和数据里的main函数。
execl
int execl(const char* path,const char* arg,...)
参数...
表示的是可变参数。execl的使用举例:
execl("usr/bin/ls","ls","-l","-a",NULL);//表示把ls这个可执行程序的代码和数据加载到内存,当前进程的页表重新映射,"-l","-a"是可变参数,可变参数以NULL结束。表示当前进程经过页表重新映射以后进程执行的任务是ls -l -a
execl("usr/bin/top","top",NULL);
int main()
{
printf("开始\n");
execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
printf("结束\n");
return 0;
}
运行结果
[slowstep@localhost day06]$ ./mybin
开始
total 20
drwxrwxr-x. 2 slowstep slowstep 49 Sep 23 15:27 .
drwxrwxr-x. 10 slowstep slowstep 118 Sep 23 14:52 ..
-rw-rw-r--. 1 slowstep slowstep 226 Sep 23 15:27 exec.c
-rw-rw-r--. 1 slowstep slowstep 63 Sep 23 15:07 makefile
-rwxrwxr-x. 1 slowstep slowstep 11024 Sep 23 15:27 mybin
在调用execl以后,进程的代码和数据全部被替换,包括进程已经被执行的代码和数据都会被替换,"开始"能被打印出来的原因是execl是在第一个printf之后才调用的。
execl函数一旦执行成功,该进程后续的所有代码都不会在被执行。execl函数调用失败返回-1,调用成功没有返回值,也不需要返回值,因为execl函数一旦调用成功,该进程的所有代码和数据都会被替换,包括调用的execl本身也会被替换。所以execl函数调用成功没有返回值。
使用execl也可以替换自己写的程序,也可以使用c语言调用fork创建子进程,让子进程使用execl函数把自己的页表映射到其它程序的代码和数据上,并执行这些程序。
test.c
int main(int argc, char *argv[], char *env[])
{
printf("这是新功能\n");
if (strcmp(argv[1], "-a") == 0)
printf("提供-a参数的功能\n");
if (strcmp(argv[2], "-b") == 0)
printf("提供-b参数的功能\n");
return 0;
}
execl("/home/slowstep/mydir/day06/test.out", "./test.out", "-a", "-b", NULL);
一般使用execl的方法都是父进程创建子进程,让子进程去调用execl函数。父进程通过创建子进程,并让子进程调用execl函数,可以让子进程的代码和数据进行替换,执行其它任务,父进程则可以专注于读数据,取数据和解析数据。
子进程调用execl函数加载新程序,会实现父子进程代码和数据的分离。一般情况下,父子进程代码共享,数据写时拷贝。但是调用execl可以把父子进程的代码和数据都分离。
execv
int execv(const char* path,char* const argv[])
,execv函数的使用与execl类似,execl的’l’可以理解为list,表示参数在execl函数的参数列表一串传进去。execv的’v’可以理解为vector,表示把参数以数组的方式传进去。execl与execv的区别在于传参的方式不同
char* _argv[]={"ls","--color=auto","-l","-a",NULL};
execv("usr/bin/ls",_argv);
execlp
int execlp(const char* file,const char*arg,...)
p表示只需要说明可执行程序的名称,不用指定路径,会自动到环境变量中去找,p表示PATH,一般使用exec系列的函数替换系统的可执行程序使用execlp.
execlp("ls","ls","--color=auto","-l","-a");
execvp
int execvp(const char* file,char* const argv[])
char* _argv[]={"ls","--color=auto","-i","-a","-l",NULL};
execvp("ls",_argv);
execle
int execle(const char* path,const char* arg,...,char* const envp[])
参数envp表示环境变量。execle会把命令行参数和环境变量进行传递。execl,execv,execlp,execvp只会把命令行参数进行传递。execle中最后一个’e’指的是env环境变量。
int main(int argc,char* argv[],char* env[])
{
char* myenv[]={"slowstep=100","SLOWSTEP=20",NULL};
execle("./a.out","a.out","-a",NULL,myenv);//环境变量具有具有全局属性的原因是execle把main函数中的env环境变量传递了下去
exit(0);
}
execvpe
int execvpe(const char* file,char* const argv[],char* const envp[])
char* _argv[]={"ls","--color=auto","-l","-a",NULL};
char* _envp[]={"SLOWSTEP=15","slowstep=20"};
execvpe("ls",_argv,_envp);
execle,execvpe,最后一个字母是e表示需要自己维护环境变量。环境变量具有全局属性,本质上是因为在调用该函数的时候把环境变量作为参数传递了下去。
execve才是系统调用
execl,execlp,execle,execv,execvp,execvpe
都不是系统调用,而是对系统调用的封装。execve
才是系统调用。int execve(const char* filename,char* const argv[],char* const envp[])
,对execve系统调用接口的封装是为了满足上层不同的调用场景。
实现一个简单地shell
原理:父进程创建子进程,让子进程执行各种命令。父进程等待子进程并且进行解析。
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#define NUM 1024
#define SEP " " //定义空格为分割标志
int main()
{
while (1) //命令行解释器一定是一个死循环
{
printf("[root@localhost root]# ");
fflush(stdout); //刷新到屏幕
static char str[NUM] = {0}; //存放读取的字符串
memset(str, 0, sizeof str);
while (fgets(str, sizeof str, stdin) == NULL) //不使用scanf,scanf不能读取空格
continue; // fgets读取失败会返回NULL
str[strlen(str) - 1] = 0; // fgets会读取\n,要把最后的\n变成\0 ls\n
static char *_argc[] = {0}; //用来保存解析的字符串
memset(_argc, 0, sizeof _argc);
int index = 0;
_argc[index++] = strtok(str, SEP); //使用strtok拆分串 "ls -a -l" -> "ls" "-a" "-l"
if (strcmp(_argc[0], "ls") == 0)
_argc[index++] = "--color=auto";
if (strcmp(_argc[0], "ll") == 0)
{
_argc[--index] = "ls";
index++;
_argc[index++] = "-l";
_argc[index++] = "--color=auto";
}
while (_argc[index++] = strtok(NULL, SEP))
;
if (strcmp(_argc[0], "cd") == 0) //子进程进行cd父进程目录没有发生变化
{
chdir(_argc[1]); //使父进程目录变化
continue;
}
pid_t id = fork();
if (id == -1)
exit(0);
else if (id == 0) //让子进程执行各种命令
execvp(_argc[0], _argc); //选择execvp函数,直接到环境变量中去找
else //每一次父进程负责回收子进程
{
int status = 0;
pid_t ret = waitpid(id, &status, 0); //父进程要调用waitpid等待回收子进程,阻塞式等待
}
}
exit(0);
}