在 UNIX 系統中,使用者建立一個新進程的唯一方法就是調用系統調用
fork。調用
fork 的進程稱為
父進程,而新建立的進程叫做
子進程。系統調用的文法格式:
pid = fork(); 在從系統調用
fork 中返回時,兩個進程除了傳回值
pid 不同外,具有 完全一樣的使用者級上下文。在子進程中,
pid 的值為零。在系統啟動時由核心內 部地建立的進程0是唯一不通過系統調用
fork 而建立的進程。核心為系統調用
fork 完成下列操作:1. 為新進程在進程表中分配一個空項。2. 為子進程賦一個唯一的進程標識號 (
PID)。
3. 做一個父進程內容相關的邏輯副本。由於進程的某些部分,如本文區,可能被幾個進程所共用,所以核心有時只要增加某個區的引用數即可,而不是真的將該區拷貝到一個 新的記憶體物理區。4. 增加與該進程相關聯的檔案表和索引節點表的引用數。5. 對父進程返回子進程的進程號,對子進程返回零。理解系統調用
fork 的實現是十分重要的,因為子進程就象從天而降一樣地開始 它的執行序列。下面是系統調用
fork 的演算法。核心首先確信有足夠的資源來成功完成
fork。 如果資源不滿足要求,則系統調用
fork 失敗。如果資源滿足要求,核心在進程 表中找一個空項,並開始構造子進程的上下文。
演算法:fork輸入:無輸出:對父進程是子進程的 PID 對子進程是0{ 檢查可用的核心資源 取一個閒置進程表項和唯一的 P識別碼 檢查使用者沒有過多的運行進程 將子進程的狀態設定為“建立”狀態 將父進程的進程表中的資料拷貝到子進程表中 目前的目錄的索引節點和改變的根目錄(如果可以)的引用數加1 檔案表中的開啟檔案的引用數加1 在記憶體中作父進程內容相關的拷貝 在子進程的系統級上下文中壓入虛設系統級上下文層 /* 虛設上下文層中含有使子進程能 * 識別自己的資料,並使子進程被調度時 * 從這裡開始運行 */
if (正在執行的進程是父進程) { 將子進程的狀態設定為“就緒”狀態 return (子進程的 PID) // 從系統到使用者 }
else { 初始化計時區 return 0; }}我們來看看下面的例子。該程式說明的是經過系統調用
fork 之後,對檔案的 共用存取。使用者調用該程式時應有兩個參數,一個是已經有的檔案名稱,另外一個是要 建立的新檔案名稱。該進程開啟已有的檔案,建立一個新檔案,然後,假定沒有遇見過錯誤,它調用
fork 來建立一個子進程。子進程可以通過使用相同的檔案描述符而繼承地存取父進程的檔案(即父進程已經開啟和建立的檔案)。當然,父進程和子進程要分別獨立地調用
rdwrt 函數,並執行一個迴圈,即從 源檔案中讀一個位元組,然後寫一個位元組到目標檔案中區。當系統調用
read 遇見 檔案尾時,函數
rdwrt 立即返回。 #include <fcntl.h> int fdrd, fdwt;char c; main(int argc, char *argv[]){ if (argc != 3) { exit(1); } if ((fdrd = open(argv[1], O_RDONLY)) == -1) { exit(1); } if ((fdwt = creat(argv[2], 0666)) == -1) { exit(1); } fork(); // 兩個進程執行同樣的代碼 rdwrt(); exit(0);} rdwrt(){ for (;;) { if (read(fdrd, &c, 1) != 1) { return ; } write(fdwt, &c, 1); }}在這個例子中,兩個進程的檔案描述符都指向相同的檔案表項。這兩個進程永遠 不會讀或寫到相同的檔案位移
量,因為核心在每次
read 和
write 調用 之後,都要增加檔案的位移量。儘管兩個進程似乎是將源檔案拷貝了
兩次,但因為他們分擔了工作任務,因此,目標檔案的內容依賴於核心調度兩個進程的次序。
如果 核心這樣調度兩個進程:使他們交替地執行他們的系統調用,或甚至使他們交替地 執行每對 read 和 write 調用,則目標檔案的內容和源檔案的內容完全一致。
但考慮這樣的情況:兩個進程正要讀源檔案中的兩個連續的字元 "ab"。假定父進程讀了字 符 "a",這時,核心在父進程寫之前,做了環境切換來執行子進程。
如果子進程 讀到字元 "b",並在父進程被調度前,將它寫到目標檔案,那麼目標檔案將不再含有 字串 "ab",而是含有 "ba"了。核心並不保證進程執行的相對速率。
再來看看另外一個例子: #include <string.h> char string[] = "Hello, world"; main(){ int count, i; int to_par[2], to_chil[2]; // 到父、子進程的管道 char buf[256]; pipe(to_par); pipe(to_chil); if (fork() == 0) { // 子進程在此執行 close(0); // 關閉老的標準輸入 dup(to_child[0]); // 將管道的讀複製到標準輸入 close(1); // 關閉老的標準輸出 dup(to_par[1]); // 將管道的寫複製到標準輸出 close(to_par[1]); // 關閉不必要的管道描述符 close(to_chil[0]); close(to_par[0]); close(to_chil[1]); for (;;) { if ((count = read(0, buf, sizeof(buf)) == 0) exit(); write(1, buf, count); } } // 父進程在此執行 close(1); // 重新設定標準輸入、輸出 dup(to_chil[1]); close(0); dup(to_par[0]); close(to_chil[1]); close(to_par[0]); close(to_chil[0]); close(to_par[1]); for (i = 0; i < 15; i++) { write(1, string, strlen(string)); read(0, buf, sizeof(buf)); }}子進程從父進程繼承了檔案描述符0和1(標準輸入和標準輸出)。
兩次執行系統調用
pipe 分別在數組 to_par 和 to_chil 中分配了兩個檔案描述符。然後該進程 執行系統調用
fork,並複製進程上下文:象前一個例子一樣,每個進程存取 自己的私人資料。
父進程關閉他的標準輸出檔案(檔案描述符1),並複製(dup)從管道線 to_chil 返回的寫檔案描述符。因為在父進程檔案描述符表中的第一個空槽是剛剛 由關閉騰出來的,所以核心將管道線寫檔案描述符複製到了檔案描述符表中的第一項中,這樣,標準輸出檔案描述符變成了管道線 to_chil 的寫檔案描述符。
父進程以類似的操作將標準輸入檔案描述符替換為管道線 to_par 的讀檔案 描述符。與此類似,子進程關閉他的標準輸入檔案(檔案描述符0),然後複製 (dup) 管道 線 to_chil 的讀檔案描述符。
由於檔案描述符表的第一個空項是原先的標準輸入項,所以子進程的標準輸入變成了管道線 to_chil 的讀檔案描述符。子進程做一組類似的操作使他的標準輸出變成管道線 to_par 的寫檔案描述符。
然後兩個進程關閉從 pipe 返回的檔案描述符。上述操作的結果是:當父進程向標準輸出寫東西的時候,他實際上是寫向 to_chil--向子進程發送資料,而子進程則從他的標準輸入讀管道線。當子進程向他的標準輸出寫的時候, 他實際上是寫入 to_par--向父進程發送資料,而父進程則從他的標準輸入 接收來自管道線的資料。兩個進程通過兩條管道線交換訊息。無論兩個進程執行的順序如何,這個程式執行的結果是不變的。他們可能去執行睡眠和喚醒來等待對方。父進程在15次迴圈後退出。然後子進程因管道線沒有寫進程而讀 到“檔案尾”標誌,並退出。
上一篇:《【linux 編程】BSD Socket 簡易入門手冊》相關文檔:《解析Linux核心擷取當前進程指標的方法》
下一篇:《Linux 核心級後門的原理和簡單實戰》