进程及 fork() 系统调用详解
進程的概念及定義
定義:進程是一個具有獨立功能的程序關于某個數據集合的運行過程。它是系統進行資源分配和調度的獨立單位,是一個活動的實體。
注意:進程的定義不唯一,這個是我覺得最準確的定義。還有一點,在沒有引入線程前,資源分配和調度的基本單位都是進程。有了線程后,進程仍然是資源分配的基本單位,而調度的最小單位是線程。
在多道環境下,引入進程的概念,以便更好地描述和控制程序的并發執行,實現操作系統的并發性和共享性(最重要的兩個特性)。為了使參與并發的程序能夠獨立地運行,必須要有一個專門的數據結構——進程控制塊(Process Control Block,PCB),系統利用PCB來描述進程的基本情況和運行狀態,進而控制和管理進程。相應地,程序段、相關數據段、和PCB三部分構成了進程實體(依然是靜態的,因為沒有獲得處理器資源)。創建進程,實質上是創建進程的PCB;而撤銷進程,實質上是撤銷進程的PCB。所以PCB是進程存在的唯一標志。
操作系統中的資源:抽象的理解成時間和空間。以時間為例,分時操作系統中的"時間片"資源。以空間為例,內存空間和一些寄存器空間。
?
進程的特征
動態性:進程是程序在系統中的執行過程,具有一定的生命周期,進程是動態產生,動態消亡的。
并發性:多個進程實體同事存在于內存中,能在一段時間內同時運行。引入進程的目的就是為了是程序能與其他進程并發執行,以提過資源利用率。
獨立性:進程是一個能獨立運行、獲取資源和接受調度的基本單位;
異步性:由于進程間的相互制約,使進程具有執行的間斷性,即進程按各自獨立的、不可預知的速度向前推進。異步性會導致執行結果的不可再現性,為此在操作系統中必須采用相應的進程同步機制。
結構性:進程由程序段、數據段和PCB三部分組成。
?
進程的狀態
由于系統中各進程之間的相互制約關系及系統的運行環境的變化,使得進程的狀態也是在不斷地發生變化。通常進程有以下五種狀態,前三種是基本狀態。
(1)就緒狀態:進程已獲得除處理器外的所有資源,等待分配處理器資源,只要分配了處理器進程就可執行。就緒進程可以按多個優先級來劃分隊列。例如,當一個進程由于時間片用完而進入就緒狀態時,排入低優先級隊列;當進程由I/O操作完成而進入就緒狀態時,排入高優先級隊列。
(2)運行狀態:進程獲得處理器資源;處于此狀態的進程的數目小于等于處理器的數目。在沒有其他進程可以執行時(如所有進程都在阻塞狀態),通常會自動執行系統的空閑進程。
(3)阻塞狀態:由于進程等待某種條件(如資源不足、I/O操作或進程同步),在條件滿足之前無法繼續執行。即使處理器資源空閑,該進程也無法運行。
(4)創建狀態:進程正在被創建,尚未轉到就緒態。創建進程通常需要多個步驟:首先申請一個空白的PCB,并向PCB中填寫一些控制和管理進程的信息;然后有系統為該進程分配運行是所需的資源;最后把該進程轉入就緒態。
(5)終止狀態:進程正從系統中消失,可能是正常結束或其他原因中斷退出運行。進程需要結束運行時,系統首先置該進程為結束態,然后再進一步處理資源釋放和回收等工作。
?
狀態轉換圖如下:
就緒態 -> 運行態:處于就緒態的進程被調度后,獲取處理器資源,于是進程由就緒態轉換為運行態。
運行態 -> 就緒態:處于運行態的進程在時間片用完后,讓出處理器,該進程有運行態轉換為就緒態。此外,在可剝奪的調度策略中,當有高優先級的進程就緒時,調度程序將正在執行的進程轉換為就緒態,讓更高優先級的進程執行。
運行態 -> 阻塞態:進程請求某一資源的使用、分配或等待某一件事發生時,它就從運行態轉換為阻塞態。
阻塞態 -> 就緒態:進程等待的事件到來時,如I/O操作結束或中斷結束時,中斷處理程序必須把相應的狀態有阻塞態轉換為就緒態。
分辨運行態是轉換成就緒態還是阻塞態,只需要看此時該進程所占有的資源的情況。若該進程的資源沒有被剝奪,也沒有申請新的資源而沒有得到,則處于就緒態。反之處于阻塞態。
?
?
fork() 的使用
fork() 是 UNIX 或 類UNIX 中的分叉函數。fork 系統調用用于創建一個新進程,稱為子進程(子進程拷貝了父進程的資源),它與父進程(調用 fork 的進程)同時運行。創建新的子進程后,兩個進程將執行 fork() 系統調用之后的下一條指令。子進程使用相同的 PC (程序計數器),相同的 CPU 寄存器,在父進程中使用的相同打開文件。fork() 函數原型如下:
pid_t fork(?void); pid_t 是一個宏定義,其實質是 int,被定義在#include <sys/types.h> 中。它不需要參數并返回一個整數值。下面是 fork() 返回的不同值:
負值:創建子進程失敗。原因:進程總數超出了限制。
0:返回到新創建的子進程。
正值:返回父進程或調用者。該值是新創建的子進程的進程 ID。
fork() 函數被調用一次,但返回兩次。兩次返回的區別是:子進程的返回值是 0,而父進程的返回值是新建子進程的 ID。將子進程 ID 返回給父進程的理由是:因為一個進程的子進程可以有多個,并且沒有一個函數使一個進程可以獲得其所有子進程的進程 ID。 fork 使子進程返回值為 0 的理由是:一個進程只會有一個父進程,所以子進程總是可以調用 getppid() 以獲得父進程的進程 ID。編程時,我們可以通過 fork() 返回的值來判斷當前進程是子進程還是父進程。
注意:進程 ID 0 總是由內核交換進程使用,所以一個子進程的進程 ID 不可能為 0。
?
為什么 fork 會返回兩次?
由于在復制時復制了父進程的堆棧段,所以兩個進程都停留在 fork() 函數中,等待返回。因此 fork() 函數會返回兩次,一次是在父進程中返回,另一次是在子進程中返回,所以這兩次的返回值是不一樣的。
?
利用 fork() 函數創建子進程:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h>int main() {pid_t pid;int count = 0;pid = fork();if(pid < 0)printf("error in fork!\n");else if(pid == 0){count++;printf("I am the child process,ID is:[%d] , count is:[%d]\n", getpid(), count);} // getpid返回當前進程標識,getppid返回父進程標識。else{ count++;printf("I am the parent process,ID is:[%d], count is:[%d]\n", getpid(), count);}return 0; }從上述代碼中可以看出,父子進程的 count 是相互獨立的變量,這是因為子進程拷貝了 count,而不是和父進程共用同一個 count 。接下來聲明不同的變量或常量,用于驗證 fork() 資源拷貝的問題(棧區變量、堆區變量及數據區的常量、變量)。
代碼如下:
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h>int a = 1; //初始化的全局變量 int asd[5] = {1,2,3,4,5}; int main() {pid_t pid;pid = fork();int count1 = 1; //棧區變量count1static int count2 = 1; //靜態變量count2char s1[] = "abc"; //棧區char *s2 = "qwe"; //s2在棧區,而內容在常量區char *p1 = (char*)malloc(10); //堆區if(pid < 0)printf("error in fork!\n");else if(pid == 0){a++; count1++; count2++;printf("I am the child process,ID is:[%d]\n", getpid());printf("a is:[%d] address:[%p], count1 is:[%d] address:[%p], count2 is:[%d] address:[%p]\n", a, &a, count1, &count1, count2, &count2);printf("asd's address:[%p], s1's address:[%p], s2's address:[%p], p1's address[%p]\n", asd, s1, s2, p1);s1[0] = 'A';printf("%s\n", s1);int childasd = 0;printf("childasd's address:[%p]\n", &childasd);printf("\n");}else{ a -= 2; count1 -=2 ; count2 -= 2;printf("I am the parent process,ID is:[%d]\n", getpid());printf("a is:[%d] address:[%p], count1 is:[%d] address:[%p], count2 is:[%d] address:[%p]\n", a, &a, count1, &count1, count2, &count2);printf("asd's address:[%p], s1's address:[%p], s2's address:[%p], p1's address[%p]\n", asd, s1, s2, p1);s1[0] = 'B';printf("%s\n", s1);int parentasd = 0;printf("childasd's address:[%p]\n", &parentasd);printf("\n");}free(p1);return 0; }當看到父子進程的變量的地址都是一樣的時候,你是否感到迷惑?其實不用迷惑,這是因為子進程和父進程執行 fork 調用之后的指令。子進程是父進程的副本。例如,子進程獲得父進程數據空間、堆和棧的副本。注意,這是子進程所擁有的副本。父進程和子進程并不共享這些存儲空間,父子進程共享的是代碼段。
還有一點:這里打印出來的地址是邏輯地址而不是物理地址,子進程拷貝父進程的變量,其地址總是一樣的,因為在 fork 時整個虛擬地址空間被復制,而物理內存沒有復制。上面的基本概念已經提到過?PCB 是進程存在的唯一標志,父子進程擁有各自的進程 ID (若不考慮誰創建了誰,它們就是兩個進程,而進程間的邏輯地址相同是互不影響的)。當它們運行的時候,會將邏輯地址轉換成物理地址。這就是為什么父子進程打印出來的地址是一樣的,但還是互不影響的完成了對數據的操作。
現在很多實現并不是執行一個父進程數據段、棧和堆的完全副本。取而代之的是使用寫時復制(Copy-On-Write,COW)技術。這些區域由父進程和子進程共享,而且內核將它們的訪問權限改變為只讀。如果父進程和子進程中的任一個試圖修改這些區域,則內核只為修改區域的那塊內存制作一個副本,通常是虛擬存儲系統中的一"頁"。
?
父子進程之間的區別具體如下:
- (1) fork 的返回值不同。
- (2) 進程 ID 不同。
- (3) 子進程的 tms_utime 、tms_stime 、tms_cutime 和 tms_ustime 的值設置為 0。
- (4) 子進程不繼承父進程設置的文件鎖。
- (5) 子進程的未處理鬧鐘被清除。
- (6) 子進程的未處理信號集設置為空。
?
fork() 的用法:
(1)一個父進程希望復制自己,是父進程的子進程同時執行不同的代碼段。這在網絡服務進程中是最常見的——父進程等待客戶端的服務請求。當這種請求到達時,父進程調用fork,使子進程處理此請求,父進程則繼續等待下一個服務請求。
(2)一個進程要執行一個不同的程序。這對shell是最常見的情況。在這種情況下,子進程從fork返回后立即調用exec。
?
參考:百度百科、《UNIX環境高級編程》
總結
以上是生活随笔為你收集整理的进程及 fork() 系统调用详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 红黑树(RB-Tree)比AVL强在哪?
- 下一篇: 进程间通信:管道和命名管道(FIFO)