Tinyshell:⼀个简易的shell命令解释器
这是⾃⼰最近学习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解析命令,去除空格,不同字符串之间以\0间隔。
定义⼀个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⽂件中
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 {
20char *args[MAXARG+1]; /* 解析出的命令参数列表 */
21int infd;
22int outfd;
23 } COMMAND;
24
25 typedef void (*CMD_HANDLER)(void); /*内置命令函数指针*/
26
27 typedef struct builtin_cmd
28 {
29char *name;
30 CMD_HANDLER handler;
31
32 } BUILTIN_CMD;
33
34void do_exit(void);
35void do_cd(void);
36void 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
45char cmdline[MAXLINE+1]; /*读到的⼀⾏命令*/
46char avline[MAXLINE+1]; /*解析过添加好\0的命令*/
47char *lineptr;
48char *avptr;
49char infile[MAXNAME+1]; /*输⼊重定向⽂件*/
50char outfile[MAXNAME+1]; /*输出重定向⽂件*/
51 COMMAND cmd[PIPELINE]; /*解析好的命令数组*/
52
53int cmd_count; /*有多少个命令*/
shell代码54int backgnd; /*是否后台作业*/
55int append; /*输出重定向是否是append模式*/
56int lastpid; /*回收最后⼀个⼦进程的pid*/
57
58#define ERR_EXIT(m) \
59do \
60 { \
61 perror(m); \
62 exit(EXIT_FAILURE); \
63 } \
64while (0)
65
66
67void setup(void);
68void init(void);
69void shell_loop(void);
70int read_command(void);
71int parse_command(void);
72int execute_command(void);
73void forkexec(int i);
74int check(const char *str);
75int execute_disk_command(void);
76int builtin(void);
77void get_command(int i);
78void getname(char *name);
79
80
81int main()
82 {
83/* 安装信号 */
84 setup();
85/* 进⼊shell循环 */
86 shell_loop();
87return0;
88 }
89
90
91void sigint_handler(int sig)
92 {
93 printf("\n[minishell]$ ");
94 fflush(stdout);
95 }
96
97
98void setup(void)
99 {
100 signal(SIGINT, sigint_handler);
101 signal(SIGQUIT, SIG_IGN);
102 }
103
104void init(void)
105 {
106 memset(cmd, 0, sizeof(cmd));
107int i;
108for (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/**主循环**/
130void shell_loop(void)
131 {
132while (1)
133 {
134/* 初始化环境 */
135 init();
136/* 获取命令 */
137if (read_command() == -1)
138break;
139/* 解析命令 */
140 parse_command();
141/*print_command();*/
142/* 执⾏命令 */
143 execute_command();
144 }
145
146 printf("\nexit\n");
147 }
148
149
150/*
151 * 读取命令
152 * 成功返回0,失败或者读取到⽂件结束符(EOF)返回-1 153*/
154int read_command(void)
155 {
156/* 按⾏读取命令,cmdline中包含\n字符 */
157if (fgets(cmdline, MAXLINE, stdin) == NULL)
158return -1;
159return0;
160 }
161
162/*
163 * 解析命令
164 * 成功返回解析到的命令个数,失败返回-1
165*/
166int parse_command(void)
167 {
168/* cat < | grep -n public > & */ 169if (check("\n"))
170return0;
171
172/* 判断是否内部命令并执⾏它 */
173if (builtin())
174return0;
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论