在写完一个最简单的命令解释器以后,我给自己的shell起了个名字——jdsh(JackalDire Shell)。
本来以为加入后台执行不是什么难事,但是认真想了一下要处理好一个字符‘&’不是一般的麻烦,各种问题接踵而至,于是决定先找点轻松的活干。
1.加入shell内建命令
每个shell都有自己的内建命令,最常见的有“cd”、“pwd”、“exit”等等。没有“cd“的shell是没法干活的。
其实很简单,标准的做法建个hash表把所有内建命令加进去,parse command以后先查一下表就行了,然后可以通过像Linux Syscall的实现方法一样,用一个字符串连接宏(##)找到对应的处理函数。但是由于只准备写寥寥几个命令,偷懒直接 if else if了(= =!)。
2.打印命令提示符Prompt
每个命令行shell都有命令提示符,告诉用户现在可以敲命令。大多数的linux shell的prompt是可以定制的。我采用了bash的默认格式“[username@hostname cwd]$ ”。实现prompt里的cwd(current working directory)时候发现了一个问题:基本上所有linux shell里都用了’~’代替$HOME。于是做个个字符串替换,使得home文件夹在显示的时候都显示为’~’(命令里的路径还不支持’~’)
3.处理SIGINT、SIGQUIT和SIGTSTP信号
SIGINT信号对应按下ctrl+c,shell对该信号的处理方式是(观察zsh/bash得到,没有查文档):若shell当前没有前台任务,则输入一个空行,放弃当前正在输入的命令;若当前有前台任务,则SIGINT信号由前台程序捕捉。
SIGQUIT信号对应按下ctrl+,shell对该信号的处理方式是(观察zsh/bash得到,没有查文档):若shell当前没有前台任务,则忽略该信号,放弃当前正在输入的命令;若当前有前台任务,则SIGQUIT信号由前台程序捕捉。
SIGQUIT信号对应按下ctrl+z,shell对该信号的处理方式是(观察zsh/bash得到,没有查文档):若shell当前没有前台任务,则忽略该信号,放弃当前正在输入的命令;若当前有前台任务,则SIGQUIT信号由前台程序捕捉。
PS:对这APUE里的信号表看了一遍,好像没有其他要特殊处理的信号了。
其实很好处理,用signal函数或者sigaction都可以(偷懒就用signal了)。在shell的主进程里改变SIGINT的信号处理函数、忽略SIGQUIT和SIGTSPT信号就可以。
有一个问题需要注意:必须在fork的子进程里恢复对这几个信号的默认处理方式。因为fork产生子进程和父进程有相同的信号处理函数signal handler。解决方法就是fork以后在子进程里手动恢复修改过的信号。另一种方法是不用fork,改用clone系统调用,通过clone flags里的CLONE_SIGHAND参数使得复制出来的进程不保留父进程的信号处理函数。
回到主题上…..
4.后台运行程序
其实单纯的实现后台运行很简单,把前面程序里的wait去掉使shell主进程不等待子进程结束就OK了。问题在于,shell是通过一个’&’字符决定后台执行,如何才能从命令里提取出这个代表后台执行的’&’字符?
看起来似乎一个字符串查找就能解决,其实并非如此:如果这’&’是在一个路径名里呢(Linux中只有一个字符不能做文件名:’/’)。看一下别的shell很快就会找到解决方法:转义字符。如果想把有特殊含义的字符当作一个普通字符处理,那么就要在前面加上转移字符”(比如空格、”、’&’、’~’、’-‘等等)。
要支持转移字符,我原来用strsep实现的优美的parse command函数就得全部扔掉重写。没办法,换成了fgetc和丑陋的switch case。
有了转义字符,就可以处理含有特殊字符的文件名了^ ^
另外,我观察到bash和zsh在后台程序运行结束后都会显示一条提示信息显示pid和命令,于是我也想把这个功能加进去。思路很简单,就是改变SIGCHLD信号的handler,捕捉到SIGCHLD后输出进程信息就可以了。默认SIGCHLD信号是通过wait和waitpid函数捕捉。signal能改变SIGCHLD信号的处理函数,但是无法获得足够的信息(pid、退出代码等),而sigaction支持复杂的信号处理函数,通过向信号处理函数传递siginfo结构,提供更充足的信息(还是sigaction强大)。
差不多就这样了…代码量翻了一倍,直接贴出来有点恐怖,所以扔到附件了~(BUG很多,就不一一列举了,sign~)
源代码:jdsh_v2.c
3 Responses to 自己动手写Linux Shell(二) —— 支持后台执行