Linux進程控制
@TOC
零、前言
前篇我們講解學習了關于進程的概念知識,本章主要講解關于進程的控制,深入學習進程
一、進程創建
1、fork函數
概念:
在linux中fork函數從已存在進程中創建一個新進程(子進程),而原進程為父進程
fork函數原型:
pid_t fork(void);
注意:
使用fork()函數需要包含頭文件
fork成功后對子進程返回0,對父進程返回子進程id,fork出錯返回-1
內核視角看待fork:
進程調用fork,內核分配新的內存塊和內核數據結構給子進程
將父進程部分數據結構內容拷貝至子進程(例如PCB進程控制塊,進程地址空間,頁表等)
添加子進程到系統進程列表當中,當fork返回后開始調度器調度進程
示圖:
fork后執行問題:
當一個進程調用fork之后,父子進程共享同一份代碼,也就是說整個代碼父子進程都可以看到,但是此時父子進程的執行位置都是相同的,也就是說fork返回后子進程也是往fork之后的代碼執行(并非再從頭執行)
示例:
#include
結果:
示圖:
2、fork返回值
返回值:
fork成功對子進程返回0,對父進程返回子進程的pid
寫時拷貝
概念:
fork成功之后父子代碼共享,當父子不寫入數據時,數據也是共享的,當任意一方試圖寫入,便以寫時拷貝的方式各自一份副本
為什么數據要進行寫時拷貝:
進程具有獨立性,多進程運行,需要獨享各種資源,多進程運行期間互不干擾,不能讓子進程的修改影響到父進程
為什么不在創建子進程的時候就進行數據的拷貝:
子進程不一定會使用父進程的所有數據,并且在子進程不對數據進行寫入的情況下,沒有必要對數據進行拷貝,我們應該按需分配,在需要修改數據的時候再分配(延時分配),這樣可以高效的使用內存空間,提高fork效率,以及fork的成功率
代碼會不會進行寫時拷貝:
90%的情況下是不會的,但這并不代表代碼不能進行寫時拷貝,例如在進行進程替換的時候,則需要進行代碼的寫時拷貝
示圖:
fork函數為什么要給子進程返回0,給父進程返回子進程的PID:
一個父進程可以創建多個子進程,而一個子進程只能有一個父進程。因此,對于子進程來說,父進程是不需要被標識的;而對于父進程來說,子進程是需要被標識的,因為父進程創建子進程的目的是讓其執行任務的,父進程只有知道了子進程的PID才能很好的對該子進程進行深入操作
為什么fork存在“兩個”返回值:
父進程創建子進程時,子進程以父進程為模板構建進程,代碼數據父子共享,返回時也是父子進程進行修改數據時,由頁表發現該數據是父子進程共享的,所以系統會找到另一個物理空間進行拷貝數據,拷貝數據后再修改數據,達到數據各有一份互不干擾的目的,保證進程的獨立性
3、fork用法
我們創建子進程并不是為了父進程執行一樣的代碼,而是為了使父子進程同時執行不同的代碼段
例如:父進程等待客戶端請求,生成子進程來處理請求
用法1:fork返回后分流執行不同代碼
示例:
#include
結果:
用法2:fork返回后調用exec函數替換進程
注:在下文有著重講解
4、fork失敗
fork本質就是向系統要資源,當某個資源不夠時則會發生fork失敗
失敗原因:
1.系統中有太多的進程
2.實際用戶的進程數超過了限制
二、進程終止
1、退出碼
概念:
其實main函數是間接性被操作系統所調用的,當main函數調用結束后就應該給操作系統返回相應的退出信息,而這個所謂的退出信息就是以退出碼的形式作為main函數的返回值返回
我們一般以0表示代碼成功執行完畢,以非0表示代碼執行過程中出現錯誤,一般來說我們寫的代碼都不太規范,沒有根據執行結果返回相應的退出碼
注:退出碼可以人為定義,也可以使用系統的錯誤碼表
示圖:系統錯誤碼表
退出碼查看:
使用指令 echo $?
示例:
注:如果main沒有return,則echo $?查看的是最近函數的退出碼,一般來說都是0
2、退出方法
進程退出場景:
代碼運行完畢,結果正確,退出碼為0
代碼運行完畢,結果不正確,邏輯存在問題,退出碼為非0
代碼異常終止,層序崩潰,退出碼沒有意義
進程常見退出方法:
1) 調用_exit函數
_exit函數原型:
#include
注意:
status 定義了進程的終止狀態,父進程通過wait來獲取該值
雖然status是int,但是僅有低8位可以被父進程所用
注:_exit(-1)時,在終端執行$?發現返回值是255
示圖:
2)調用exit函數
exit函數原型:
#include
exit與_exit的區別:
_exit僅僅是退出進程
exit在退出進程前,先執行用戶通過 atexit或on_exit定義的清理函數,關閉所有打開的流,所有的緩存數據均寫入(刷新緩沖區),最后調用_exit
示圖:
示例:
3)main函數return
return是一種更常見的退出進程方法,執行return n等同于執行exit(n),因為調用main的運行時函數會將main的返回值當做exit的參數
示圖:
注意:
只有是在main函數的的return才算是終止進程,其他函數return只是結束函數,因為系統調用的是main函數,main函數返回才是進程的返回
調用main函數運行結束后,main函數的return返回值當做exit的參數來調用exit函數,而使用exit函數退出進程前,exit函數會先執行用戶定義的清理函數、沖刷緩沖,關閉流等操作,然后再調用_exit函數終止進程
4)異常退出
向進程發生信號
如在進程運行過程中向進程發生kill -9信號使得進程異常退出,或是使用Ctrl+C迫使進程退出
代碼運行異常
如代碼當中存在野指針問題等bug問題使得進程運行時異常退出
3、理解終止
以OS角度理解:核心思想-歸還資源
釋放曾經為管理進程所維護的數據結構資源,并非銷毀釋放數據結構對象,而是將狀態設置為無效并保存起來,下一次需要就直接使用不用申請,相當于建立對應的數據結構“內存池”
釋放程序數據和代碼占用的空間,并非清空數據和代碼,而是將對應內存區域設置為無效,要再次使用時直接覆蓋數據和代碼就行了
取消曾經該進程在進程隊列里的鏈接關系(避免”野指針“)
三、進程等待
進程等待必要性:
當子進程退出,并不是完全退出,子進程的PCB任然存在,父進程如果不等待回收,就可能造成‘僵尸進程’的問題,進而造成內存泄漏
注:進程一旦變成僵尸狀態,并不能被父進程給kill掉,因為子進程已經死去,只能父進程等待回收
子進程的PCB保留著退出前任務執行的信息,而通過回收子進程我們可以知道子進程運行完成,結果對還是不對,或者是否正常退出
注:非必須,依執行的程序和需求而定
盡量使父進程晚于子進程退出,可以規范化進行資源的回收
注:所以一般來說,當我們fork之后,就需要父進程等待回收子進程
1、等待方法
wait方法:
wait函數原型:
#include
注意:
wait函數作用的等待任意子進程
返回值:成功返回被等待進程pid,失敗返回-1
參數:輸出型參數,獲取子進程退出狀態,不關心則可以設置成為NULL
waitpid方法:
waitpid函數原型:
#include
注意:
返回值:
當正常返回的時候waitpid返回收集到的子進程的進程ID
如果設置了選項options為WNOHANG,而調用中waitpid發現沒有已退出的子進程可收集,則返回0;如果調用中出錯,則返回-1,這時errno會被設置成相應的值以指示錯誤所在
參數pid:
Pid=-1,等待任一個子進程,與wait等效
Pid>0,等待其進程ID與pid相等的子進程
參數status:
參數status是一個輸出型參數,需要我們傳入一個整形變量的地址,以此獲取子進程退出的信息
使用對應的宏可以查看我們需要的退出信息:WIFEXITED(status): 若為正常終止子進程返回的狀態,則為真(查看進程是否是正常退出);WEXITSTATUS(status): 若WIFEXITED非零,提取子進程退出碼(查看進程的退出碼)
參數options:
設置為0:表示默認的阻塞式等待子進程退出,即子進程沒退出就不返回,一直等待到子進程退出回收子進程
設置為WNOHANG:若pid指定的子進程沒有結束,則waitpid()函數返回0,不予以等待;若正常結束,則返回該子進程的ID
示例1:阻塞等待
#include
結果:
示例2:非阻塞等待
#include
結果:
總結:
如果子進程已經退出,調用wait/waitpid時,wait/waitpid會立即返回,并且釋放資源,獲得子進程退出信息
如果在任意時刻調用wait/waitpid,子進程存在且正常運行,則進程可能阻塞
如果不存在該子進程,則立即出錯返回
示圖:
2、獲取status
概念:
wait和waitpid,都有一個status參數,該參數是一個輸出型參數,由操作系統進行將退出信息填充
如果傳遞NULL,表示不關心子進程的退出狀態信息;如果傳遞變量地址,操作系統會根據該參數將子進程的退出信息反饋給父進程
使用對應的宏可以方便查看我們需要的退出信息:WIFEXITED(status): 若為正常終止子進程返回的狀態,則為真(查看進程是否是正常退出);WEXITSTATUS(status): 若WIFEXITED非零,提取子進程退出碼(查看進程的退出碼)
注:status不能簡單的當作整形來看待,可以當作位圖來看待(只有status的低16比特位有有效信息)
示圖:
注意:
如果是正常退出,我們可以進一步獲取子進程退出的退出碼(退出狀態),通過退出碼判斷進程執行的結果如何,是對還是錯
如果是異常退出,那么退出碼變沒有意義(執行任務已經失敗),只需要考慮低7位的信息查看是怎樣的異常
示例:
#include
結果:
3、理解等待
以OS的視角理解:
父進程創建子進程,并調用系統接口wait/waitpid進行等待
系統會將當前進程放進等待隊列,并將進程的狀態設置為非R
當到一定程度時,系統會喚醒進程,進程由等待隊列轉為運行隊列,同時狀態變為R
四、進程替換
1、替換原理
用fork創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支)
如果想執行不同程序,子進程可以調用一種exec函數以執行另一個程序
當進程調用一種exec函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行
注:調用exec并不創建新進程,只是將進程的代碼和數據寫時拷貝成新程序的代碼和數據(達到替換的效果),所以調用exec前后該進程的id并未改變
示圖:
2、替換方法
exec系列函數原型:
#include
注意:
這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回(已經將代碼和數據全部替換,執行新程序的執行邏輯)
如果調用出錯則返回-1,所以exec函數只有出錯的返回值而沒有成功的返回值
命名理解:
l(list) : 表示參數采用列表的形式傳入如何使用程序或者命令 v(vector) : 參數用數組 p(path) : 有p自動搜索環境變量PATH e(env) : 表示自己維護環境變量
示圖:
具體使用介紹:
//子進程替換程序為ls命令 execl("/user/bin/ls","ls","-i","-a","-l",NULL); //注:l表示列表形式,即以可變參數的形式使用程序,最后一個參數需要傳入NULL,表示參數傳入結束 execlp("ls","ls","-i","-a","-l",NULL); //注:對于ls這樣的系統命令,其路徑被儲存在PATH環境變量里,execlp函數會自動到PATH里通過各路徑去尋找ls命令;如果系統程序指令,則要么拷貝程序到PATH里的某個路徑下,或者添加程序路徑到PATH變量里 //注:對于這里兩個ls其實并不沖突,第一個表示程序的名稱,第二個表示如何通過參數列表使用程序(使用時需要帶上名稱) char* const MY_Env[]={ "MYENV=hello linux",NULL } execle("./mycmd","mycmd",NULL,MY_Env); //注:對于不是當前環境變量,需要自己組裝,或者將添加到當前環境變量里 char* const MY_acgv[]={ "ls", ,"-l" ,"-a" ,"-i" ,NULL } execv("/user/bin/ls",MY_acgv); //注:v表示數組的形式傳入參數列表 execvp("ls",MY_acgv); execve("/user/bin/ls",MY_acgv,env);
示例:替換程序為mycmd
test_exec.c: #include
結果:
注:本質上只有execve是真正的系統調用,其它五個函數最終都調用execve(在系統調用上的一個封裝),所以execve在man手冊 第2節,其它函數在man手冊第3節
示圖:
注:對于軟件或者程序執行,要預先將存在磁盤里的軟件或者程序加載到CPU上,而我們也可以將exec系列函數看作是一種特殊的加載器
五、實現簡易shell
shell視角執行:
shell讀取新的一行輸入,建立一個新的進程,在這個進程中運行程序并等待這個進程結束,再進行新的輸入讀取
注意:
對于shell來說作為命令行解釋器,執行命令需要將執行結果給用戶看到,這時候就需要子進程執行,讓子進程的結果返回,即父進程等待回收子進程
但是對于一些內建命令則需要shell自己執行,例如執行cd …返回上層目錄,我們希望的并不是子進程返回上層目錄,所以需要shell自己執行
具體流程:
獲取命令行
解析命令行
建立一個子進程(fork)
替換子進程(execvp)
父進程等待子進程退出(wait)
示圖:
注:根據這些思路,和我們前面的學的技術,就可以自己來實現一個shell了
實現代碼:
#include
效果:
Linux 任務調度
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。