Shell实现:基本功能
Shell的功能
Shell是操作系统中管理进程和运行程序的程序。所有常用的shell都有三个主要功能:
- 运行程序:grep, date, ls等都是一些普通的程序,shell将它们载入内存并运行它们
- 管理输入和输出:使用<,>和 | 符号可以将输入、输出重定向。这样就可以告诉shell将进程的输入和输出连接到一个文件或是其他的进程
- 可编程:shell同时也是带有变量和流程控制的编程语言
本文将介绍如何编写包含最基本功能,即能运行和终止程序的shell。
Shell的主流程
Shell打印提示符、输入命令、运行命令,如此反复。因此,主流程由下面的循环组成:
1 | for (;;) { |
打印提示符
下面是一个提示符的示例:
1 | [psh]krist@linux-szhw:/home/krist/Workspace$ |
加上前缀[psh]
以和系统自带的shell区别开来。后面依次是用户名(用getlogin_r
函数获取)、主机名(用gethostname
函数获取)、当前目录(用getcwd
函数获取)和命令提示符。如果当前登录的用户是root,命令提示符为#
,普通用户则打印美元符号$
,因此需要用geteuid
函数获取当前的Effective
User ID。
处理用户输入
一般需要限制用户单次输入的最多字符数MAX_ARG_LEN
以及参数的最大个数MAX_ARG_NUM
。用户输入命令,按下回车键即表示当前的命令输入完毕。用getline
函数读取一行用户输入,然后将一整条命令拆分。例如命令ls -al /home
拆分之后变成ls
,-al
和/home
三个字符串,第一个字符串是命令的名称,后面依次是命令参数。拆分字符使用strsep
函数,分割符(delimeter)为空格(Space)。下面是拆分过程的代码:
1 | stringp = input_buf; |
其中input_buf
是一个字符数组,存放用户输入的单条命令字符串,arg_vec
是字符串数组,存放拆分后的多个字符串。
运行命令
fork和exec
想必大家都熟悉shell运行命令的流程:用户输入一条命令后,父进程会fork一个子进程,在子进程中使用exec函数运行用户输入的命令,父进程等待子进程退出后,等待用户输入下一条命令,如此反复。代码如下:
1 | void exec_cmd(char **arg_vec) |
exec函数毫无疑问选择最方便的execvp
,主进程使用wait
函数等待子进程退出。
内建命令
这个时候一个可以跑的shell就完成了。但是却无法运行cd
命令,提示错误如下:
1 | execvp: No such file or directory |
运行whereis cd
命令,并没有显示可执行文件的路径。原来cd
命令是内建命令(build
in
command),需要在shell中自己实现,而且它的执行不需要建立子进程,需要额外判断和处理。
我写了一个函数判断是否是内建命令,如果不是内建命令则返回-1,否则按照内建命令的方式运行。目前只实现了cd
和exit
两个内建命令。代码如下:
1 | int builtin_cmd(char **arg_vec) |
此时,shell主循环中的run_cmd
函数演变为:
1 | void run_cmd(char **arg_vec) |
处理信号
我们期望shell能够处理信号。在shell的命令行中敲命令运行程序,在程序还在运行时按下Ctrl-C
或Ctrl-\
键(分别产生SIGINT
和SIGQUIT
信号),运行的程序会退出,即中断运行子进程;在shell的命令行中不运行命令,直接按下这两个键组合,不能退出shell程序,即父进程不会中断运行,退出shell程序只能通过Ctrl-D
按键。实现上述功能需要在父进程中忽略信号SIGINT
和SIGQUIT
,但是在子进程中恢复对信号SIGINT
和SIGQUIT
的默认操作。代码很简单,在这里不列出。
这样,一个具备基本功能的shell就完成了,完整的代码请参考这里。请继续看下一篇:Shell实现:重定向和管道。