目录
一、进程替换
1.1 进程替换的概念
1.2 替换函数
二、命令行解释器-Shell
2.1 shell的实现与运行
2.2 步骤讲解
当我们使用 fork 函数创建子进程后,父子进程各自执行父进程代码的一部分。那如果创建的子进程想要执行一个全新的程序呢?那我们便可以通过进程替换来实现该功能。
程序替换:是通过特定的接口,加载到磁盘上的一个权限的程序(代码和数据),加载到调用的进程的地址空间中。
子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新进程替换,从新程序的启动历程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
其中有六中以exec开头的函数,统称exec函数:
#include
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[]);
函数解释
- 这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回
- 如果函数调用出错返回-1
- 所以exec函数只有出错的返回值没有成功的返回值
命名理解
- l(list):表示参数采用列表
- v(vector):参数用数组
- p(path):有 p 自动搜索环境变量PATH
- e(env):表示自己维护环境变量
事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用 execve,所以 execve 在 man手册第2节,其他函数在第3节,这些函数之间的关系如下图所示:
首先我们来介绍 execl 函数。
其中第一个参数 path 为路径,我们需要传入路径+目标文件名;第二个参数 arg,是可变参数列表,我们可以向其中传入不定的参数;其中 arg 中的参数,我们在命令行上是如何执行代码的,就在其参数中如果传参,注意:最后一个参数必须是NULL,表示传参结束。
结果:(没执行程序结束处,转而执行了 ls 命令)
因为程序替换会替换掉当前调用替换接口的进程,所以这些程序替换接口的调用往往配合着进程创建(fork)一起使用。
为什么要创建子进程?
为了不影响父进程,我们想让父进程聚焦在读取数据,解析数据,指派进程执行代码的功能!如果不创建新的子进程,那么被替换的进程只能是当前运行的父进程;如果创建了,替换的进程就是子进程,而不影响父进程。
说了这么多,我们可以举一个配合使用的代码例子:
运行结果:
然后我们再来看看 execv 函数是如果调用的
该函数要求第二个参数传入指针数组,数组中存放着调用的命令以及选项。
运行结果:
接下来是 execlp 函数的使用
其中这个p表示,该函数会自己再环境变量PATH中进行查找,不用告知路径。
结果如下:
execvp 函数的使用:
第一个参数会自动在环境变量中进行查找,第二个参数传入指针数组即可。
关于执行 execle 函数
首先我们要先实现一个功能:使用C/C++程序,如果调用其他语言的可执行程序。
我们首先编写一个python脚本,然后使用进程替换接口进行调用:
结果如下:
关于execle的第三个参数 envp[] ,即环境变量,应该如何传递呢?
第一个参数传入要执行文件的路径,第二个参数为可变参数列表,第三个参数为环境变量,可以是自己设置的,也可以是主函数中传入的。
代码如下(myproc、mycmd、makefile):
结果:
学习了fork和程序替换的众多接口,我们便可以来实现一个Linux中的命令行解释器shell。
其运行原理就是通过让子进程执行命令,父进程等待&&解析指令。
这里先把代码附上,然后我们来逐步讲解
#include
#include
#include
#include
#define NUM 1024
#define SIZE 32
#define SEP " "
char cmd_line[NUM];
char* g_argv[SIZE];
int main()
{
//0.命令行解释器,一定是一个常驻内存的进程,不退出
while (1)
{
//1.打印出命令提示信息 [root@localhost myshell]#
printf("[root@localhost myshell]#");
fflush(stdout);
sleep(1);
memset(cmd_line, '\0', sizeof(cmd_line));
//2.获取用户的键盘输入[输入的为各种指令和选项 "ls -a -l "]
if (fgets(cmd_line, sizeof(cmd_line), stdin) == NULL)
{
continue;
}
cmd_line[strlen(cmd_line) - 1] = '\0';
//3.命令行字符串解析:"ls -a -l " -> "ls" "-a" "-l"
g_argv[0] = strtok(cmd_line, SEP);
//给ls加颜色
if (strcmp(g_argv[0], "ls") == 0)
{
g_argv[index++] = "--color=auto";
}
while (g_argv[index - 1])
{
g_argv[index++] = strtok(NULL, SEP);
}
//4.父进程执行内置命令
if (strcmp(g_argv[0], "cd") == 0)
{
if (g_argv[1] != NULL)chdir(g_argv[1]);
}
//5.fork(),子进程执行任务
pid_t id = fork();
if (id == 0)
{
printf("下面功能是子进程执行的:\n");
execvp(g_argv[0], g_argv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0)
printf("exit code:%d\n", WEXITSTATUS(status));
}
return 0;
}
我们来看看运行结果:
好的,接下来我们来分析myshell。
1. 打印命令提示符
在linux中,我们用户是这样输入的
这种实现是基于printf'打印出用户和盘符后,调用了fflush立即刷新了缓冲区,然后我们在后面继续输入实现的,然后调用sleep函数让其打印稍显自然,不要立即弹出。
printf("[root@localhost myshell]#");
fflush(stdout); //立即刷新缓冲区
sleep(1);
2.获取用户输入
我们将用户输入的数据存放到一个全局变量数组中,使用fgets函数进行接受(getline、gets也行),因为用户最有的输入会有一个\n,所以我们还要对该\n进行删除。
//2.获取用户的键盘输入[输入的为各种指令和选项 "ls -a -l "]
memset(cmd_line, '\0', sizeof(cmd_line));
if (fgets(cmd_line, sizeof(cmd_line), stdin) == NULL)
{
continue;
}
//清除用户输入的\n
cmd_line[strlen(cmd_line) - 1] = '\0';
3.命令行字符串解析
接受了字符串后我们就要对其进行解析,例如"ls -a -l " -> "ls" "-a" "-l"。
就是将其拆分为字串,然后存放到一个指针数组中。我们就可以使用 strtok 函数来切割。
其中SEP为" ",因为strtok第二个参数是以什么字符串进行切割,所以传入" "。
strtok函数第一次切割传入字符串,第二次以及后续切割直接传入NULL即可。
//3.命令行字符串解析:"ls -a -l " -> "ls" "-a" "-l"
g_argv[0] = strtok(cmd_line, SEP);
//给ls加颜色
if (strcmp(g_argv[0], "ls") == 0)
{
g_argv[index++] = "--color=auto";
}
while (g_argv[index - 1])
{
g_argv[index++] = strtok(NULL, SEP);
}
4.内置命令
内置命令就是让父进程(shell)自己执行的命令,其本质就是shell中的一个函数调用。如果用户输入cd ..,就是改变当前 myshell 的路径,让子进程进行切换路径是无法让所在路径进行切换的,所以这里我们要进行额外的处理。
if (strcmp(g_argv[0], "cd") == 0)
{
if (g_argv[1] != NULL)chdir(g_argv[1]);
}
其中 chdir 是一个系统调用,传入路径,就可以让当前进程进入该路径。
5.fork()子进程执行
接下来就是子进程执行的部分,我们可以直接使用fork创建出一个子进程,然后用 execvp 通过调用环境变量,因为输入的指令本来就以及被添置进了环境变量,然后我们传入我们切割好的指针数组,就可以很好的进行进程替换。
然后使用waitpid让父进程阻塞等待子进程执行完毕。
pid_t id = fork();
if (id == 0)
{
printf("下面功能是子进程执行的:\n");
execvp(g_argv[0], g_argv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret > 0)
printf("exit code:%d\n", WEXITSTATUS(status));