• Tiny Shell


    Intro

    本项目要实现一个简易版Shell,支持以下Features:

    • 命令提示符tsh>
    • 若用户输入的命令第一个单词是内置命令,在当前进程tsh(Tiny Shell)执行命令:
    jobs   列出运行/挂起的后台job
    bg <job>   通过发送SIGCONT信号重启进程,将后台挂起的job设为在后台运行,<job>可以是PID或%JID
    fg <job>   通过发送SIGCONT信号重启进程,将后台运行/挂起的job设为在前台运行,<job>可以是PID或%JID
    kill <job>   结束<job>
    quit   退出tsh
    
    • 若用户输入的命令第一个单词是可执行文件路径,后续单词是命令行参数,tsh会fork一个子进程,在子进程运行
    • 支持job control(前后台切换),改变进程状态(running/stopped/terminated)
    • 支持管道|和I/O重定向< >(TODO)

    Background Knowledge

    • Shell
      shell是一个交互式的命令行解释器,可以执行用户输入的指令,显示计算结果。
      用户输入既可以是内置命令,也可以是可执行文件路径。用户既可以在前台运行,也可以在后台运行。后台job可以在命令最后加一个&,否则视为前台。比如/bin/ls -l -d &表示在后台执行ls程序。前台job只能有1个,后台job则可以有多个。
    • Signals
      信号机制的作用就是允许进程/内核打断其他进程运行,是进行进程间通信的一种方式。
      Linux的常用信号有:
      在这里插入图片描述

    Implementation

    • useful functions
      int fork(void):父进程创建一个子进程,在子进程中返回0,父进程中返回子进程的PID。
      int kill(pid_t pid, int sig):进程向其它进程(包括自己)发送信号,成功返回0,错误返回-1。可以通过改变参数pid调整发送目标:pid>0,给该进程发信号;pid==0,给包括自己在内的当前进程组发信号;pid<0,给pgid=|pid|的进程组的每个进程发信号。
      int execve(char *filename, char *argv[], char *envp[]):在当前进程的上下文环境中装载并运行新的程序,成功不返回,失败返回-1。filename可以是可执行目标文件或脚本文件,会覆盖原进程的data/code/stack,会保留原进程的PID/open files/signal context。
      pid_t waitpid(pid_t pid, int *status, int options):指定进程终止父进程会进行回收,否则等待。内核会将子进程的退出状态传给父进程,之后清除子进程。options=WNOHANG | WUNTRACED时,如果wait set中没有终止/暂停的子进程,立即返回0;否则返回任意一个子进程的PID。
      int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)how=SIG_BLOCKset中的信号加入阻塞向量;how=SIG_UNBLOCKset中的信号从阻塞向量中移除;how=SIG_SETMASK将阻塞向量设置为set。如果oldset!=NULL之前的阻塞向量就存储在oldset中。
      handler_t *signal(int signum, handler_t *handler):改变信号的默认行为。
    • step by step
      -- 整体结构:除了测试文件和Makefile外,全部实现都在tsh.c中,main函数有一个死循环,不停调用eval()实现命令的解析、执行。
      -- eval(char* cmdline)
      接收到用户输入后,第一件事就是解析。解析是通过int parseline(const char* cmdline, char** argv)完成,将cmdline解析到argv中,如果用户要求后台运行就返回1,否则返回0。
      解析后,我们需要通过int builtin_cmd(char** argv)判断是否为内置命令。如果是内置命令,就在builtin_cmd里立刻执行;否则需要创建子进程执行,这里需要区分前后台进程:如果是前台,需要等待terminate才能返回并接受新的输入;如果是后台,则可以立即接收新输入。
    • key point 1
      父进程在fork子进程之前,要用sigprocmask阻塞SIGCHLD信号。否则由于父子进程执行顺序不确定,可能导致:
      子进程首先执行完毕,内核向父进程发送SIGCHLD信号;
      从内核态切换到用户态时,检测到SIGCHLD信号并且执行sigchld_handler,删除该job;
      父进程执行addjob操作,显然删除和添加顺序反了。
      如果我们正确阻塞了SIGCHLD信号,还是按照上面的顺序:
      子进程首先执行完毕,内核向父进程发送SIGCHLD信号;
      从内核态切换到用户态时,由于父进程阻塞了SIGCHLD信号,所以不会执行sigchld_handler
      父进程添加该job,解除SIGCHLD信号的阻塞,下一次context switch时删除job。
    • key point 2
      用户从键盘输入ctrl-c时,内核给shell发SIGINT信号(默认终止shell进程),在main里面安装handler,在sigint_handler中处理:终止所有的前台进程及其子进程;
      用户从键盘输入ctrl-z时,内核给shell发SIGTSTP信号(默认暂停当前进程直到收到SIGCONT),在main里面安装handler,在sigtstp_handler中处理:暂停所有的前台进程及其子进程。
    • key point 3
      默认情况下,fork出来的子进程和他爹属于同一个进程组。当我们在机器上运行Tiny Shell时,程序运行在前台进程组中,这时如果Tiny Shell创建一些子进程,这些子进程也会同属于这个前台进程组,用户输入ctrl-c会终止所有前台进程,包括Tiny Shell,这显然不是我们想要的。
      解决方案是:fork之后,子进程调用setpgid(0,0)将其放到一个新的进程组里,这个组的group id和PID相同。这样就可以确保前台进程组里只有Tiny Shell一个进程,用户输入ctrl-c时,就可以在sigint_handler中调用kill()终止特定的前台job。
    • key point 4
      当子进程终止或者暂停,内核会给父进程发送SIGCHLD信号,我们在sigchld_handler中根据子进程的状态做相应的处理:
      WIFEXITED(status):子进程通过exitreturn正常终止;
      WIFSIGNALED(status):子进程通过信号终止;
      WIFSTOPPED(status):子进程暂停;
      WTERMSIG(status):当WIFSIGNALED()为真,返回造成子进程终止的信号ID;
      WSTOPSIG(status):当WIFSTOPPED()为真,返回造成子进程暂停的信号ID。
    • key point 5
      对于前台进程,需要一直等待其执行完毕,然后回收,可以在waitfg()中调用sigsuspend完成;但对于后台进程,由于不用等待其完成,所以为了避免其成为zombie,需要在其执行完毕或者暂停时通知父进程,这个机制就是signal,具体的就是我们的sigchld_handler做的事情。

    Test

    一方面通过提供的脚本测试,共有16个脚本测试文件,测试通过make test01~make test16进行;
    另一方面通过实际执行去测试各项功能。

    Code&Reference

    code here
    reference here

    TODO

    管道、重定向、https://github.com/mit-pdos/xv6-riscv/blob/riscv//user/sh.c#L1

  • 相关阅读:
    Leetcode 647 回文子串
    计算机专业课程——答案搜集
    Centos搭建go环境以及go入门
    android studio实现圆角的button
    关于区块链的认识和看法
    LeetCode 96. 不同的二叉搜索树
    LeetCode 120. Triangle (三角形最小路径和)详解
    LeetCode 64最小路径和
    微信发起摄像头接口请求 上传头像 图片
    Vue解决项目白屏以及(反向代理)
  • 原文地址:https://www.cnblogs.com/EIMadrigal/p/13363535.html
Copyright © 2020-2023  润新知