UNIX再学习 -- 函数 fork 和 vfork
生活随笔
收集整理的這篇文章主要介紹了
UNIX再学习 -- 函数 fork 和 vfork
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
一、進程標識
每個進程都有一個非負整數形式的唯一編號,即 PID。PID 在任何時刻都是唯一的,但是可以重用,當進程終止并被回收以后,其 PID 就可以為其它進程所用。進程的 PID 由系統內核根據延遲重用算法生成,以確保新進程的 PID 不同于最近終止進程的 PID。?1、系統中有些 PID 是專用的
(1)0 號進程,調度進程
通常是調度進程,常常被稱為交換進程(swapper)。該進程是內核的一部分,所有進程的根進程,它并不執行任何磁盤上的程序,因此也被稱為系統進程。(2)1 號進程,init進程
通常是 init 進程,在自舉過程結束時由內核調用。該進程的程序文件在 UNIX 的早起版本中是 /etc/init,在較新版本中是 /sbin/init。此進程負責在自舉內核后啟動一個 UNIX 系統。init 通常讀取與系統有關的初始化文件(/etc/rc*文件或 /etc/inittab 文件,以及在 /etc/init.d 中的文件),并將系統引導到一個狀態(如多用戶)。init 進程決不會終止。它是一個普通的用戶進程(與交換進程不同,它不是內核中的系統進程),但是它以超級用戶特權運行。(3)2號進程,頁守護進程
負責虛擬內存系統的分頁操作(4)其他
除調度進程以外,系統中的每個進程都有唯一的父進程,對于一個子進程而言,其父進程的 PID 即是它的 PPID。 進程 0 是系統內部的進程,它負責啟動進程 1 (inti),也會啟動進程 2 ,而其他所有的進程都是進程 1 / 進程 2 直接/間接 地啟動起來。?2、獲取進程 ID 函數
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); 返回:調用進程的進程 ID pid_t getppid(void); 返回:調用進程的父進程 ID uid_t getuid(void); 返回:調用進程的實際用戶 ID uid_t geteuid(void); 返回:調用進程的有效用戶 ID gid_t getgid(void); 返回:調用進程的實際組 ID gid_t getegid(void); 返回:調用進程的有效組 ID 注意,這些函數都沒有出錯返回(1)示例說明
#include <stdio.h> #include <unistd.h> #include <sys/types.h>int main (void) {printf ("pid = %d\n", getpid ());printf ("ppid = %d\n", getppid ());printf ("uid = %d\n", getuid ());printf ("euid = %d\n", geteuid ());printf ("gid = %d\n", getgid ());printf ("egid = %d\n", getegid ());return 0; } 輸出結果: pid = 3028 ppid = 2808 uid = 0 euid = 0 gid = 0 egid = 0 //每次執行結果都不一定相同二、函數 fork
#include <unistd.h> pid_t fork(void);1、函數功能
主要用于以復制正在調用進程的方式去創建一個新的進程,新進程叫做子進程,原來的進程叫做父進程,2、返回值
成功時父進程返回子進程的 ID,子進程返回 0,失敗返回 -13、創建新進程
由 fork 創建的新進程被稱為子進程。fork 函數調用一次,但返回兩次。 兩次返回的區別是:子進程的返回值是 0,而父進程的返回值則是新建子進程的進程 ID。 將新建子進程 ID 返回給父進程的理由:因為一個進程的子進程可以有多個,并且沒有一個函數使一個進程可以獲得其所有子進程的 ID。 fork 使子進程得到返回值 0 的理由:一個進程只會有一個父進程,所以子進程總是可以調用 getppid 以獲得其父進程的進程 ID(進程 ID 0 總是由內核交換進程使用,所以一個子進程的進程 ID 不可能為 0)。 子進程和父進程繼續執行 fork 調用之后的指令。子進程是父進程的不完全副本。子進程的數據區、bbs區、堆棧區(包括 I/O 流緩沖區),甚至參數和環境區都從父進程拷貝,唯有代碼區與父進程共享。(1)示例說明:
參看:fork()函數詳解
1》》創建新進程
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int n = 10; //數據段 int m; //bbs段 const int i = 10; //代碼段 int main (void) {pid_t pid;int cnt = 0; //棧區pid = fork ();if (pid == -1)perror ("fail to fork"), exit (1);else if (pid == 0){printf("The returned value is %d\nIn child process!!\nMy PID is %d\n",pid, getpid());cnt++;n++;m++;}else{sleep (3); //可以保證子進程先被調度printf("The returned value is %d\nIn father process!!\nMy PID is %d\n",pid, getpid());cnt++;n++;m++;}printf ("cnt = %d, n = %d, m = %d, i = %d\n", cnt, n, m, i);return 0; } 輸出結果: The returned value is 0 In child process!! My PID is 3010 cnt = 1, n = 11, m = 1, i = 10 The returned value is 3010 In father process!! My PID is 3009 cnt = 1, n = 11, m = 1, i = 102》》示例解析
上例很好的說明了,fork 函數調用一次,返回兩次。在子進程中返回 0, 父進程中返回子進程 ID,錯誤返回 -1。 子進程是父進程的不完全副本。子進程的數據區、bbs區、堆棧區(包括 I/O 流緩沖區),甚至參數和環境區都從父進程拷貝,唯有代碼區與父進程共享。 ? 因為,代碼區是可執行指令 、字面值常量 、具有常屬性且被 初始化的全局、靜態全局 和 靜態局部變量。 再有我在父進程使用了 sleep (3); 來確保子進程先調度(但是有時不一定保證 3 秒已經足夠)。因為,一般來說,在 fork 之后是父進程先執行還是子進程先執行是不確定的,這取決于內核所使用的調度算法。如果要求父進程和子進程之間相互同步,則要求某種形式的進程間通信,后續會講這些。4、文件共享
上例中如果將可執行文件重定向: # ./a.out > test.out # cat test.out The returned value is 0 In child process!! My PID is 3047 cnt = 1, n = 11, m = 1, i = 10 The returned value is 3047 In father process!! My PID is 3046 cnt = 1, n = 11, m = 1, i = 10 可發現,在重定向父進程的標準輸出時,子進程的標準輸出也被重定向了。 實際上,fork 的一個特性是進程的所有打開文件描述符都被復制到子進程中。我們說的“復制”是因為對每個文件描述符來說,就好像執行了 dup 函數。父進程和子進程每個相同的打開描述符共享一個文件表項。 重要的一點是,父進程和子進程共性同一個文件偏移量。(1)示例說明
1》》父子進程共享文件表
#include <stdio.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h> #include <errno.h> #include <stdbool.h>#define BUFSIZE 5*5//定義函數lock設置寫鎖 bool lock(int fd) {struct flock lock;lock.l_type = F_WRLCK;lock.l_whence = SEEK_SET;lock.l_start = 0;lock.l_len = 0;lock.l_pid = -1;if (fcntl (fd, F_SETLK, &lock) == -1){if (errno != EAGAIN){perror ("fcntl");exit (EXIT_FAILURE);}return false;}return true; }//定義函數unlock解除寫鎖 void unlock(int fd) {struct flock lock;lock.l_type = F_UNLCK;lock.l_whence = SEEK_SET;lock.l_start = 0;lock.l_len = 0;lock.l_pid = -1;if (fcntl (fd, F_SETLKW, &lock) == -1){perror ("fcntl");exit (EXIT_FAILURE);} }void writedata(int fd, char *buf, char c) {int i = 0;//設置寫鎖,防止子進程和父進程同時寫入造成數據混亂while(!lock(fd));//將緩沖區用要寫入的字符填充。for (i = 0; i < BUFSIZE - 1; i++)buf[i] = c;for (i = 0; i < 5; i++){int writed;//向文件fd中寫入字符。if ((writed = write (fd, buf + i, 1)) == -1){perror ("write");exit (EXIT_FAILURE);}printf("111111111->%c,buf[0]=%c, writed = %d\n", c, *(buf + i), writed);}//解除寫鎖unlock(fd); }int main() {int fd = open ("data.txt", O_RDWR | O_CREAT | O_TRUNC, 0666);if (fd == -1){perror ("open");exit (EXIT_FAILURE);}pid_t pid;if ((pid = fork()) < 0){perror("fork");return 1;}//創建子進程if (pid == 0){char buf[BUFSIZE] = {};writedata(fd, buf, '0'); //向 fd 寫入'0'}//父進程else{sleep (3);char buf[BUFSIZE] = {};writedata(fd, buf, '1'); //向 fd 寫入 '1'}if (close (fd) == -1){perror ("close");exit (EXIT_FAILURE);}return 0; } 輸出結果: 111111111->0,buf[0]=0, writed = 1 111111111->0,buf[0]=0, writed = 1 111111111->0,buf[0]=0, writed = 1 111111111->0,buf[0]=0, writed = 1 111111111->0,buf[0]=0, writed = 1 111111111->1,buf[0]=1, writed = 1 111111111->1,buf[0]=1, writed = 1 111111111->1,buf[0]=1, writed = 1 111111111->1,buf[0]=1, writed = 1 111111111->1,buf[0]=1, writed = 1查看 data.txt # cat data.txt 00000111112》》示例解析
在子進程和父進程中分別使用 writedata 函數向文件 fd 中寫入字符 0 和字符 1。由于 fork 函數成功返回以后,系統內核為父進程維護的文件描述符表也被復制到子進程的進程表項中,文件表項并不復制,所以子進程和父進程寫入同一文件。(2)在 fork 之后處理文件描述符有以下兩種常用的操作模式?
1》》父進程等待子進程完成。 (重點)
在這種情況下,父進程無需對其描述符做任何處理。當子進程終止后,它曾進行過讀、寫操作的任一共享描述符的文件偏移量已做了相應更新。 上例,就是用的這種方法,子進程寫 '0',文件偏移量做相應更新,父進程在其后面繼續寫入 '1'。2》》父進程和子進程各自執行不同的程序段。
在這種情況下,在 fork 之后,父進程和子進程各自關閉它們不需使用的文件描述符,這樣就不會干擾對方使用的文件描述符。這種方法是網絡服務進程經常使用的。5、進階
(1)fork 失敗原因
1》》當前的進程數已經達到了系統規定的上限,這時errno的值被設置為EAGAIN。2》》系統內存不足,這時errno的值被設置為ENOMEM。 例如: 俗稱 fork 炸彈 while(1) {fork; } 失敗后效果:內存耗盡,系統死機無法操作。 參看:ulimit 命令? ?? 系統總線程數達到上限 # cat /proc/sys/kernel/threads-max 15785用戶總進程數達到上限 # ulimit -u 7892
(2)并發運行
一個進程如果希望創建自己的副本并執行一份代碼,或希望與另一個進程并發運行,都可以使用 fork 函數。(3)執行代碼
調用 fork 函數前的代碼只有父進程執行,fork 函數成功返回后的代碼父子進程都會執行,受邏輯控制進入不同分支。 調用fork函數前的代碼只有父進程執行pit_t pid = fork (); if (pid == -1)perror ("fork"), exit (1); if (pid == 0)子進程執行的代碼 else父進程執行的代碼父子進程都執行的代碼(4)除了打開文件之外,父進程的很多其他屬性也由子進程繼承,包括:
實際用戶ID、實際組ID、有效用戶ID、有效組ID。 添加組ID。 進程組ID。 對話期ID。 控制終端。 設置用戶ID標志和設置組ID標志。 當前工作目錄。 根目錄。 文件方式創建屏蔽字。 信號屏蔽和排列。 對任一打開文件描述符的在執行時關閉標志。 環境。 連接的共享存儲段。 資源限制。6、孤兒和僵尸
(1)父子進程
1》》父進程和子進程的關系
UNIX/Linux 系統中的進程存在父子關系,一個父進程可以創建多個子進程,但每個子進程最多只能有一個父進程。整個系統中只有一個根進程,即 PID 為 0 ?的調度進程。系統中的所有進程構成了一棵以調度進程為根的進程樹。2》》父進程和子進程之間的區別
fork 的返回值不同,子進程返回 0, 而父進程返回新建子進程 ID。 進程 ID 不同這兩個進程的父進程 ID 不同:子進程的父進程 ID 是創建它的進程的 ID,而父進程的父進程 ID 則不變。 子進程的tms_utime , tms_stime , tms_cutime以及tms_ustime設置為0。
子進程不繼承父進程設置的文件鎖。 子進程的未處理鬧鐘被清除。 子進程的未處理信號集設置為空集。
(2)孤兒進程
父進程創建子進程以后,子進程在操作系統的調度下與其父進程同時運行。 如果父進程先于子進程終止,子進程即成為孤兒進程,同時被 init 進程收養,即成為 init 進程的子進程,因此 inti 進程又被成為孤兒院進程。 一個進程成為孤兒進程是正常的,系統中的很多守護進程都是孤兒進程。1》》示例說明
#include <stdio.h> #include <unistd.h> #include <stdlib.h>int main (void) {pid_t pid;if ((pid = fork ()) < 0)perror ("fork"), exit (1);else if (pid == 0){sleep (3); printf ("這是子進程 pid = %d", getpid ());printf ("父進程的 ppid = %d\n", getppid ());}else {printf ("這是父進程 ppid = %d\n", getpid ());}return 0; } 輸出結果: 這是子進程 pid = 2430父進程的 ppid = 2331 這是子進程 pid = 2431父進程的 ppid = 12》》示例解析
子進程被暫停 3 秒,所以當父進程退出時,子進程仍然未退出,這樣子進程即成為孤兒進程。根據輸出結果可知,當暫停 3 秒結束時,子進程的父進程變成了 1,即 init 進程,又被稱為孤兒進程。(3)僵尸進程
如果子進程先于父進程終止,但父進程由于某種原因,沒有回收子進程的退出狀態,子進程即成為僵尸進程。 僵尸進程雖然已經不再活動,但其終止狀態仍然保留,也會占用系統資源,直到被其父進程回收才得以釋放。 如果父進程直到終止都未回收它的已成僵尸的子進程,init 進程會立即收養并回收這些處于僵尸狀態的子進程,因此一個進程不可能既是孤兒進程同時又是僵尸進程。 一個進程成為僵尸進程需要引起注意,如果它的父進程長期運行而不終止,僵尸進程所占用的資源將長期得不到釋放。1》》示例說明
#include <stdio.h> #include <stdlib.h> #include <unistd.h>int main (void) {pid_t pid;pid = fork ();if (pid == -1)perror ("fail to fork"), exit (1);else if (pid == 0){printf ("這是子進程 pid = %d", getpid ());printf ("父進程的 ppid = %d\n", getppid ());}else{//while (1);sleep (10); printf ("這是父進程 ppid = %d\n", getpid ());}return 0; } 輸出結果: 這是子進程 pid = 2652父進程的 ppid = 2651 這是父進程 ppid = 26512》》示例解析
函數sleep的作用是讓父進程休眠指定的秒數,在這10秒內,子進程已經退出,而父進程正忙著睡覺,不可能對它進行收集,這樣,我們就能保持子進程 10秒 的僵尸狀態。三、函數 vfork
#include <sys/types.h> #include <unistd.h> pid_t vfork(void);1、函數功能:
創建輕量級子進程,vfork 與 fork 的功能基本相同。2、兩者區別:
有以下兩點區別:第一,vfork 函數創建的子進程不復制父進程的物理內存,也不擁有自己獨立的內存映射,而是與父進程共享全部地址空間;第二,vfork 函數會在創建子進程的同時掛起其父進程,直到子進程終止,或通過 exec 函數啟動了另一個可執行程序。3、函數典型用法:
終止 vfork 函數創建的子進程,不要使用 return 語句,也不要調用 exit 函數,而要調用 _exit 函數,以避免對其父進程造成不利影響。 vfork 函數的典型用法就是在所創建的子進程里直接調用 exec 函數啟動另外一個進程取代其自身,這比調用 fork 函數完成同樣的工作要快得多。 pid_t pid = vfork (); if (pid == -1)perror ("vfork"), exit (1); if (pid == 0)if (execl ("ls", "ls", "-l", NULL) == -1)perror ("execl"), _exit (1);4、寫時復制:
傳統意義上的 fork 系統調用,必須把父進程地址空間中的內容一頁一頁地復制到子進程的地址空間中(代碼區除外)。這無疑是十分漫長的過程(在系統內核看來)。 而多數情況下的子進程其實只是想讀一讀父進程的數據,并不想改變什么。更有甚者,可能連讀一讀都覺得多余,比如直接通過 exec 函數啟動另一個進程的情況。漫長的內核復制在這里顯得笨拙毫無意義。 寫時復制以惰性優化的方式避免了內存復制所帶來的系統開銷。在子進程創建伊始,并不復制父進程的物理內存,只復制它的內存映射表即可,父子進程共享同一個地址空間,直到子進程需要寫這些數據時,再復制內存亦不為遲。 寫時復制帶來的好處是,子進程什么時候寫就什么時候復制,寫幾頁就復制幾頁,沒有寫的就不復制。惰性優化算法的核心思想就是盡一切可能將代價高昂的操作,推遲到非做不可的時候再做,而且最好局限在盡可能小的范圍里。 現代版本的 fork 函數已經廣泛采用了寫時復制技術,從這個意義上講,vfork 函數的存在純粹只是一個歷史遺留的產物,盡管它的速度還是比 fork 要快一點(連內存映射表都不復制),但它的地位已遠不如寫時復制技術被應用到 fork 函數的實現中以前那么重要了。5、示例說明
//示例一 #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h>int main(void) {//使用vfork函數創建子進程pid_t pid = vfork();if(-1 == pid){perror("vfork"),exit(-1);}if(0 == pid) //子進程{printf("子進程%d開始運行\n",getpid());sleep(3);printf("子進程結束\n");//子進程不退出,則結果不可預知_exit(0);//終止子進程}printf("父進程%d開始執行\n",getpid());printf("父進程結束\n");return 0; } 輸出結果: 子進程2762開始運行 子進程結束 父進程2761開始執行 父進程結束 //示例二 #include <stdio.h> #include <unistd.h> #include <stdlib.h>int main (void) {printf ("父進程開始執行\n");pid_t pid = vfork ();if (pid == -1)perror ("vfork"), exit (1);if (pid == 0){printf ("子進程開始執行\n");if (execl ("/bin/ls", "ls", "-l", NULL) == -1)perror ("execl"), _exit (1);}sleep (1);printf ("父進程執行結束\n");return 0; } 輸出結果: 父進程開始執行 子進程開始執行 總用量 16 -rwxr-xr-x 1 root root 7380 Apr 20 10:22 a.out -rw-r--r-- 1 root root 383 Apr 20 10:22 test.c -rw-r--r-- 1 root root 151 Apr 20 09:56 test.c~ 父進程執行結束總結
以上是生活随笔為你收集整理的UNIX再学习 -- 函数 fork 和 vfork的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 第二部分Calendar原理和思想
- 下一篇: 就编程而言,可移植性意味着什么?