【内部命令,外部命令,作业控制我全都要】从零开始写一个属于你自己的shell

0 前言

  都说程序员的三大终极梦想是操作系统、数据库、编译器。可现在太弱鸡了,没有linus大神两周写内核的本事,但自己写一个shell还是可行的。本文将会从头记录如何编写一个支持大多数外部命令,支持cd、jobs、bg、fg等功能的shell,称之为Gshell。

1 功能概述

  Gshell支持Bash的大部分外部命令与数个内置命令,能够完成shell的基础功能。对于支持的指令,Gshell可以返回正确结果,对于不支持的指令,Gshell将返回错误信息,具有较好的鲁棒性。
  Gshell的命令提示符参照zsh,美观并且便于开发;拥有自定义提示符等多个额外功能,能够满足个性化需求;支持作业控制,包括bg、fg、ctrl+z、ctrl+c指令;底层支持前后端进程组的变更。

2 功能实现

2.1 基础功能
2.1.1 功能描述

简介:
  实现shell的基础功能。
使用规则:
  程序从控制台执行,启动后显示一个命令提示符,默认为$。用户可以通过给特定的环境变量赋值来改变命令提示符的形式。
  通过某个特殊的命令或按键组合可以正常地关闭本程序,默认为exit。
  提供后台运行机制。用户提交的任务可以通过某种指示使之在后台运行,例如: -> bg job1 将使任务 job1 在后台运行,并马上返回给用户一个新的提示符。
  提供输出重定向。通过指定文件名将任务的所有输出覆盖写到文件中而不是送到标准输出上。
  提供输入重定向。通过指定文件名使得任务从相应的文件中去获取所需的数据,而不是从标准输入上。

2.1.2 业务流程

基础功能流程V2

2.1.3 功能需求

业务规则:
  在进入shell时、一条合法指令执行成功后(exit除外)、一条非法指令执行失败后新起一行,显示命令提示符。
  大部分外部命令,如ls,rm等能够正常执行。
  使用命令&,可以实现任务的后台运行;返回任务对应的pid并新起一行返回命令提示符。
  使用<、>,实现输出重定向和输入重定向。
  使用exit退出shell。
  非法指令执行后应能够输出对应的错误信息。

基础功能实现V2
  在Termail中输入Gshell进入命令提示符界面。
  第一条指令cs实现命令提示符的修改,由$变为%,执行成功,返回新的一行。
  more指令执行成功,可以正常显示文件内容。tt文件目前的内容为HelloWorld!,inputtext文件目前的内容为HelloWorld!。ppp实现接收输入并输出。
  可以正常执行用户的可执行文件ppp,实现重定向输入和输出。首先将tt的内容改为Worldhello!。然后从文件inputtext中选取一行作为输入;输出到文件tt中。tt文件在执行ppp之后内容为HelloWorld!,说明重定向成功,执行用户的可执行文件成功。
  使用&指令将sleep1放入后台执行,并返回对应进程的pid。sleep1功能为十秒钟后输出一串字符串,成功输出,说明后台功能正常。
  使用exit退出shell。

2.2 扩展功能
2.2.1 功能描述

简介:
  实现shell的额外功能。
使用规则:
  程序不仅显示命令提示符,而且显示当前使用shell的用户的用户名、显示当前工作目录、显示当前系统时间。
  实现内置命令cd、jobs。
  实现使用&指令后僵尸进程的清除。

2.2.2 业务流程

额外功能流程V2

2.2.3 功能需求

业务规则:
  在进入shell时、一条合法指令执行成功后(exit除外)、一条非法指令执行失败后新起一行,显示当前使用shell的用户的用户名、显示当前工作目录、显示当前系统时间和命令提示符。
  内置命令cd和jobs等能够正常执行。
  使用命令&,可能会产生僵尸进程占用系统资源,需要避免产生僵尸进程。

额外功能实现V2
  cd命令实现从当前目录到home目录的跳转。使用ls指令确认跳转。
  使用&将三个可执行文件sleep1、sleep2、sleep3放入后台执行,它们将分别在10s,20s,30s后输出一串字符串。随后使用jobs查看当前后台进程。可以发现后台进程减少,并未产生僵尸进程。

2.3进阶功能
2.3.1 功能描述

简介:
  实现作业控制与进程组控制。
使用规则:
  拥有基础的作业控制功能。能够实现作业在前台后台的调入调出,暂停执行与继续执行。

2.3.2 业务流程

进阶功能流程

2.3.3 功能需求

业务规则:
  运行指令时,按下ctrl+z组合键将使当前指令转入后台并暂停,立即返回命令提示符,可以输入下一条指令。
  使用bg pid指令可以在后台重启暂停的进程,该进程并不调往前台。bg后可立即进行新的指令的执行。
  使用fg pid指令可以将后台暂停的进程调往前台执行。直到该指令执行完毕才进行新的指令的执行。
  运行时,按下ctrl+c组合键将强制终止当前指令并立刻返回命令提示符。在命令提示符行按下ctrl+c将结束Gshell。
进阶功能实现
  在Termail中输入Gshell进入命令提示符界面。
  第一条指令为sleep2,睡眠二十秒然后输出。在运行时输入ctrl+z退出。输入jobs查看进程状态,为stopped,暂停中。随后使用bg pid让其在后台继续运行。输入jobs查看进程状态。由stopped变为sleeping,说明在后台运行。
  仍然输入sleep2,然后ctrl+z退出。这次使用fg命令让其在前台继续运行。能够正常输出。
  当fg之后输入ctrl+c时,前台的sleep2退出,说明当前sleep2对应的进程在前台,而不是Gshell。说明进程控制功能正常。

3 程序设计

3.1 程序总体结构

程序总体结构

  当进入main函数,程序首先将全部变量初始化。并使用对应函数获取当前用户的用户名,获取当前工作目录,获取当前时间。随后进入主循环。除非输入exit,否则此循环不退出。
  进入主循环后,首先将循环需要用到的旗语初始化。然后将输入的指令存入字符串数组。将其分割。根据指令不同选择对应的函数执行即可。

3.2 函数实现
3.2.1 my_cd 函数

  my_cd函数用于实现cd指令,核心在于调用chdir函数以进行目标路径的跳转。若目标函数合法,则正常结束;若非法,则返回错误原因并结束。

3.2.2 my_cs 函数

  my_cs函数用于修改命令提示符。因此需要维护一个变量,将输入的字符作为新的命令提示符更新。目前仅支持一个字符,超过一个字符则报错。且禁止使用>、<等功能性字符。

3.2.3 my_jobs 函数

  my_jobs函数用于实现jobs指令,显示当前在后台运行的所有进程。为此需要维护一个数组,用于保存当前在后台运行的所有进程的进程号。当执行jobs时,从该数组中取出pid,然后到/proc/pid/status中去寻找进程状态并返回。该函数目前支持显示pid以及进程名。

3.2.4 redirect_input 与 redirect_output 函数

  这两个函数分别实现输入重定向和输出重定向。函数需要保证,当指令中出现>时进行输出重定向,出现<时进行输入重定向。可以同时实现输入和输出重定向。要求无论重定向符号与文件名之间是否有空格,都能够正常识别。核心在于调用dup2函数。

3.2.5 sig_chld 函数

  作业控制的核心函数。进程状态的变化全部在这里处理。每当进程的状态发生变化:包括由运行转为终止、暂停、由暂停变为继续等,子进程都会向主进程发送SIGCHLD信号。因此使用信号注册函数,修改默认行为,将信号对应的行为改为该函数。函数中调用waitpid函数。该函数返回发送信号的进程的pid,原因保存在status中。使用旗语更新原因。在主进程中进行处理。

3.2.6 my_bg函数

  my_bg函数用于将后台暂停的进程变为继续进行。核心为调用kill函数向目标进程发送SIGCONT信号。

3.2.7 my_fg函数

  my_fg函数用于将后台暂停的进程调到前台,并继续执行。核心为调用kill函数向目标进程发送SIGCONT信号,然后使用tcsetpgrp函数修改前台进程组。

3.3 其他机制
3.3.1 后台运行机制

  当指令为bg加其余指令时,启用后台机制。后台机制要求程序在后台运行,立即返回命令提示符。因此无法使用waitpid进行回收,将产生大量僵尸进程占用系统资源。为防止该情况发生,需要使用两次fork来避免产生僵尸进程。如下所示:
双fork

  当父进程第一次调用fork,产生子进程一,父进程执行waitpid等待子进程一结束;子进程一调用fork,产生子进程二,随后子进程一返回,子进程一被回收;由于父进程已经结束,子进程二变为孤儿进程,由init接管,执行execlp,执行后被init回收。因此用这种方式不会产生僵尸进程,适合shell使用。
  bg指令需要维护数组,保存当前在后台运行的所有进程的进程号,使用双fork方式,则需要保存子进程二的进程号,不同于使用fork可以在父进程中直接得到子进程一的进程号,在父进程中无法直接得到子进程二的进程号,因此需要使用管道进行进程间的交流。在子进程一结束前,先在管道中写入子进程二的进程号,然后再返回即可。随后父进程读取管道,得到对应的pid,更新数组。

3.3.2 命令提示符优化机制

  Gshell要求命令提示符所在行显示当前用户、当前工作目录与当前时间。其中,当前用户通过调用getuid函数得到当前用户的uid,然后使用getpwuid函数得到当前用户对应的数据结构,然后从对应数据结构中得到用户名。当前工作目录通过调用getcwd函数得到。当前时间通过time函数得到,然后使用localtime函数进行转换。

3.3.3 作业控制机制

  加入作业控制机制,则底层需要加入进程组的控制功能。
作业控制流程

  如上图所示,exit、cd、cs、jobs这些指令不涉及作业控制,因此没有变化。新的作业加入主要是靠其他指令与&指令。因此进程组的设定重点也在这里。每当一条指令执行,在fork之后的主进程中要设置该进程归入一个新的进程组,在fork后的子进程中也要设置该进程归入新的进程组。这是为了确保新进程在出现之后直接进入新的进程组。然后进行对应的执行工作。并将该进程组设置为前台进程组,Gshell对应的进程转为后台运行。在执行完毕之后向Gshell进程发送信号,根据信号进行对应处理,退出阻塞。更改前台进程组为Gshell对应的进程组。进入下一循环。
  作业控制需要用到一系列各个进程之间进行通信的变量,这些变量的类型只能是volatile sig_atomic_t,实际上是int类型,只有这种类型,才能够保证异步通信的准确性。对这类变量的更改均为原子操作。
PS:代码下载请前往https://download.csdn.net/download/erwugumo/12539461

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值