目录
execl()
execv()
execlp()
execvp()
如何利用execl执行自己写的C/C++可执行程序?
如何利用makefile同时编译两个文件
execle()
execvpe()
简单shell的编写
什么是进程替换?
我们之前fork之后,是父子进程各自执行代码的一部分,然后父子代码共享,数据写时拷贝各自一份.
但是如果子进程就想执行一个全新的程序呢?子进程想拥有自己的代码,这就用到了程序的替换.
程序替换,是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中.
比如只有一个进程,先不考虑父子进程.
先把可执行程序加载到内存中,然后进程通过地址空间及页表的映射找到代码地址,然后执行.
此时如果发生了程序替换,要替换成磁盘中另一个可执行程序other.exe,此时便会将新的磁盘上的程序加载到内存,并和当前页表建立映射.而原本的程序myproc.exe几乎不发生变化.
而这些工作,可以使用exec系列的接口来实现的.
所以进程替换的原理是:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
其中,exec系列函数的本质就是如何加载程序的函数.
我们来演示两个例子,一个是单独一个进程,一个是父子两个进程.
先来看一个进程的:
make编译然后运行:
正如我们的预期.
然后我们进行进程替换,当然需要用到上面所提到的exec系列函数.
execl()
我们man execl查看用法:
这里一共有6个相关的函数,但学会一个,后面的基本上也不是什么问题.
那我们先来第一个execl.
path是要新加载程序的路径。path:路径+目标文件名
第二个是传入一个字符串作为参数,等下会细说.
...这个叫做可变参数列表,即可以传入多个、不定个数的参数.最后一个参数必须是NULL结尾标识参数传递完毕.
我们在命令行上怎么写的,在这里参数就怎么填.什么意思呢,我们看下面的例子.
打开刚才那段代码,利用execl函数,然后我们先用一下系统的程序,例如填写ls的路径,然后参数的话,我们想执行ls,会在命令行上输入ls,所以参数写一个ls即可,如下:
退出来之后,make编译执行.
首先,我们发现execl之前的代码正常运行了,而execl之后的代码并没有运行,这说明此时程序已经被ls替换了.它会将当前代码所有的代码和数据都进行替换,包括执行的和未执行的,一但替换成,后面的代码都不会被执行了.
其次我们发现ls的命令也被执行了,说明替换的程序也正常运行了.
这便是execl的一个用法.
当然还可以加更多的参数,例如:
然后运行:
此时发现程序相当于执行了ls -a -l,显示了隐藏的文件及详细信息.
当然可以换成其它的命令,which,pwd,top...等等.只要填写其路径即可.
知道了用法,然后返回值还没说
说execl只有返回失败的时候才有返回值,返回-1.
也就是说,execl进程替换成功是不会有返回值的.
其实我们仔细想一下也是这样的,execl进程替换成功后,会连自己的execl这一行代码都替换掉,取而代之的一个全新的程序,所以返回值在这里也是没有意义的.
接下来我们演示一个父子两个进程的例子.
然后我们预期的结果应该是子进程ls之后,父进程显示等待成功,并输出子进程的退出码.
可以发现,正如我们预期的所示.
为什么要创建子进程?
为什么不影响父进程,父进程聚焦在读取数据,解析数据,指派进程执行代码的功能!
父进程负责fork,管理这些子进程,子进程负责程序替换,完成自己的工作.
execl之后,父子进程的关系?
加载新程序前,父进程和子进程的关系是代码共享,数据写时拷贝.
当子进程execl加载新程序后,它们父子间代码需要分离,代码需要进行写时拷贝,这样父子进程在代码和数据上就彻底分开了.
execv()
我们同样的先用man查看一下用法
注意和execl的区别:
execl的传参类似list的方式,
而execv是指针数组,和execl没有本质的区别,只有传参上的区别,execv需要我们传入一个指针,这个指针指向的是这个argv数组.我们事先把选项写好到argv数组中,而execl需要我们把每个选项都当做参数传入.
以上是execv的传参方式.我们看用代码是如何写的.
注意和execl的区别,execv相当于是先在外面写好之后,再把写好的数组传进来.
运行之后,结果依然正确.
execlp()
我们依然man查看一下用法:
这个file和之前的path有什么区别呢?
我们上面说的,要寻找替换的文件,需要写它的路径。那么如果不带路径,可以找到程序吗?
当然是可以的,环境变量便是如此。例如我们平常运行ls的时候,不需要加路径即可运行,而我们运行我们自己的可执行程序时,需要加上路径./来运行.
所以execlp的file意思是它会自动在环境变量PATH中寻找,不用告诉它程序在哪里.
用代码这样写:
首先它会自动在环境变量PATH中寻找"ls",然后执行ls -a -l.
同样地,照样结果正确.
同时你要分清以上两个ls的区别:第一个ls意思是你想执行谁(查找路径),第二个ls是你想怎么执行(匹配).
execvp()
这个类似于execl和execp的区别,只是第二个参数不同,即除了传参方式不一样,别的本质都是一样的.
注意和上一个execlp的区别:
只有第二个参数不一样,只是传参方式改变:
照样可以正常运行:
如何利用execl执行自己写的C/C++可执行程序?
我们可以在myproc.c文件中利用execl函数调用 由mycmd.c文件编译形成的mycmd可执行文件.
然后利用make编译两个文件,再执行myproc,便可以调用到自己写的C可执行程序(mycmd).
首先继续刚才的,再myproc.c文件中,先把mycmd的路径写入,为什么方便以后修改,我们可以直接#define一下,然后传入到execl函数中,执行-a选项.
然后编写mycmd.c文件:
需要用到main函数里的命令行参数,如果输入的没有两个参数,就直接结束程序,若输入
mycmd -a ,则输出a.
mycmd -b,则输出b.
然后退出,make编译,那么有一个问题:
如何利用makefile同时编译两个文件
我们之前在make/makefile里讲过,如果直接make,便会从makefile中从上到下执行只执行第一条语句,这样只能编译一个文件,到时候在编译很多文件时,得一个一个编译,会很麻烦.
这个时候便用到了伪对象,也是那一章说过的.
定义一个伪对象all,比如我们最后要形成两个可执行文件mycmd和myproc.
然后我们只维护一个依赖关系,让伪对象all依赖于mycmd与myproc.
这样编译器遇到all时,会自动向下找到这两个语句,然后再分别编译得到它们,这样就成功了.
make编译好之后,我们运行myproc文件
可以发现已经成功运行了我们自己写的C程序了.
execle()
可以发现前两个参数和我们之前的那一套一模一样,所以我们只用看第三个参数即可.
第三个参数是一个环境变量,准确来说是来传递环境变量给新的程序的.
我们在原来的程序myproc.c文件中,写入一个环境变量:
然后再mycmd.c文件中获取环境变量
此时我们再次make编译并运行.
便成功获取到了环境变量.
当我们不传入环境变量时,即新的进程获取不到,便会返回null.
execvpe()
这个接口无非是前面几个接口参数的叠加,只要会前面的几个接口,这个接口也照样可以使用.
file是从环境变量里面查找,argv是要传入的参数选项,envp是要传递的环境变量,前面几个接口都有所提到,这里便不再做演示.
当然以上6个都不是严格意义上的系统接口,只是系统提供的基本封装.真正的系统接口是execve.
filename需要把文件的全路径写上,argv同样也是同上,是参数和选项,最后一个参数也是传递环境变量.
到这里进程替换就讲完了,这些函数很多而且名字相近,各函数的作用也不相同,要记忆起来也是比较困难.
其实仔细观察这些函数名,也是有规律的.
l(list) : 表示参数采用列表,即将参数全部传入到函数中,例如execl,execlp,execle.
v(vector) : 参数用数组,即先在外部将参数选项写入到数组里,再传入到函数中,例如execv,execvp.
p(path) : 有p自动搜索环境变量PATH,自带path,不需要写文件的全部路径,如execlp,execvp.
e(env) : 表示自己维护环境变量,可以传入环境变量,如execle.
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[]);
这张图也是对以上的一种总结,不用死记硬背,理解以上几个意思,看到函数便知道需要怎么传入参数了.
简单shell的编写
简单shell的编写基本上覆盖了之前所有进程控制的知识点,包括进程创建,进程终止,进程等待以及今天的进程替换,具体细节可以查看源代码,会有详细的注释.
这里唯一需要注意的是,当父进程bash创建子进程后,子进程执行的进程替换,即命令是无法完成内置命令的,例如cd这样的,子进程刚完成cd然后进程就退出了,这样并没有意义.
所以我们需要让父进程bash亲自完成,这里需要用到一个chdir函数,具体怎么使用可以man chdir来查看用法,总体代码如下:
include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 #include<unistd.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7
8 #define NUM 1024
9 #define SIZE 32
10 #define SEP " "
11 //保存打散之后的命令行字符串
12 char* g_argv[SIZE];
13 //保存完整的命令行字符串
14 char cmd_line[NUM];
15
16 //shell 运行原理: 通过让子进程执行命令,父进程等待 和 解析命令
17 int main()
18 {
19 //0.命令行解释器,一定是一个常驻内存的进程,不退出
20 while(1)
21 {
22 //1.打印出提示信息:[root@localhost myShell]$
23 printf("[root@localhost myShell]# ");
24 fflush(stdout);
25 memset(cmd_line,'\0',sizeof(cmd_line));
26 //2.获取用户的输入[输入的是各种指令和选项:"ls -a -l"]
27 if(fgets(cmd_line,sizeof(cmd_line),stdin) == NULL)
28 {
29 continue;
30 }
31 cmd_line[strlen(cmd_line)-1] = '\0';
32 //ls -a -l\n
33 //printf("echo: %s\n",cmd_line);
34 //3.命令行字符串解析:"ls -a -l" -> "ls" "-a" "-l"
35
36 g_argv[0] = strtok(cmd_line,SEP);//第一次调用,要传入原始字符串
37 int index = 1;
38 //这段代码等价于下面while(g_argv[index++] = strtok(NULL,SEP));
39 // while(1)
40 // {
41 // g_argvp[index] = strtok(NULL,SEP);//第二次,如果还要解析原始字符串,则传入NULL
42 // index++;
43 // }
//如果是ls命令,我们可以给它加上颜色
44 if(strcmp(g_argv[0],"ls") == 0)
45 {
46 g_argv[index++] = "--color=auto";
47 }
48 while(g_argv[index++] = strtok(NULL,SEP));
49 //for DEBUG
50 // for(index = 0; g_argv[index]; index++)
51 // {
52 // printf("g_argv[%d]:%s\n",index,g_argv[index]);
53 // }
54 //
55 //4.TODO 内置命令:让父进程(shell)自己执行的命令,我们叫做内置命令(内建命令)
56 //内置命令本质就是shell中的一个函数调用
57 if(strcmp(g_argv[0],"cd") == 0)//不想让子进程执行,而是父进程执行
58 {
59 if(g_argv[1] != NULL)
60 {
61 chdir(g_argv[1]);
62 continue;
63 }
64 }
65 //5.fork()
66 pid_t id = fork();
67 if(id == 0)
68 {
69 //child process
70 printf("功能让子进程执行\n");
71 execvp(g_argv[0],g_argv);//ls -a -l
72 exit(1);
73 }
74 else
75 {
76 //father process
77 int status = 0;
78 pid_t ret = waitpid(id,&status,0);
79 if(ret > 0)
80 {
81 printf("exit code:%d\n",WEXITSTATUS(status));
82 }
83 }
84 }
85 return 0;
86 }
我们make编译好后然后运行:
发现功能都可以正常使用了.