目录
前言
我们已经接触了很长时间的Linux,我们对shell特别的好奇,正好前面我们学习了shell的运行原理,以及进程相关概念,所以我们现在来实现一个简单的shell
一、大概思路
我们知道shell是一个永不退出的程序,所以他应该是一个死循环,并且shell为了防止影响到自己,我们在命令行上输入的所有命令都是由shell的子进程来执行的,所以它应该要有创建子进程的相关函数,当然也会有进程替换的相关函数,因为我们直接创建子进程,父子进程是共享代码的,如果没有进程替换,shell根本无法让子进程执行特定的命令。
二、命令行显示及获取用户输入命令
我们都知道,shell会有一行提示信息 例如:[ww@VM-8-14-centos 10.9]$
所以我们要在屏幕上打印相关信息
printf("[root@localhost myshell]# \n");
我们可能会写出这样的代码,我们先测试一下
打印的效果很成功
接下来是获取用户在命令行上输入的命令
我们可以使用fgets函数来实现
同时我们也需要一个字符串来保存命令
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM 1024
char cmd_line[NUM];
int main()
{
//shell是一个死循环
while(1)
{
printf("[root@localhost myshell]# \n");
//防止读取失败
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
{
continue;
}
}
return 0;
}
[ww@VM-8-14-centos 10.9]$ ls
a.out Makefile myshell.c
[ww@VM-8-14-centos 10.9]$ ./a.out
[root@localhost myshell]#
ls
[root@localhost myshell]#
rm
[root@localhost myshell]#
^C
[ww@VM-8-14-centos 10.9]$
输出出来的是这些,好像与系统的shell不一样,系统的shell输入命令时是与提示信息在一行的,我们现在是在两行,我们可以去掉\n,同时也要刷新缓冲区,因为前面在我们写进度条的时候就已经引入了行缓冲区的概念以及回车与换行的区别
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM 1024
char cmd_line[NUM];
int main()
{
//shell是一个死循环
while(1)
{
//打印提示信息
printf("[root@localhost myshell]# ");
//刷新缓冲区,因为没有\n
fflush(stdout);
memset(cmd_line, '\0', sizeof cmd_line);
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
{
continue;
}
}
return 0;
}
接下来是检验字符串内部是否获取了我们输入的命令
我们将数组打印出来,打印之前需要将死循环屏蔽一下,防止输出混乱
[root@localhost myshell]# 123
123
我们发现他竟然还会多出一个空格,因为fgets会读取我们输入的全部信息,包括回车,所以我们只要去掉cmd_line的最后一个字符就可以了
cmd_line(strlen(cmd_line)-1) = '\0';
基本的提示信息打印以及获取命令也就完成了
三、分析命令
我们现在输入的命令都在cmd_line这个数组中,接下来就是分析命令
所有的shell命令都是由‘ ’ 来分隔的,前面是命令,后面是选项
所以接下来的步骤是分隔字串,我们这里就使用库函数strtok来分隔
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
char cmd_line[NUM];
char *g_argv[SIZE];
int main()
{
//shell是一个死循环
while(1)
{
//打印提示信息
printf("[root@localhost myshell]# ");
//刷新缓冲区,因为没有\n
memset(cmd_line, '\0', sizeof cmd_line);
fflush(stdout);
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
{
continue;
}
cmd_line[strlen(cmd_line)-1] = '\0';
size_t index = 1;
g_argv[0] = strtok(cmd_line, SEP);
while(g_argv[index++] = strtok(NULL, SEP));//如果还对原串操作,就传入NULL
}
return 0;
}
这样分析字串基本上就完成了,不过我们可以针对不同的命令定制
例如我们有ll命令,它是ls -l的缩写,我们可以支持这个缩写
ls命令是带颜色的,我们也可以添加
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
char cmd_line[NUM];
char *g_argv[SIZE];
int main()
{
//shell是一个死循环
while(1)
{
//打印提示信息
printf("[root@localhost myshell]# ");
//刷新缓冲区,因为没有\n
memset(cmd_line, '\0', sizeof cmd_line);
fflush(stdout);
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
{
continue;
}
cmd_line[strlen(cmd_line)-1] = '\0';
size_t index = 1;
g_argv[0] = strtok(cmd_line, SEP);
if(strcmp(g_argv[0], "ls") == 0)
{
g_argv[index++] = "--color=auto";
}
if(strcmp(g_argv[0], "ll") == 0)
{
g_argv[0] = "ls";
g_argv[index++] = "-l";
g_argv[index++] = "--color=auto";
}
while(g_argv[index++] = strtok(NULL, SEP));//如果还对原串操作,就传入NULL
}
return 0;
}
四、创建子进程执行命令
我们的shell父进程是聚焦于获取命令以及分析命令,在我们的简单shell中,只要让父进程阻塞式等待就好了,父进程在这个过程中主要是负责创建子进程,等待子进程,获取子进程的退出状态
子进程进行进程替换,执行命令
在进行进程替换时,我们最好使用exec系列中的带v和p的函数,比较方便我们调用,如果想要传入环境变量可以采用带e的
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
char cmd_line[NUM];
char *g_argv[SIZE];
int main()
{
//shell是一个死循环
while(1)
{
//打印提示信息
printf("[root@localhost myshell]# ");
//刷新缓冲区,因为没有\n
memset(cmd_line, '\0', sizeof cmd_line);
fflush(stdout);
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
{
continue;
}
cmd_line[strlen(cmd_line)-1] = '\0';
size_t index = 1;
g_argv[0] = strtok(cmd_line, SEP);
if(strcmp(g_argv[0], "ls") == 0)
{
g_argv[index++] = "--color=auto";
}
if(strcmp(g_argv[0], "ll") == 0)
{
g_argv[0] = "ls";
g_argv[index++] = "-l";
g_argv[index++] = "--color=auto";
}
while(g_argv[index++] = strtok(NULL, SEP));//如果还对原串操作,就传入NULL
pid_t id = fork();//创建子进程
if(id < 0)
{
perror("fork");
exit(1);
}
else if(id == 0)
{
//子进程
printf("准备执行\n");
execvp(g_argv[0], g_argv);
//子进程能够运行到这里说明替换失败
exit(1);
}
else
{
//父进程
int status = 0;
pid_t res = waitpid(-1, &status, 0);//阻塞式等待
if(res > 0)//等待成功
{
printf("exit code: %d \n", WEXITSTATUS(status));
}
}
}
return 0;
}
我们可以测试一下
好像没有问题,接下来我们可以测试一下cd命令
我们发现cd命令是失效的,这就与shell命令的种类有关了
shell命令通常有两种
1、第三方提供的对应的放在磁盘中有具体二进制文件的可执行程序(由子进程执行)
2、shell内部自己实现的方法,由自己(父进程)来进行执行
通俗的来说有些命令是影响shell本身的,所以这时候就不能让子进程去执行
我们只要判断一下是否是cd命令,然后对cd命令进行特殊处理就可以了
cd命令可以使用chdir来实现
然后我们发现就成功了
五、导入环境变量
这里导入环境变量为什么要单说呢?
这里面有一个巨大的坑,等会细说
我们先看一下导入环境变量的函数
我们可以使用putenv来获取环境变量
同时我们在写一个子程序来获取环境变量
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("i am process\n");
printf("ww=", getenv("ww"));
return 0;
}
我们发现它有一点奇怪,我们获取的环境变量,并不是我们想要的
这就是那个大坑,我们发现环境变量在父进程中是导入成功的,可是我们的子程序却无法成功获取
我们打印一下,父进程的环境变量
我们仔细查看是能够找到ww=0305的
证明父进程没有问题
那么问题出在哪里了?
实际上是我们在分隔字符串时,只存入了它的地址,并没有真正意义上的分离,子程序在寻找环境变量时他无法找到我们设定的所以就会出现奇怪的现象
我们只要将环境变量用另一个数组存储就解决了
这就解决了
六、源码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
char cmd_line[NUM];
char *g_argv[SIZE];
char g_myval[32];
extern char** environ;
int main()
{
//shell是一个死循环
while(1)
{
//打印提示信息
printf("[root@localhost myshell]# ");
//刷新缓冲区,因为没有\n
memset(cmd_line, '\0', sizeof cmd_line);
fflush(stdout);
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
{
continue;
}
cmd_line[strlen(cmd_line)-1] = '\0';
size_t index = 1;
g_argv[0] = strtok(cmd_line, SEP);
if(strcmp(g_argv[0], "ls") == 0)
{
g_argv[index++] = "--color=auto";
}
if(strcmp(g_argv[0], "ll") == 0)
{
g_argv[0] = "ls";
g_argv[index++] = "-l";
g_argv[index++] = "--color=auto";
}
while(g_argv[index++] = strtok(NULL, SEP));//如果还对原串操作,就传入NULL
if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)
{
strcpy(g_myval, g_argv[1]);
int ret = putenv(g_myval);
if(ret == 0) printf("export success\n");
continue;
}
if(strcmp(g_argv[0], "cd") == 0)
{
if(g_argv[1] != NULL)
{
chdir(g_argv[1]);
}
continue;
}
pid_t id = fork();//创建子进程
if(id < 0)
{
perror("fork");
exit(1);
}
else if(id == 0)
{
//子进程
printf("准备执行\n");
execvp(g_argv[0], g_argv);
//子进程能够运行到这里说明替换失败
exit(1);
}
else
{
//父进程
int status = 0;
pid_t res = waitpid(-1, &status, 0);//阻塞式等待
if(res > 0)//等待成功
{
printf("exit code: %d \n", WEXITSTATUS(status));
}
}
}
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("i am process\n");
printf("ww=%s\n", getenv("ww"));
return 0;
}
总结
以上就是今天要讲的内容,本文仅仅简单模拟实现了shell