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+]; /* 解析出的命令参数列表 */
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[]);
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[];
for (i=; i<cmd_count; ++i)
{
/* 如果不是最后一条命令,则需要创建管道 */
if (i<cmd_count-)
{
pipe(fds);
cmd[i].outfd = fds[];
cmd[i+].infd = fds[];
} forkexec(i); if ((fd = cmd[i].infd) != )
close(fd); if ((fd = cmd[i].outfd) != )
close(fd);
} //forkexec中相关代码
if (cmd[i].infd != )
{
close();
dup(cmd[i].infd);
}
if (cmd[i].outfd != )
{
close();
dup(cmd[i].outfd);
} int j;
for (j=; j<; ++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 == )
signal(SIGCHLD, SIG_IGN);
else
signal(SIGCHLD, SIG_DFL);
5.4 前台任务如何回收子进程? 在看到的参考中有的方案提到while循环只到回收到最后一个子进程为止。
即
while (wait(NULL) != lastpid)
;
但此方法应该有bug,fork出的子进程的顺序与子进程结束的顺序不一定相同,所以还是采用计数的方式,等待所以子进程被回收。
int cnt = ;
while (wait(NULL) != - && 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文件中
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <linux/limits.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h> #define MAXLINE 1024 /* 输入行的最大长度 */
#define MAXARG 20 /* 每个简单命令的参数最多个数 */
#define PIPELINE 5 /* 一个管道行中简单命令的最多个数 */
#define MAXNAME 100 /* IO重定向文件名的最大长度 */ typedef struct command
{
char *args[MAXARG+]; /* 解析出的命令参数列表 */
int infd;
int outfd;
} COMMAND; typedef void (*CMD_HANDLER)(void); /*内置命令函数指针*/ typedef struct builtin_cmd
{
char *name;
CMD_HANDLER handler; } BUILTIN_CMD; void do_exit(void);
void do_cd(void);
void do_type(void);
BUILTIN_CMD builtins[] =
{
{"exit", do_exit},
{"cd", do_cd},
{"type", do_type},
{NULL, NULL}
}; char cmdline[MAXLINE+]; /*读到的一行命令*/
char avline[MAXLINE+]; /*解析过添加好\0的命令*/
char *lineptr;
char *avptr;
char infile[MAXNAME+]; /*输入重定向文件*/
char outfile[MAXNAME+]; /*输出重定向文件*/
COMMAND cmd[PIPELINE]; /*解析好的命令数组*/ int cmd_count; /*有多少个命令*/
int backgnd; /*是否后台作业*/
int append; /*输出重定向是否是append模式*/
int lastpid; /*回收最后一个子进程的pid*/ #define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} \
while () void setup(void);
void init(void);
void shell_loop(void);
int read_command(void);
int parse_command(void);
int execute_command(void);
void forkexec(int i);
int check(const char *str);
int execute_disk_command(void);
int builtin(void);
void get_command(int i);
void getname(char *name); int main()
{
/* 安装信号 */
setup();
/* 进入shell循环 */
shell_loop();
return ;
} void sigint_handler(int sig)
{
printf("\n[minishell]$ ");
fflush(stdout);
} void setup(void)
{
signal(SIGINT, sigint_handler);
signal(SIGQUIT, SIG_IGN);
} void init(void)
{
memset(cmd, , sizeof(cmd));
int i;
for (i=; i<PIPELINE; ++i)
{
cmd[i].infd = ;
cmd[i].outfd = ;
}
memset(cmdline, , sizeof(cmdline));
memset(avline, , sizeof(avline));
lineptr = cmdline;
avptr = avline;
memset(infile, , sizeof(infile));
memset(outfile, , sizeof(outfile));
cmd_count = ;
backgnd = ;
append = ;
lastpid = ; printf("[minishell]$ ");
fflush(stdout);
} /**主循环**/
void shell_loop(void)
{
while ()
{
/* 初始化环境 */
init();
/* 获取命令 */
if (read_command() == -)
break;
/* 解析命令 */
parse_command();
/*print_command();*/
/* 执行命令 */
execute_command();
} printf("\nexit\n");
} /*
* 读取命令
* 成功返回0,失败或者读取到文件结束符(EOF)返回-1
*/
int read_command(void)
{
/* 按行读取命令,cmdline中包含\n字符 */
if (fgets(cmdline, MAXLINE, stdin) == NULL)
return -;
return ;
} /*
* 解析命令
* 成功返回解析到的命令个数,失败返回-1
*/
int parse_command(void)
{
/* cat < test.txt | grep -n public > test2.txt & */
if (check("\n"))
return ; /* 判断是否内部命令并执行它 */
if (builtin())
return ; /* 1、解析第一条简单命令 */
get_command();
/* 2、判定是否有输入重定向符 */
if (check("<"))
getname(infile);
/* 3、判定是否有管道 */
int i;
for (i=; i<PIPELINE; ++i)
{
if (check("|"))
get_command(i);
else
break;
}
/* 4、判定是否有输出重定向符 */
if (check(">"))
{
if (check(">"))
append = ;
getname(outfile);
}
/* 5、判定是否后台作业 */
if (check("&"))
backgnd = ;
/* 6、判定命令结束‘\n’*/
if (check("\n"))
{
cmd_count = i;
return cmd_count;
}
else
{
fprintf(stderr, "Command line syntax error\n");
return -;
}
} /*
* 解析简单命令至cmd[i]
* 提取cmdline中的命令参数到avline数组中,
* 并且将COMMAND结构中的args[]中的每个指针指向这些字符串
*/
void get_command(int i)
{
/* cat < test.txt | grep -n public > test2.txt & */ int j = ;
int inword;
while (*lineptr != '\0')
{
/* 去除空格 */
while (*lineptr == ' ' || *lineptr == '\t')
*lineptr++; /* 将第i条命令第j个参数指向avptr */
cmd[i].args[j] = avptr;
/* 提取参数 */
while (*lineptr != '\0'
&& *lineptr != ' '
&& *lineptr != '\t'
&& *lineptr != '>'
&& *lineptr != '<'
&& *lineptr != '|'
&& *lineptr != '&'
&& *lineptr != '\n')
{
/* 参数提取至avptr指针所向的数组avline */
*avptr++ = *lineptr++;
inword = ;
}
*avptr++ = '\0';
switch (*lineptr)
{
case ' ':
case '\t':
inword = ;
j++;
break;
case '<':
case '>':
case '|':
case '&':
case '\n':
if (inword == )
cmd[i].args[j] = NULL;
return;
default: /* for '\0' */
return;
}
}
} /*
* 将lineptr中的字符串与str进行匹配
* 成功返回1,lineptr移过所匹配的字符串
* 失败返回0,lineptr保持不变
*/
int check(const char *str)
{
char *p;
while (*lineptr == ' ' || *lineptr == '\t')
lineptr++; p = lineptr;
while (*str != '\0' && *str == *p)
{
str++;
p++;
} if (*str == '\0')
{
lineptr = p; /* lineptr移过所匹配的字符串 */
return ;
} /* lineptr保持不变 */
return ;
} void getname(char *name)
{
while (*lineptr == ' ' || *lineptr == '\t')
lineptr++; while (*lineptr != '\0'
&& *lineptr != ' '
&& *lineptr != '\t'
&& *lineptr != '>'
&& *lineptr != '<'
&& *lineptr != '|'
&& *lineptr != '&'
&& *lineptr != '\n')
{
*name++ = *lineptr++;
}
*name = '\0';
} /*
* 执行命令
* 成功返回0,失败返回-1
*/
int execute_command(void)
{
execute_disk_command();
return ;
} /*执行命令 fork + exec */
void forkexec(int i)
{
pid_t pid;
pid = fork();
if (pid == -)
ERR_EXIT("fork"); if (pid > )
{
/* 父进程 */
if (backgnd == )
printf("%d\n", pid);
lastpid = pid;
}
else if (pid == )
{
/* backgnd=1时,将第一条简单命令的infd重定向至/dev/null */
/* 当第一条命令试图从标准输入获取数据的时候立即返回EOF */ if (cmd[i].infd == && backgnd == )
cmd[i].infd = open("/dev/null", O_RDONLY); /* 将第一个简单命令进程作为进程组组长 */
if (i == )
setpgid(, );
/* 子进程 */
if (cmd[i].infd != )
{
close();
dup(cmd[i].infd);
}
if (cmd[i].outfd != )
{
close();
dup(cmd[i].outfd);
} int j;
for (j=; j<; ++j)
close(j); /* 前台作业能够接收SIGINT、SIGQUIT信号 */
/* 这两个信号要恢复为默认操作 */
if (backgnd == )
{
signal(SIGINT, SIG_DFL);
signal(SIGQUIT, SIG_DFL);
}
execvp(cmd[i].args[], cmd[i].args);
exit(EXIT_FAILURE);
}
} /*执行非内置的命令*/
int execute_disk_command(void)
{
if (cmd_count == )
return ; if (infile[] != '\0')
cmd[].infd = open(infile, O_RDONLY); if (outfile[] != '\0')
{
if (append)
cmd[cmd_count-].outfd = open(outfile, O_WRONLY | O_CREAT
| O_APPEND, );
else
cmd[cmd_count-].outfd = open(outfile, O_WRONLY | O_CREAT
| O_TRUNC, );
} /* 因为后台作不会调用wait等待子进程退出 */
/* 为避免僵死进程,可以忽略SIGCHLD信号 */
if (backgnd == )
signal(SIGCHLD, SIG_IGN);
else
signal(SIGCHLD, SIG_DFL); int i;
int fd;
int fds[];
for (i=; i<cmd_count; ++i)
{
/* 如果不是最后一条命令,则需要创建管道 */
if (i<cmd_count-)
{
pipe(fds);
cmd[i].outfd = fds[];
cmd[i+].infd = fds[];
} forkexec(i); if ((fd = cmd[i].infd) != )
close(fd); if ((fd = cmd[i].outfd) != )
close(fd);
} if (backgnd == )
{
/* 前台作业,需要等待管道中最后一个命令退出 */
int cnt = ;
while (wait(NULL) != - && cnt != cmd_count) {
cnt++;
}
// while (wait(NULL) != lastpid)
// ;
} return ;
} /*
* 内部命令解析
* 返回1表示为内部命令,0表示不是内部命令
*/
int builtin(void)
{
/*
if (check("exit"))
do_exit();
else if (check("cd"))
do_cd();
else
return 0; return 1;
*/ int i = ;
int found = ;
while (builtins[i].name != NULL)
{
if (check(builtins[i].name))
{
builtins[i].handler();
found = ;
break;
}
i++;
} return found;
} void do_exit(void)
{
printf("exit\n");
exit(EXIT_SUCCESS);
} void do_cd(void)
{
printf("do_cd ... \n");
} void do_type(void)
{
printf("do_type ... \n");
}
参考资料:
1. BruceMolay, 莫莱, Molay,等. Unix/Linux编程实践教程[M]. 清华大学出版社, 2004.
2. c++教程网, linux_miniShell实践
3. RandalE.Bryant, DavidR.O'Hallaron, 布莱恩特,等. 深入理解计算机系统[M]. 机械工业出版社, 2011.
Tinyshell: 一个简易的shell命令解释器的更多相关文章
- c#如何用代码开启cmd指定命令(如:运行一个手机adb shell命令)
else if (this.Mode == TravelMode.AutoRecodeMode) { DateTime StartDate = DateTime.Now; string args = ...
- 一个方便的shell命令,查看软件安装目录
查看软件安装路径:whereis phpfind / -name nginx.configfind 查找 / 从根目录 -name 文件查找
- 2、Shell命令学习笔记
1.Shell命令行解释器 1.1 Shell命令解释器 Shell是一个特殊的应用程序,介于操作系统内核和用户之间,负责接收用户输入的操作指令(命令)并进行解释,将需要执行的操作传递给内核执行. 因 ...
- Scrapy的shell命令(转)
scrapy python MrZONT 2015年08月29日发布 ...
- Android 执行 adb shell 命令
Android 执行Adb shell 命令大多需要root权限,Android自带的Runtime. getRuntime().exec()容易出错,在网上找到了一个执行adb shell命令的类 ...
- python之commands和subprocess入门介绍(可执行shell命令的模块)
一.commands模块 1.介绍 当我们使用Python进行编码的时候,但是又想运行一些shell命令,去创建文件夹.移动文件等等操作时,我们可以使用一些Python库去执行shell命令. com ...
- shell脚本就是由Shell命令组成的执行文件,将一些命令整合到一个文件中,进行处理业务逻辑,脚本不用编译即可运行。它通过解释器解释运行,所以速度相对来说比较慢。
shell脚本?在说什么是shell脚本之前,先说说什么是shell. shell是外壳的意思,就是操作系统的外壳.我们可以通过shell命令来操作和控制操作系统,比如Linux中的Shell命令就包 ...
- ipython, 一个 python 的交互式 shell,比默认的python shell 好用得多,支持变量自动补全,自动缩进,支持 bash shell 命令,内置了许多很有用的功能和函数
一个 python 的交互式 shell,比默认的python shell 好用得多,支持变量自动补全,自动缩进,支持 bash shell 命令,内置了许多很有用的功能和函数. 若用的是fish s ...
- execl执行解释器文件以及shell命令
问题描述: execl执行解释器文件以及shell命令 问题解决: 具体源文件:
随机推荐
- IOS开发-OC学习-NSTimer的使用
上一篇博客中在改变属性值的时候使用了timer进行自动改变.关于NSTimer的更详细的用法如下: 定义一个NSTimer类型的timer,和一个count,其中timer是定时器,count是计数的 ...
- CSS如何让DIV的宽度随内容的变化
[css]CSS如何让DIV的宽度随内容的变化 让div根据内容改变大小 div{ width:auto; display:inline-block !important; display:inlin ...
- UVa 10684 - The jackpot
题目大意:给一个序列,求最大连续和. 用sum[i]表示前i个元素之和,那么以第i个元素结尾的最大连续和就是sum[i]-sum[j] (j<i)的最大值,也就是找前i-1个位置sum[]的最小 ...
- 无法打开登录 'ASPState' 中请求的数据库。登录失败。
问题: 无法打开登录 'ASPState' 中请求的数据库.登录失败.用户 'WH\Administrator' 登录失败. 解决方法: (启动SQL Server Agent服务) 从本系统中找到: ...
- Angular - - ngRoute Angular自带的路由
ngRoute $routeProvider 配置路由的时候使用. 方法: when(path,route); 在$route服务里添加一个新的路由. path:该路由的路径. route:路由映射信 ...
- 时钟(AnalogClock和DigitalClock)的功能与用法
时钟UI组件是两个非常简单的组件,DigitalClock本身就继承了TextView——也就是说它本身就是文本框,只是它里面显示的内容总是当前时间.与TextView不同的是为DigitalCloc ...
- ThinkPHP--IS_AJAX
增加IS_GET,IS_POST,IS_PUT,IS_DELETE,IS_AJAX常量,方便除控制器外的地方判断方法,Action类的isGet isPost等方法暂时保留,但不建议使用.
- FMS带宽的需求计算法
在开始一个使用 FLASH MEDIA SERVER的项目开始之前,最好能够对你项目使用FLASH MEDIA SERVER 3的带宽需求进行计算.这样对你的项目最终的实现效果,会有一个稳定的结果:去 ...
- Asp.net mvc 知多少(三)
本系列主要翻译自<ASP.NET MVC Interview Questions and Answers >- By Shailendra Chauhan,想看英文原版的可访问http:/ ...
- 3D游戏开发之在UE4中创建非玩家角色(NPC)
接着上节我们继续学习,现在我们来创建一些NPC(non-playable characters,非玩家角色).在这个游戏中,当我们靠近NPC时,它们会做出相应的反应. 一 创建C++类 1) 在UE编 ...