MSH:一个简单SH工具实现

前端之家收集整理的这篇文章主要介绍了MSH:一个简单SH工具实现前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。


本文将分为不同的Part,分别实现Shell的一部分功能
mshCSAPPSHLAB出发,逐渐完善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,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,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),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",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",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",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 目标

  • 接收从键盘传来的信号,处理SIGINTSIGTSTP的信号
  • BGFG内嵌功能实现
  • 简单错误处理

支持前台的终止与暂停操作

SIGINTSIGTSTP的处理函数稍加修改,即可实现该功能。需要找出前台进程,并向该进程组发送信号即可。(之前将进程加到了与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;
}

因为bgfg支持PIDJID两种输入方式,故利用parse arg块来实现。对于atoi的异常情况,在jid2pid中对0进行了处理。而对于不属于该shellpid,会无法找到其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,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功能实现

获取当前的工作路径,拼接工作路径,并改变工作路径,即可完成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,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,BOLD,lt->tm_hour,lt->tm_min,lt->tm_sec,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,environ)<0){
        printf("%s: Execve error.\n",execfile);
        exit(0);
    }
}
else{
    printf("%s: Command not found.\n",argv[0]);
    exit(0);
}

出现问题

运行前台程序,输入没有转换过去?

前台的程序无法读取输入,并被中断。

tcsetpgrp is 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).

stackoverflow

对于子程序,在执行前,需要将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

Part Extra

尝试获取功能按键

由于unix下没有wingetch,便利用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,&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,"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';
}

猜你在找的Bash相关文章