1. 程式人生 > >【Linux】程序等待與程序替換

【Linux】程序等待與程序替換

程序的銷燬     1.釋放資源     2.記賬資訊     3.將程序狀態設定成殭屍狀態     4.轉儲存排程 程序終止的方法     正常退出         1.exit  (C庫函式)  exit函式要先執行一些清除操作,然後才將控制權交給核心             a.使用者執行atexit或者onexit定義的清理函式;             b.關閉所有的流,所有的快取資料均被寫入;             c.呼叫_exit函式。         2.main函式退出         3._exit()  (作業系統提供的API,程序終止函式)  _exit函式執行後會立即返回給核心

    異常退出         1.ctrl + c         2.assert()         3.abort()         4.訊號終止(ps:段錯誤、棧溢位)          "_NR"是在Linux的原始碼中為每個系統呼叫加上的字首     _exit終止呼叫程序,但不關閉檔案,不清除輸出快取,也不調用出口函式。     _exit()定義在unistd.h中,直接使程序停止執行,清除其使用的記憶體空間,並銷燬其在核心中的各種資料結構。     exit()函式定義在stdlib.h中,在這些基礎上作了一些包裝,在執行退出之前加了若干道工序.     exit()函式與_exit()函式最大的區別就在於exit()函式在呼叫_exit系統呼叫之前要檢查檔案的開啟情況,     把檔案緩衝區中的內容寫回檔案,就是”清理I/O緩衝”。  重新整理快取     實際上,main函式執行完畢後,OS會註冊一個清理函式並交給使用者執行,其呼叫了atexit函式或者onexit函式     atexit()回撥函式 最多能註冊32個,後註冊先執行,類似於棧。     開啟一個檔案就是開啟一個流,在執行exit函式退出的時候,實際上會把檔案的內容重新整理到緩衝區,     並且關閉流,最後執行_exit函式。

    儘量使用_exit函式          return是一種常見的退出程序的方法,執行return n相當於exit(n),因為會把main函式的返回值當做exit函式的引數傳入。      echo $?可以檢視程式上一次退出的狀態碼  退出碼的範圍 0-255

程序等待:回收殭屍子程序

wait

pid_t wait(int *status);

    正常退出返回值為被回收的子程序ID     錯誤返回-1。      8-15位是子程序的退出碼

    一直等待子程序結束,才將返回結果反饋給wait。     如果傳遞NULL,表示不關心子程序的退出狀態資訊。     一次等待只能結束一個子程序。     注意:wait必須和子程序的個數相匹配。(eg:接兒子上下學)

waitpid pid_t waitpid(pid_t pid, int * status, int options) status 子程序的退出碼 舉例:接指定的兒子(指定pid)上下學

引數pid     >0   等待與其PID相等的子程序死亡     =0   等待本組中的任何一個子程序死亡   fork建立的父子程序屬於同一個組     =-1  等待當前程序中的任何一個子程序死亡     <-1  等待其組ID等於pid的絕對值|pid|的任一子程序死亡 status     可以按照點陣圖去理解,是個輸出型引數,         正常終止時,0-7位是0,8-15位表示退出的狀態,即就是退出狀態碼。         異常終止時,實際上就是被訊號所終止,0-7位是終止訊號 core dump標誌,8-15位沒有用。 option選項 一般寫0  實際上也是個點陣圖     WNOHANG           非阻塞型等待         輪詢         如果有子程序結束,則回收並返回子程序ID(ret > 0),如果此時沒有子程序需要等待那麼ret < 0         如果子程序還沒有結束,那麼ret == 0

    WIFEXITED(status)         如果正常退出,返回真     WEXITSTATUS(status)           返回子程序的退出碼     WIFSIGNALED(status)         如果被訊號幹掉,則返回真     WTERMSIG(status)         獲得搞死該程序的訊號值

標頭檔案的訪問順序     系統標頭檔案->庫函式標頭檔案->自定義標頭檔案

注意:只要PCB不被刪掉,那麼程序就一直存在。程序替換後,其原來程序後面的內容將不會被執行。

程序替換     fork出子程序後,希望子程序執行和父程序(ps:比如執行a.out)不一樣的程序(例如執行磁碟上的ls),這就需要程序替換了。

    一開始,虛擬記憶體通過三級對映(虛擬記憶體->頁目錄->頁表)與實體記憶體建立連線,這時候如果想要執行磁碟上的ls程序,就需要呼叫     exec系列函式,然後PCB不變,幹掉虛擬記憶體通過三級對映與實體記憶體建立的連線,裡面的程式碼段以及資料段都沒有了,     再將ls的程式碼段以及資料段放入實體記憶體的某個空間,然後再建立三級對映,     通過readelf -h a.out可以看到程式執行的起始(入口)地址,這時候替換掉eip的值(改為ls的入口地址),那麼就可以     找到ls的入口地址,接下來執行ls這個程序,然後棧和堆需要重新開始(之前的函式棧幀並不能保證找到下個指令的開始地址),     最終達到替換程序的目的。          實際上,作業系統在建立PCB的同時,會建立一個載入器,載入器會解析檔案格式,並將解析的結果存入實體記憶體中的程式碼段以及資料段中。          載入器:也叫作程式載入器,主要用於載入程式和庫,它負責將程式送入記憶體,為程式的執行提供準備,一旦載入完成,     OS才會把控制權移交給執行程式碼的程式。     Unix: loader(載入器)是系統呼叫(execve)的控制代碼。          a.out是個ELF檔案,通過readelf -h a.out可以看到程式執行的起始(入口)地址。可以看到這個欄位Entry point address      exec系列函式     下面這些函式如果呼叫成功,則載入新的程式從啟動程式碼開始執行,不再返回,如果失敗則返回-1,成功則沒有返回值。     如果執行exec系列函式後面還有別的語句,那麼替換程序成功後,就不會有返回值。          list     int execl(const char *path, const char *arg, ...);    //必須指明路徑           execl("/bin/ls", "/bin/ls", "-l", "-t", NULL);         execl("./hello", "./hello", NULL);         execl("/bin/bash", "ps", "-ef", NULL);     int execlp(const char *file, const char *arg, ...);         execlp("ls", "ls", "-l", "-t", NULL);     int execle(const char *path, const char *arg, ...,char *const envp[]);         需要提前將envp傳入指標陣列中。         execl("./hello", "./hello", NULL, envp);             path絕對路徑或者相對路徑                   envp環境變數              vector      向量

char *argv[] = {"/bin/ls", "-l", NULL};    

    int execv(const char *path, char *const argv[]);         execv("/bin/ls", argv);     int execvp(const char *file, char *const argv[]);         execvp("ls", argv);    //實際上exec系列函式只是將引數傳進去,並不會進行校驗,因此直接替換掉argv[0]的值。     int execve(const char *path, char *const argv[], char *const envp[]);      //系統呼叫         execve("./h", argv, env);             file可執行檔名   argv(main函式的命令列引數)     其餘5個都是C庫函式      l(list)    可變引數列表 v(vector)    陣列 p(path)    自動搜尋已存在的環境變數 e(env)    需要自己維護的環境變數

模擬system

    /bin/sh -c "ls -l"          if (0 == pid)     {         char *argv[] = {              "sh", "-c", cmd, NULL         };

        execvp("/bin/sh", argv);         exit(127);     }     else     {         int status ;           waitpid(pid, &status, 0);                  if ( WIFEXITED(status) )         {              ret = WEXITSTATUS(status);         }         else         {              ret = -1;         }     }

find / -name ls 2>/dev/null   查詢ls的位置,並且過濾掉列印的錯誤資訊     這裡2表示strerr,也就是檔案描述符中的標準錯誤對應的返回值,/dev/null是linux中的一個特殊檔案,相當於一個垃圾桶。     它丟棄一切寫入其中的資料(但報告寫入操作成功),讀取它則會立即得到一個EOF。通常被用於丟棄不需要的資料輸出。      清空檔案內容:     1.  echo "" > test.txt  檔案大小被截斷成1個位元組 echo > test.txt     2.  > test.txt     3.  cat /dev/null > test.txt     4.  cp /dev/null test.txt     5.  dd if=/dev/null of=test.txt     6.  truncate -s 0 test.txt      truncate用來將一個檔案縮小或者擴充套件到給定大小   -s來指定檔案大小          /dev/zero實際上產生連續不斷的null的流(二進位制的零流,而不是ASCII型的)。寫入它的輸出會丟失不見, /dev/zero主要的用處是用來建立一個指定長度用於初始化的空檔案,像臨時交換檔案。 為特定的目的而用零去填充一個指定大小的檔案, 如掛載一個檔案系統到環回裝置。 該裝置無窮盡地提供0,可以使用任何你需要的數目——裝置提供的要多的多。他可以用於向裝置或檔案寫入字串0。 bash     首先bash可以看成是一個父程序,那麼輸入ls命令時,實際上是從磁碟上獲取到ls的程式碼載入到實體記憶體上,然後再執行ls程序, 這也就是程序替換,ls程序執行結束後,被bash程序回收,(實際上bash一直在等待ls程序執行結束)。

簡易版的shell     1.從標準輸入中讀取字串到記憶體中;     2.對使用者輸入進行解析,要執行的指令是什麼,引數是什麼;         引數開始                 當前狀態為引數結束狀態,並且當前字元為非空格,此時進入引數開始狀態,並且把當前指標儲存在一個數組中。         引數結束                 當前狀態為引數開始狀態,並且當前字元為空格,此時進入引數結束狀態,並且把當前指標指向的字元改為'\0'。         strtok         ll是個別名     3.建立子程序fork         a.子程序進行父程序的程式替換execvp;         b.父程序進行程序等待wait。     4.當子程序執行完畢後從wait中返回,繼續下一次迴圈。 關於myshell中cd不生效:     執行cd ..時,myshell又會建立一個子程序,實際上子程序對cd進行了程式替換, 但是由於子程序執行結束後替換的目錄就被銷燬了,然而myshell的目錄仍是之前的目錄, 因此看不到切換目錄。     如果發現輸入的指令是cd,直接呼叫chdir函式修改程序自身的工作目錄。

1.普通命令:shell建立子程序進行程式替換來實現; 2.內建指令:shell程序自身判定輸入的指令後,呼叫相關係統函式實現,本質上是對shell程序自身來操作。    

myshell不支援     1.alias     2.內建指令(比如cd)     3.管道     4.輸出重定向     5.命令提示符中不能顯示使用者名稱,主機名,當前目錄