本文目标
Shell 是一套能够监听使用者命令、解析命令再告知作业系统核心完成命令 (System call) 的工具。
因为这套工具是利用 OS kernel 提供的系统呼叫完成作业的,所以可以把它想像成 OS kernel 的外壳。
一般来说,Shell 的运作流程如下:
通常来说,系统都会支援以下万用字元
*
: 表示不限长度的任何字元cat *.html
?
: 表示任一字元cat in?ex.html
[]
: 表示中括号内的其中一个字元cat index[12].html
\
: 取消後面字元的意义*
字元的内容,就不能直接使用 *
,因为该符号已经是万用字元了。这时候,可以这样做:echo \* star \*
'
: 取消字串中的特殊意义"
: 取消字串中除了 $
、 、
、 '
与 \
的特殊意义`
: 执行字串中的命令请直接参考本系列的档案系统篇,该文已经探讨过什麽是 File descriptor。
pipe 可以让使用者连结多个命令,参考以下命令:
cat file_1 | sort
在不使用 pipe |
时,cat 会将 file_1 的内容输出到终端机上。使用 pipe 後,cat 的输出会作为 sort 程序的输入,等到排序完成後才做输出。
我们可以利用转向符号 >
与 <
改变终端机输出与输入的对象。
cat file_1 > file_2
将 file_1 的内容输出到 file_2。
2. 输出附加转向
cat file_1 >> file_2
如果 file_2 原先就有内容且使用者需要保留该资料,可以使用 >>
让 file_1 的内容接续到 file_2 的内容後面。
3. 输入转向
cat < file_1 > file_2
输入转向会将原来由键盘输入的资料改为使用者指定的周边、档案替代。
上面的命令会将 file_1 作为输入并输出到 file_2 上面。
碍於篇幅问题,笔者仅罗列几个常见工具并大概说明其用途,详细的使用方法请服用连结内的文章。
cat (concatenate) 工具能够连结并显示文字。
cat index.html
强大的档案搜寻工具。
cat index.html | grep head
分页工具,在读取内容巨大的档案时不会一次载入全部的内容,可以加快载入的速度。
cat index.html | less
查找行程开启的档案
lsof -c chrome
查看目前执行中的 Process。
ps -A
该专案为成功大学的 Linux 核心设计小考题目,题目中提供了一个未完成的迷你 shell 程序,让学员透过考试检验 C 语言程序设计的认知:
首先看到 main()
:
int main()
{
while (1) {
prompt();
char buf[512] = {0}; /* input buffer */
char *c = buf;
if (!fgets(c + 1, sizeof(buf) - 1, stdin))
exit(0);
for (; *++c;) /* skip to end of line */
;
run(c, 0);
}
return 0;
}
我们可以得知 main() 会重复执行 while loop 的内容:
执行 prompt()
/*
* 印出 $
*/
static void prompt()
{
write(2, "$ ", 2);
}
设置 buffer 大小并将 stdin 的内容读入
利用 for loop 将 c 指标指到 buffer 的最後一个字元
这边要注意的是 fgets(c + 1, sizeof(buf) - 1, stdin)
,这个行为保留了 buffer 的第一个位置,等等看到 run()
时就会明白为什麽要这麽做。
运行 run()
:
在查看 run()
的原始码之前,我们先看到其他定义好的 static 函式,这些函式会被 run()
呼叫,帮助 shell 判断特殊的 token 以及印出错误讯息:
/* Display error message, optionally - exit */
static void fatal(int retval, int leave)
{
if (retval >= 0)
return;
write(2, "?\n", 2);
if (leave)
exit(1);
}
/* Helper functions to detect token class */
static inline int is_delim(int c)
{
return c == 0 || c == '|';
}
static inline int is_redir(int c)
{
return c == '>' || c == '<';
}
static inline int is_blank(int c)
{
return c == ' ' || c == '\t' || c == '\n';
}
static int is_special(int c)
{
return is_delim(c) || is_redir(c) || is_blank(c);
}
run()
中的字串处理size_t length;
char *redir_stdin = NULL, *redir_stdout = NULL;
int pipefds[2] = {0, 0}, outfd = 0;
char *v[99] = {0};
char **u = &v[98]; /* end of words */
for (;;) {
c--;
if (is_delim(*c)) /* if NULL (start of string) or pipe: break */
break;
if (!is_special(*c)) {
/* Copy word of regular chars into previous u */
length = 0;
while(!is_special(*c)){
length++;
c--;
}
u--;
c++;
strncpy(*u, c, length);
u[length] = '\0';
}
if (is_redir(*c)) { /* If < or > */
if (*c == '<')
redir_stdin = *u;
else
redir_stdout = *u;
if ((u - v) != 98)
u++;
}
}
if ((u - v) == 98) /* empty input */
return;
if (!strcmp(*u, "cd")) { /* built-in command: cd */
fatal(chdir(u[1]), 0);
return; /* actually, should run() again */
}
run()
中的 v
变数为指向 char pointer 的 array,也就代表他是用来存放多个字串的,可以让我们在解析 command 後把内容存进去。u
变数则是指向 char pointer 的 pointer,在这边被用来指向 v
存放的最後一个字串。fgets(c + 1, sizeof(buf) - 1, stdin)
) 或是 遇到 pipe。
if (is_delim(*c)) /* if NULL (start of string) or pipe: break */
break;
!is_special(*c)
会在字元为一般字母时成立。当条件成立以後,笔者让指标持续移动直到遇到特定字元,这时候我们就可以确定指标之後的 length
个字元就是我们要的命令:length = 0;
while(!is_special(*c)){
length++;
c--;
}
u--;
c++;
strncpy(*u, c, length);
u[length] = '\0';
execvp
执行我们一开始输入的 command: if (*c) {
pipe(pipefds);
outfd = pipefds[1]; /* write end of the pipe */
}
pid_t pid = fork();
if (pid) { /* Parent or error */
fatal(pid, 1);
if (outfd) {
run(c, outfd); /* parse the rest of the cmdline */
close(outfd); /* close output fd */
close(pipefds[0]); /* close read end of the pipe */
}
wait(0);
return;
}
if (outfd) {
dup2(pipefds[0], 0); /* dup read fd to stdin */
close(pipefds[0]); /* close read fd */
close(outfd); /* close output */
}
if (redir_stdin) {
close(0); /* replace stdin with redir_stdin */
fatal(open(redir_stdin, 0), 1);
}
if (t) {
dup2(t, 1); /* replace stdout with t */
close(t);
}
if (redir_stdout) {
close(1);
fatal(creat(redir_stdout, 438), 1); /* replace stdout with redir */
}
fatal(execvp(*u, u), 1);
}
Jserv 老师在题目设计上的用心不言而喻,之前在修大学部的作业系统时,shell 跟 pipe ... 等观念都只有在恐龙书上看到,所以笔者在看到这个作业时就把他拿来玩看看了,虽然正解只需要补齐大约 10 行程序码,但要补出这短短的程序码需要掌握 C 语言技巧并且理解 shell 的行为、如何使用 fork()
与 exec()
。
然後笔者在本文附上的个人解答其实不够精简,建议可以自行去 Trace 老师提供的正解。
此外,可能会有读者想要问我为何不在 mini-riscv-os 中加入 Shell 的实作,主要是因为 mini-riscv-os 缺乏了这些要素:
当然,如果有读者兴趣提交相关 PR 的话,我绝对举双手赞成 XD
<<: 找LeetCode上简单的题目来撑过30天啦(DAY23)
>>: Day23-Kubernetes 那些事 - CronJob
今日题目 题目连结:501. Find Mode in Binary Search Tree 题目主...
前言 因为UI和UX方面的需求,这几天上网搜寻了如何自定义下一页的弹出大小,弹出位置和动画,发现有蛮...
上一篇我们设计了 I2C Master 的状态机,那麽我们今天要来引用上次完成的状态机模块来实现 I...
前言 上两篇中我们在介绍 Go 网路的操作中,有稍微提到 json 格式,那这篇将介绍有关何为 js...
首先,作为一个 Web 的框架,主要就是当作网页的 Server 在运作的,网页中必不可少的就是网址...