進(jìn)程是Unix系統(tǒng)中僅次于文件的基本抽象概念琳猫。當(dāng)目標(biāo)代碼執(zhí)行的時候,進(jìn)程不僅僅包括匯編代碼私痹,它由數(shù)據(jù)脐嫂、資源、狀態(tài)和一個虛擬的計算機(jī)組成紊遵。
-
進(jìn)程ID
每個進(jìn)程都由一個唯一的標(biāo)識符表示账千,即進(jìn)程ID,簡稱pid暗膜。系統(tǒng)保證在某時刻每個pid是唯一的匀奏。
空閑進(jìn)程(idle process)——當(dāng)沒有其他進(jìn)程在運行時,內(nèi)核所運行的進(jìn)程——它的pid是0学搜。在啟動后娃善,內(nèi)核運行的第一個進(jìn)程稱為init進(jìn)程,它的pid是1瑞佩。 -
分配進(jìn)程ID
缺省情況下聚磺,內(nèi)核將進(jìn)程ID的最大值限制為32768(和老Unix系統(tǒng)兼容,16位來表示進(jìn)程ID)炬丸。
內(nèi)核分配進(jìn)程ID是以嚴(yán)格的線性函數(shù)的方式進(jìn)行的瘫寝, 一直遞增,直到分配的pid達(dá)到了/proc/sys/kernel/pid_max稠炬,內(nèi)核是不會重用以前已分配過的值焕阿。
Linux分配pid的方式在短期內(nèi)至少是穩(wěn)定的和并保證了pid值的唯一性。 -
進(jìn)程體系
創(chuàng)建新進(jìn)程的那個進(jìn)程叫父進(jìn)程酸纲,而新進(jìn)程被稱為子進(jìn)程捣鲸。每個進(jìn)程都是由其他進(jìn)程創(chuàng)建的(除了init進(jìn)程),因此每個子進(jìn)程都有一個父進(jìn)程闽坡,這個關(guān)系保存在每個進(jìn)程的父進(jìn)程ID號(ppid)中栽惶。
每個進(jìn)程都被一個用戶和組擁有。每個子進(jìn)程都繼承了父進(jìn)程的用戶和組疾嗅。
每個進(jìn)程都是某個進(jìn)程組的一部分外厂,它簡單的表明了自己和其他進(jìn)程之間的關(guān)系。子進(jìn)程通常屬于其父進(jìn)程所在的那個進(jìn)程組代承。 -
獲得進(jìn)程ID和父進(jìn)程的ID
getpid( )返回調(diào)用進(jìn)程的ID:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
getppid( )返回調(diào)用進(jìn)程的父進(jìn)程的ID:
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
一般把pid_t當(dāng)成int形來printf汁蝶。
運行新進(jìn)程
在unix中,載入內(nèi)存并執(zhí)行程序映像的操作與創(chuàng)建一個新進(jìn)程是分離的。
運行一個新進(jìn)程:將二進(jìn)制文件的程序映像載入內(nèi)存掖棉,替換原先進(jìn)程的地址空間墓律,并開始運行它,該系統(tǒng)調(diào)用為exec系統(tǒng)調(diào)用(實際上是一系列的系統(tǒng)調(diào)用)幔亥。
創(chuàng)建一個新的進(jìn)程:基本上就是復(fù)制父進(jìn)程耻讽。通常情況下新的進(jìn)程會立刻執(zhí)行一個新的程序。完成創(chuàng)建新進(jìn)程的這種行為叫做派生(fork)帕棉,完成這個功能的系統(tǒng)調(diào)用就是fork( )针肥。
exec系列系統(tǒng)調(diào)用
沒有單一的exec系統(tǒng)調(diào)用,它們由基于單個系統(tǒng)調(diào)用的一組exec函數(shù)構(gòu)成香伴。
- execl( ):
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
對execl( )的調(diào)用會將path所指路徑的映像載入內(nèi)存慰枕,替換當(dāng)前進(jìn)程的映像。它的參數(shù)列表是可變長度的即纲,但參數(shù)列表必須是以NULL結(jié)尾的具帮。
例如,下面的代碼會用/bin/vi替換當(dāng)前運行的程序:
int ret;
ret = execl("/bin/vi", "vi", NULL);
if (ret == -1)
perror("execl");
當(dāng)fork/exec進(jìn)程時崇裁,shell會把path的后一個成分匕坯,即本例中的"vi",放入新進(jìn)程的第一個參數(shù)argv[0]拔稳。這樣一個程序就可以檢測argv[0]葛峻,從而得知二進(jìn)制映像文件的名字。
很多情況下术奖,用戶會看到一些系統(tǒng)工具有不同的名字,實際上這些名字都是指向同一個程序的硬連接轻绞。所以程序需要第一個參數(shù)來決定它的具體行為采记。
如果你想要編輯/home/kidd/hooks.txt,執(zhí)行以下代碼:
int ret;
ret = execl("/bin/vi", "vi", "/home/kidd/hooks.txt", NULL);
if (ret == -1)
perror("execl");
execl( )成功調(diào)用的話不會返回政勃,而是以跳到新的程序的入口點為結(jié)束唧龄,而剛剛才被運行的代碼是不會存在于進(jìn)程的地址空間中的。
execl( )成功的調(diào)用不僅僅改變了地址空間和進(jìn)程的映像奸远,還改變了進(jìn)程的一些屬性:
- 任何掛起的信號都會丟失
- 捕捉的任何信號會還原為缺省的處理方式既棺,因為信號處理函數(shù)已經(jīng)不存在于地址空間中了
- 任何內(nèi)存的鎖定會丟失
- 多數(shù)線程的屬性會還原到缺省值
- 多數(shù)關(guān)于進(jìn)程的統(tǒng)計信息會復(fù)位
- 與進(jìn)程內(nèi)存相關(guān)的任何數(shù)據(jù)都會丟失,包括映射的文件
- 包括C語言庫的一些特征(例如atexit( ))等獨立存在于用戶空間的數(shù)據(jù)都會丟失
然而也有很多進(jìn)程的屬性沒有改變懒叛,例如pid丸冕、ppid、優(yōu)先級薛窥、所屬的用戶和組胖烛。
-
其他exec系列系統(tǒng)調(diào)用
除了execl( )外眼姐,還有其他五個系統(tǒng)調(diào)用:
#include <unistd.h>
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[ ]);
int execv(const char *path, char *const argv[ ]);
int evecvp(const char *file, char *const argv[ ]);
int execve(const char *filename, char *const argv[ ], char *const envp[ ]);
- l:列表方式 v:數(shù)組(向量)方式提供參數(shù)
- p:在用戶的PATH環(huán)境變量中尋找可執(zhí)行文件,只要出現(xiàn)在用戶的路徑中佩番,帶p的exec函數(shù)可以簡單的只提供文件名众旗。帶p的exec函數(shù)主要用于shell的,因為在shell所執(zhí)行的進(jìn)程通常會從shell繼承環(huán)境變量趟畏。
- e:表示會提供給新進(jìn)程以新的環(huán)境變量逝钥。
- 除了需要構(gòu)造一個數(shù)組并用它代替列表作為參數(shù)傳遞以外,使用數(shù)組作為參數(shù)的exec函數(shù)基本上沒什么區(qū)別拱镐。使用數(shù)組可以在運行時動態(tài)的構(gòu)造參數(shù),且數(shù)組也必須以NULL結(jié)尾持际。
用execvp( )來執(zhí)行vi的例子:
const char *args[ ] = {"vi", "/home/kidd/hooks.txt", NULL};
int ret;
/* 這里假設(shè)/bin在用戶的路徑中 */
ret = execvp("vi", args);
if (ret == -1)
perror ("evecvp");
fork( )系統(tǒng)調(diào)用
創(chuàng)建一個和當(dāng)前進(jìn)程映像一樣的進(jìn)程可以通過fork( )系統(tǒng)調(diào)用:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
成功調(diào)用fork( )會創(chuàng)建一個新的進(jìn)程沃琅,它幾乎與調(diào)用fork( )的進(jìn)程一模一樣。這兩個進(jìn)程都會繼續(xù)運行蜘欲。
父進(jìn)程和子進(jìn)程在每個方面都非常相近:
- 子進(jìn)程的pid是新分配的益眉,它是與父進(jìn)程不同的
- 子進(jìn)程的ppid會設(shè)置為父進(jìn)程的pid
- 子進(jìn)程中的資源統(tǒng)計信息會清零
- 任何掛起的信號都會清除,也不會被子進(jìn)程繼承
- 任何文件鎖都不會被子進(jìn)程所繼承
fork( )的用法如下:
pid_t pid;
pid = fork( );
if (pid > 0)
/* 在父進(jìn)程中fork( )返回子進(jìn)程的pid */
printf("I am the parent of pid = %d \n", pid);
else if (!pid)
/* 成功調(diào)用時會返回0 */
printf("I am the baby! \n");
else if (pid == -1)
/* 錯誤時返回-1 */
perror("fork");
最常見的fork( )用法是創(chuàng)建一個新的進(jìn)程姥份,然后載入二進(jìn)制映像郭脂,這種派生加執(zhí)行的方式是很常見的。下面的例子創(chuàng)建了一個新的進(jìn)程來運行/bin/windlass:
pid_t pid;
pid = fork( );
if (pid == -1)
perror("fork");
/* the child ... */
if (!pid) {
const char *args[] = {"windlass", NULL};
int ret;
ret = execv("/bin/windlass", args);
if (ret == -1) {
perror("execv");
exit(EXIT_FAILURE);
}
}
-
寫時復(fù)制:
早期的Unix系統(tǒng)中澈歉,fork時會把所有的內(nèi)部數(shù)據(jù)結(jié)構(gòu)復(fù)制一份展鸡,復(fù)制進(jìn)程的頁表項,然后把父進(jìn)程的地址空間中的內(nèi)容逐頁的復(fù)制到子進(jìn)程的地址空間中埃难。這樣是十分耗時且不必要的。
現(xiàn)代Unix系統(tǒng)采用寫時復(fù)制COW的方法。
如果多個進(jìn)程要讀取它們自己的那部分資源的副本年缎,那么每個進(jìn)程只需要保存一個指向這個資源的指針就可以了歼争。如果一個進(jìn)程要“修改”自己的那份資源“副本”,那么就會復(fù)制那份資源考抄,并把復(fù)制的那份提供給進(jìn)程细疚。這就是寫入時復(fù)制。
子進(jìn)程們共享父進(jìn)程的原始頁川梅,接下來這些頁又可以被其他的父進(jìn)程或者子進(jìn)程共享疯兼。
寫時復(fù)制在內(nèi)核中的實現(xiàn)很簡單,要修改時挑势,產(chǎn)生缺頁中斷镇防,內(nèi)核處理缺頁中斷的方式就是對該頁進(jìn)行一次透明復(fù)制,這時會清楚頁面的COW屬性潮饱,表示著它不再被共享来氧。
現(xiàn)在的計算機(jī)結(jié)構(gòu)體系中都在內(nèi)存管理單元MMU提供了硬件級別的COW支持,所以實現(xiàn)是很容易的。