这是自己最近学习Linux系统编程之后写的一个练手的小程序,能很好地复习系统编程中的进程管理、信号、管道、文件等内容。
通过回顾写的过程中遇到的问题的形式记录程序的关键点,最后给出完整程序代码。
0. Tinyshell的功能
这个简易的shell解释器可以解析磁盘命令,支持管道和输入输出重定向,内置命令只实现了exit,可以判定后台执行命令(&),但未实现bg功能(后台命令直接返回)。
1. shell是如何运行程序的
基本的模式就是主进程从键盘获取命令、解析命令,并fork出子进程执行相应的命令,最后主进程在子进程结束后回收(避免僵尸进程)。
这里执行命令可以采用exec家族中的execvp
int execvp(const char *file, char *constargv[]);
两个参数分别传递程序名(如ls)和命令行参数(如 -l)即可。
2. 怎么解析命令?
由于命令行解析要实现管道功能和I/O重定向功能,所以解析命令也稍微有些复杂。
首先用cmdline读取完整的一行命令;
avline解析命令,去除空格,不同字符串之间以 间隔。
定义一个COMMAND数据结构,包含一个字符串指针数组和infd,outfd两个文件描述符变量。
typedef struct command { char *args[MAXARG+1]; /* 解析出的命令参数列表 */ int infd; int outfd; } COMMAND;
每个COMMAND存储一个指令,其中args中的每个指针指向解析好的命令行参数字符串,infd,outfd存这个命令的输入输出对应的文件描述符。
COMMAND之间以< > |符号间隔,每个COMMAND中空格间隔出命令和不同的参数。大致结构如下图所示:(注:命令行处理方法和图片均学习自[2])
3. 输入输出重定向怎么处理?
理解I/O重定向首先要理解最低可用文件描述符的概念。即每个进程都有其打开的一组文件,这些打开的文件被保持在一个数组中,文件描述符即为某文件在此数组中的索引。
所以当打开文件时,为文件安排的总是此数组中最低可用位置的索引。
同时stdin, stdout, stderr分别对应文件描述符0,1,2被打开。
文件描述符集通过exec调用传递,不会被改变。
所以shell可以通过fork产生子进程与子进程调用exec之间的时间间隔来重定向标准输入输出到文件。
利用的函数是dup / dup2
#include <unistd.h> int dup(int oldfd); int dup2(int oldfd, int newfd)
以输入重定向为例。 open(file) -> close(0) -> dup(fd) -> close(fd)
open(file)打开将要重定向的文件,close(0)使得文件描述符0空闲,dup(fd)对fd进行复制,利用最低文件描述符0,此时该文件与文件描述符0连接在一起。
close(fd)来关闭文件的原始连接,只留下文件描述符0的连接。 或直接利用dp2将文件描述符pld复制到文件描述符new(open -> dup2 -> close)
同时利用append变量记录输出重定向是否是追加模式(“>>”)来决定打开文件的方式。
4. 管道怎么处理?
管道就是利用linux的管道创建函数并将管道的读写端分别绑定即可。
#include <unistd.h> int pipe(int pipefd[2]);
pipefd[0]为管道读端,pipefd[1]为管道写端。
前后进程利用管道,采用如下逻辑:(以ls | wc为例)
前一个进程(ls) : close(p[0]) -> dup2(p[1], 1) -> close(p[1]) -> exec(ls)
后一个进程 (wc):close(p[1]) -> dup(p[0], 0) -> close(p[0]) -> exec(wc)
注意,有N个COMMAND意味着要建立N-1个管道,所以可以用变量cmd_count记录命令个数。
int fds[2]; for (i=0; i<cmd_count; ++i) { /* 如果不是最后一条命令,则需要创建管道 */ if (i<cmd_count-1) { pipe(fds); cmd[i].outfd = fds[1]; cmd[i+1].infd = fds[0]; } forkexec(i); if ((fd = cmd[i].infd) != 0) close(fd); if ((fd = cmd[i].outfd) != 1) close(fd); } //forkexec中相关代码 if (cmd[i].infd != 0) { close(0); dup(cmd[i].infd); } if (cmd[i].outfd != 1) { close(1); dup(cmd[i].outfd); } int j; for (j=3; j<1024; ++j) close(j);
5.信号处理
分析整个流程中不同阶段的信号处理问题。
5.1 首先是shell主循环运行阶段,显然shell是不会被Ctrl + C停止的,所以初始化要忽略SIG_INT,SIGQUIT
5.2 当前台运行其他程序时,是可以用Ctrl + C来终止程序运行的,所以这时要恢复SIG_INT, SIGQUIT。
但注意Ctrl + C会导致内核向当前进程组的所有进程发送SIGINT信号。所以当fork出子进程处理前台命令时,应该让第一个简单命令作为进程组的组长。
这样接收到信号时,不会对shell进程产生影响。
设置进程组采用setpgid函数。setpgid(0,0)表示用当前进程的PID作为进程组ID。
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
5.3 后台运行程序,不会调用wait等待子进程退出,所以采用linux下特有的处理方式,忽略SIGCHLD,避免僵尸进程(在linux下交给init处理)
但在前台运行时需要再把SIGCHLD回复,显示等待子进程退出。
if (backgnd == 1) signal(SIGCHLD, SIG_IGN); else signal(SIGCHLD, SIG_DFL);
5.4 前台任务如何回收子进程? 在看到的参考中有的方案提到while循环只到回收到最后一个子进程为止。
即
while (wait(NULL) != lastpid) ;
但此方法应该有bug,fork出的子进程的顺序与子进程结束的顺序不一定相同,所以还是采用计数的方式,等待所以子进程被回收。
int cnt = 0; while (wait(NULL) != -1 && cnt != cmd_count) { cnt++; }
6.其他开始没注意到的小bug和补充
6.1 cmd_count == 0时,不执行任何操作,直接返回。不加这一句判断会出错。
6.2 因为没有实现bg功能,所以后台作业将第一条简单命令的infd重定向至/dev/null, 当第一条命令试图从标准输入获取数据的时候立即返回EOF。
6.3 内置命令只实现了exit退出。
7. 还有什么可以优化?
7.1 这里主进程中采用的是阻塞等待回收子进程的策略,一个更好的方案应该是利用SIGCHLD信号来处理。但这里便存在很多容易出错的地方。
比如子进程可能结束了,父进程还没有获得执行的机会,父进程再执行后再也收不到SIGCHLD信号。
所以需要通过显示的阻塞SIGCHLD信号来对其进行同步(利用sigprocmask函数)
其次,信号的接收是不排队的,所以对于同时到来的子进程结束信号,一些信号可能被丢弃。所以一个未处理的信号表明至少一个信号到达了。要小心处理。
关于这部分内容,可以参考CSAPP【3】。
7.2 可以考虑加入更多的内置命令,同时实现shell的流程控制和变量设置。
8. 完整代码
为了好在博客中上传,没有采取头文件形式,所有内容在一个.c文件中
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 #include <stdlib.h> 2 #include <stdio.h> 3 #include <unistd.h> 4 #include <sys/types.h> 5 #include <sys/wait.h> 6 #include <linux/limits.h> 7 #include <fcntl.h> 8 #include <signal.h> 9 #include <string.h> 10 11 12 #define MAXLINE 1024 /* 输入行的最大长度 */ 13 #define MAXARG 20 /* 每个简单命令的参数最多个数 */ 14 #define PIPELINE 5 /* 一个管道行中简单命令的最多个数 */ 15 #define MAXNAME 100 /* IO重定向文件名的最大长度 */ 16 17 18 typedef struct command 19 { 20 char *args[MAXARG+1]; /* 解析出的命令参数列表 */ 21 int infd; 22 int outfd; 23 } COMMAND; 24 25 typedef void (*CMD_HANDLER)(void); /*内置命令函数指针*/ 26 27 typedef struct builtin_cmd 28 { 29 char *name; 30 CMD_HANDLER handler; 31 32 } BUILTIN_CMD; 33 34 void do_exit(void); 35 void do_cd(void); 36 void do_type(void); 37 BUILTIN_CMD builtins[] = 38 { 39 {"exit", do_exit}, 40 {"cd", do_cd}, 41 {"type", do_type}, 42 {NULL, NULL} 43 }; 44 45 char cmdline[MAXLINE+1]; /*读到的一行命令*/ 46 char avline[MAXLINE+1]; /*解析过添加好 的命令*/ 47 char *lineptr; 48 char *avptr; 49 char infile[MAXNAME+1]; /*输入重定向文件*/ 50 char outfile[MAXNAME+1]; /*输出重定向文件*/ 51 COMMAND cmd[PIPELINE]; /*解析好的命令数组*/ 52 53 int cmd_count; /*有多少个命令*/ 54 int backgnd; /*是否后台作业*/ 55 int append; /*输出重定向是否是append模式*/ 56 int lastpid; /*回收最后一个子进程的pid*/ 57 58 #define ERR_EXIT(m) 59 do 60 { 61 perror(m); 62 exit(EXIT_FAILURE); 63 } 64 while (0) 65 66 67 void setup(void); 68 void init(void); 69 void shell_loop(void); 70 int read_command(void); 71 int parse_command(void); 72 int execute_command(void); 73 void forkexec(int i); 74 int check(const char *str); 75 int execute_disk_command(void); 76 int builtin(void); 77 void get_command(int i); 78 void getname(char *name); 79 80 81 int main() 82 { 83 /* 安装信号 */ 84 setup(); 85 /* 进入shell循环 */ 86 shell_loop(); 87 return 0; 88 } 89 90 91 void sigint_handler(int sig) 92 { 93 printf(" [minishell]$ "); 94 fflush(stdout); 95 } 96 97 98 void setup(void) 99 { 100 signal(SIGINT, sigint_handler); 101 signal(SIGQUIT, SIG_IGN); 102 } 103 104 void init(void) 105 { 106 memset(cmd, 0, sizeof(cmd)); 107 int i; 108 for (i=0; i<PIPELINE; ++i) 109 { 110 cmd[i].infd = 0; 111 cmd[i].outfd = 1; 112 } 113 memset(cmdline, 0, sizeof(cmdline)); 114 memset(avline, 0, sizeof(avline)); 115 lineptr = cmdline; 116 avptr = avline; 117 memset(infile, 0, sizeof(infile)); 118 memset(outfile, 0, sizeof(outfile)); 119 cmd_count = 0; 120 backgnd = 0; 121 append = 0; 122 lastpid = 0; 123 124 printf("[minishell]$ "); 125 fflush(stdout); 126 } 127 128 129 /**主循环**/ 130 void shell_loop(void) 131 { 132 while (1) 133 { 134 /* 初始化环境 */ 135 init(); 136 /* 获取命令 */ 137 if (read_command() == -1) 138 break; 139 /* 解析命令 */ 140 parse_command(); 141 /*print_command();*/ 142 /* 执行命令 */ 143 execute_command(); 144 } 145 146 printf(" exit "); 147 } 148 149 150 /* 151 * 读取命令 152 * 成功返回0,失败或者读取到文件结束符(EOF)返回-1 153 */ 154 int read_command(void) 155 { 156 /* 按行读取命令,cmdline中包含 字符 */ 157 if (fgets(cmdline, MAXLINE, stdin) == NULL) 158 return -1; 159 return 0; 160 } 161 162 /* 163 * 解析命令 164 * 成功返回解析到的命令个数,失败返回-1 165 */ 166 int parse_command(void) 167 { 168 /* cat < test.txt | grep -n public > test2.txt & */ 169 if (check(" ")) 170 return 0; 171 172 /* 判断是否内部命令并执行它 */ 173 if (builtin()) 174 return 0; 175 176 177 /* 1、解析第一条简单命令 */ 178 get_command(0); 179 /* 2、判定是否有输入重定向符 */ 180 if (check("<")) 181 getname(infile); 182 /* 3、判定是否有管道 */ 183 int i; 184 for (i=1; i<PIPELINE; ++i) 185 { 186 if (check("|")) 187 get_command(i); 188 else 189 break; 190 } 191 /* 4、判定是否有输出重定向符 */ 192 if (check(">")) 193 { 194 if (check(">")) 195 append = 1; 196 getname(outfile); 197 } 198 /* 5、判定是否后台作业 */ 199 if (check("&")) 200 backgnd = 1; 201 /* 6、判定命令结束‘ ’*/ 202 if (check(" ")) 203 { 204 cmd_count = i; 205 return cmd_count; 206 } 207 else 208 { 209 fprintf(stderr, "Command line syntax error "); 210 return -1; 211 } 212 } 213 214 215 /* 216 * 解析简单命令至cmd[i] 217 * 提取cmdline中的命令参数到avline数组中, 218 * 并且将COMMAND结构中的args[]中的每个指针指向这些字符串 219 */ 220 void get_command(int i) 221 { 222 /* cat < test.txt | grep -n public > test2.txt & */ 223 224 int j = 0; 225 int inword; 226 while (*lineptr != '