進程的基本概念
UNIX98規範和UNIX95規範把進程定義爲“一個其中運行着一個或多個線程的地址空間和這些線程所需要的系統資源。”
實際上,正在運行的程序或進程由程序代碼、數據、變量(佔用着系統內存)、打開的文件(文件描述符)和環境組成。一般來說,Linux系統會在進程之間共享程序代碼和系統函數庫,所以在任何時刻內存中都只有程序的一份拷貝。
每個進程都會被分配一個唯一的數字編號,稱爲進程標識符或PID,它通常是一個範圍從2到32768的正整數。當進程被啓動時,系統將按順序選擇下一個未被使用的數字作爲它的PID,當數字已經迴繞一圈時,新的PID重新從2開始。數字1爲特殊進程init保留,它負責管理其他的進程。所有其他的系統進程要麼是由init進程啓動,要麼由被init進程啓動的其他進程啓動。
在許多Linux系統上,目錄/proc中有一組特殊的文件,這些文件的特殊之處在於它們允許你“窺視”這在運行的進程的內部情況,就好像這些進程是目錄中的文件一樣。這在學習筆記03的/proc文件系統部分提到過。
Linux進程表就像一個數據結構,它把當前加載在內存中的所有進程的有關信息保存在一個表中,其中包括進程的PID、進程的狀態、命令字符串和其他一些ps命令輸出的各類信息。操作系統通過進程的PID對它們進行管理,這些PID是進程表的索引。進程表的長度是有限制的,所有系統能夠支持的同時運行的進程數也是有限制的。早期的UNIX系統只能同時運行256個進程。最新的實現版本已大幅度放寬這一限制,可以同時運行的進程數可能只與用於建立進程表項的內存容量有關,而沒有具體的數字限制了。
我們可以使用ps命令查看當前正在運行的進程。默認情況下,ps程序只顯示與終端、主控臺、串行口或僞終端(比如pts/0)保持連接的進程的信息。其他進程在運行時不需要通過終端與用戶通信,它們通常是一些系統進程,Linux用它們來管理共享的資源。我們可以使用ps命令的-a選項查看所有的進程,用-f選項顯示進程完整的信息。ps命令的詳細資料請查閱手冊。
進程調度
在一臺單處理器計算機上,同一時間只能有一個進程可以運行,其他進程處於等待運行狀態。每個進程輪到的運行時間(時間片)是相當短暫的,這就給人一種多個程序在同時運行的印象。
Linux內核用進程調度器來決定下一個時間片應該分配給哪個進程。它的判斷依據是進程的優先級,優先級高的進程運行得更爲頻繁。在Linux中,進程的運行時間不可能超過分配給它們的時間片,它們採用的是搶佔式多任務處理,所以進程的掛起和僅需運行無需彼此之間的協作。
在一個如Linux這樣的多任務系統中,多個程序可能會競爭使用同一個資源,在這種情況下,我們認爲,執行短期的突發性工作並暫停運行以等待輸入的程序,要比持續佔用處理器以進行計算或不斷輪詢系統以查看是否有新的輸入到達的程序要更好。我們稱表現良好的程序爲nice程序。一個進程的nice值默認爲0並將根據這個程序的表現而不斷變化。我們可以使用nice命令設置進程的nice值,使用renice命令調整它的值。可以使用ps命令的-f或-l詳細查看這在運行的進程的nice值(NI欄)。
如果你對進程調度感興趣,可以去參閱《操作系統》或《Linux內核》相關的書籍。
啓動新進程
在《精通UNIX環境下C語言編程及項目實踐》的學習筆記04中曾提過有三種執行新進程的方法。
一種就是直接調用庫函數system來實現。然而一般來說,使用system函數遠非啓動其他進程的理想手段,因爲它必須用一個shell來啓動需要的程序。由於在啓動程序之前需要先啓動一個shell,而且對shell的安裝情況及使用的環境的依賴也很大,所以使用system函數的效率不高。
另兩種方式則是fork-exec和vfork-exec,日常編程中則常用前者。
Exec函數系列有六個函數,具體定義如下:
- #include <unistd.h>
- extern char **environ;
- int execl(const char *path, const char *arg0, ..., (char *)0);
- int execle(const char *path, const char *arg0, ..., (char *)0, char *const envp[]);
- int execlp(const char *file, const char *arg0, ..., (char *)0);
- int execv(const char *path, const char *argv[]);
- int execve(const char *path, const char *argv[], const char *envp[]);
- int execvp(const char *file, const char *argv[]);
當我們在程序中直接調用exec函數時,指定運行的程序將替換當前的程序,看下面的一個簡單程序pexec.c:
- #include <unistd.h>
- #include <stdio.h>
- int main()
- {
- printf("Running ps with execlp /n");
- sleep(3);
- execl("/bin/ps", "ps", "-f", 0); // 語句 0
- printf("Done./n");
- return 0;
- }
我們使用make pexec & ./pexec & 運行程序,在語句0執行前使用ps命令查看當前的進程列表,你將發現pexec進程存在於列表中;但在語句0執行的結果中卻找不到pexec進程,實際上當execl函數執行時,新啓動的ps進程已經把pexec進程替換掉了。
注意:對於exec函數啓動的進程來說,它的參數表和環境加在一起的總長度是有限制的。上限由ARG_MAX給出,在Linux系統上它是128K字節。其他系統可能會設置一個非常有限的長度,這有可能會導致出現問題,POSIX規範要求ARG_MAX至少要有4096個字節。
提示:在原進程中已打開的文件描述符在新進程中仍將保持打開,除非它們的“執行時關閉標誌”(close on exec flag)被置位。任何在原進程中已經打開的目錄流將在新進程中被關閉。
我們可以通過調用fork創建一個新進程。通過與exec函數配合,我們可以實現多進程編程的目的。當用fork啓動一個子進程時,子進程就有了它自己的生命週期並將獨立運行。我們可以通過在父進程中調用wait或waitpid函數來等待子進程的結束。
用fork來創建進程確實很有用,但必須清楚子進程的運行情況。子進程終止時,它與父進程之間的關聯還會保持,直到父進程也正常地終止或父進程調用wait才結束。因此,進程表中代表子進程的表項不會立刻釋放。雖然子進程已經不再運行,但它仍然存在於系統中,因爲它的退出碼還需要保存起來以備父進程今後的wait調用使用。這時它將成爲一個死進程(defunct)或殭屍進程(zombie)。關於殭屍進程的詳細介紹同樣在《精通UNIX環境下C語言編程及項目實踐》的學習筆記04中有所描述。
信號
信號是UNIX和Linux系統響應某些條件而產生的一個事件,接收到信號的進程會相應地採取一些行動。信號的名稱在頭文件signal.h中定義,每個都以SIG開頭。
我們可以使用signal函數處理信號,信號的處理方式可以是SIG_IGN(忽略信號)、SIG_DEF(默認方式)或者自行定義處理方式。關於如何使用signal處理信號在《精通UNIX環境下C語言編程及項目實踐》的學習筆記05中有所描述。
注意:在信號處理程序中,調用如printf這樣的函數是不安全的。一個有用的技巧是,在信號處理程序中設置一個標誌,然後在主程序中檢查該標誌,如需要就打印一條信息。書中的表11-6列出了可以在信號處理程序中被安全調用的函數。
進程可以通過調用kill函數向包括它本身在內的其他進程發送一個信號。如果程序沒有發送該信號的權限,對kill函數的調用就將失敗,失敗的常見原因是目標進程由另一個用戶所擁有。
提示:不推薦使用signal接口,應該使用定義更清晰、執行更可靠的函數sigaction,在所有的新程序中都應該使用這個函數。
X/Open和UNIX規範推薦了一個更新和更健壯的信號編程接口:sigaction,定義如下
- #include <signal.h>
- int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
下面是一個簡單的例程,它用sigaction來截獲SIGINT信號:
- #include <stdio.h>
- #include <unistd.h>
- #include <signal.h>
- void ouch(int sig){
- printf("OUCH! - I got signal %d/n", sig);
- }
- int main()
- {
- struct sigaction act;
- act.sa_handler = ouch;
- sigemptyset(&act.sa_mask);
- act.sa_flags = 0;
- sigaction(SIGINT, &act, 0);
- while(1){
- printf("hello. /n");
- sleep(1);
- }
- return 0;
- }
sigaction函數的調用方式與signal函數差不多。sigaction結構定義在文件signal.h中,它的作用是定義在接受到參數sig指定的信號後應該採取的行動。該結構應該至少包括以下幾個成員:
- void (*) (int) sa_handler // function, SIG_DFL or SIG_IGN
- sigset_t sa_mask // signals to block in sa_handler
- int sa_flags // signal action modifiers
其中,sa_mask字段指定了一個信號集,在調用sa_handler所指向的信號處理函數之前,該信號集將被加入到進程的信號屏蔽字中。這是一組將被阻塞且不會傳遞給該進程的信號,在使用signal函數時,可能會出現有些信號在處理函數中還未運行結束時就被接收到,設置信號屏蔽字可以防止這種現象的發生。頭文件signal.h中有一組函數用來操作信號集,它們分別是sigaddset、sigemptyset、sigfillset和sigdelset等。