【內部命令,外部命令,作業控制我全都要】從零開始寫一個屬於你自己的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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章