在 UNIX 系統中,使用者建立一個新進程的唯一方法就是調用系統調用 fork。調 用 fork 的進程稱為父進程,而新建立的進程叫做子進程。系統調用的文法格式:
pid = fork();
在從系統調用 fork 中返回時,兩個進程除了傳回值 pid 不同外,具有完全一樣的使用者級上下文。在子進程中,pid 的值為零。在系統啟動時由核心內 部地建立的進程0是唯一不通過系統調用 fork 而建立的進程。
核心為系統調用 fork 完成下列操作:
- 為新進程在進程表中分配一個空項。
- 為子進程賦一個唯一的進程標識號 (PID)。
- 做一個父進程內容相關的邏輯副本。由於進程的某些部分,如本文區,可能被幾個 進程所共用,所以核心有時只要增加某個區的引用數即可,而不是真的將該區拷貝到一個 新的記憶體物理區。
- 增加與該進程相關聯的檔案表和索引節點表的引用數。
- 對父進程返回子進程的進程號,對子進程返回零。
理解系統調用 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次迴圈後退出。然後子進程因管道線沒有寫進程而讀到“檔案尾”標誌,並退出。
(http://docs.huihoo.com/gnu/linux/fork.html )
對fork函數的體會 進程的建立 (http://blog.csdn.net/binghuiliang/archive/2007/10/22/1836710.aspx)
建立一個進程的系統調用很簡單.我們只要調用fork函數就可以了。
#include <unistd.h>
pid_t fork();
當一個進程調用了fork以後,系統會建立一個子進程。這個子進程和父進程不同的地方只有他的進程ID和父進程ID,其他的都是一樣。就象符進程複製(clone)自己一樣。當然建立兩個一模一樣的進程是沒有意義的。為了區分父進程和子進程,我們必須跟蹤fork的傳回值。 當fork掉用失敗的時候(記憶體不足或者是使用者的最大進程數已到)fork返回-1,否則fork的傳回值有重要的作用。對於父進程fork返回子進程的ID,而對於子進程,fork返回0。我們就是根據這個傳回值來區分父子進程的。父進程為什麼要建立子進程呢?前面我們已經說過了Linux是一個多使用者作業系統,在同一時間會有許多的使用者在爭奪系統的資源。有時進程為了早一點完成任務就建立子進程來爭奪資源。 一旦子進程被建立,父子進程一起從fork處繼續執行,相互競爭系統的資源。有時候我們希望子進程繼續執行,而父進程阻塞,直到子進程完成任務。這個時候我們可以調用wait或者waitpid系統調用。
fork函數學習:(http://blog.csdn.net/saturnbj/archive/2009/06/19/4282639.aspx)
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
main ()
{
pid_t pid; pid=fork();
if (pid < 0)
printf("error in fork!");
else if (pid == 0)
printf("i am the child process, my process id is %dn",getpid());
else
printf("i am the parent process, my process id is %dn",getpid());
}
這段代碼寫了一個使用fork函數建立子進程,父子進程同時運行而產生交錯的,不一樣的運行結果。運行結果如下:
# ./a.out
i am the child process, my process id is 4286
i am the parent process, my process id is 4285
fork在英文中是叉子,分叉的意思,在函數fork中,取後面的意思。很形象的表示程式從這裡分叉,fork函數建立了子進程,子進程和父進程同時(其實是cpu分時處理)開始運行分叉之後的程式。
程式改寫下:
main()
{
pid_t pid;
printf("/n[%d]not fork pid=%d/n",getpid(),pid);
pid=fork();
printf("/n[%d]forked pid=%d/n",getpid(),pid);
if(pid<0)
{
printf("error in fork!/n");
getchar();
exit(1);
}
else if(pid==0)
printf("/n[%d]in child process,p_id=%d/n",getpid(),getpid());
else
{
printf("/n[%d]in parent process,my pid=%d/n",getpid(),pid);
printf("/n[%d]in parent process,my getpid=%d/n",getpid(),getpid());
}
}
程式運行結果如下:
$ ./fork
[3819]not fork
[3820]forked pid=0
[3820]in child process,p_id=3820
[3819]forked pid=3820
[3819]in parent process,my pid=3820
[3819]in parent process,my getpid=3819
可以清楚的看到 not fork只列印了一次,其中[3819]是父進程的進程號,建立fork以後,fork函數返回給父進程的值pid是子進程的進程號[3820],而在子進程中,pid值為零。也就是說子進程中,pid被置零。 引用網上一位網友的解釋“其實就相當於鏈表,進程形成了鏈表,父進程pid(p 意味point)指向子進程的進程id, 因為子進程沒有子進程,所以其pid為0. ”
一個很有意思的程式:
int main()
{
int i;
for( i= 0; i< 3; i++)
{
int pid= fork();
if(pid== 0)
{
printf("son/n");
}
else
{
printf("father/n");
}
}
return 0;
}
大家想想看最後將出現幾個son 幾個father呢?。。。。。。。
對一下答案吧:
$ ./fork
father
son
son
son
father
father
son
father
son
son
father
father
son
father
總共7個son7個father。你答對了嗎? 這道題需要在紙上畫畫才好理解 for i=0 1 2 father father father son son father son son father father son son father son 其中每一行分別代表一個進程的運行列印結果。當產生子進程的時刻,子進程列印son,當子進程調用fork的產生子子進程,他就提升為father。總結來說,father永遠列印father,son在fork之前是son,fork之後就為father,同時產生新的son。