目錄
3.2.3 在命令行執行./a.out,程序是如何運行起來的
6.2.3 從進程終止狀態中提取進程終止的原因、返回值或者信號編號
1. 有關進程
1.1 什麼是進程
1.2進程ID(PID)
OS爲了能夠更好地管理進程,爲每個進程分配了一個唯一的編號(非負整數),這個編號就是PID
- 如果當前進程結束了,這個PID可以被可以被重複使用,但是所有“活着”的進程,它們的進程ID一定都是唯一的。
- 因爲ID唯一性,當我們想創建一個名字唯一的文件時,往往可以在文件名中加入PID,這樣就能保證文件名唯一性。
1.3 三個特殊的進程
0、1、2這個三個進程,是OS啓動起來後會一直默默運行的進程,直到關機OS結束運行
進程 ID == 0 的進程
進程ID == 1的進程
進程ID == 2的進程
1.4獲取與進程相關的各種ID的函數
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); pid_t getppid(void); uid_t getuid(void); gid_t getgid(void);
2. 程序的運行過程
2.1 程序如何運行起來
(1)在內存中劃出一片內存空間
(2)將硬盤上可執行文件中的代碼(機器指令)拷貝到劃出的內存空間中
(3)pc指向第一條指令,cpu取指運行在Linux下,OS提供兩個非常關鍵的API,一個是fork,另一個是exec。
fork :開闢出一塊內存空間
exec:將程序代碼(機器指令)拷貝到開闢的內存空間中,並讓pc指向第一條指令,CPU開始執行,進程就運行起來了
運行起來的進程會與其它的進程切換着併發運行。
2.2 fork
2.2.1 函數原型
#include <unistd.h> pid_t fork(void);
2.2.2複製的原理
Linux有虛擬內存機制,所以父進程是運行在虛擬內存上的,虛擬內存是OS通過數據結構基於物理內存模擬出來的,因此底層的對應的還是物理內存
複製時子進程時,會複製父進程的虛擬內存數據結構,那麼就得到了子進程的虛擬內存,相應的底層會對應着一片新的物理內存空間,裏面放了與父進程一模一樣代碼和數據,
2.2.3 父子進程各自會執行哪些代碼
驗證子進程複製了父進程的代碼和數據
printf("befor fork"); 在父子進程中都輸出 因爲沒加換行符 所以 在子進程中遇見換行緩衝區被刷新到屏幕
2.3 父子進程共享操作文件
(1)情況1:獨立打開文件
獨立打開同一文件時,父子進程各自的文件描述符,指向的是不同的文件表。
因爲擁有不同的文件表,所以他們擁有各自獨立的文件讀寫位置,會出現相互覆蓋情況
如果不想相互覆蓋需要加O_APPEND標誌。
(2)情況2:fork之前打開文件
子進程會繼承父進程已經打開的文件描述符,
如果父進程的3描述符指向了某個文件,子進程所繼承的文件描述符3也會指向這個文件。
由於共享的是相同的文件表,所以擁有共同的文件讀寫位置,不會出現覆蓋的情況。
子進程的0 1 2這三個打開的文件描述符,其實也是從父進程那裏繼承過來的,並不是子進程自己去打開的
同樣的父進程的 1 2又是從它的父進程那裏繼承過來的,最根溯源的話,都是從最原始的進程哪裏繼承過來的
2.4 子進程會繼承父進程的哪些屬性
2.4.1 子進程繼承如下性質
- (1)用戶ID,用戶組ID
- (2)進程組ID
- (3)會話期ID
- (4)控制終端
- (5)當前工作目錄
- (6)根目錄
- (7)文件創建方式屏蔽字
- (8)環境變量
- (9)打開的文件描述符
- 等等
2.4.2 子進程獨立的屬性
- (1)進程ID。
- (2)不同的父進程ID。
- (3)父進程設置的鎖,子進程不能被繼承。
- 等等
3. exec加載器
3.1 exec的作用
父進程fork複製出子進程的內存空間後,子進程內存空間的代碼和數據和父進程是相同的,這樣沒有太大的意義,我們需要在子進 程空間裏面運行全新的代碼,這樣纔有意義。
有了exec後,我們可以單獨的另寫一個程序,將其編譯好後,使用exec來加載即可。
3.2 exec函數族
exec的函數有很多個,它們分別是execve、execl、execv、execle、execlp、execvp,都是加載函數。
其中execve是系統函數,其它的execl、execv、execle、execlp、execvp都是基於execve封裝得到的庫函數,
3.2.1 execve函數原型
#include <unistd.h> int execve(const char *filename, char **const argv, char **const envp);
3.2.2 exec的作用
將新程序代碼加載(拷貝)到子進程的內存空間,替換掉原有的與父進程一模一樣的代碼和數據,讓子進程空間運行全新的程序。
3.2.3 在命令行執行./a.out,程序是如何運行起來的
(1)窗口進程先fork出子進程空間
(2)調用exec函數加載./a.out程序,並把命令行參數和環境變量表傳遞給新程序的main函數的形參
3.2.4 雙擊快捷圖標,程序是怎麼運行起來的
(1)圖形界面進程fork出子進程空間
(2)調用exec函數,加載快捷圖標所指向程序的代碼
以圖形界面方式運行時,就沒有命令行參數了,但是會傳遞環境變量表
4. system函數
如果我們需要創建一個進子進程,讓子進程運行另一個程序的話,可以自己fork、execve來實現,但是這樣的操作很麻煩
所以就有了system這個庫函數,這函數封裝了fork和execve函數
調用時會自動的創建子進程空間,並把新程序的代碼加載到子進程空間中,然後運行起來。
#include <stdlib.h> int system(const char *command);
(1)功能:創建子進程,並加載新程序到子進程空間,運行起來。
(2)參數:新程序的路徑名
5. 回收進程資源
進程運行終止後,不管進程是正常終止還是異常終止的,必須回收進程所佔用的資源。
5.1 爲什麼要回收進程的資源?
(1)程序代碼在內存中動態運行起來後,纔有了進程,進程既然結束了,就需要將代碼佔用的內存空間讓出來(釋放)。
(2)OS爲了管理進程,爲每個進程在內存中開闢了一個task_stuct結構體變量,進程結束了,那麼這個結構體所佔用的內存空間也需要被釋放。
(3)等其它資源
5.2 由誰來回收進程資源
由父進程來回收,父進程運行結束時,會負責釋放子進程資源。
- R 正在運行
- S 處於休眠狀態
- Z 殭屍進程,進程運行完了,等待被回收資源
5.3 殭屍進程和孤兒進程
ps查看到的進程狀態
5.3.1 殭屍進程
子進程終止了,但是父進程還活着,父進程在沒有回收子進程資源之前,子進程就是殭屍進程
爲什麼子進程會變成殭屍進程?
子進程已經終止不再運行,但是父進程還在運行,它沒有釋放子進程佔用的資源,所以就變成了佔着不拉屎殭屍進程。
就好比人死後不腐爛,身體佔用的資源得不到回收是一樣的,像這種情況就是所謂的殭屍。
5.3.2 孤兒進程
沒爹沒媽的孩子就是孤兒,子進程活着,但是父進程終止了,子進程就是孤兒進程。
爲了能夠回收孤進程終止後的資源,孤兒進程會被託管給我們前面介紹的pid==1的init進程
每當被託管的子進程終止時,init會立即主動回收孤兒進程資源
回收資源的速度很快所以孤兒進程沒有變成殭屍進程的機會。
6. wait函數
作用:父進程調用這個函數的功能有兩個
- (1)主動獲取子進程的“進程終止狀態”。
- (2)主動回收子進程終止後所佔用的資源。
wait函數,在實際開發中用的很少
6.1 進程的終止
6.1.1 正常終止
(1)main調用return
(2)任意位置調用exit
(3)任意位置調用_exit
不管哪種方式來正常終止,最終都是通過_exit返回到OS內核的。
6.1.2 異常終止
如果是被某個信號終止的,就是異常終止。
(1)自殺:自己調用abort函數,自己給自己發一個SIGABRT信號將自己殺死。
(2)他殺:由別人發一個信號,將其殺死。
6.1.3 進程終止狀態
(1)退出狀態與“進程終止狀態”
return、exit、_exit的返回值稱爲“進程終止狀態”,嚴格來說應該叫“退出狀態”,
return(退出狀態)、exit(退出狀態)或_exit(退出狀態)當退出狀態被_exit函數交給OS內核,OS對其進行加工之後得到的纔是“進程終止狀態”,父進程調用wait函數便可以得到這個“進程終止狀態”。
(2)OS是怎麼加工的?
1)正常終止
進程終止狀態 = 終止原因(正常終止)<< 8 | 退出狀態的低8位
2)異常終止
進程終止狀態 = 是否產生core文件位 | 終止原因(異常終止)<< 8 | 終止該進程的信號編號
(3)父進程調用wait函數,得到“進程終止狀態”有什麼用
父進程得到進程終止狀態後,就可以判斷子進程終止的原因是什麼,如果是正常終止的,可以提取出返回值,如果是異常終止的,可以提取出異常終止進程的信號編號。
當有OS支持時,進程return、exit、_exit正常終止時,所返回的返回值(退出狀態),最終通過“進程終止狀態”返回給了父進程。
這有什麼用,比如,父進程可以根據子進程的終止狀態來判斷子進程的終止原因,返回值等等,以決定是否重新啓動子進程, 或則做一些其它的操作,不過一般來說,子進程的終止狀態對父進程並沒有太大意義。
6.2 父進程如何從內核獲取子終止狀態
6.2.1 如何獲取
(1)父進程調用wait等子進程結束,如果子進程沒有結束的話,父進程調用wait時會一直休眠的等(或者說阻塞的等)。
(2)子進程終止返回內核,內核構建“進程終止狀態”
(3)內核向父進程發送SIGCHLD信號,通知父進程子進程結束了,你可以獲取子進程的“進程終止狀態”了。
6.2.2 wait函數原型
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
(1)功能:獲取子進程的終止狀態,主動釋放子進程佔用的資源
(2)參數:用於存放“進程終止狀態”的緩存(3)返回值:成功返回子進程的PID,失敗返回-1,errno被設置。
6.2.3 從進程終止狀態中提取進程終止的原因、返回值或者信號編號
(1)進程狀態中所包含的信息
(2)如何提取裏面的信息
系統提供了相應的帶參宏,使用這個帶參宏就可以從“進程終止狀態”中提取出我們要的信息。
提取原理:相應屏蔽字&進程終止狀態,屏蔽掉不需要的內容,留下的就是你要的信息。
(3)wait的缺點
如果父進程fork創建出了好多子進程,wait只能獲取最先終止的那個子進程的“終止”狀態,其它的將無法獲取,
如果你想獲取所有子進程終止狀態,或者只想獲取指定子進程的進程終止狀態,需要使用wait的兄弟函數waitpid,
它們的原理是相似的
6. 進程狀態
每個進程與其它進程併發運行時,該進程會在不同的“進程狀態”之間進行轉換
7. java進程
如何運行編譯型和解釋型語言的程序
(1)java程序的運行
1)父進程(命令行窗口、圖形界面)會fork複製出子進程空間
2)調用exec加載java虛擬機程序,將虛擬機程序的代碼拷貝到子進程空間中
其實最簡單的理解就是,java虛擬機就代表了java進程。
當你運行另一個java程序時,又會自動地啓動一個虛擬機程序來解釋java字節碼,此時另一個java進程又誕生了。
也就是說你執行多少個java進程,就會運行多少個java虛擬機,當然java虛擬機程序在硬盤上只有一份,只不過被多次啓動而已。
(2)java虛擬機怎麼得到
當我們運行java程序時,虛擬機會被自動啓動。
虛擬機一般是運行在OS上的,不過其實虛擬機也可以運行在沒有OS的裸機上
(3)在java程序裏面,也可以調用java庫提供的類似的fork和exec函數,我們自己來創建一個java子進程,並執行新程
java庫提供的類似的fork、exec函數,下層也是調用OS的fork、exec函數。
8.進程關係
進程間的關係,大致有三種,即父子關係、進程組關係、會話期關係。
8.1 父子關係
已有進程調用fork創建出一個新的進程,那麼這兩個進程之間就是父子進程關係,子進程會繼承和父進程的屬性。
8.2 進程組
8.2.1 什麼是進程組
多個進程可以在一起組成一個進程組,其中某個進程會擔任組長,組長進程的pid就是整個進程組的組ID。
8.2.2 進程組的生命週期
就算進程組的組長終止了,只要進程中還有一個進程存在,這個進程組就存在。
8.3 會話期關係
多個進程組在一起,就組成了會話期。
9. 守護進程
守護進程也被稱爲精靈進程。