1.6 程序和進程
程序
程序是存在于磁盤上目錄中的可執行文件。6個exec函數中的任意一個,都可以將一個程序讀入內存中并由內核執行(感覺這句沒有翻譯好,原句是:A program is read into memory and is executed by the kernel as a result of one of the six exec functions.)。我們將在8.10節中介紹這些函數。
進程和進程ID
一個正在執行中的程序實例被稱為進程(process),該詞語(進程)幾乎會出現在本書中的每一頁。一些操作系統用任務(task)來稱呼一個正在運行中的程序。
UNIX系統確保每一個進程都擁有一個唯一的數字標識符,稱為進程ID。進程ID總是非負整數。
例子
圖1.6中的程序打印出它的進程ID。
如果我們把程序編譯成a.out并執行它,我們會看到
$ ./a.out
hello world from process ID 851
$ ./a.out
hello world from process ID 854
該程序運行時調用getpid函數來獲得進程ID。
1 #include "apue.h"
2
3 int
4 main(void)
5 {
6 printf("hello world from process ID %d\n", getpid());
7 exit(0);
8 }
圖
1.6 打印進程ID
進程控制
進程控制主要使用三個函數:fork,exec和waitpid。(exec函數有6個變體,我們通常把它們統稱為exec函數。)
例子
使用一個簡單的程序(圖1.7)來展示UNIX系統的進程控制特性,該程序從標準輸入讀取命令并且執行這些命令。這是一個類似shell程序的本質(翻譯得不好,原句是:This is a bare-bones implementation of a shell-like program.)。在這個30行的程序中有許多特性值得思考。
l 使用標準I/O函數fgets,一次從標準輸入中讀取一行。當輸入文件終止符(通常是Control-D)作為一行的第一個字符時,fgets返回null指針,同時終止循環,接著終止進程。在18章中,我們描述所有特殊的終端字符(文件終止符,退格字符,整行擦除字符等等),并且介紹怎樣改變它們。
l fgets返回的每一行都終止于一個換行符和跟在換行符后面的一個null字節,我們使用標準的C函數strlen來計算字符串的長度,接著把換行符替換為一個null字節。這樣做是因為execlp函數需要一個以null結束的參數,而不是以換行符結束的參數。
l 調用fork函數來建立一個進程,這個進程是一個調用者的拷貝。我們把調用者稱為父進程,把新建立的進程稱為子進程。那么fork函數返回子進程的非負進程ID給父進程,同時返回0給子進程。因為fork創建了一個新進程,我們說fork被調用一次就返回兩次,一次返回給父進程,一次返回給子進程。
l 在子進程中,調用execlp來執行從標準輸入讀取的命令。這就把子進程替換為一個新的程序文件。fork函數后跟exec函數的組合,就是一些操作系統所謂的產生一個新進程。(翻譯得不好,原句是:This replaces the child process with the new program file. The combination of a fork, followed by an exec, is what some operating systems call spawning a new process.)在UNIX系統中,這兩部分被分為了獨立的函數。第八章中將會介紹更多這方面的內容。
l 因為子進程調用execlp來執行新的程序文件,父進程就會等待直到子進程結束。這些工作由waitpid函數完成。waitpid函數中的pid參數代表了子進程的進程ID,該參數用來標識出需要等待的進程。waitpid函數也可以得到子進程的終止狀態。在這個簡單程序中的的status變量就代表了子進程的終止狀態,在程序中我們沒有使用這個值,但是我們可以通過檢查這個值來確定子進程的終止狀態。
l 這個程序最根本的限制在于我們不能像我們所執行的命令傳遞參數。例如,不能列舉特定的目錄。我們只可以在工作目錄執行ls命令。為了能夠傳遞參數,我們需要分析輸入行,按某種習慣(可能的情況是使用空格或者制表符)區別出參數,接著把每一個參數作為獨立的參數傳遞給execlp函數。盡管如此,這個程序仍然對UNIX系統進程控制函數進行了有用說明。
運行這個程序,我們得到下面的輸出。注意我們的程序有一個不同的提示符,使用百分號(%)來區別于shell的提示符。
$ ./a.out
% date
Sun Aug 1 03:04:47 EDT 2004
% who
sar :0 Jul 26 22:54
sar pts/0 Jul 26 22:54 (:0)
sar pts/1 Jul 26 22:54 (:0)
sar pts/2 Jul 26 22:54 (:0)
% pwd
/home/sar/bk/apue/2e
% ls
Makefile
a.out
shell1.c
% ^D 輸入文件終止符
$ shell提示符
#include "apue.h"
#include <sys/wait.h>
int
main(void)
{
char buf[MAXLINE]; /* from apue.h */
pid_t pid;
int status;
printf("%% "); /* print prompt (printf requires %% to print %) */
while (fgets(buf, MAXLINE, stdin) != NULL) {
if (buf[strlen(buf) - 1] == '\n')
buf[strlen(buf) - 1] = 0; /* replace newline with null */
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
execlp(buf, buf, (char *)0);
err_ret("couldn't execute: %s", buf);
exit(127);
}
/* parent */
if ((pid = waitpid(pid, &status, 0)) < 0)
err_sys("waitpid error");
printf("%% ");
}
exit(0);
}
圖 1.7 從標準輸入讀取命令并執行它們
符號^D用代表一個控制字符。控制字符是一種特殊的的字符,可以通過同時按下控制鍵(在你的計算機上通常為Control鍵或Ctrl鍵)和另一個鍵來產生它。Control-D,或者說^D,是默認的文件終止符。在18章討論終止I/O的時候會看到更多的控制字符。
線程和線程ID
通常,一個進程只有一個線程(原句是:Usually, a process has only one thread of control one set of machine instructions executing at a time.不知道怎么翻譯,留給大蝦翻譯)。當有多于一個的線程來控制不同部分時,一些問題就變得很容易解決。另外,多線程在多處理器系統上能夠獲得平衡性(又一句翻譯得不爽:Additionally, multiple threads of control can exploit the parallelism possible on multiprocessor systems.)。
一個進程中的所有線程共享同一個地址空間,文件描述符,棧和進程相關的屬性。因為能夠訪問相同的內存,線程必須同步訪問它們自己的共享數據,以避免沖突。
和進程一樣,線程由線程ID標識。盡管如此,線程ID對于進程ID來說是本地的。也就是說,一個進程中的線程ID在另一個進程中是沒有意義的。當我們在進程中操作線程的時候,使用線程ID來指出特定的線程。
控制線程的函數和控制進程的函數是一樣的。在進程模型建立很久以后,線程才被加入到UNIX系統中,然而,線程模型和進程模型之間有一些復雜的交互,在12章將會看到這些。