【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.命令提示符中不能顯示使用者名稱,主機名,當前目錄