MSH:一个简单SH工具实现
本文将分为不同的Part,分别实现Shell的一部分功能。
msh从CSAPP的SHLAB出发,逐渐完善SHELL功能,并移植到自己的OS上。
Github: https://github.com/He11oLiu/msh
Part1
Part1 目标
- 首先,
tsh需要支持内嵌指令功能,使用int builtin_cmd(char **argv)实现。 再,
tsh需要支持前后台执行程序的功能,shell需要接收SIGCHLD进程,回收僵死进程或处理暂停进程。在给出的
handout中已经把语义分析写好了,直接用即可。
命令求值函数
/*
* eval - Evaluate the command line that the user has just typed in
*
* If the user has requested a built-in command (quit, jobs, bg or fg)
* then execute it immediately. Otherwise, fork a child process and
* run the job in the context of the child. If the job is running in
* the foreground, wait for it to terminate and then return. Note:
* each child process must have a unique process group ID so that our
* background children don't receive SIGINT (SIGTSTP) from the kernel
* when we type ctrl-c (ctrl-z) at the keyboard.
*/
void eval(char *cmdline) {
char *argv[MAXARGS];
int bg;
pid_t pid;
sigset_t mask;
bg = parseline(cmdline,argv);
if(argv[0] == NULL)
return ;
if(!builtin_cmd(argv)){
sigemptyset(&mask);
sigaddset(&mask,SIGCHLD);
sigaddset(&mask,SIGINT);
sigaddset(&mask,SIGTSTP);
sigprocmask(SIG_BLOCK,&mask,NULL);
/* child proc */
if((pid = Fork()) == 0){
setpgid(0, 0);
if(verbose){
pid = getpid();
printf("Child proc started with pid %d\n",(int)pid);
}
sigprocmask(SIG_UNBLOCK,&mask,NULL);
if(execve(argv[0],argv,environ)<0){
printf("%s: Command not found.\n",argv[0]);
exit(0);
}
}
/* parent proc */
else{
addjob(jobs,pid,bg?BG:FG,cmdline);
sigprocmask(SIG_UNBLOCK,&mask,NULL);
if(!bg){
/* Use waitfg to wait until proc(pid) is no longer a frontgroud proc. */
waitfg(pid);
}
else{
printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline);
}
}
}
return ;
}
在第一阶段的目标下,不需要在课本的基础上改多少内容,一个是对于前台的子进程不再使用waitpid来阻塞,而是使用更加优雅的waitfg来阻塞。再一个是添加了SIGCHLD的处理函数,用于处理回收僵死或暂停进程。
前台阻塞处理
对于前台,最初实现是直接waitpid。但是一旦使用SIGCHLD的处理函数来回收,两个waitpid会导致结构不好,全局FLAG之类也不够优雅。
发现其提供了waitfg的接口,思路一用以下方法实现。
void waitfg(pid_t pid){
struct job_t *cur = getjobpid(jobs,pid);
while(cur != NULL && cur->state == FG){
cur = getjobpid(jobs,pid);
}
/* 2 cases:
BG : switch to BG from FG
NULL : delete from jobs
*/
return;
}
性能不佳,且不够优雅。每次都观察前台的proc是不是pid即可,再利用pause暂停,直到有下一个信号来临。
void waitfg(pid_t pid){
while(pid == fgpid(jobs))
pause();
return;
}
这样就优雅多了。
回收僵死进程
需要使用sigchld_handler来接收SIGCHLD信号对僵死进程进行回收或者处理被暂停的进程。
/*
* sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
* a child job terminates (becomes a zombie), or stops because it
* received a SIGSTOP or SIGTSTP signal. The handler reaps all
* available zombie children, but doesn't wait for any other
* currently running children to terminate.
*/
void sigchld_handler(int sig) {
pid_t pid;
int status;
struct job_t *job;
while((pid = waitpid(-1,&status,WNOHANG | WUNTRACED))>0){
if(WIFEXITED(status)){ /*process is exited in normal way*/
if(verbose)
printf("Job [%d] (%d) terminated normally with exit status %d\n",pid2jid(pid),pid,WEXITSTATUS(status));
deletejob(jobs,pid);
}
else if(WIFSTOPPED(status)){/*process is stop because of a signal*/
printf("Job [%d] (%d) stopped by signal %d\n",pid2jid(pid),pid,WSTOPSIG(status));
job = getjobpid(jobs,pid);
if(job !=NULL) job->state = ST;
}
else if(WIFSIGNALED(status)){/*process is terminated by a signal*/
printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
deletejob(jobs,pid);
}
}
return;
}
其中要注意的是,如果使用了while轮巡检查回收所有的僵死进程,需要在options参数中加入WNOHANG来确保其立即返回。为了未来能够处理暂停的应用程序,这里options的参数选择为WNOHANG|WUNTRACED,其表达为如果没有等待集合中的任何子进程停止或终止,则立即返回,且返回值为0.
利用status来接收返回的状态,在用宏定义打包的位操作来判断状态。
- 正常退出的进入
WIFEXITED(status)的分支,在有-v选项时,利用WEXITSTATUS(status)来获取其返回值。 - 由信号造成的进程终止,进入
WIFSIGNALED(status)分支。利用WTERMSIG(status)来获取引起进程终止的信号数量。 - 由信号造成的进程暂停,进入
WIFSTOPPED(status)分支。利用WSTOPSIG(status)来获取引起进程终止的信号数量。
内嵌功能实现
默认handout中已经实现了与job有关的操作函数,提供了基本操作接口,在不同的位置加入或删除或修改jobs列表即可。这里要注意阻塞对应的信号量避免陷入错误状态,书上给了详细的示例。
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv) {
if(!strcmp(argv[0],"quit") || !strcmp( argv[0],"exit"))
exit(0);
if(!strcmp(argv[0],"jobs")){
listjobs(jobs);
return 1;
}
return 0; /* not a builtin command */
}
至此,可以通过test05以及之前的需求。
Part2
Part2 目标
- 接收从键盘传来的信号,处理
SIGINT与SIGTSTP的信号 BG与FG内嵌功能实现- 简单错误处理
支持对前台的终止与暂停操作
对SIGINT与SIGTSTP的处理函数稍加修改,即可实现该功能。需要找出前台进程,并向该进程组发送信号即可。(之前将进程加到了与pid相同的组内)
/*
* sigint_handler - The kernel sends a SIGINT to the shell whenver the
* user types ctrl-c at the keyboard. Catch it and send it along
* to the foreground job.
*/
void sigint_handler(int sig){
pid_t pid = fgpid(jobs);
if(pid) kill(-pid,SIGINT);
return;
}
/*
* sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever
* the user types ctrl-z at the keyboard. Catch it and suspend the
* foreground job by sending it a SIGTSTP.
*/
void sigtstp_handler(int sig){
pid_t pid = fgpid(jobs);
if(pid) kill(-pid,SIGTSTP);
return;
}
添加这两个处理函数后,test08及之前的测试可过。
BG与FG功能实现
从JID转换到PID,异常处理。
/* jid2pid - Map job ID to process ID */
pid_t jid2pid(int jid){
if(jid>=MAXJID || jid <= 0) return 0;
return jobs[jid-1].pid;
}
因为bg与fg支持PID与JID两种输入方式,故利用parse arg块来实现。对于atoi的异常情况,在jid2pid中对0进行了处理。而对于不属于该shell的pid,会无法找到其JID,而不发消息。
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv){
pid_t pid;
int jid;
struct job_t *job;
if(argv[1]== NULL){
printf("fg command requires PID or %%jobid argument\n");
return;
}
/* parse arg */
if(argv[1][0] == '%'){
/* JID */
jid = atoi(argv[1]+1);
if(jid == 0 && argv[1][1] != '0'){
/* Not number */
printf("fg command requires PID or %%jobid argument\n");
return;
}
if(!(pid=jid2pid(jid))){
/* JID not exits */
printf("%%%d: No such job\n",jid);
return;
}
}
else{
/* PID */
pid = atoi(argv[1]);
if(pid == 0 && argv[1][0] != '0'){
/* Not number */
printf("fg command requires PID or %%jobid argument\n");
return;
}
if(!(jid = pid2jid(pid))) {
printf("(%d): No such process\n",pid);
return;
}
}
job = getjobjid(jobs,jid);
if(!strcmp(argv[0],"bg")){
/* bg function
* The bg <job> command restarts <job> by sending it a SIGCONT signal,
* and then runs it in the background.
* The <job> argument can be either a PID or a JID.
*/
job->state = BG;
kill(-pid,SIGCONT);
printf("[%d] (%d) %s",jid,pid,job->cmdline);
}
else{
/* fg function
* The fg <job> command restarts <job> by sending it a SIGCONT signal,
* and then runs it in the foreground.
* The <job> argument can be either a PID or a JID.
*/
job->state = FG;
kill(-pid,SIGCONT);
waitfg(pid);
}
return;
}
添加这两个处理函数后,所有的test测试可过。
Part3
Part3目标
- 实现
cd功能 - 更改
shell promp - 增加默认
PATH目录
cd功能实现
获取当前的工作路径,拼接工作路径,并改变工作路径,即可完成cd。
char *getcwd( char *buffer, int maxlen );
int chdir(const char *);
完成上述逻辑:
int do_cd(char **argv){
char buf[MAXLINE];
buf[0] = '\0';
if(argv[1][0] != '/' && argv[1][0] != '.'){
if(getcwd(buf, MAXLINE) == NULL){
fprintf(stderr, "Getcwd failed: %s\n", strerror(errno));
return -1;
}
strncat(buf, "/", MAXLINE - strlen(buf));
}
strncat(buf, argv[1], MAXLINE - strlen(buf));
printf("cd path : %s\n",buf);
if(chdir(buf) == -1){
fprintf(stderr, "cd error : %s %s\n",strerror(errno),argv[1]);
}
return 0;
}
修改prompt
由于增加了cd功能,prompt改为更有意义的工作路径 时间
/* Get prompt */
if(getcwd(workpath, MAXLINE) == NULL){
fprintf(stderr, "Getcwd failed: %s\n", strerror(errno));
return -1;
}
time (&t);
lt = localtime (&t);
sprintf (prompt, "\n%s#%s %s%s%s %s[%d:%d:%d]%s\n%sμ%s ",B_BLUE,FINISH,B_YELLOW,workpath,FINISH,BOLD,lt->tm_hour, lt->tm_min, lt->tm_sec,FINISH,B_RED,FINISH);
增加默认PATH目录
获取PATH变量,并切分放入pathargv中,数量放入pathargc
/* Get $PATH */
pathvar = getenv("PATH");
if(verbose)
printf("Loaded with PATH = %s\n",pathvar);
/* For macOS PATH devided by ':' */
pathargv[pathargc] = strtok( pathvar, ":" );
while( pathargv[pathargc] != NULL ) {
pathargv[++pathargc] = strtok( NULL, ":" );
}
在执行命令之前,该用access检测文件是否可执行
/* check execuable & check if needed add path */
if(!access(argv[0],1)){
execfile = argv[0];
}
else if(argv[0][0]!='/' && argv[0][0]!='.'){
for(i = 0; i< pathargc;i++){
sprintf(buf,"%s/%s",pathargv[i],argv[0]);
if(!access(buf,1)){ execfile = buf;break;}
}
}
如果可执行,则execfile指针指向文件名,否则不可执行
if(execfile!=NULL){
if(execve(execfile,argv,environ)<0){
printf("%s: Execve error.\n",execfile);
exit(0);
}
}
else{
printf("%s: Command not found.\n",argv[0]);
exit(0);
}
出现问题
运行前台程序,输入没有转换过去?
前台的程序无法读取输入,并被中断。
tcsetpgrpis to specify what is the foreground job. When your shell spawns a job in foreground (without&), it should create a new process group and make that the foreground job (of the controlling terminal, not whatever’s on STDIN).
对于子程序,在执行前,需要将STDIN STDOUT的前台程序设为自己。
if(!bg){
tcsetpgrp(STDIN_FILENO, getpid());
tcsetpgrp(STDOUT_FILENO,pid);
}
对于父程序,在等待结束后需要重新将STDIN STDOUT的前台程序设为自己。
if(!bg){
tcsetpgrp(STDIN_FILENO, pid);
//tcsetpgrp(STDOUT_FILENO,pid);
/* Use waitfg to wait until proc(pid) is no longer a foreground proc. */
waitfg(pid);
tcsetpgrp(STDIN_FILENO, getpid());
//tcsetpgrp(STDOUT_FILENO,getpid());
}
注意do_bgfg同样也要做相同操作。
利用scanf.c程序可以测试是否正确。
Part 4
支持
pipe支持文件
i/o redirection
Part Extra
尝试获取功能按键
由于unix下没有win的getch,便利用cfmakeraw实现了类似功能
char unix_getch(void){
struct termios tm, tm_old;
char ch;
Tcgetattr(STDIN_FILENO,&tm);
Tcgetattr(STDIN_FILENO,&tm_old);
cfmakeraw(&tm);
Tcsetattr(STDIN_FILENO, TCSANOW, &tm);
ch = getchar();
Tcsetattr(STDIN_FILENO, TCSANOW, &tm_old);
return ch;
}
void Tcgetattr(int fd,struct termios *termios_p){
if (tcgetattr(STDIN_FILENO,termios_p) < 0){
fprintf(stderr,"tcgetattr error");
exit(0);
}
}
void Tcsetattr(int fd, int optional_actions, const struct termios *termios_p){
if (tcsetattr(fd, optional_actions, termios_p) < 0){
fprintf(stderr,"tcsetattr error");
exit(0);
}
}
而输入计划利用handler函数来处理,handler的框架如下。
void input_handler(char *cmdline){
char ch;
int index = 0;
int cursorloc = 0;
ch = unix_getch();
memset(cmdline,'\0',sizeof(char)*MAXLINE);
while(ch!= '\r'){
/* key handler (unix type) */
if(ch == 0x1b){
ch = unix_getch();
if(ch == 0x5b){
ch = unix_getch();
switch(ch){
case KEY_UP:
printf("KEY UP");
break;
case KEY_DOWN:
printf("KEY DOWN");
break;
case KEY_LEFT:
printf("KEY LEFT");
break;
case KEY_RIGHT:
printf("KEY RIGHT");
index ++;
break;
}
ch = unix_getch();
}
else{
cmdline[index++] = 0x1b;
cmdline[index++] = 0x5b;
}
}
else{
cursorloc ++;
cmdline[index++] = ch;
printf("%s",cmdline+index-1);
ch = unix_getch();
}
}
printf("\n");
cmdline[index] = '\0';
}
MSH:一个简单SH工具实现的更多相关文章
- 开发一个简单的工具,导出github仓库所有issue列表
Jerry有一个github仓库,专门用来存放自己的知识管理,通过一条条的issue来记录具体的知识点: https://github.com/i042416/KnowlegeRepository/i ...
- grunt配置太复杂?发布一个前端构建工具,简单高效,自动跳过未更新的文件
做前端项目,如果没有一个自动化构建工具,手动处理那简直就是坑爹O(∩_∩)O.于是上网了解了下,grunt用的人不少,功能也挺强大.看了一下grunt的配置(包括gulp),感觉稍显复杂.当时项目结构 ...
- 用Qt写软件系列三:一个简单的系统工具(上)
导言 继上篇<用Qt写软件系列二:QIECookieViewer>之后,有一段时间没有更新博客了.这次要写的是一个简单的系统工具,需求来自一个内部项目.功能其实很简单,就是查看当前当前系统 ...
- [Winform]一个简单的账户管理工具
最近一直觉得注册的账户越来越多,帐号密码神马的容易弄混.自己就折腾了一个简单的账户管理工具,其实实现也挺简单,将每个账户的密码及相关密码提示信息,经aes算法加密之后保存到数据库,当前登录用户可以查询 ...
- 封装一个简单好用的打印Log的工具类And快速开发系列 10个常用工具类
快速开发系列 10个常用工具类 http://blog.csdn.net/lmj623565791/article/details/38965311 ------------------------- ...
- 实现一个简单的http请求工具类
OC自带的http请求用起来不直观,asihttprequest库又太大了,依赖也多,下面实现一个简单的http请求工具类 四个文件源码大致如下,还有优化空间 MYHttpRequest.h(类定义, ...
- 一个简单IP防刷工具类, x秒内最多允许y次单ip操作
IP防刷,也就是在短时间内有大量相同ip的请求,可能是恶意的,也可能是超出业务范围的.总之,我们需要杜绝短时间内大量请求的问题,怎么处理? 其实这个问题,真的是太常见和太简单了,但是真正来做的时候,可 ...
- 基于SOUI开发一个简单的小工具
基于DriectUI有很多库,比如 Duilib (免费) soui (免费) DuiVision (免费) 炫彩 (界面库免费,UI设计器付费,不提供源码) skinui (免费使用,但不开放源码, ...
- ubuntu14.04 配置g++工具,并运行一个简单的c++文件
首先,对Ubuntu 14.04 LTS进行源更新,摘自下述链接: http://chenrongya.blog.163.com/blog/static/8747419620143185103297/ ...
随机推荐
- TP3.2 配置最新的阿里大于sdk
TP3.2 配置最新的阿里大于sdk 最近公司买了阿里云的阿里大于短信验证 ,这里记录下本人接入短信验证的过程和心得. 大家是不是一开始都是和本人一样直接去百度下怎么有没有现成的demo 或者是封装好 ...
- proxifier配合ss,实现全局代理
proxfixer配合ss的话,基本可以实现全局代理,分应用代理,或者玩外服的游戏(一般的游戏默认不走代理,本软件可以强制应用代理) 由于ss使用的是sockets5代理,一般情况下只有浏览 ...
- Oracle中dblink如何使用
Oracle中dblink应用小结Oracle应用服务器OS 1.查看数据库的global_name SELECT * FROM GLOBAL_NAME; 2.查看global ...
- coder该何去何从
无论是什么语言的学习,都不是一帆风顺的,如今随着编程大军的壮大,工作越来越难找,各位coder已经把中心偏移到了学历上面,导致技术水平的参差不齐,以及虚假学历的泛滥,这样的恶性循环下,不知前路在何方?
- Oracle用户的初始化问题
上一篇博文介绍了关于.bashrc和.bash_profile的相关知识,在oracle的用户设置中能发挥作用. 场景:上周准备学习一下oracle,下载了安装文件后,在linux上新建了一个用户or ...
- win10下vagrant+centos7 rails虚拟开发机配置流程
此文写于2017.8.21 在写本文前,笔者已经尝试了多种其他的替代方法,例如wmware虚拟机安装kylin.然而发现总是还有各种问题.经大佬指点安装了virtualbox + vagrant.于是 ...
- JavaScript 加号运算符详解
将介绍JavaScript中 '+'加号运算符在一元.二元运算时的表现. 目录 1.一元运算符 2. 二元运算符 1. 一元运算符 语法: + Expression 说明:'+'号运算符作为一元运算符 ...
- 一个基于JRTPLIB的轻量级RTSP客户端(myRTSPClient)——解码篇:(一)用ffmpeg解码视频
一.概述 myRTSPClient(RTSPClient)获取音视频数据之后,接下来的工作便是将音视频数据交给解码器去解码(ffmpeg),ffmpeg解码之后于是便有了呈现在终端用户(USER)面前 ...
- OpenCms JSP 模板开发——创建一个简单的JSP模板
OpenCms中的JSP模板就是一个普通的JSP页面,在特定的位置使用标签来包含内容,在这个的例子中,我们将要开发一个简单JSP模板,这个模板只是在内容(如<html>.<body& ...
- 【Tomcat】batch获得war包
功能: 将maven项目打包复制到tomcat/webapps set git=C:\Users\zhengwenqiang\git set tomcat=e:\tomcat7.0.64 c: cd ...