题目:基于UNIX编程的命令解释器(MyShell)
简介
Advanced Programming in the UNIX® Environment, Third Edition 这本书的英文版的Figure 1.7 和Figure 1.10 演示了一个简单的shell 的实现方法。本题就是将这两个例子加以扩展,使之具备更强大的功能。
功能需求
尽可能完善地模仿Linux自带的shell命令解释器编写C语言代码,使程序在操作方式、执行机制、用户习惯等方面与shell保持一致,重点需要实现以下功能点:
- 程序从控制台执行,启动后显示一个命令提示符。例如“->”。用户可以通过给特定的环境变量赋值来改变命令提示符的形式;
- 通过某个特殊的命令或按键组合可以正常地关闭本程序;
- 提供后台运行机制。用户提交的任务可以通过某种指示使之在后台运行,例如:-> bg job1 <CR>将使任务job1 在后台运行,并马上返回给用户一个新的提示符;
- 提供输出重定向。通过指定文件名将任务的所有输出覆盖写到文件中而不是送到标准输出上;
- 提供输入重定向。通过指定文件名使得任务从相应的文件中去获取所需的数据,而不是从标准输入上。
分析
核心功能
一、执行command命令:
程序主要功能为命令解释执行,如ls
、ps
等命令,同时本功能要求可以附带参数进行传入。此外,需要定义一些程序预设的特定command字符。
分析:
由于最终必须进行系统调用交给kernal执行,因此本程序可以在逻辑上视为用户命令的中转。具体可分为两步:Step1为判断用户输入的命令是否合法;Step2将用户的命令交给系统运行。
拟解决方案:
-
exec Function Family 均可以用于执行command命令,需要指定将命令名称和参数分别传入。
#include <unistd.h> int execl(const char *pathname , const char *arg0 , ...); int execv(const char *pathname , char *const argv[]); int execle(const char *pathname , const char *arg0 , ..., /* (char *)0, char *const envp[] */); int execve(const char *pathname , char *const argv[], char *const envp[]); int execlp(const char *filename , const char *arg0 , ...); int execvp(const char *filename , char *const argv[]); int fexecve(int fd, char *const argv[], char *const envp[]);
函数execl、execle和execlp要求要指定的新程序的命令行参数作为单独的参数,结尾为空指针。对于其他四个函数,我们必须建立一个指向参数,这个数组的地址就是参数这三个功能。
名字以e结尾的三个函数允许我们将指针传递到指向环境的指针数组串。但是,其他四个函数使用调用进程中的变量,以复制现有的新程序的环境。
此外,exec函数簇的函数执行成功后是无返回的,一般需要和fork()函数同时使用。在使用时需要另外的fork一个进程。
-
system Function 将通过调用/bin/sh-c string执行string中指定的命令,并在命令完成后返回。传入参数为完成的命令、空格、参数的组合字符串。
#include <stdlib.h>c int system(const char *string );
虽然可以执行command但是此函数系统依赖性强,使用后可能会降低本程序的健壮性和可移植性。
在执行命令期间,SIGCHLD将被阻止,SIGINT和SIGQUIT将被忽略。
-
popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程。这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。
二、基本的输入输出交互
当程序运行后必须能够通过终端与用户进行交互,包括读取用户的输入命令和在屏幕中打印提示信息、程序运行信息等。
输入读取:需要调用 Standard I/O Library 从 stdin 流读取用户输入,使用 fgets 函数。
#include <stdio.h>
char *fgets(char *restrict buf , int n, FILE *restrict fp);
此函数可以读取用户的一整行输入,由于 fgets 函数需要指定读取字符数量,因此需要在程序中定义固定大小的宏。
输出写入:向屏幕打印信息,需要调用 Standard I/O Library 中的 Formatted Output Functions。
#include <stdio.h>
int printf(const char *restrict format , ... );
int fprintf(FILE *restrict fp,
const char *restrict format , ... );
int dprintf(int fd, const char *restrict format , ...);
int sprintf(char *restrict buf ,
const char *restrict format , ...);
int snprintf(char *restrict buf , size_t n,
const char *restrict format , ...);
通过这些函数以特定格式向 stdout 流输出信息,也可以使用以下函数。
#include <stdarg.h>
#include <stdio.h>
int vprintf(const char *restrict format , va_list arg);
int vfprintf(FILE *restrict fp ,
const char *restrict format , va_list arg);
int vdprintf(int fd , const char *restrict format , va_list arg);
int vsprintf(char *restrict buf , const char *restrict format , va_list arg);
int vsnprintf(char *restrict buf , size_t n, const char *restrict format , va_list arg);
程序需要即时与用户交互响应,而这些函数由于 Buffering 机制可能不能实时显示到屏幕上,因此在调用完成后需要再次调用 fflush 强制输出流刷新。
#include <stdio.h>
int fflush (FILE *fp);
三、输出和输出重定向
程序需要支持用户的输出和输出重定向,默认情况下输入从标准输入(0号文件描述符)读入,输出从标准输出(1号文件描述符)写入,错误信息从错误输出(3号文件描述符)写入。在重定向情况下,这些输出和输出需要替换为用户给定的输入和输出。此外,输入和输出是可选的,不是每一条命令都一定有,因此需要根据用户输入确定,进而程序需要约定相应的操作符号并进行判断。
拟解决方案:
首先根据命令判断是否存在重定向关键字,例如>
和<
符号,并根据符号确定指定的输入和输出,利用open 先打开输入输出得到文件描述符,但由于这些是随即给定的,因此需要利用 dup 和 dup2 Function 进行拷贝,将指定的输出输出覆盖到0号文件描述符和1号文件描述符,使得命令执行功能直接输入输出即可,不需要考虑重定向问题,此外,在命令结束之后需要对重定向进行恢复,使得用户下一次正常输入可以正确执行。需要用到的函数如下:
#include <unistd.h>
int dup(int filedes);
int dup2(int filedes , int filedes2);
#include <stdio.h>
FILE *fopen(const char* restrict pathname ,
const char* restrict type);
FILE *freopen(const char* restrict pathname ,
const char* restrict type ,
FILE* restrict fp);
FILE *fdopen(int fd , const char *type);
四、任务后台运行功能
用户提交的任务可以通过某种指示使之在后台运行,程序首先需要定义特定的命令关键字标识用户希望后台运行,其次命令的执行需要由产生新的进程执行。当用户不后台执行,程序需要监听子进程结果等待返回信息然后提示用户,当用户后台运行时,程序将任务交给子进程并不监听等待,马上返回给用户一个新的提示符。需要用到的函数如下。
#include <unistd.h>
pid_t fork(void);
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *statloc );
pid_t waitpid(pid_t pid , int *statloc , int options );
五、根据环境变量确定命令提示符
用户可以通过给特定的环境变量赋值来改变命令提示符的形式。程序需要读取特定名称的环境变量并分类讨论。如果未设置,则遵从默认的命令提示符,如果已经设置,判断是否合法,如果内容合法(非空且不与其他关键字冲突)则替换为新的命令提示符进行展示,此处应该在程序开头运行一次。读取特定环境变量使用以下函数。
#include <stdlib.h>
char *getenv(const char *name);
六、通过特定的命令和按键组合正常关闭程序
当用户输入特定的命令时或者在键盘上按下特定按键组合后程序正常退出。正常退出指 exit(0) 。
特定命令:程序需要指定特定的输入命令,并进行判断,当用户在某一回合输入后程序退出。
特定按键组合:使用 signal 机制,在程序运行一开始注册相应 signal 的处理函数,当用户按下后,kernal 将信号发送给本程序,指定的退出函数开始执行。
#include <signal.h>
void (* signal(int signo , void (*func)(int )))( int);
第一个参数signum:指明了所要处理的信号类型,它可以取除了SIGKILL和SIGSTOP外的任何一种信号。 第二个参数handler:描述了与信号关联的动作,它可以取以下三种值:SIG_IGN这个符号表示忽略该信号;SIG_DFL这个符号表示恢复对信号的系统默认处理,不写此处理函数默认也是执行系统默认操作; sighandler_t类型的函数指针,此函数必须在signal()被调用前申明,handler中为这个函数的名字。当接收到一个类型为sig的信号时,就执行handler 所指定的函数。(int)signum是传递给它的唯一参数。执行了signal()调用后,进程只要接收到类型为sig的信号,不管其正在执行程序的哪一部分,就立即执行func()函数。当func()函数执行结束后,控制权返回进程被中断的那一点继续执行。
常见的信号如下表所示,可以使用SIGINT、SIGQUIT、SIGCONT、SIGTSTP、SIGSTOP均满足需要要求,由于信号处理和平台关系密切,为了提高程序稳定性和可靠性,使用SIGINT和SIGQUIT信号实现功能较为可靠。
信号 | 起源 | 默认行为 | 含义 |
---|---|---|---|
SIGHUP | POSIX | Term | 控制终端挂起 |
SIGINT | ANSI | Term | 键盘输入以中断进程(ctrl + C) |
SIGQUIT | POSIX | Core | 键盘输入使进程退出(Ctrl + \) |
SIGILL | ANSI | Core | 非法指令 |
SIGTRAP | POSIX | Core | 断点陷阱,用于调试 |
SIGABRT | ANSI | Core | 进程调用abort函数时生成该信号 |
SIGIOT | 4.2BSD | Core | 和SIGABRT相同 |
SIGBUS | 4.2BSD | Core | 总线错误,错误内存访问 |
SIGFPE | ANSI | Core | 浮点异常 |
SIGKILL | POSIX | Term | 终止一个进程。该信号不可被捕获或被忽略 |
SIGUSR1 | POSIX | Term | 用户自定义信号之一 |
SIGSEGV | ANSI | Core | 非法内存段使用 |
SIGUSR2 | POSIX | Term | 用户自定义信号二 |
SIGPIPE | POSIX | Term | 往读端关闭的管道或socket链接中写数据 |
SIGALRM | POSIX | Term | 由alarm或settimer设置的实时闹钟超时引起 |
SIGTERM | ANSI | Term | 终止进程。kill命令默认发生的信号就是SIGTERM |
SIGSTKFLT | Linux | Term | 早期的Linux使用该信号来报告数学协处理器栈错误 |
SIGCLD | System V | Ign | 和SIGCHLD相同 |
SIGCHLD | POSIX | Ign | 子进程状态发生变化(退出或暂停) |
SIGCONT | POSIX | Cont | 启动被暂停的进程(Ctrl+Q)。如果目标进程未处于暂停状态,则信号被忽略 |
SIGSTOP | POSIX | Stop | 暂停进程(Ctrl+S)。该信号不可被捕捉或被忽略 |
SIGTSTP | POSIX | Stop | 挂起进程(Ctrl+Z) |
SIGTTIN | POSIX | Stop | 后台进程试图从终端读取输入 |
SIGTTOU | POSIX | Stop | 后台进程试图往终端输出内容 |
SIGURG | 4.3 BSD | Ign | socket连接上接收到紧急数据 |
SIGXCPU | 4.2 BSD | Core | 进程的CPU使用时间超过其软限制 |
SIGXFSZ | 4.2 BSD | Core | 文件尺寸超过其软限制 |
SIGVTALRM | 4.2 BSD | Term | 与SIGALRM类似,不过它只统计本进程用户空间代码的运行时间 |
SIGPROF | 4.2 BSD | Term | 与SIGALRM 类似,它同时统计用户代码和内核的运行时间 |
SIGWINCH | 4.3 BSD | Ign | 终端窗口大小发生变化 |
SIGPOLL | System V | Term | 与SIGIO类似 |
SIGIO | 4.2 BSD | Term | IO就绪,比如socket上发生可读、可写事件。因为TCP服务器可触发SIGIO的条件很多,故而SIGIO无法在TCP服务器中用。SIGIO信号可用在UDP服务器中,但也很少见 |
SIGPWR | System V | Term | 对于UPS的系统,当电池电量过低时,SIGPWR信号被触发 |
SIGSYS | POSIX | Core | 非法系统调用 |
SIGUNUSED | Core | 保留,通常和SIGSYS效果相同 |
辅助功能
一、统一的错误处理机制
-
程序应定义好全局特定的错误信息,包括但不限于使用宏等。
-
错误信息应分为严重错误和功能错误,功能错误只给出提示、用户可以继续执行,严重错误才应终止程序。
-
任何情况下,程序退出必须要有提示信息。
-
对于上述核心功能使用的函数都需要通过返回值判断是否出错并进行相应的错误处理。
-
错误的处理可以封装一个或多个顺序调用的函数以便主函数使用。
二、良好的输出提示
-
程序输出内容上应具有良好的可读性。
-
程序可以提供 help 命令帮助用户使用本程序。
-
输出信息的格式应该美观整齐。
三、快速简便的程序安装(编译)过程和运行体验
程序的安装过程应尽量简单,对于测试环境下的其他计算机可以直接拷贝可执行文件到计算机后直接执行,对于其他Linux系统应使用gcc命令直接编译生成可执行文件后直接执行。由于示例文件加入了 apue.h 的头文件,因此应该将需要的内容(头文件、函数和变量)拷贝出来。
-
程序的安装(编译)仅使用gcc命令一行语句执行,不应使用make等额外操作。
-
程序的文件应尽可能简单,只包含一个c语言的文本文件和对于的一个可执行文件,不应依赖于 apue 等其他项目文件。
技术要求
-
C 语言实现
-
程序代码的运行效率
-
程序代码的可读性
-
程序的 Robust 性
详细设计
变量定义
-
最大长度定义
#define BUF_MAX 1024
-
命令关键字定义
const char* COMMAND_EXIT = "exit"; const char* COMMAND_HELP = "help"; const char* COMMAND_BGRUN = "bg"; const char* COMMAND_IN = "<"; const char* COMMAND_OUT = ">"; char* COMMAND_PROMPT = "%"; /* can be changed by env */
-
关键数据变变量(环境变量和命令二维数组)
const char* ENV_NAME ="MYSHELL_COMMAND_PROMPT"; /* use to get shell prompt */ char commands[BUF_MAX][BUF_MAX]; /* just like argvs but each argv is a char[] end with '/0' */
-
返回值错误代码
/* status of all errors may product */ enum { RESULT_NORMAL, ERROR_OUTPUT, ERROR_FORK, ERROR_EXECUTE, ERROR_WAITPID, ERROR_DUP, ERROR_MISS_PARAMETER, ERROR_EXIT, /* Error message of redirect */ ERROR_REPEAT_INPUT_REDIRECTION, ERROR_REPEAT_OUTPUT_REDIRECTION, ERROR_FILE_NOT_EXIST, };
使用枚举类型标识正常返回和非零返回值,同时区分不同非零返回值的错误信息。在代码中使用在return和exit语句中,提高代码可读性。
-
所需头文件
#include <stdio.h> /* for convenience */ #include <stdlib.h> /* for convenience */ #include <string.h> /* for convenience */ #include <unistd.h> /* for convenience */ #include <errno.h> /* for definition of errno */ #include <stdarg.h> /* ISO C variable aruments */ #include <sys/types.h> /* some systems still require this */ #include <sys/signal.h> #include <sys/wait.h>
由于代码应不依赖于apue项目,因此需要提取具体的所需头文件,不能使用
include <apue.h>
函数模块
int main(void)
程序入口,获取环境变量设置命令提示符,注册中断信号,循环执行:打印提示符、等待用户输入、执行用户命令、返回值错误处理。
处理命令提示符时需要判断是否设置环境变量以及环境变量是否设置为空,并给出用户提示。
处理用户输入时需要区分退出命令和帮助命令以及其他并分别进行对应处理。
int splitCommandString(char command[BUF_MAX]); /* Split the command input buffer and return argc (the number of agrs +1) */
将输入命令从一行按照空格进行分隔,分隔后存到全局变量二维数组,同时进行计数得到参数数目。由main
函数调用。
int callCommand(int commandNum); /* Execute commands entered by the user */
传入参数数目,调用fork
产生子进程,令子进程备份输入输出后调用命令解析函数,执行完毕后恢复输入输出,父进程等待子进程返回,获取状态后返回。由main
函数调用。
int parseCommand(int argvNum); /* Process command with redirect */
解析命令,读取命令内容,判断是否由后台运行并进行标记,比较判定是否由输入输出重定向并进行记录,同时标记输入输出目标位置,统计重定向数量并判断、返回,验证重定向目标是否有效及可打开否在返回错误,vfork
子进程,由子进程重定向输入输出,然后从命令字符中删除与重定向相关的命令,按照原顺序交给execvp
执行,根据errorno
判断返回,父进程判断是否有后台运行,有则不等待子进程状态之间返回,如果没有则等待子进程返回结果然后返回。由callCommand
函数调用。
程序产生子进程两次的原因是为了更安全的进程管理,当子进程销毁后,孙进程如果没有执行完毕会交由init
进程管理。
int print_prompt(); /* Print command prompt */
辅助函数,功能封装,打印命令提示符。
static void normal_exit(); /* Call exit() to normal exit */
辅助函数,功能封装,正常退出。
static void sig_int(int); /* our signal-catching function */
辅助函数,语法封装,注册中断信号使用,当收到终端信号时直接执行此函数。
static void err_ret(const char *fmt, ...);
辅助函数、功能封装,非致命错误调用,打印错误信息。拷贝修改自apue项目。
static void err_sys(const char *fmt, ...);
辅助函数、功能封装,致命错误调用,打印错误信息后退出。拷贝修改自apue项目。
static void err_doit(int, int, const char *, va_list);
底层函数,提供调用,err_ret
和err_sys
使用。拷贝修改自apue项目。
程序运行流程图
① main函数流程图
② callCommand函数流程图
③ parseCommand函数流程图
注:其中黄色部分为下一张流程图。
详细代码设计
https://gitee.com/Erroryi/myshell
参考文章(开发前必看)
1.c语言实现shell - 徐志瀚 - 博客园:https://www.cnblogs.com/xzh1996/p/7710025.html
2.Linux signal 信号列表(sigint,sigtstp..)_种花家的奋斗兔的博客-CSDN博客_sigint:https://eternal-sun.blog.csdn.net/article/details/89554946
3.Linux后台执行的方法 - 关闭、退出不影响_a736933735的专栏-CSDN博客_linux后台运行:https://blog.csdn.net/a736933735/article/details/89577557
4.Linux环境变量配置全攻略:https://www.cnblogs.com/youyoui/p/10680329.html
5.Linux设置环境变量的方法:https://www.cnblogs.com/skaarl/p/11414931.html
6.linux下signal()函数超详细介绍_魏波-CSDN博客_linux signal函数:https://blog.csdn.net/weibo1230123/article/details/81505152
7.Linux信号之signal函数_此处不归牛顿管_-CSDN博客_linux signal:https://blog.csdn.net/lixiaogang_theanswer/article/details/80301624
8.myshell_myshell.c at master · mufeng964497595_myshell · GitHub:https://github.com/mufeng964497595/myshell/blob/master/myshell.c
9.train_myshell at master · aijingyi_train:https://github.com/aijingyi/train/tree/master/myshell
10.tx_myshell · 徐志瀚_ComputerSystems - 码云 - 开源中国:https://gitee.com/xzh1996/ComputerSystems/tree/master/tx/myshell
11.用C语言实现简易的shell程序,支持多重管道及重定向_木风feng的博客-CSDN博客:https://blog.csdn.net/feng964497595/article/details/80297318
README文件
本程序一种类似shell命令解释器,shell 是一个用 C 语言编写的程序,它是用户使用 Linux 的桥梁。命令解释器shell提供了一个界面,用户通过这个界面访问操作系统内核的服务。本程序从控制台启动后可以完成用户输入的各种命令。
初次使用请先阅读/doc目录下的用户手册!
主要特点:
1. 支持自定义命令提示符;
2. 支持快捷键快速结束程序;
3. 支持任务后台运行机制;
4. 支持输入输出重定向。
运行环境:
Linux操作系统(支持64位)
▶ Ubuntu
▶ Debian
▶ Fedora Core/CentOS
▶ SuSE Linux Enterprise Desktop
▶ FreeBSD
▶ Solaris
▶ MacOS
测试环境:
Linux ubuntu 5.3.0-40-generic #32-Ubuntu SMP Fri Jan 31 20:24:34 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
代码包文件结构:
-codes 代码文件夹
--myshell.c 程序源代码C语言文件
--myshell Linux平台可执行程序(由myshell.c在上述测试环境下编译得到)
-docs 文档文件夹
--测试报告.md
--详细设计文档.md
--需求分析文档.md
--用户手册.md
--总结报告.md
-figures 图片文件夹(部分文档依赖的图片,如:代码架构和流程图等)
有关常见问题解答、设计文档和测试报告,请参见同目录下doc文件夹。
用户手册
保留代码版权,欢迎讨论交流!