關於Linux進程你所需要知道的一切都在這裏!!

進程

說明:本文雖出現在野火書籍中,但作者卻是本人~

簡單瞭解進程

在進入正題之前,我們不打算像其他書籍一樣,講一大堆原理,而是希望想通過實驗現象來引導讀者瞭解進程相關的知識,緊接着再來補充進程相關的知識點。

首先在虛擬機中打開一個終端(相信讀者能閱讀到此處,就已經瞭解什麼是終端了,而由於作者用是的公司的服務器,使用ssh方式連接的,所以下文的截圖有可能與讀者的顯示界面不是一樣的,但這不會對本章的閱讀造成任何影響),一般來說ubuntu中有很多shell終端,而這個終端就是一個進程,或許讀者很可能打開多個終端,那麼這些所有被打開的終端就一個個進程,這些進程是不一樣的,獨立運行在系統中,作者打開三個shell終端,這些終端各自有輸入輸出,互不干擾,如圖37‑1所示。

proces002

圖 37‑1 三個shell終端

每個運行中的 shell 都是一個單獨的進程,假如讀者從一個 shell裏面調用一個程序的時候,對應的程序在一個新進程中運行,運行結束後 shell繼續工作,高級的程序員經常在一個應用程序中同時啓用多個協作的進程以使程序可以並行更多任務、使程序更健壯,或者可以直接利用已有的其它程序,這樣子整個系統中運行的進程就可以相互配合,而不是獨立運行,這就涉及到進程間的通信,這部分內容我們在後續講解。

查看進程

即使讀者剛打開linux電腦,沒有運行任何程序,電腦中也會有進程存在,因爲系統中必須要有進程在處理一些必要的程序,以保證系統能正常運行。其實在Linux中是通過檢查表記錄與進程相關的信息的,進程表就像一個數據結構,它把當前加載在內存中的所有進程的有關信息保存在一個表中,其中包括進程的PID、進程的狀態、命令字符串和其他一些ps命令輸出的各類信息。操作系統通過進程的ID對它們進行管理,這些PID是進程表的索引,就目前的linux系統而言,系統支持可以同時運行的進程數可能只與用於建立進程表項的內存容量有關,而沒有具體的數量的限制,也就是說系統有足夠的內存的話,那麼理論上就可以運行無數個進程。

進程ID

Linux系統中的每個進程都都會被分配一個唯一的數字編號,我們稱之爲進程ID(ProcessID,通常也被簡稱爲 PID)。進程ID 是一個 16位的正整數,默認取值範圍是從2到32768(可以修改),由 Linux在啓動新進程的時候自動依次分配,當進程被啓動時,系統將按順序選擇下一個未被使用的數字作爲它的PID,當PID的數值達到最大時,系統將重新選擇下一個未使用的數值,新的PID重新從2開始,這是因爲PID數字爲1的值一般是爲特殊進程init保留,即系統在運行時就存在的第一個進程,
init進程負責管理其他進程。

父進程ID

任何進程(除init進程)都是由另一個進程啓動,該進程稱爲被啓動進程的父進程,被啓動的進程稱爲子進程,父進程號無法在用戶層修改。父進程的進程號(PID)即爲子進程的父進程號(PPID)。用戶可以通過調用getppid()函數來獲得當前進程的父進程號。

爲了更直觀看到這些進程,作者就使用ps命令去查看系統中的進程情況,ps命令可以顯示我們正在運行的進程、其他用戶正在運行的進程或者目前在系統上運行的所有進程。輸出結果如圖37‑2所示,可以很明顯看到,編號爲1的進程是init進程。它位於/sbin/init目錄中。當然,整個系統的進程可不止這一些,由於太多沒法截圖,就只展示這小部分的進程,讀者可以自己使用下面命令去嘗試一下,ps 命令可以顯示當前系統中運行的進程, 其實在linux中, ps命令有很多選項,因爲它試圖與很多不同 UNIX 版本的 ps命令兼容,這些選項決定顯示哪些進程以及要顯示的信息。

命令:

ps –aux

proces003

圖 37‑2 ps –aux輸出結果

父進程與子進程

進程啓動時,啓動進程爲新進程的父進程,新進程是啓動進程的子進程。

每個進程都有一個父進程(除了系統中如“殭屍進程”這種特殊進程外),因此,讀者可以把 Linux
中的進程結構想象成一個樹狀結構,其中 init進程就是樹的“根”;或者可以把init進程看作爲操作系統的進程管理器,它是其他所有進程的祖先進程。我們將要看到的其他系統進程要麼是由init進程啓動的,要麼是由被init進程啓動的其他進程啓動的。

總的來說init進程下有很多子進程,這些子進程又可能存在子進程,就像家族一樣。系統中所有的父進程ID被稱爲PPID,不同進程的父進程是不同的,這個值只是當前進程的父進程的ID,系統中的父進程與子進程是相對而言的,就好比爺爺<->爸爸<->兒子之間的關係,爸爸相對於爺爺而言是兒子,相對於兒子而言則是爸爸。

爲了更加直觀看出系統中父進程與子進程,作者決定使用pstree命令將進程以樹狀關係列出來,具體見圖
37‑3。

命令:

pstree

proces004

圖 37‑3 pstree命令結果

程序與進程

進程相關信息也簡單瞭解過了,可能很多讀者還是有疑問,我們寫的代碼,它是程序,怎麼變成進程了呢?那麼在本小節作者就講解一下程序與進程的關係。

程序

程序(program)是一個普通文件,是爲了完成特定任務而準備好的指令序列與數據的集合,這些指令和數據以“可執行映像”的格式保存在磁盤中。正如我們所寫的一些代碼,經過編譯器編譯後,就會生成對應的可執行文件,那麼這個就是程序,或者稱之爲可執行程序。

進程

進程(process)則是程序執行的具體實例,比如一個可執行文件,在執行的時候,它就是一個進程,直到該程序執行完畢。那麼在程序執行的過程中,它享有系統的資源,至少包括進程的運行環境、CPU、外設、內存、進程ID等資源與信息,同樣的一個程序,可以實例化爲多個進程,在Linux系統下使用 ps命令可以查看到當前正在執行的進程,當這個可執行程序運行完畢後,進程也會隨之被銷燬(可能不是立即銷燬,但是總會被銷燬)。

程序並不能單獨執行,只有將程序加載到內存中,系統爲他分配資源後才能夠執行,這種執行的程序稱之爲進程,也就是說進程是系統進行資源分配和調度的一個獨立單位,每個進程都有自己單獨的地址空間。

舉個例子,我們可以看到/bin目錄下有很多可執行文件,如圖37‑4所示,我們在系統中打開一個終端就是一個進程,這個進程由bash可執行文件(程序)實例化而來,而一個linux系統可用打開多個終端,並且這些終端是獨立運行在系統中的。

proces005

圖 37‑4 /bin目錄下的可執行文件

程序變成進程

在linux系統中,程序只是個靜態的文件,而進程是一個動態的實體,進程的狀態(後續講解進程狀態)會在運行過程中改變,那麼問題來了,程序到底是如何變成一個進程的呢?

其實正如我們運行一個程序(可執行文件),通常在 Shell中輸入命令運行就可以了,在這運行的過程中包含了程序到進程轉換的過程,整個轉換過程主要包含以下
3 個步驟:

  1. 查找命令對應程序文件的位置。
  2. 使用 fork()函數爲啓動一個新進程。
  3. 在新進程中調用 exec 族函數裝載程序文件,並執行程序文件中的main()函數。

補充:關於具體的函數介紹將在後續講解。

總結

總的來說,程序與進程有以下的關係:

  1. 程序只是一系列指令序列與數據的集合,它本身沒有任何運行的含義,它只是一個靜態的實體。而進程則不同,它是程序在某個數據集上的執行過程,它是一個動態運行的實體,有自己的生命週期,它因啓動而產生,因調度而運行,因等待資源或事件而被處於等待狀態,因完成任務而被銷燬。

  2. 進程和程序並不是一一對應的,一個程序執行在不同的數據集上運行就會成爲不同的進程,可以用進程控制塊來唯一地標識系統中的每個進程。而這一點正是程序無法做到的,由於程序沒有和數據產生直接的聯繫,既使是執行不同的數據的程序,他們的指令的集合依然是一樣的,所以無法唯一地標識出這些運行於不同數據集上的程序。一般來說,一個進程肯定有一個與之對應的程序,而且有且只有一個。而一個程序有可能沒有與之對應的進程(因爲這個程序沒有被運行),也有可能有多個進程與之對應(這個程序可能運行在多個不同的數據集上)。

  3. 進程具有併發性而程序沒有。

  4. 進程是競爭計算機資源的基本單位,而程序不是。

進程狀態

在學習進程狀態之前,作者決定還是先讓讀者看看系統中常見的進程狀態,可以通過ps命令將系統中運行的進程信息打印出來,我們只需要關注STAT那一列的信息即可,進程的狀態非常多種,具體見圖37‑5:

命令:

ps –ux

# 輸出(已刪減):
USER   PID  %CPU  %MEM    VSZ   RSS   TTY      STAT   START    TIME     COMMAND
xxx    11132   0.0      0.0     15492  5568  pts/1    Ss      00:45    0:00       /bin/bash
xxx    11340   0.0      0.0     15508  5636  pts/2    Ss+    00:50    0:01       /bin/bash
xxx    11807   0.0      0.0     14916  4572  pts/3    Ss      01:05    0:00        /bin/bash
xxx    18319   0.0      0.0     18260   588  pts/1     Ss+     10月09   0:00      bash
xxx    21862   0.0      0.0      7928   824     ?         S         07:57    0:00      sleep 180
xxx    26124   0.0      0.0     29580  1540  pts/1     R+      07:58    0:00         ps -ux

由於作者用的是公司服務器,所以只將作者用戶當前的進程信息輸出,而不是將系統所有進程信息輸出,因此ps命令不需要–a選項。

proces006

圖 37‑5 進程狀態

從圖37‑5中可以看到進程的狀態有比較多種,有些是S,有些是Ss,還有些是Sl、Rl、R+等狀態,具體是什麼含義呢?其實是這些狀態只是linux系統進程的一部分,還有一些狀態是沒有顯示出來的,因爲作者當前用戶下的所有進程並沒有處於那些狀態,所以就沒顯示出來,下面作者就簡單介紹一下linux系統中所有的進程狀態,如表格 37‑1所示。

表格 37‑1 linux系統中進程狀態說明

狀態 說明
R 運行狀態。嚴格來說,應該是“可運行狀態”,即表示進程在運行隊列中,處於正在執行或即將運行狀態,只有在該狀態的進程纔可能在 CPU 上運行,而同一時刻可能有多個進程處於可運行狀態。
S 可中斷的睡眠狀態。處於這個狀態的進程因爲等待某種事件的發生而被掛起,比如進程在等待信號。
D 不可中斷的睡眠狀態。通常是在等待輸入或輸出(I/O)完成,處於這種狀態的進程不能響應異步信號。
T 停止狀態。通常是被shell的工作信號控制,或因爲它被追蹤,進程正處於調試器的控制之下。
Z 退出狀態。進程成爲殭屍進程。
X 退出狀態。進程即將被回收。
s 進程是會話其首進程。
l 進程是多線程的。
+ 進程屬於前臺進程組。
< 高優先級任務。

進程狀態轉換

從前文的介紹我們也知道,進程是動態的活動的實例,這其實指的是進程會有很多種運行狀態,一會兒睡眠、一會兒暫停、一會兒又繼續執行。雖然Linux操作系統是一個多用戶多任務的操作系統,但對於單核的CPU系統來說,在某一時刻,只能有一個進程處於運行狀態(此處的運行狀態指的是佔用CPU),其他進程都處於其他狀態,等待系統資源,各任務根據調度算法在這些狀態之間不停地切換。但由於CPU處理速率較快,使用戶感覺每個進程都是同時運行。

圖 37‑6 展示了Linux進程從被啓動到退出的全部狀態,以及這些狀態發生轉換時的條件。

proces007

圖 37‑6 進程狀態轉換

  1. 一般來說,一個進程的開始都是從其父進程調用fork()開始的,所以在系統一上電運行的時候,init進程就開始工作,在系統運行過程中,會不斷啓動新的進程,這些進程要麼是由init進程啓動的,要麼是由被init進程啓動的其他進程所啓動的。

  2. 一個進程被啓動後,都是處於可運行狀態(但是此時進程並未佔用CPU運行)。處於該狀態的進程可以是正在進程等待隊列中排隊,也可以佔用CPU正在運行,我們習慣上稱前者爲“就緒態”,稱後者爲“運行態”(佔用CPU運行)。

  3. 當系統產生進程調度的時候,處於就緒態的進程可以佔用CPU的使用權,此時進程就是處於運行態。但每個進程運行時間都是有限的,比如10毫秒,這段時間被稱爲“時間片”。當進程的時間片已經耗光了的情況下,如果進程還沒有結束運行,那麼會被系統重新放入等待隊列中等待,此時進程又轉變爲就緒狀態,等待下一次進程的調度。另外,正處於“運行態”的進程即使時間片沒有耗光,也可能被別的更高優先級的進程“搶佔”,被迫重新回到等到隊列中等待。

  4. 處於“運行態”的進程可能會等待某些事件、信號或者資源而進入“可中斷睡眠態”,比如進程要讀取一個管道文件數據而管道爲空,或者進程要獲得一個鎖資源而當前鎖不可獲取,甚至是進程自己調用sleep()來強制將自己進入睡眠,這些情況下進程的狀態都會變成“可中斷睡眠態”。顧名思義,“可中斷睡眠態”就是可以被中斷的,能響應信號,在特定條件發生後,進程狀態就會轉變爲“就緒態”,比如其他進程想管道文件寫入數據後,或者鎖資源可以被獲取,或者是睡眠時間到達等情況。

  5. 當然,處於“運行態”的進程還可能會進入“不可中斷睡眠態”,在這種狀態下的進程不能響應信號,但是這種狀態非常短暫,讀者幾乎無法通過ps命令將其顯示出來,一般處於這種狀態的進程都是在等待輸入或輸出(I/O)完成,在等待完成後自動進入“就緒態”。

  6. 當進程收到 SIGSTOP 或者 SIGTSTP 中的其中一個信號時,進程狀態會被置爲“暫停態”,該狀態下的進程不再參與調度,但系統資源不會被釋放,直到收到SIGCONT信號後被重新置爲就緒態。當進程被追蹤時(典型情況是使用調試器調試應用程序的情況),收到任何信號狀態都會被置爲
    TASK_TRACED狀態,該狀態跟暫停態是一樣的,一直要等到 SIGCONT信號後進程纔會重新參與系統進程調度。

  7. 進程在完成任務後會退出,那麼此時進程狀態就變爲退出狀態,這是正常的退出,比如在main函數內 return 或者調用 exit()函數或者線程調用pthread_exit()都是屬於正常退出。爲什麼作者要強調正常退出呢?因爲進程也會有異常退出,比如進程收到kill信號就會被殺死,其實不管怎麼死,最後內核都會調用do_exit()函數來使得進程的狀態變成“殭屍態(殭屍進程)”,這裏的“殭屍”指的是進程的PCB(Process Control Block,進程控制塊)。爲什麼一個進程的死掉之後還要把屍體(PCB)留下呢?因爲進程在退出的時候,系統會將其退出信息都保存在進程控制塊中,比如如果他正常退出,那進程的退出值是多少呢?如果被信號殺死?那麼是哪個信號將其殺死呢?這些“死亡信息”都被一一封存在該進程的PCB當中,好讓別人可以清楚地知道:我是怎麼死的。那誰會關心他是怎麼死的呢?那就是它的父進程,它的父進程之所以要啓動它,很大的原因是要讓這個進程去幹某一件事情,現在這個孩子已死,那事情辦得如何,因此需要把這些信息保存在進程控制塊中,等着父進程去查看這些信息。

  8. 當父進程去處理殭屍進程的時候,會將這個殭屍進程的狀態設置爲EXIT_DEAD,即死亡態(退出態),這樣子系統才能去回收殭屍進程的內存空間,否則系統將存在越來越多的殭屍進程,最後導致系統內存不足而崩潰。那麼還有兩個問題,假如父進程由於太忙而沒能及時去處理殭屍進程的時候,要怎麼處理呢?又假如在子進程變成“殭屍態”之前,它的父進程已經先它而去了(退出),那麼這個子進程變成僵死態由誰處理呢?第一種情況可能不同的讀者有不同的處理,父進程有別的事情要幹,不能隨時去處理殭屍進程。在這樣的情形下,讀者可以考慮使用信號異步通知機制,讓一個孩子在變成殭屍的時候,給其父進程發一個信號,父進程接收到這個信號之後,再對其進行處理,在此之前父進程該幹嘛就幹嘛。而如果如果一個進程的父進程先退出,那麼這個子進程將變成“孤兒進程”(沒有父進程),那麼這個進程將會被他的祖先進程收養(adopt),它的祖先進程是init(該進程是系統第一個運行的進程,他的 PCB是從內核的啓動鏡像文件中直接加載的,系統中的所有其他進程都是init進程的後代)。那麼當子進程退出的時候,init進程將回收這些資源。

啓動新進程

在linux中啓動一個進程有多種方法,比如可以使用system()函數,也可以使用fork()函數去啓動(在其他的一些linux書籍也稱作創建進程,本書將全部稱之爲啓動進程)一個新的進程,第一種方法相對簡單,但是在使用之前應慎重考慮,因爲它效率低下,而且具有不容忽視的安全風險。第二種方法相對複雜了很多,但是提供了更好的彈性、效率和安全性。

system()

這個system ()函數是C標準庫中提供的,它主要是提供了一種調用其它程序的簡單方法。讀者可以利用system()函數調用一些應用程序,它產生的結果與從 shell中執行這個程序基本相似。事實上,system()啓動了一個運行着/bin/sh的子進程,然後將命令交由它執行。

我們舉個例子,在野火提供的application/system目錄下,找到system.c文件,它裏面的應用例程就是使用system()函數啓動一個新進程ls,具體的代碼如代碼清單37‑1所示:

代碼清單 37‑1 system.c文件源碼

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    pid_t result;

    printf("This is a system demo!\n\n");

    /*調用 system()函數*/
    result = system("ls -l");

    printf("Done!\n\n");

    return result;
}

在代碼的第13行,就調用了這個system()函數,並且傳入了一個命令“ls -l”這個命令與在shell中運行的結果是一樣的,調用 system()函數的返回值就是被調用的 shell 命令的返回值。如果系統中 shell自身無法運行,system() 函數返回 127;如果出現了其它錯誤, system()函數將返回-1,爲了簡單,作者在這個例子中並沒有檢查system調用是否能夠真正的工作。因爲system() 函數使用 shell 調用命令,它受到系統 shell自身的功能特性和安全缺陷的限制,因此,作者並不推薦使用這種方法去啓動一個進程。

我們可以嘗試編譯它,在application/system目錄下還會提供對應編譯的Makefile文件,這是一個通用的Makefile文件,所有application的例程都使用這個Makefile文件編譯,具體見代碼清單
37‑2。

代碼清單 37‑2 Makefile源碼

CC = gcc
SRC = $(wildcard *.c */*.c)
OBJS = $(patsubst %.c, %.o, $(SRC))
DEP_FILES := $(patsubst %, .%.d,$(OBJS))
DEP_FILES := $(wildcard $(DEP_FILES))
FLAG = -g -Werror -I. -Iinclude 
TARGET = targets

$(TARGET):$(OBJS)
	$(CC) -o $@ $^ $(FLAG)

ifneq ($(DEP_FILES),)
include $(DEP_FILES)
endif

%.o:%.c
	$(CC) -o $@ -c $(FLAG) $< -g -MD -MF [email protected]

clean:
	rm -rf $(TARGET) $(OBJS)

distclean:
	rm -rf $(DEP_FILES)

.PHONY:clean

解釋一下Makefile文件中的代碼含義:

  • 第3行指定編譯器爲 gcc,可以根據需要修改爲 g++ 或者 arm-linux-gcc等交叉編譯工具鏈,使用CC變量保存。

  • 第4行是爲了獲取匹配模式的文件名,*.c 表示當前工程目錄的 c文件,*/*.c表示所有下一級目錄的 .c文件,這些文件名保存在SRC變量中。

  • 第5行是將 $(SRC) 中的 .c 文件都替換成對應的目標 .o文件,並且保存在OBJS變量中。

  • 第6 - 7行將根據是有的目標文件替換成 .o.d文件(隱藏的依賴文件),並且通過DEP_FILES變量保存。

  • 第8行用於指定編譯選項並且保存在FLAG變量中,讀者根據需要添加,比如-g、-ml、-Wall、-O2等等,在這裏作者提個小建議,編譯選項最後選上-Werror,這個選項的含義是存在警告就會報錯,它會使我們的代碼更加嚴謹。

  • 第9行指定最終生成的可執行文件名爲targets

  • 第11行的$(TARGET):$(OBJS)表示由 .o 文件鏈接成可執行文件。

  • 注意第12行前面是一個 <tab> 鍵,而 $@ 表示目標,也就是$(TARGET),$^ 表示依賴目標,也就是 $(OBJS)
    ,編譯選項則是$(FLAG)

  • 第14 - 16行則是判斷,判斷依賴文件是否存在,如果不存在則需要包含DEP_FILES變量。

  • 第18行表示將所有的.c文件編譯編譯成.o文件 。

  • 第19行的開頭也是一個<tab>鍵,$< 表示搜索到的第一個匹配的文件,而接下來的-g -MD -MF則是編譯器的語法,-g表示以操作系統的本地格式產生調試信息,GDB能夠使用這些調試信息進行調試; -MD -MF則表示生成文件的依賴關係,同時也把一些標準庫的頭文件包含了進來。本質是告訴預處理器輸出一個適合 make 的規則,用於描述各目標文件的依賴關係。

  • 第21 – 25行表示清除相關的依賴文件,目標文件等。

  • .PHONY表示clean是個僞目標文件。

進入application/system目錄下,運行make命令將system.c編譯,然後可以看到application/system目錄下多了一個可執行文件——target,然後運行這個文件,可以看到調用system()函數啓動一個進程輸出的結果,它與我們在shell終端中執行ls –l命令產生的結果是一致的,具體見圖 37‑7。

命令:

make

# 輸出:

gcc -o system.o -c -g -Werror -I. -Iinclude system.c -g -MD -MF
.system.o.dgcc -o targets system.o -g -Werror -I. –Iinclude

ps:此時已生成target可執行文件

proces008

圖 37‑7 system()函數運行結果與ls命令運行結果

從程序運行的結果可以看到,只有當system()函數運行完畢之後,纔會輸出Done,這是因爲程序從上往下執行,而無法直接返回結果。雖然system()函數很有用,但它也有侷限性,因爲程序必須等待由system()函數啓動的進程結束之後才能繼續,因此我們不能立刻執行其他任務。

當然,你也可以讓“ls -l”命令在後臺運行,只需在命令結束位置加上“&”即可,具體命令如下:

命令:

ls –l &

如果在system()函數中使用這個命令,它也是可以在後臺中運行的,那麼system()函數的調用將在shell命令結束後立刻返回。由於它是一個在後臺運行程序的請求,所以ps程序一啓動shell就返回了,代碼如代碼清單37‑3所示。

代碼清單 37‑3 修改system.c源碼:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    pid_t result;

    printf("This is a system demo!\n\n");

    /*調用 system()函數*/
    result = system("ls -l &");

    printf("Done!\n\n");

    return result;
}

重新執行make編譯,然後運行程序,實驗現象具體見圖 37‑8。

proces009

圖 37‑8 system後臺運行

從圖 37‑8就可以看出來,在ls命令還未來得及打印出它的所有輸出結果之前,system()函數就程序就打印出字符串Done然後退出了。在system()程序退出後,ls命令繼續完成它的輸出。這類的處理行爲往往會給用戶帶來很大的困惑,也不一定如用戶所預料的結果一致,因此如果讀者想要讓進程按照自己的意願執行,就需要能夠對它們的行爲做更細緻的控制,接下來作者就會講解其他方式啓動新的進程。

fork()

在前面的文章我們也瞭解到,init進程可以啓動一個子進程,它通過fork()函數從原程序中創建一個完全分離的子進程,當然,這只是init進程啓動子進程的第一步,後續還有其他操作的。不管怎麼說,fork()函數就是可以啓動一個子進程,其示意圖具體見圖37‑9。

在父進程中的fork()調用後返回的是新的子進程的PID。新進程將繼續執行,就像原進程一樣,不同之處在於,子進程中的fork()函數調用後返回的是0,父子進程可以通過返回的值來判斷究竟誰是父進程,誰是子進程。

proces010

圖 37‑9 fork()示意圖

fork()函數用於從一個已存在的進程中啓動一個新進程,新進程稱爲子進程,而原進程稱爲父進程。使用fork()函數的本質是將父進程的內容複製一份,正如細胞分裂一樣,得到的是幾乎兩個完全一樣的細胞,因此這個啓動的子進程基本上是父進程的一個複製品,但子進程與父進程有不一樣的地方,作者就簡單列舉一下它們的聯繫與區別。

子進程與父進程一致的內容:

  • 進程的地址空間。
  • 進程上下文、代碼段。
  • 進程堆空間、棧空間,內存信息。
  • 進程的環境變量。
  • 標準 IO 的緩衝區。
  • 打開的文件描述符。
  • 信號響應函數。
  • 當前工作路徑。

子進程獨有的內容:

  • 進程號 PID。 PID 是身份證號碼,是進程的唯一標識符。
  • 記錄鎖。父進程對某文件加了把鎖,子進程不會繼承這把鎖。
  • 掛起的信號。這些信號是已經響應但尚未處理的信號,也就是“懸掛”的信號,子進程也不會繼承這些信號。

因爲子進程幾乎是父進程的完全複製,所以父子兩個進程會運行同一個程序,但是這種複製有一個很大的問題,那就是資源與時間都會消耗很大,當發出fork()系統調用時,內核原樣複製父進程的整個地址空間並把複製的那一份分配給子進程。這種行爲是非常耗時的,因爲它需要做一些事情:

  • 爲子進程的頁表分配頁面。
  • 爲子進程的頁分配頁面。
  • 初始化子進程的頁表。
  • 把父進程的頁複製到子進程相應的頁中

創建一個地址空間的這種方法涉及許多內存訪問,消耗許多CPU週期,並且完全破壞了高速緩存中的內容,因此直接複製物理內存對系統的開銷會產生很大的影響,更重要的是在大多數情況下,這樣直接拷貝通常是毫無意義的,因爲許多子進程通過裝入一個新的程序開始它們的執行,這樣就完全丟棄了所繼承的地址空間。因此在linux中引入一種寫時複製技術(Copy On Write,簡稱COW),我們知道,linux系統中的進程都是使用虛擬內存地址,虛擬地址與真實物理地址之間是有一個對應關係的,每個進程都有自己的虛擬地址空間,而操作虛擬地址明顯比直接操作物理內存更加簡便快捷,那麼顯而易見的,寫時複製是一種可以推遲甚至避免複製數據的技術。內核此時並不複製整個進程的地址空間,而是讓父子進程共享同一個地址空間(頁面)。

那麼寫時複製的思想就是在於:父進程和子進程共享頁面而不是複製頁面。而共享頁面就不能被修改,無論父進程和子進程何時試圖向一個共享的頁面寫入內容時,都會產生一個錯誤,這時內核就把這個頁複製到一個新的頁面中並標記爲可寫。原來的頁面仍然是寫保護的,當還有進程試圖寫入時,內核檢查寫進程是否是這個頁面的唯一屬主,如果是則把這個頁面標記爲對這個進程是可寫的。

總的來說,寫時複製只會用在需要寫入的時候纔會複製地址空間,從而使各個進行擁有各自的地址空間,資源的複製是在需要寫入的時候纔會進行,在此之前,父進程與子進程都是以只讀方式共享頁面,這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。而在絕大多數的時候共享的頁面根本不會被寫入,例如,在調用fork()函數後立即執行exec(),地址空間就無需被複制了,這樣一來fork()的實際開銷就是複製父進程的頁表以及給子進程創建一個進程描述符。

理論相關的知識就講解到這裏就好了,作者也不打算再深入講解,下面就看看fork()函數的使用,它的函數原型如下:

pid_t fork(void);

在fork()啓動新的進程後,子進程與父進程開始併發執行,誰先執行由內核調度算法來決定。fork()函數如果成功啓動了進程,會對父子進程各返回一次,其中對父進程返回子進程的
PID,對子進程返回0;如果fork()函數啓動子進程失敗,它將返回-1。失敗通常是因爲父進程所擁有的子進程數目超過了規定的限制(CHILD_MAX),此時errno將被設爲EAGAIN。如果是因爲進程表裏沒有足夠的空間用於創建新的表單或虛擬內存不足,errno變量將被設爲ENOMEM。

在野火提供的application/fork目錄下,找到fork.c文件,它裏面的應用例程就是使用fork()函數啓動一個新進程,並且在進程中打印相關的信息,如在父進程中打印出“In father process!!”等信息,例程源碼具體見代碼清單 37‑4。

代碼清單 37‑4 fork.c源碼

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    pid_t result;

    printf("This is a fork demo!\n\n");

    /*調用 fork()函數*/
    result = fork();

    /*通過 result 的值來判斷 fork()函數的返回情況,首先進行出錯處理*/
    if(result == -1) {
        printf("Fork error\n");
    }

    /*返回值爲 0 代表子進程*/
    else if (result == 0) {
        printf("The returned value is %d, In child process!! My PID is %d\n\n", result, getpid());

    }

    /*返回值大於 0 代表父進程*/
    else {
        printf("The returned value is %d, In father process!! My PID is %d\n\n", result, getpid());
    }

    return result;
}

我們來分析一下這段代碼:

  • 首先在第12行的時候調用了fork函數,調用fork函數後系統就會啓動一個子進程,並且子進程與父進程執行的內容是一樣的(代碼段),讀者可以通過返回值result判斷fork()函數的執行結果。
  • 如果result的值爲-1,那代表着fork()函數執行出錯,出錯的原因在前文也提到,在此具體不細說。
  • 如果返回的值爲0,則表示此時執行的代碼是子進程,那麼就打印返回的結果、“In child process!!”與子進程的PID,進程的PID通過getpid()函數獲取得到。
  • 如果返回的值大於0,則表示此時執行的代碼是父進程,同樣也打印出返回的結果、“In father process!!”與父進程的PID。

在application/fork目錄下也提供了對應的Makefile文件,可以直接運行make進行編譯,然後執行編譯後生成的可執行文件“targets”,現象具體見圖37‑10。

proces011

圖 37‑10 fork實驗現象

細心的同學就會發現,在這個實驗現象中,父進程的返回值就是子進程的PID,而子進程的返回值則是0。

exce系列函數

事實上,使用fork()函數啓動一個子進程是並沒有太大作用的,因爲子進程跟父進程都是一樣的,子進程能幹的活父進程也一樣能幹,因此世界各地的開發者就想方設法讓子進程做不一樣的事情,因此就誕生了exce系列函數,這個系列函數主要是用於替換進程的執行程序,它可以根據指定的文件名或目錄名找到可執行文件,並用它來取代原調用進程的數據段、代碼段和堆棧段,在執行完之後,原調用進程的內容除了進程號外,其他全部被新程序的內容替換。另外,這裏的可執行文件既可以是二進制文件,也可以是Linux下任何可執行腳本文件。簡單來說就是覆蓋進程,舉個例子,A進程調用exce系列函數啓動一個進程B,此時進程B會替換進程A,進程A的內存空間、數據段、代碼段等內容都將被進程B佔用,進程A將不復存在。

exec 族函數有 6 個不同的 exec 函數,函數原型分別如下:

 int execl(const char *path, const char *arg, ...)

 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 execvp(const char *file, char *const argv[])

 int execve(const char *path, char *const argv[], char *const envp[])

這些函數可以分爲兩大類, execl、 execlp和execle的參數個數是可變的。execv、execvp和execve的第2個參數是一個字符串數組,參數以一個空指針NULL結束,無論何種函數,在調用的時候都會通過參數將這些內容傳遞進去,傳入的參數一般都是要運行的程序(可執行文件)、腳本等。

總結來說,可以通過它們的後綴來區分他們的作用:

  • 名稱包含 l 字母的函數(execl、 execlp 和execle)接收參數列表“list”作爲調用程序的參數。

  • 名稱包含 p 字母的函數(execvp 和execlp)接受一個程序名作爲參數,然後在當前的執行路徑中搜索並執行這個程序;名字不包含p字母的函數在調用時必須指定程序的完整路徑,其實就是在系統環境變量“PATH”搜索可執行文件。

  • 名稱包含 v 字母的函數(execv、execvp 和 execve)的命令參數通過一個數組“vector”傳入。

  • 名稱包含 e 字母的函數(execve 和 execle)比其它函數多接收一個指明環境變量列表的參數,並且可以通過參數envp傳遞字符串數組作爲新程序的環境變量,這個envp參數的格式應爲一個以 NULL 指針作爲結束標記的字符串數組,每個字符串應該表示爲“environment =
    : virables”的形式。

下面作者就具體某個函數做介紹:

函數:

int execl(const char *path, const char *arg, ...)

execl()函數用於執行參數path字符串所代表的文件路徑(必須指定路徑),接下來是一系列可變參數,它們代表執行該文件時傳遞過去的argv[0]、argv[1]… argv[n],最後一個參數必須用空指針NULL作爲結束的標誌。

代碼清單 37‑5 execl()函數實例

int main(void)
{
    int err;

    printf("this is a execl function test demo!\n\n");

    err = execl("/bin/ls", "ls", "-la", NULL);

    if (err < 0) {
        printf("execl fail!\n\n");
    }
    
    printf("Done!\n\n");
}

如以上的execlp()函數實例代碼,它其實就是與我們在終端上運行“ls
-la”產生的結果是一樣的。

函數:

int execlp(const char *file, const char *arg, ...)

execlp()函數會從PATH環境變量所指的目錄中查找符合參數file的文件名(不需要指定路徑),找到後便執行該文件,然後將第二個以後的參數當做該文件的argv[0]、argv[1]… argv[n], 最後一個參數必須用空指針NULL作爲結束的標誌。

代碼清單 37‑6 execlp()函數實例

int main(void)
{
    int err;

    printf("this is a execlp function test demo!\n\n");

    err = execlp("ls", "ls", "-la", NULL);

    if (err < 0) {
        printf("execlp fail!\n\n");
    }
}

函數:

int execle(const char *path, const char *arg, ..., char *const envp[])

execle()函數用於執行參數path字符串所代表的文件路徑(必須指定路徑),併爲新程序複製最後一個參數所指示的環境變量(envp)。

代碼清單 37‑7 execle()函數實例

int main(void)
{
    int err;
    char *envp[] = {
        "/bin", NULL
    };

    printf("this is a execle function test demo!\n\n");

    err = execle("/bin/ls", "ls", "-la", NULL, envp);

    if (err < 0) {
        printf("execle fail!\n\n");
    }
}

函數:

int execv(const char *path, char *const argv[])

execv()函數用於執行參數path字符串所代表的文件路徑(必須指定路徑),接着傳入一個數組作爲執行該文件時傳遞過去的參數argv[0]、argv[1]… argv[n],以空指針NULL結束。

代碼清單 37‑8 execv()函數實例

int main(void)
{
    int err;
    char *argv[] = {
        "ls", "-la", NULL
    };

    printf("this is a execv function test demo!\n\n");

    err = execv("/bin/ls", argv);

    if (err < 0) {
        printf("execv fail!\n\n");
    }
}

函數

int execvp(const char *path, char *const argv[])

execvp()函數會從PATH環境變量所指的目錄中查找符合參數file的文件名(不需要指定路徑),找到該文件後便執行該文件,接着傳入一個數組作爲執行該文件時傳遞過去的參數argv[0]、argv[1] … argv[n],以空指針NULL結束。

代碼清單 37‑9 execvp()函數實例

int main(void)
{
    int err;
    char *argv[] = {
        "ls", "-la", NULL
    };

    printf("this is a execvp function test demo!\n\n");

    err = execvp("ls", argv);

    if (err < 0) {
        printf("execvp fail!\n\n");
    }
}

函數:

int execve(const char *path, char *const argv[], char *const envp[])

execve()函數用於執行參數path字符串所代表的文件路徑(必須指定路徑),執行該文件時會傳入一個數組作爲執行該文件時傳遞過去的參數argv[0]、argv[1] … argv[n],除此之外該函數還會爲新程序複製最後一個參數所指示的環境變量(envp)。

代碼清單 37‑10 execve ()函數實例

int main(void)
{
    int err;
    char *argv[] = {
        "ls", "-la", NULL
    };
    char *envp[] = {
        "/bin", NULL
    };

    printf("this is a execve function test demo!\n\n");

    err = execve("/bin/ls", argv, envp);

    if (err < 0) {
        printf("execve fail!\n\n");
    }
}

以上函數實例代碼均在application/exce目錄下,選擇對應的代碼進行編譯即可,該目錄也提供了對應的Makefile文件,可以直接運行make進行編譯,然後執行編譯後生成的可執行文件“targets”,具體現象如圖
37‑11所示。

proces012

圖 37‑11 exce系列函數實驗現象

程序先打印出它的第一條消息“this is a execl function test demo!”,接着調用exec系列函數(實驗中使用execl()函數),這個函數在/bin/ls目錄中搜索程序ls,然後用這個程序替換targets程序,這與直接在終端中使用以下所示的shell命令一樣,如圖37‑12所示。

命令:

ls -la

proces013

圖 37‑12 ls –la命令

注意,exce系列函數是直接將當前進程給替換掉的,當調用exce系列函數後,當前進程將不會再繼續執行,我們可以測試一下,在調用exce系列函數後再打印一句話,具體代碼如代碼清單37‑11加粗部分所示。

代碼清單 37‑11 exce系列函數測試代碼

int main(void)
{
    int err;

    printf("this is a execl function test demo!\n\n");

    err = execl("/bin/ls", "ls", "-la", NULL);

    if (err < 0) {
        printf("execl fail!\n\n");
    }
    
    printf("Done!\n\n");
}

在程序運行後,“Done!”將不被輸出,因爲當前進程已經被替換了,一般情況下,
exec系列函數函數是不會返回的,除非發生了錯誤。出現錯誤時,
exec系列函數將返回-1,並且會設置錯誤變量errno。

因此我們可以通過調用fork()複製啓動一個子進程,並且在子進程中調用exec系列函數替換子進程,這樣子
fork()和exec系列函數結合在一起使用就是創建一個新進程所需要的一切了。

終止進程

在linux系統中,進程終止(或者稱爲進程退出,爲了統一,下文均使用“終止”一詞)的常見方式有5種,可以分爲正常終止與異常終止:

正常終止:

  • 從main函數返回。

  • 調用exit()函數終止。

  • 調用_exit()函數終止。

異常終止:

  • 調用abort()函數異常終止。

  • 由系統信號終止。

在linux系統中,exit()函數定義在stdlib.h中,而_exit()定義在unistd.h中,exit()和_exit()函數都是用來終止進程的,當程序執行到exit()或_exit()函數時,進程會無條件地停止剩下的所有操作,清除包括 PCB在內的各種數據結構,並終止當前進程的運行。不過這兩個函數還是有區別的,具體如圖37‑13所示。

proces014

圖 37‑13 exit()和_exit()函數的區別

從圖中可以看出,_exit()函數的作用最爲簡單:直接通過系統調用使進程終止運行,當然,在終止進程的時候會清除這個進程使用的內存空間,並銷燬它在內核中的各種數據結構;而exit()函數則在這些基礎上做了一些包裝,在執行退出之前加了若干道工序:比如exit()函數在調用exit系統調用之前要檢查文件的打開情況,把文件緩衝區中的內容寫回文件,這就是“清除I/O緩衝”。

由於在 Linux 的標準函數庫中,有一種被稱作“緩衝 I/O(buffered I/O)”操作,其特徵就是對應每一個打開的文件,在內存中都有一片緩衝區。每次讀文件時,會連續讀出若干條記錄,這樣在下次讀文件時就可以直接從內存的緩衝區中讀取;同樣,每次寫文件的時候,也僅僅是寫入內存中的緩衝區,等滿足了一定的條件(如達到一定數量或遇到特定字符等),再將緩衝區中的內容一次性寫入文件。這種技術大大增加了文件讀寫的速度,但也爲編程帶來了一些麻煩。比如有些數據,認爲已經被寫入文件中,實際上因爲沒有滿足特定的條件,它們還只是被保存在緩衝區內,這時用_exit()函數直接將進程關閉,緩衝區中的數據就會丟失。因此,若想保證數據的完整性,就一定要使用 exit()函數。

不管是那種退出方式,系統最終都會執行內核中的同一代碼,這段代碼用來關閉進程所用已打開的文件描述符,釋放它所佔用的內存和其他資源。

下面一起看看_exit()與exit()函數的使用方法:

頭文件:

#include <unistd.h>
#include <stdlib.h>

函數原型:

void _exit(int status);
void exit(int status);

這兩個函數都會傳入一個參數status,這個參數表示的是進程終止時的狀態碼,0表示正常終止,其他非0值表示異常終止,一般都可以使用-1或者1表示,標準C裏有EXIT_SUCCESS和EXIT_FAILURE兩個宏,表示正常與異常終止。

這些函數的使用都是非常簡單的,只需要在需要終止的地方調用一下即可,此處就不深入講解。

等待進程

在linux中,當我們使用fork()函數啓動一個子進程時,子進程就有了它自己的生命週期並將獨立運行,在某些時候,可能父進程希望知道一個子進程何時結束,或者想要知道子進程結束的狀態,甚至是等待着子進程結束,那麼我們可以通過在父進程中調用wait()或者waitpid()函數讓父進程等待子進程的結束。

從前面的文章我們也瞭解到,當一個進程調用了exit()之後,該進程並不會立刻完全消失,而是變成了一個殭屍進程。殭屍進程是一種非常特殊的進程,它已經放棄了幾乎所有的內存空間,沒有任何可執行代碼,也不能被調度,僅僅在進程列表中保留一個位置,記載該進程的退出狀態等信息供其他進程收集,除此之外,殭屍進程不再佔有任何內存空間。那麼無論如何,父進程都要回收這個殭屍進程,因此調用wait()或者waitpid()函數其實就是將這些殭屍進程回收,釋放殭屍進程佔有的內存空間,並且瞭解一下進程終止的狀態信息。

我們可以在終端中通過man命令查看關於wait相關的函數,具體命令如下:

命令:

man 2 wait

# 輸出

NAME
       wait, waitpid, waitid - wait for process to change state

SYNOPSIS
       #include <sys/types.h>
       #include <sys/wait.h>

       pid_t wait(int *wstatus);

       pid_t waitpid(pid_t pid, int *wstatus, int options);

       int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
# ……(省略其他)

可能很多讀者對man命令不瞭解,那我就簡單說一下man命令相關的內容吧,其實在linux系統中是提供了豐富的幫助手冊,當你需要查看某個命令的參數時不必到處上網查找,只要man一下即可,man命令是就是用於找出這些幫助手冊的內容的,比如有什麼shell命令,有什麼可以調用的函數
等等。

man 命令是按照章節存儲的,linux的man手冊共有8個章節,具體見表格 37‑2。

表格 37‑2 man 命令說明:

章節編號 章節名稱 章節主要內容
1 General Commands 用戶在shell中可以操作的指令或者可執行文檔
2 System Calls 系統調用的函數與工具等
3 Sunroutines C語言庫函數
4 Special Files 設備或者特殊文件
5 File Formats 文件格式與規則
6 Games 遊戲及其他
7 Macros and Conventions 表示宏、包及其他雜項
8 Maintenence Commands 表示系統管理員相關的命令

例如我們想找與wait相關的函數,那麼我們只需要輸入以下命令即可:

man 2 wait

例如我們想要了解ls命令相關的內容,我們可以直接輸入以下命令,就可以看到關於ls相關的用法等內容。

命令:

man ls

# 輸出

NAME
       ls - list directory contents

SYNOPSIS
       ls [OPTION]... [FILE]...

DESCRIPTION
       List information about the FILEs (the current directory by default).  Sort entries alphabetically if none of -cftuvSUX nor --sort is specified.

       Mandatory arguments to long options are mandatory for short options too.

       -a, --all
              do not ignore entries starting with .

……(省略其他)

當然啦,man手冊是英文的,這是屬於全世界通用的技術交流語言,因此讀者還是需要對英文有一定熟悉程度。

wait()

我們通過man命令就知道了wait()、waitpid()函數原型,那麼我們就首先了解下wait()函數。

函數原型

pid_t wait(int *wstatus);

wait()函數在被調用的時候,系統將暫停父進程的執行,直到有信號來到或子進程結束,如果在調用wait()函數時子進程已經結束,則會立即返回子進程結束狀態值。子進程的結束狀態信息會由參數wstatus返回,與此同時該函數會返子進程的PID,它通常是已經結束運行的子進程的PID。狀態信息允許父進程瞭解子進程的退出狀態,如果不在意子進程的結束狀態信息,則參數wstatus可以設成NULL。

wait()函數有幾點需要注意的地方:

  1. wait()要與fork()配套出現,如果在使用fork()之前調用wait(),wait()的返回值則爲-1,正常情況下wait()的返回值爲子進程的PID。
  2. 參數wstatus用來保存被收集進程退出時的一些狀態,它是一個指向int類型的指針,但如果我們對這個子進程是如何死掉毫不在意,只想把這個殭屍進程消滅掉,(事實上絕大多數情況下,我們都會這樣做),我們就可以設定這個參數爲NULL。

當然,除此之外,linux系統中還提供關於等待子進程退出的一些宏定義,我們可以使用這些宏定義來直接判斷子進程退出的狀態:

  • WIFEXITED(status) :如果子進程正常結束,返回一個非零值

  • WEXITSTATUS(status): 如果WIFEXITED非零,返回子進程退出碼

  • WIFSIGNALED(status) :子進程因爲捕獲信號而終止,返回非零值

  • WTERMSIG(status) :如果WIFSIGNALED非零,返回信號代碼

  • WIFSTOPPED(status): 如果子進程被暫停,返回一個非零值

  • WSTOPSIG(status): 如果WIFSTOPPED非零,返回一個信號代碼

wait()函數使用實例如下:

代碼清單 37‑12 wait()函數使用實例

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
    pid_t pid, child_pid;
    int status;

    pid = fork();                  //(1)

    if (pid < 0) {
        printf("Error fork\n");
    }
    /*子進程*/
    else if (pid == 0) {                  //(2)

        printf("I am a child process!, my pid is %d!\n\n",getpid());

        /*子進程暫停 3s*/
        sleep(3);

        printf("I am about to quit the process!\n\n");

        /*子進程正常退出*/
        exit(0);                          //(3)
    }
    /*父進程*/
    else {                                //(4)

        /*調用 wait,父進程阻塞*/
        child_pid = wait(&status);        //(5)

        /*若發現子進程退出,打印出相應情況*/
        if (child_pid == pid) {
            printf("Get exit child process id: %d\n",child_pid);
            printf("Get child exit status: %d\n\n",status);
        } else {
            printf("Some error occured.\n\n");
        }

        exit(0);
    }
}

我們來分析一下這段代碼:

代碼清單 37‑12 (1):首先調用fork()函數啓動一個子進程。

代碼清單 37‑12 (2):如果fork()函數返回的值pid爲0,則表示此時運行的是子進程,那麼就讓子進程輸出一段信息,並且休眠3s。

代碼清單37‑12 (3):休眠結束後調用exit()函數退出,退出狀態爲0,表示子進程正常退出。

代碼清單 37‑12 (4):如果fork()函數返回的值pid不爲0,則表示此時運行的是父進程,那麼在父進程中調用wait(&status)函數等待子進程的退出,子進程的退出狀態將保存在status變量中。

代碼清單37‑12 (5):若發現子進程退出(通過wait()函數返回的子進程pid判斷),則打印出相應信息,如子進程的pid與status。

以上函數實例代碼在application/wait目錄下,選擇對應的代碼進行編譯即可,該目錄也提供了對應的Makefile文件,可以直接運行make進行編譯,然後執行編譯後生成的可執行文件“targets”,執行結果如圖
37‑14所示。

proces015

圖 37‑14 wait()函數現象

waitpid()

waitpid()函數 的作用和wait()函數一樣,但它並不一定要等待第一個終止的子進程,它還有其他選項,比如指定等待某個pid的子進程、提供一個非阻塞版本的wait()功能等。實際上 wait()函數只是 waitpid() 函數的一個特例,在 linux內部實現 wait 函數時直接調用的就是 waitpid 函數。

函數原型

pid_t waitpid(pid_t pid, int *wstatus, int options);

waitpid()函數的參數有3個,下面就簡單介紹這些參數相關的選項:

  • pid:參數pid爲要等待的子進程ID,其具體含義如下:
  1. pid < -1:等待進程組號爲pid絕對值的任何子進程。
  2. pid = -1:等待任何子進程,此時的waitpid()函數就等同於wait()函數。
  3. pid =0:等待進程組號與目前進程相同的任何子進程,即等待任何與調用waitpid()函數的進程在同一個進程組的進程。
  4. pid > 0:等待指定進程號爲pid的子進程。
  • wstatus:與wait()函數一樣。
  • options:參數options提供了一些另外的選項來控制waitpid()函數的行爲。如果不想使用這些選項,則可以把這個參數設爲0。
  1. WNOHANG:如果pid指定的子進程沒有終止運行,則waitpid()函數立即返回0,而不是阻塞在這個函數上等待;如果子進程已經終止運行,則立即返回該子進程的進程號與狀態信息。
  2. WUNTRACED:如果子進程進入了暫停狀態(可能子進程正處於被追蹤等情況),則馬上返回。
  3. WCONTINUED:如果子進程恢復通過SIGCONT信號運行,也會立即返回(這個不常用,瞭解一下即可)。

很顯然,當waitpid()函數的參數爲(-1, status, 0)時,waitpid()函數就完全退化成了wait()函數。

下面看一下waitpid()函數使用實例,具體見代碼清單 37‑13。

代碼清單 37‑13 waitpid()函數使用實例

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>


int main()
{
    pid_t pid, child_pid;
    int status;

    pid = fork();

    if (pid < 0) {
        printf("Error fork\n");
    }
    /*子進程*/
    else if (pid == 0) {

        printf("I am a child process!, my pid is %d!\n\n",getpid());

        /*子進程暫停 3s*/
        sleep(3);

        printf("I am about to quit the process!\n\n");
        /*子進程正常退出*/
        exit(0);
    }
    /*父進程*/
    else {

        /*調用 waitpid,且父進程不阻塞*/
        child_pid = waitpid(pid, &status, WUNTRACED);

        /*若發現子進程退出,打印出相應情況*/
        if (child_pid == pid) {
            printf("Get exit child process id: %d\n",child_pid);
            printf("Get child exit status: %d\n\n",status);
        } else {
            printf("Some error occured.\n");
        }

        exit(0);
    }
}

編譯後運行,它的實驗現象與wait()函數的是一樣的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章