【进程控制⑦】:制作简易shell&&理解shell运行原理
- 一.交互问题,获取命令行
- 二.字串分割问题,解析命令行
- 三.指令的判断
- 四.普通命令的执行
- 五.shell原理本质
一.交互问题,获取命令行
shell刚启动时就会出现一行命令行,这一行命令行分别表示的用户是谁,主机是谁,当前目录在哪等。
而我们如果想要制作一个shell,肯定也需要输出这些信息,那这些信息从哪里获得呢?
我们直接从环境变量里获取:
所以我们可以利用getenv系统接口获取环境变量里的内容:
分别获取用户,主机和当前目录信息
然后我们按照shell刚启动时的方式输出这些信息:
并且可以输入命令行:注意我们要用fgets来输入命令行,不能用scanf输入,因为scanf遇到空格就会阻塞。我们要完整的输出一行。
1
2 #include <stdio.h>
3 #include <stdlib.h>
4 #include <assert.h>
5 #include <string.h>
6 #include <unistd.h>
7 #include <sys/wait.h>
8 #include <sys/types.h>
9 #define LEFT "["
10 #define RIGHT "]"
11 #define LABLE "#"
12 #define LINE 1024
19 char pwd[LINE];//存储当前目录
20 char commandline[LINE];//用户输入的命令行
27 const char* getusername()
28 {
29 return getenv("USER");
30 }
31
32 const char* gethostname()
33 {
34 return getenv("HOSTNAME");
35 }
36 void getpwd()
37 {
38 //直接调用系统接口获取当前目录,并写入字串里
39 getcwd(pwd,sizeof(pwd));
40 }
//1.获取用户输入的命令
43 void interact(char* cline,int size)
44 {
45 getpwd();//更新一下当前的目录并放入到pwd字符串里
E> 46 printf(LEFT"%s@%s%s"RIGHT""LABLE" ",getusername(),gethostname(),pwd);
47 char*s= fgets(cline,size,stdin); //因为后面不会再用到s,操作系统会报错,所以下面处理一下表示用过
48 assert(s);//断言声明一下,当s为null时就报错
49 (void)s;//表示用过
50
51 cline[strlen(cline)-1]='\0';
52 //因为最后无论如何都要按enter,所以必定会有\换行,abcd\n\0但是我们并不想要这个换行,所以将这个换行替换成\0
53 }
int main()
121 {
122 //shell本质上是一个死循环,一直在使用
123 int quit=0;
124 while(!quit)
125 {
126
127 interact(commandline,sizeof(commandline));
136 return 0;
137
138 }
这样当我们运行程序时,就会输出像shell刚启动时的样子:
这样我们就可以输入命令行了!当我们输入命令行后,我们知道shell会将这些命令行分割成一个一个字串然后执行。所以我们也需要将命令行分割出来,然后解析命令行!分析命令行要干什么
二.字串分割问题,解析命令行
我们分割的字串都放在哪里呢?它们的地址都在哪呢?
这时我们是不是就想当一个表:叫命令行参数表!
其实分割的命令行参数都放在命令行参数表里了,命令行参数表是一个指针数组,里面的都是存在各个字串的地址。
而这里我们定义的命令行参数表作为一个输出型参数,将我们命令行分割的字串带出来。并且计算分割的字串的个数是多少。
13 #define ARGV_SIZE 32
17 char *argv[ARGV_SIZE];//命令行参数表,作为输出型参数,将分割的命令行子串保存,保存每个字串的地址
//2.子串分割问题,解析命令行
55 int splitstring(char _commandline[],char *argv[])
56 {
56 int i=0;//用来计算切割的字符个数
57
57 //利用strtok来分割字串串,strtok的用法是第一次传字符串参数,以后就不用传,设为NULL
58 argv[i++]=strtok(_commandline,DELIM);
W> 59 while(argv[i++]=strtok(NULL,DELIM));
60
61 return i-1;//最后还会加1所以需要减1.
62
63 }
int main()
121 {
122 //shell本质上是一个死循环,一直在使用
123 int quit=0;
124 while(!quit)
125 {
126
127 interact(commandline,sizeof(commandline));
128 int argc=splitstring(commandline,argv);
129 if(argc==0)continue;//表明是空串
136 return 0;
137 }
三.指令的判断
第三步可以先跳过看第四步,然后再回来看第三步。
为什么要对命令判断呢?判断什么呢?直接执行不行吗?
不行!因为shell中存在内建命令和普通命令之分,普通命令就是通过子进程程序替换来执行,而内建命令必须是由父进程来执行,不能是子进程来执行。为什么呢?
比如cd命令,cd命令是进入某个目录中,如果让子进程执行,当打印当前目录时,进程的当前目录并没有发生改变。因为进程进入了cd要进入的目录,可是这跟父进程有什么关系呢?父进程就不会进入。
所以当命令是cd时,就需要父进程来执行而不能创建子进程来执行。
还有比如export命令,echo命令等都是内建命令,需要父进程执行。
export命令是创建环境变量,必须是由父进程执行,子进程执行父进程就获取不到,而echo命令存在这样的场景:echo $? 会输出最近进程的退出码。这个应该是由父进程来执行的。还有当输入"内容" 输出的应该是内容而不是"内容".还有输出环境变量时应该会输出环境变量对于的内容。这些单纯的程序替换是做不到的。
int buildcommand(char*_argv[],int _argc)
99 {
100
101 if(_argc==2&&strcmp(_argv[0],"cd")==0)
102 {
103 chdir(argv[1]);//直接调用系统接口,跳到指定目录
104 //跳到指定目录后,环境变量里的PWD也需要更改到当前目录
105 getpwd();
106 sprintf(getenv("PWD"),"%s",pwd);//获取到PWD环境变量的内容并将pwd写入到PWD里
107 return 1;//执行完内建命令后,普通命令就跳过
108 //对于内建命令,本质就是shell的内部的一个函数
109 }
else if(_argc == 2 && strcmp(_argv[0], "export") == 0)//export直接利用putenv接口将环境变量创建
100 {
96 strcpy(myenv, _argv[1]);
97 putenv(myenv);
98 return 1;
99 }
100 else if(_argc == 2 && strcmp(_argv[0], "echo") == 0)//输出最近进程的退出码
100 {
101 if(strcmp(_argv[1], "$?") == 0)
102 {
103 printf("%d\n", lastcode);//将子进程的退出码输出
104 lastcode=0;
105 }
106 else if(*_argv[1] == '$')//输出环境变量
{
107 char *val = getenv(_argv[1]+1);
108 if(val) printf("%s\n", val);
109 }
110 else//单纯的打印里面的内容
{
111 printf("%s\n", _argv[1]);
112 }
113
114 return 1;
115 }
116 return 0
int main()
127 {
128 while(!quit){
129
131 interact(commandline, sizeof(commandline));
132
133
135 int argc = splitstring(commandline, argv);
136 if(argc == 0) continue;
137
138
141 //内键命令,本质就是一个shell内部的一个函数
142 int n = buildCommand(argv, argc);
143
144 // 5. 普通命令的执行
145 if(!n) NormalExcute(argv);
146 }
147 return 0;
148 }
四.普通命令的执行
分割完命令行参数,并存放在命令行参数表里后,我们就可以执行命令了!如何执行呢?通过程序替换来执行!也就是我们可以直接可以替换成库里已经提供的可执行程序。比如我们输入ls命令,那么我们就可以直接替换成系统里已经提供的ls命令程序。
对于命令的执行,我们都是通过创建子进程来执行,也就是让子进程进行程序替换。然后父进程等待子进程。
18 int lastcode = 0;
65 //4.普通命令的执行--->通过创建子进程来执行,子进程执行,退出,父进程等待
66 void normalexcute(char *_argv[])
67 {
68
69 //对于cd命令,执行命令的是子进程关父进程什么事,所以pwd显示的还是父进程当前目录,所以cd应该是父进程执行,而> 不是子进程执行,其实cd是内健命令
70 //父进程创建子进程
71 pid_t id =fork();
72 if(id<0)
73 {
74 perror("fork错误");
75 return;
76 }
77 else if(id==0)//子进程
78 {
79 //子进程如何执行普通命令呢?通过进程替换!exec* 借助库函数里的,需要带p的v的
80 execvp(_argv[0],_argv);
81 //不会返回,如果返回了那么就说明进程替换错误
82 exit(EXIT_CODE);
83
84 }
85 else//父进程获取子进程的退出结果
86 {
87
88 int status=0;
89 pid_t ret=waitpid(id,&status,0);
90 if(ret==id)
91 {
92 lastcode=WEXITSTATUS(status);
93 }
94 }
95
96 }
97
122 //shell本质上是一个死循环,一直在使用
123 int quit=0;
124 while(!quit)
125 {
126
127 interact(commandline,sizeof(commandline));
128 int argc=splitstring(commandline,argv);
129 if(argc==0)continue;//表明是空串
130
134 normalexcute(argv);//命令执行
135
136 return 0;
137 }
五.shell原理本质
shell本质就是一个进程,当Xshell程序启动时,就创建了一个进程,这个进程本质是一个死循环。进程首先会输出当前使用者是谁,主机是谁,当前目录在哪。然后当你输入命令时,shell进程会将这个命令行分割成各个字串并存放在命令行参数表里。然后shell就会对这个参数表进行解析,当是普通命令时,就创建子进程进行程序替换执行,当是内建命令时,就是shell进程自己执行。对于环境变量,shell这个进程的环境变量是操作系统就分配好了,创建时就从一个配置文件中直接获取到。然后子进程的环境变量就从父进程继承下去。以上就是shell运行的原理本质!