作者:@小萌新
专栏:@Linux
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:使用进程的基础知识和进程控制知识做出一个简单的shell程序
命令行解释器
- 介绍
- 搭架子
- 缓冲区
- 获取命令
- 如何从标准输入中获取字符串
- 解析命令
- strtok分隔字符串
- 进程替换
- 内建命令
- 工作目录
- chdir函数
- 全部代码
介绍
我们从之前的学习可以知道 实际上我们的命令行解释器就是一个进程 bash
我们在执行例如 ls ll这些命令的时候实际上就是 bash 在创建子进程 让子进程执行这些命令 那么我们能不能模仿 bash 做一个简单的命令行解释器呢?
答案显然是可以的
本篇博客会使用到进程相关概念以及进程控制相关知识 如果没有前置知识的储备建议先看我之前写的两篇博客
进程相关概念
进程控制相关
搭架子
我们首先先观察我们自己使用的命令行是什么格式的
我们发现 这里的格式就是 用户名@主机名+地址
我们可以直接复制到我们的程序中使用
int main()
{
while(1)
{
printf("[root@VM-8-3-centos lesson11]# ");
sleep(1);
}
return 0;
}
我们之后运行这个程序
我们发现 这个程序竟然没有任何输出的内容
这是为什么呢?
缓冲区
我们这里再次介绍下缓冲区的概念
为了提高效率 我们在打印数据的时候并不是直接打印到输出设备当中而是会先把数据存放到一个叫做输出缓冲区的地方 直到我们主动去刷新这个输出缓冲区里面的数据才会打印到输出设备
输入同理
我们平时打印语句的时候一般会在末尾加上一个 \n 换行符 实际上它就具有刷新缓冲区的功能
此外我们还可以使用fflush(stdout) 去主动刷新缓冲区
至于stdout是什么 我们在后面的基础IO部分会着重讲解
加上刷新缓冲区代码之后我们的代码如下
int main()
{
while(1)
{
printf("[root@VM-8-3-centos lesson11]# ");
fflush(stdout);
sleep(10);
}
return 0;
}
之后我们再次编译运行便会出现这样的情况
我们现在已经可以基本模拟bash的框架了
当然为了区分我们自己的minishell和bash 我们将最前面的用户改为shy
获取命令
搭好架子之后我们就可以开始模仿bash的行为了
bash的本质是一个命令行解释器
所以说我们必须要先获命令是什么
我们可以使用字符数组来获取我们输入的命令
我们定义一个字符数组 它的默认大小为128
#define NUM = 128
char command[128];
之后我们每次输入命令之后要清空字符串 这里有一种十分简便的方式
command[0] = 0;
因为这个字符数组的第一位被设置为了0 所以说这个字符串就是为空的
如何从标准输入中获取字符串
我们可以使用c语言中的scanf来获取我们输入的字符串吗
答案显然是不行的
因为我们输入的命令是这样子的格式
ls -a -l
我们可以发现 中间使用了空格号分割 但是我们的scanf遇到空格就是停止读取
这样子我们就只能使用另外的函数
这里我们使用fgets这个函数
这个函数具有三个参数 我们下面分别介绍下这三个参数
char* str
是我们要读取数据到哪里
int num
是我们要读取数据的个数
FILE* stream
是我们要从哪个文件读取数据
所以我们可以这么写
fgets(command , NUM , stdin);
它的意思是从标准输入流中读取数据 读取NUM个数据到command中
我们实验下到目前为止的代码
8 #define NUM 120
9
10
11 int main()
12 {
13 char command[NUM];
14 while(1)
15 {
16 command[0] = 0;
17 printf("[shy@VM-8-3-centos lesson11]# ");
18 fgets(command, NUM , stdin);
19 printf("echo: %s",command);
20 fflush(stdout);
21 sleep(10);
22 }
23 return 0;
24 }
我们编译运行下试试看
我们输入在缓冲区的数据确实是被打印到屏幕上了
可是为什么这里多打了一个换行呢?(/n)
这里其实是因为我们敲回车的时候往缓冲区多输入了一个/n
要解决这个办法也很简单 我们找到这个/n字符的所在位置 将它置0就好了
它在缓冲区中应该是这个样子的
我们只要将字符串有效字符后一位置0就可以了
我们使用strlen测出长度之后减去一便是\n的下标
将它置0就可以
代码表示如下
command[strlen(str)-1];
这样就能完美获取标准输入中的字符串了
解析命令
我们输入的命令是这样子的格式
要将这一串字符串解析成一个个的命令我们必须要分隔字符串
在c语言中 我们使用strtok命令来分隔字符串
strtok分隔字符串
它的返回值会返回我们分隔字符串的第一个字符位置
它有两个参数
第一个参数 char * str
表示要分割的字符串
第二个参数 const char *delim
表示分隔符
如果我们想要一直获取同一个字符串 则第一个参数在后面的时候都要传入空指针
我们可以写一段代码验证下这个函数的使用
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "Hello, World! Welcome to C programming.";
const char delim[] = " ,.!"; // 以空格、逗号和句号作为分隔符
char *token;
// 获取第一个子字符串
token = strtok(str, delim);
// 依次获取剩下的子字符串
while (token != NULL)
{
printf("%s\n", token);
token = strtok(NULL, delim);
}
return 0;
}
它运行的结果如下
我们使用strtok去获取我们的指令到一个字符数组中
24 int i = 1;
25 char* delim = " ";
26 argv[0] = strtok(command , delim);
27 while(argv[i] = strtok(NULL , delim))
28 {
29 i++;
30 }
进程替换
我们这里要想清楚一点 绝对不能使用父进程去进行进程替换
因为如果我们使用父进程进行进程替换的话父进程的代码和数据就会全部被覆盖
这样子的话命令行就成一次性工具了
所以说我们的父进程需要创建一个子进程去做这个任务
父进程在此时等待子进程做完任务回收资源
代码表示如下
32 if (fork() == 0)
33 {
34 // child
35 execvp(aegv[0] , argv);
36 exit(1);
37 }
38
39 waitpid(-1 , NULL , 0);
此时我们的程序就基本完成了
我们运行下试试效果
我们发现它基本上可以模仿bash进程的各种行为了
中途有一次我敲击了 ll 命令系统没有反应是因为我们没有定义这个命令
内建命令
内建命令 就是由 Bash 自身提供的命令
但是我们目前的代码仍然会遇到一些问题
我们敲出下面的命令
我们发现我们使用cd … 命令之后竟然没有回到上一级目录 这是为什么呢
工作目录
实际上我们的进程中有一个叫做工作目录的东西
我们可以在 /proc/进程号中看到
该路径也叫做进程的当前路径
我们使用cd …命令的时候实际上就是对于这个路径在进行操作
回到我们之前的问题 为什么我们使用cd命令的时候系统不能识别呢?
因为我们是通过子进程去调用的cd命令 子进程有着自己的进程目录
它的修改经过写时拷贝不会影响父进程
所以说我们如果是遇到内建命令的话就需要父进程自己去执行之
那么我们如何改变父进程的工作目录呢?
chdir函数
我们可以通过chdir函数来改变 当前进程的工作目录
它的使用方式如下
我们直接在它后面输入我们要改变的路径就可以
知道如何修改之后我们继续编写代码
32 // 内建命令
33 if (strcmp(argv[0], "cd") == 0)
34 {
35 if (argv[1] != NULL)
36 {
37 chdir(argv[1]);
38 }
39
40 continue; // 不执行下面了
41 }
这样子我们的所有代码就完成了
全部代码
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5 #include <sys/types.h>
6 #include <string.h>
7
8 #define NUM 120
9 #define CMD_NUM 40
10
11 int main()
12 {
13 char command[NUM];
14 char* argv[CMD_NUM];
15 while(1)
16 {
17 command[0] = 0;
18 printf("[shy@VM-8-3-centos lesson11]# ");
19 fflush(stdout);
20 fgets(command, NUM , stdin);
21 command[strlen(command)-1] = 0;
22 // printf("echo: %s",command);
23 // ½âÎöÃüÁî
24 int i = 1;
W> 25 char* delim = " ";
26 argv[0] = strtok(command , delim);
W> 27 while(argv[i] = strtok(NULL , delim))
28 {
29 i++;
30 }
31
32 // ÄÚ½¨ÃüÁî
33 if (strcmp(argv[0], "cd") == 0)
34 {
35 if (argv[1] != NULL)
36 {
37 chdir(argv[1]);
38 }
39
40 continue; // ÏÂÃæ²»Ö´ÐÐÁË
41 }
42
43 if (fork() == 0)
44 {
45 // child
46 execvp(argv[0] , argv);
47 exit(1);
48 }
49
50 waitpid(-1 , NULL , 0);
51 }
52 return 0;
53 }