🌠专栏:Linux
目录
[🌠 小贴士:](#🌠 小贴士:)
如若对你有帮助,记得关注、收藏、点赞哦~ 您的支持是我最大的动力🌹🌹🌹🌹!!!
若有误,望各位,在评论区留言或者私信我 指点迷津!!!谢谢 ヾ(≧▽≦*)o \( •̀ ω •́ )/
主体:
cpp
int main()
{
while(true)
{
PrintCommandLine(); // 1. 命令行提示符
GetCommandLine(); // 2. 获取用户命令
AnalysisCommandLine(); // 3. 分析命令
ExecuteCommand(); // 4. 执行命令
}
return 0;
}
一、命令行提示符
命令行提示符一般由:用户名+主机名+当前工作路径 组成。我们如何获取呢?这就不得不提到我们之前提到过的 环境变量表了,我们知道每个子进程都会继承父进程的环境变量表,所以我们可以从环境变量表来获取用户名、主机名、当前工作路径。
cpp
1 #include <iostream>
2 #include <cstdio>
3 #include <cstdlib>
4 #include <cstring>
5 #include <unistd.h>
6 #include <sys/types.h>
7 #include <wait.h>
8
9 using namespace std;
10
11 int basesize = 1024;
12
13 string GetUserName()
14 {
15 string username = getenv("USER");
16 return username.empty() ? "None" : username;
17 }
18
19 string GetHostName()
20 {
21 string hostname = getenv("HOSTNAME");
22 return hostname.empty() ? "None" : hostname;
23 }
24
25 string GetPWD()
26 {
27 string pwd = getenv("PWD");
28 return pwd.empty() ? "None" : pwd;
29 }
30
31 string MakeCommandLine()
32 {
33 char command_line[basesize];
34 snprintf(command_line, basesize, "[%s@%s %s]# ",\
35 GetUserName().c_str(), GetHostName().c_str(), GetPWD().c_str());
36 return command_line;
37
38 }
39 void PrintCommandLine() // 1. 命令行提示符
40 {
41 printf("%s",MakeCommandLine().c_str());
42 fflush(stdout);
43 }
44
45 int main()
46 {
47 while(true)
48 {
49 PrintCommandLine(); // 1. 命令行提示符
50 printf("\n");
51 sleep(1);
52 //GetCommandLine(); // 2. 获取用户命令
53
54 //ParseCommandLine(); // 3. 分析命令
55
56 //ExecuteCommand(); // 4. 执行命令
57 }
58 return 0;
59 }
二、获取用户命令
• 从键盘当中获取出来的字符串,放到command_buffer缓冲区里面
• 将用户输入的命令行,当作完整的字符串
• "ls -a -l -n" 包括空格
• fgets(获取字符串, 指定大小, 从特定的文件流当中获取)
• 获取失败返回null,获取成功返回获取成功的字符串的起始地址
cpp
46 bool GetCommandLine(char command_buffer[], int size) // 2. 获取用户命令
47 {
48 char *result = fgets(command_buffer, size, stdin);
49 if(!result)
50 {
51 return false;
52 }
53 command_buffer[strlen(command_buffer)-1] = 0; // 把最后输入的回车字符改为0,即纯净版输入
54 if(strlen(command_buffer) == 0) return false;
55 return true;
56 }
57
58 int main()
59 {
60 char command_buffer[basesize];
61 while(true)
62 {
63 PrintCommandLine(); // 1. 命令行提示符
64
65 if(!GetCommandLine(command_buffer, basesize)) // 2. 获取用户命令
66 {
// 依次获取每个子字符串
67 continue; // 获取到空格继续
68 }
69 printf("%s\n",command_buffer);
70
71 //ParseCommandLine(); // 3. 分析命令
72
73 //ExecuteCommand(); // 4. 执行命令
74 }
75 return 0;
76 }
三、分析命令
• 解析出来的命令,放到自己构建的全局的环境变量表当中
• strtok(一个字符串分多个,分隔符) 返回值:按照该分隔符从源字符串里切出来的第一个区域
• strtok(str, " "); 如果切割有效的字符串,返回具体的字符串的第一个地址,
• strtok(nullptr, " "); 否则,返回nullptr,即切到最后没有了,自动返回nullptr
cpp
const int argvnum = 64;
const int envnum = 64;
char *gargv[argvnum];// 全局的环境变量表
char gargc = 0; // 记录环境变量的个数
65 void ParseCommandLine(char command_buffer[], int len) // 3. 分析命令
66 {
67 (void)len;
68 memset(gargv, 0, sizeof(gargv)); // 初始化/清空环境变量表里面的内容
69 gargc = 0;
70
71 const char *sep = " ";
72
73 gargv[gargc++] = strtok(command_buffer, sep); // 放到全局的环境列表里面
74
75 // 循环放入环境变量表中
76 while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
77 gargc--;
78 }
79
80 void debug()
81 {
82 printf("argc: %d\n",gargc);
83 for(int i=0; gargv[i]; i++)
84 {
85 printf("argv[%d]: %s\n", i, gargv[i]);
86 }
87 }
88
89 int main()
90 {
91 char command_buffer[basesize];
92 while(true)
93 {
94 PrintCommandLine(); // 1. 命令行提示符
95
96 if(!GetCommandLine(command_buffer, basesize)) // 2. 获取用户命令
97 {
98 continue;
99 }
100 printf("%s\n",command_buffer);
101
102 ParseCommandLine(command_buffer, strlen(command_buffer)); // 3. 分析命令
103 debug();
104
105 //ExecuteCommand(); // 4. 执行命令
106 }
107 return 0;
108 }
四、执行命令
到底是谁在执行命令?是shell自己执行吗?
shell不能自己执行命令,如果shell自己可以自行命令,当命令错误的时候,整个程序就会全挂掉了。shell是一个单进程。shell 创建子进程来执行命令。
在命令行中输入命令,是如何解析的?谁解析的?如何传递给目标子进程的?
shell来帮助我们做解析,形成一张表,然后在进行程序替换的时候,直接调用系统的execvp()函数来执行我的程序,并把这张表传递给我自己对应的进程。
cpp
89 bool ExecuteCommand() // 4. 执行命令
90 {
91 pid_t id = fork(); // 创建子进程
92 if(id < 0) return false;
93 if(id == 0)
94 {
95 // child
96 // 1.执行命令 调用系统函数
97 execvp(gargv[0], gargv);
98 // 2.退出
99 exit(1);
100 }
101 // father
102 int status = 0;
103 pid_t rid = waitpid(id, &status, 0);
104 if(rid > 0)
105 {
106 return true;
107 }
108 return false;
109 }
五、内建命令
每个进程都会有一个当前路径的概念,改变当前路径【chdir】,[cd ..] 就会改变当前工作路径【改变子进程的工作路径】。
当我们在自己写的shell中,我们 [cd ..] 应该改变的是子进程的路径还是自己写的shell进程的路径?
改变shell的工作路径【shell本身就是父shell的一个子进程】,因为子进程执行完命令就会退出,shell当前的工作路径根本就没有改变;改变shell的工作路径,之后执行的命令都会使用shell当前的路径。即:在shell中,有些命令必须由子进程来执行,有些命令不能由子进程执行,要由shell自己执行 --- 内建命令。【shell自己内部调用自己的函数,并没有创建子进程】
我们自己所在的cwd是在进程的PCB当中的,pwd查找的时候也是在PCB里面查找的,PWD是一个环境变量,所以当路径发生变化的时候,要更新一下环境变量。
cpp
114 void AddEnv(const char *item)
115 {
116 int index = 0;
117 while(genv[index])
118 {
119 index++;
120 }
121 // 指向为空的下标
122 genv[index] = (char*)malloc(strlen(item)+1);// 不能使用局部变量,要重新申请
123 strncpy(genv[index], item, strlen(item)+1);
124
125 genv[++index] = nullptr;
126 }
127
128 bool CheckAndExecBuiltCommand()
129 {
130 if(strcmp(gargv[0], "cd") == 0)
131 {
132 if(gargc == 2)
133 {
134 chdir(gargv[1]);
135 }
136 return true;
137 }
138 else if(strcmp(gargv[0], "export") == 0)// 导入环境变量
139 {
140 if(gargc == 2)
141 {
142 AddEnv(gargv[1]);
143 }
144 return true;
145 }
146 else if(strcmp(gargv[0], "env") == 0)
147 {
148 for(int i=0; genv[i]; i++)
149 {
150 printf("%s\n",genv[i]);
151 }
152 return true;
153 }
154 return false;
155 }
我们虽然改变了工作路径,但是命令行提示符并没有发生改变,这是为什么呢?
🌠 小贴士:
• 环境变量是由shell自己维护的,即当路径发生变化时,环境变量表需要由shell自己取更新。
• 我们可以更改环境变量的原因:shell支持用户自己去更改,shell本来就是为用户服务的。
shell不是从0开始读配置文件的,它是从系统额shell直接启动的,所以我们写的shell启动的时候,它默认继承的是系统所对应的环境变量。
• ./myshell 它的环境变量,根本就没有维护环境变量表,它的环境变量其实是从(父进程)系统的shell直接继承的。
• 环境变量表的指针数组,默认是在系统的shell的全局数组给我们维护好的,它也是一张全局的表。
• putenv() 实则就是在父进程所对应的全局指针数组里面,找到一个没有使用的位置,把这个环境变量加进来。
• 导入到系统的shell里面,并不是子进程修改父进程的表,因为当发生修改时,会发生写时拷贝,当子进程不修改,父子进程就是共享的,一旦子进程进行修改,操作系统就会对父shell的指针数组进行写诗拷贝【把整张表全部拷贝一份给子进程】,此时父子进程的环境变量表就是分开的。
• getcwd(字符串,大小);获取当前工作路径【系统级接口,直接从进程的PCB里面拿】。
• 不能用子进程来导入环境变量,子进程不能影响当前的shell。
• 内建命令无法被子进程继承,环境变量可以,所以环境变量具有全局性。
cpp
36 string GetPWD()
37 {
38 //string pwd = getenv("PWD");
39 //return pwd.empty() ? "None" : pwd;
40
41 // 系统调用
42 // 1. 获取当前工作路径
43 if(getcwd(pwd, sizeof(pwd)) == nullptr) return "None";
44
45 // 2. 更新环境变量
46 snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
47 putenv(pwdenv);// PWD=XXXX
48 return pwd;
49 }
六、环境变量
我们在自己写的shell里面创建子进程,这个子进程继承的环境变量表是继承谁的?我自己写的shell的环境变量表?
自己写的shell 和 自己写的shell创建的子进程 的环境变量表,都是继承父shell的。
那我们如何让自己写的shell 创建的子进程继承的环境变量表是自己写的shell的环境变量表呢?
父shell的环境变量表是从系统的配置文件里面来的【shell脚本】,我们从系统里把系统的环境变量拷一份到我们自己的shell里面,模拟一下从配置文件里面去读。
cpp
const int envnum = 64;
// 自己维护的环境变量表
char *genv[envnum];
void InitEnv()
{
// 从父shell获取环境变量
extern char **environ;
int index = 0;
while(environ[index])
{
// 导入环境变量,实则就是向shell自己的环境变量表当中,进行插入一个新的环境变量
// 即 再malloc一段空间,让指针数组指向它对应的那个位置
genv[index] = (char*)malloc(strlen(environ[index]+1));
strncpy(genv[index], environ[index], strlen(environ[index])+1);
index++;
}
genv[index] = nullptr;
}
所以当我们执行命令的时候,我们要把自己的环境变量参数传给系统去执行,因此 在执行一个新的程序时,把命令行参数表、环境变量表都传给execvpe() 系统调用,程序执行时,就能获得这两个参数。
cpp
101 bool ExecuteCommand() // 4. 执行命令
102 {
103 pid_t id = fork(); // 创建子进程
104 if(id < 0) return false;
105 if(id == 0)
106 {
107 // child
108 // 1.执行命令 调用系统函数
109 //execvp(gargv[0], gargv);
110 execvpe(gargv[0], gargv, genv);
111 // 2.退出
112 exit(1);
113 }
114 // father
115 int status = 0;
116 pid_t rid = waitpid(id, &status, 0);
117 if(rid > 0)
118 {
119 return true;
120 }
121 return false;
122 }
🌠小贴士:
• 环境变量:本质是shell自己维护的一张全局的表,最后可以直接通过地址空间,或者通过execvpe(),把自己的环境变量信息传递给目标被替换进程。
• 本地变量:本地变量当字符串。不会通过execvp() 传递给子进程。
七、总结
1、在命令行中,一个命令是如何执行的?对于普通命令来讲,就是解析命令行,然后通过exec*系列的函数接口,进行fork()程序替换。
2、命令行参数是从命令行依次获取的,被shell自己解析自己维护。
3、环境变量表是从系统的配置文件读取出来的,由shell自己维护,维护好就是一个全局的指针数组,通过 execvpe() 接口函数来调用,这个系统的调用接口把环境变量传递给所有的子进程。
4、echo命令,是内建命令。系统除了维护环境变量表、命令行参数表,还维护了一张本地变量表,这张本地变量表无法通过 exec* 这样的接口传递给子进程,所以子进程看不到。在本地变量的这张表,让shell自己去维护,在echo的时候,就去打印。本地变量表属于在shell自己内部维护的全局数据当中的一个字符串。
八、完整代码
cpp
1 #include <iostream>
2 #include <cstdio>
3 #include <cstdlib>
4 #include <cstring>
5 #include <string>
6 #include <unistd.h>
7 #include <sys/types.h>
8 #include <wait.h>
9
10 using namespace std;
11
12 const int basesize = 1024;
13 const int argvnum = 64;
14 const int envnum = 64;
15
16 // 全局的命令行参数表
17 char *gargv[argvnum];// 全局的环境变量表
18 int gargc = 0; // 记录环境变量的个数
19
20 // 自己维护环境变量表
21 char *genv[envnum];
22
23 // 系统的环境变量默认是从配置文件来的【shell脚本】,
24 // 从系统里把系统环境变量拷一份到我的环境变量里面,模拟一下从配置文件里面去读
25
26 // 全局的当前shell工作路径
27 char pwd[basesize];
28 char pwdenv[basesize];
29
30 string GetUserName()
31 {
32 string user = getenv("USER");
33 return user.empty() ? "None" : user;
34 }
35
36 string GetHostName()
37 {
38 string hostname = getenv("HOSTNAME");
39 return hostname.empty() ? "None" : hostname;
40 }
41
42 string GetPwd()
43 {
44 // 获取当前的工作路径不能从环境变量里面获取,因为环境变量要更新
45 // string pwd = getenv("PWD");
46 // return pwd.empty() ? "None" : pwd;
47
48 // 系统调用
49
50 // 直接从系统里面获取环境变量
51 // 获取完之后,把环境变量更新
52 // getcwd(字符串,大小) 获取当前工作路径【系统级接口,直接从进程的PCB里面拿】被封装的接 口gunc封装的接口
53 // 失败返回值为NULL,成功就把当前工作路径写到buff里面
54
55 // 1、获取当前工作路径
56 if(nullptr == getcwd(pwd, sizeof(pwd))) return "None"; // 获得当前工作路径
57
58 // 2、更新环境变量
59 snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
60 putenv(pwdenv); // PWD=XXXX
61 return pwd;
62
63 // 环境变量是由shell自己维护的,即当路径发生变化时,环境变量表需要由shell自己去更新
64 // 我们可以更改环境变量的原因:shell支持用户自己去更改,shell本来就是为用户服务的
65 // shell不是从0开始读配置文件的,它时从系统的shell直接启动的,
66 // 所以我们写的shell启动的时候,它默认继承的是系统所对应的环境变量
67 // ./myshell它自己的环境变量,根本就没有维护环境变量表,
68 // 它的环境变量其实是从(父进程)系统的shell直接继承
69 //
70 // getenv
71 //
72 // 环境变量表的指针数组,默认是在系统的shell的全局数组给我们维护好的,它也是一张全局的表
73 // putenv()实则就是在父进程所对应的全局指针数组里面,
74 // 找一个没有使用的位置,然后把这个环境变量加进来
75 //
76 // 导入到系统额shell里面,并不是子进程修改父进程的表,
77 // 因为发生修改时,会发生写时拷贝机制,
78 // 当子进程不修改,父子进程时共享的,一旦子进程进行修改,
79 // 操作系统就会对父shell的指针数组进行写时拷贝。【把整张表全部拷贝一份给子进程】
80 // 此时父子进程的环境变量表就是分开的
81
82 }
83
84 string MakeCommandLine()
85 {
86 char command_line[basesize];// 输入的命令行长度
87 // snprintf(写入到指定的缓冲区里,指定长度,按照指定的格式)
88 snprintf(command_line, basesize, "[%s@%s %s]# ",\
89 GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());// c_str()
为C风格的字符串
90 return command_line;
91 }
92
93 void PrintCommandLine() // 1.命令行提示符
94 {
95 printf("%s",MakeCommandLine().c_str());
96 fflush(stdout);
97 }
98
99 bool GetCommandLine(char command_buffer[], int size) // 2.获取用户命令
100 {
101 // 从键盘当中获取出来的字符串,放到command_buffer缓冲区里面
102 // 将用户输入的命令行,当作完整的字符串
103 // "ls -a -l -n" 包括空格
104 // fgets(获取字符串, 指定大小, 从特定的文件流当中获取) 获取失败返回null,获取成功返回获 取成功的字符串的起始地址
105 char *result = fgets(command_buffer, size, stdin);
106 if(!result)
107 {
108 return false;
109 }
110 command_buffer[strlen(command_buffer)-1] = 0;// 最后输入的回车,把回车的字符改为0,字符 串为空,即输入就是纯净版的没有换行
111 if(strlen(command_buffer) == 0) return false; // 若输入为空,就终止
112 return true;
113 }
114
115 void ParseCommandLine(char command_buffer[], int len) // 3.分析命令
116 {
117 (void)len;
118 // 解析出来的命令,放到全局的环境变量表当中
119 // strtok(一个字符串分多个,分隔符) 返回值:按照该分隔符从源字符串里切出来的第一个区域
120 // strtok(str, " "); 如果切割有效的字符串,返回具体的字符串的第一个地址,
121 // strtok(nullptr, " "); 否则,返回nullptr,即切到最后没有了,自动返回nullptr
122 // "ls -a -l -n"
123 memset(gargv, 0 , sizeof(gargv)); // 初始化/清空环境变量表里面的内容
124 gargc = 0;
125
126 const char *sep = " ";
127 // 把首个子字符串拿出来
128 gargv[gargc++] = strtok(command_buffer, sep);// 放到全局的环境列表里面
129
130 // =是刻意写的 循环放入环境变量表中
131 while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
132 gargc--;
133 }
134
135 void debug()
136 {
137 printf("argc: %d\n",gargc);
138 for(int i=0; gargv[i]; i++)
139 {
140 printf("argv[%d]: %s\n", i, gargv[i]);
141 }
142 }
143
144 bool ExecuteCommand() // 4.执行命令
145 {
146 // shell 不能自己执行这个命令,如果shell可以自己执行命令,当命令错误的时候就全挂掉了,
147 // shell是一个单进程的。
148 // 让子进程执行命令
149 pid_t id = fork();// 创建子进程
150 if(id < 0) return false;
151 if(id == 0)
152 {
153 // child
154 // 1. 执行命令
155 // exec*
156 // execvp(gargv[0], gargv);
157 // 在命令行当中输入的命令,是如何解析的?谁解析的?如何传递给目标子进程的?
158 // shell来帮我们做解析,形成一张表,然后在进行程序替换的时候,
159 // 直接以execvp的形式执行起来我的程序,并把这张表传递给对应的自己的进程
160
161 // 如果我们在自己写的shell里面创建子进程,
162 // 那这个子进程继承的环境变量表是父shell的,自己写的shell也是继承父shell的环境表
163 // 那我们如何让我们自己写的shell,创建的子进程继承的环境变量表是我自己写的shell的环 境变量表呢?
164 execvpe(gargv[0], gargv, genv);// 把自己写的环境变量参数传给它
165 // 在执行一个新的程序时,把命令行参数表、环境变量表都传给execvpe系统,
166 // 所以自己的程序是父进程通过系统调用,把这两个参数交给了你的程序,
167 // 程序执行时,就能获得这两个参数
168
169
170
171 // 2. 退出
172 exit(1);
173 }
174
175 // father
176 int status = 0;
177 pid_t rid = waitpid(id, &status, 0);
178 if(rid > 0)
179 {
180 // Do Nothing
181 return true;
182 }
183 return false;
184 }
185
186 void AddEnv(const char *item)
187 {
188 int index = 0;
189 while(genv[index])
190 {
191 index++;
192 }
193 // 指向为空的下标
194 genv[index] = (char*)malloc(strlen(item)+1);// 不能使用局部的变量,要重新申请
195 strncpy(genv[index], item, strlen(item)+1);
196
197 genv[++index] = nullptr;
198 }
199
200 // shell自己执行命令,本质是shell调用自己的函数
201 bool CheckAndExexcBuiltCommand() // shell调用自己的内部函数,把自己的路径发生变化
202 {
203 if(strcmp(gargv[0], "cd") == 0)
204 {
205 // 内建命令
206 if(gargc == 2) //
207 {
208 // 系统接口
209 chdir(gargv[1]); // 切换路径
210 }
211 return true;
212 }
213 else if(strcmp(gargv[0], "export") == 0)
214 {
215 // export也是内建命令
216 if(gargc == 2)
217 {
218 AddEnv(gargv[1]);
219 }
220 return true;
221 }
222 else if(strcmp(gargv[0], "env") == 0)
223 {
224 for(int i=0; genv[i]; i++)
225 {
226 printf("%s\n",genv[i]);
227 }
228 return true;
229 }
230 return false;
231 }
232
233 // 从系统里把系统环境变量拷一份到我的环境变量里面,模拟一下从配置文件里面去读
234 // 作为一个shell,获取环境变量应该从系统的配置文件来获取
235 // 我们现在是直接从父shell中获取环境变量
236 void InitEnv()
237 {
238 // 从父进程获取环境变量
239 extern char **environ;
240 int index = 0;
241 while(environ[index])
242 {
243 // 导入环境变量,实则就是向shell自己的环境变量表当中,进行插入一个新的环境变量
244 // 即 再malloc 一段空间,让指针数组指向它对应的那个位置
245 genv[index] = (char*)malloc(strlen(environ[index]+1));
246 strncpy(genv[index], environ[index], strlen(environ[index])+1);
247 index++;
248 }
249 genv[index] = nullptr;
250 }
251
252 int main()
253 {
254 InitEnv();
255 char command_buffer[basesize];// 命令行缓冲区大小
256 while(true)
257 {
258 PrintCommandLine(); // 1.命令行提示符
259
260 // command_buffer --> 输出型参数
261 if(!GetCommandLine(command_buffer, basesize)) // 2.获取用户命令
262 { // 依次获取每个子字符串
263 continue;// 获取到空格继续获取
264 }
265 //printf("%s\n",command_buffer);
266
267 // "ls -a -l -n" --> "ls" "-a" "-l" "-n" 拆成一个一个的子字符串
268 ParseCommandLine(command_buffer, strlen(command_buffer)); // 3.分析命令
269 //debug();
270
271 // 先判断该命令是否是内建命令
272 if(CheckAndExexcBuiltCommand())
273 {
274 continue;
275 }
276
277 ExecuteCommand(); // 4.执行命令
278
279 }
280
281 return 0;
282 }