進程? 請點評
目錄
1.?引言? 請點評
我們知道,每個進程在內核中都有一個進程控制塊(PCB)來維護進程相關的信息,Linux內核的進程控制塊是
task_struct
結構體。現在我們全面了解一下其中都有哪些信息。
目前讀者并不需要理解這些信息的細節,在隨后幾章中講到某一項時會再次提醒讀者它是保存在PCB中的。
fork
和
exec
是本章要介紹的兩個重要的系統調用。
fork
的作用是根據一個現有的進程復制出一個新進程,原來的進程稱為父進程(Parent Process)
,新進程稱為子進程(Child Process)
。系統中同時運行著很多進程,這些進程都是從最初只有一個進程開始一個一個復制出來的。在Shell下輸入命令可以運行一個程序,是因為Shell進程在讀取用戶輸入的命令之后會調用
fork
復制出一個新的Shell進程,然后新的Shell進程調用
exec
執行新的程序。
我們知道一個程序可以多次加載到內存,成為同時運行的多個進程,例如可以同時開多個終端窗口運行
/bin/bash
,另一方面,一個進程在調用
exec
前后也可以分別執行兩個不同的程序,例如在Shell提示符下輸入命令
ls
,首先
fork
創建子進程,這時子進程仍在執行
/bin/bash
程序,然后子進程調用
exec
執行新的程序
/bin/ls
,如下圖所示。
- ?
在
第?3?節 “open/close”
中我們做過一個實驗:用
umask
命令設置Shell進程的
umask
掩碼,然后運行程序
a.out
,結果
a.out
進程的
umask
掩碼也和Shell進程一樣。現在可以解釋了,因為
a.out
進程是Shell進程的子進程,子進程的PCB是根據父進程復制而來的,所以其中的
umask
掩碼也和父進程一樣。同樣道理,子進程的當前工作目錄也和父進程一樣,所以我們可以用
cd
命令改變Shell進程的當前目錄,然后用
ls
命令列出那個目錄下的文件,
ls
進程其實是在列自己的當前目錄,而不是Shell進程的當前目錄,只不過
ls
進程的當前目錄正好和Shell進程相同。有一個例外,子進程PCB中的進程id和父進程是不同的。
2.?環境變量? 請點評
先前講過,
exec
系統調用執行新程序時會把命令行參數和環境變量表傳遞給
main
函數,它們在整個進程地址空間中的位置如下圖所示。
?
和命令行參數
argv
類似,環境變量表也是一組字符串,如下圖所示。
?
libc
中定義的全局變量
environ
指向環境變量表,
environ
沒有包含在任何頭文件中,所以在使用時要用
extern
聲明。例如:
例?30.1.?打印環境變量
#include <stdio.h> int main(void) { extern char **environ; int i; for(i=0; environ[i]!=NULL; i++) printf("%s\n", environ[i]); return 0; }
?
執行結果為
$ ./a.out SSH_AGENT_PID=5717 SHELL=/bin/bash DESKTOP_STARTUP_ID= TERM=xterm ...
由于父進程在調用
fork
創建子進程時會把自己的環境變量表也復制給子進程,所以
a.out
打印的環境變量和Shell進程的環境變量是相同的。
按照慣例,環境變量字符串都是
name=value
這樣的形式,大多數
name
由大寫字母加下劃線組成,一般把
name
的部分叫做環境變量,
value
的部分則是環境變量的值。環境變量定義了進程的運行環境,一些比較重要的環境變量的含義如下:
- PATH
-
可執行文件的搜索路徑。
ls
命令也是一個程序,執行它不需要提供完整的路徑名/bin/ls
,然而通常我們執行當前目錄下的程序a.out
卻需要提供完整的路徑名./a.out
,這是因為PATH
環境變量的值里面包含了ls
命令所在的目錄/bin
,卻不包含a.out
所在的目錄。PATH
環境變量的值可以包含多個目錄,用:
號隔開。在Shell中用echo
命令可以查看這個環境變量的值:$ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
- SHELL
-
當前Shell,它的值通常是
/bin/bash
。 - TERM
-
當前終端類型,在圖形界面終端下它的值通常是
xterm
,終端類型決定了一些程序的輸出顯示方式,比如圖形界面終端可以顯示漢字,而字符終端一般不行。 - LANG
-
語言和locale,決定了字符編碼以及時間、貨幣等信息的顯示格式。
- HOME
-
當前用戶主目錄的路徑,很多程序需要在主目錄下保存配置文件,使得每個用戶在運行該程序時都有自己的一套配置。
用
environ
指針可以查看所有環境變量字符串,但是不夠方便,如果給出
name
要在環境變量表中查找它對應的
value
,可以用
getenv
函數。
#include <stdlib.h> char *getenv(const char *name);
getenv
的返回值是指向
value
的指針,若未找到則為
NULL
。
修改環境變量可以用以下函數
#include <stdlib.h> int setenv(const char *name, const char *value, int rewrite); void unsetenv(const char *name);
putenv
和
setenv
函數若成功則返回為0,若出錯則返回非0。
setenv
將環境變量
name
的值設置為
value
。如果已存在環境變量
name
,那么
-
若rewrite非0,則覆蓋原來的定義;
-
若rewrite為0,則不覆蓋原來的定義,也不返回錯誤。
unsetenv
刪除
name
的定義。即使
name
沒有定義也不返回錯誤。
例?30.2.?修改環境變量
#include <stdlib.h> #include <stdio.h> int main(void) { printf("PATH=%s\n", getenv("PATH")); setenv("PATH", "hello", 1); printf("PATH=%s\n", getenv("PATH")); return 0; }
?
$ ./a.out PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games PATH=hello $ echo $PATH /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
可以看出,Shell進程的環境變量
PATH
傳給了
a.out
,然后
a.out
修改了
PATH
的值,在
a.out
中能打印出修改后的值,但在Shell進程中
PATH
的值沒變。父進程在創建子進程時會復制一份環境變量給子進程,但此后二者的環境變量互不影響。
3.?進程控制? 請點評
3.1.?fork函數? 請點評
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
fork
調用失敗則返回-1,調用成功的返回值見下面的解釋。我們通過一個例子來理解
fork
是怎樣創建新進程的。
例?30.3.?fork
#include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; char *message; int n; pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid == 0) { message = "This is the child\n"; n = 6; } else { message = "This is the parent\n"; n = 3; } for(; n > 0; n--) { printf(message); sleep(1); } return 0; }
$ ./a.out This is the child This is the parent This is the child This is the parent This is the child This is the parent This is the child $ This is the child This is the child
這個程序的運行過程如下圖所示。
-
父進程初始化。
-
父進程調用
fork
,這是一個系統調用,因此進入內核。 -
內核根據父進程復制出一個子進程,父進程和子進程的PCB信息相同,用戶態代碼和數據也相同。因此, 子進程現在的狀態看起來和父進程一樣,做完了初始化,剛調用了
fork
進入內核,還沒有從內核返回 。 -
現在有兩個一模一樣的進程看起來都調用了
fork
進入內核等待從內核返回(實際上fork
只調用了一次),此外系統中還有很多別的進程也等待從內核返回。是父進程先返回還是子進程先返回,還是這兩個進程都等待,先去調度執行別的進程,這都不一定,取決于內核的調度算法。 -
如果某個時刻父進程被調度執行了,從內核返回后就從
fork
函數返回,保存在變量pid
中的返回值是子進程的id,是一個大于0的整數,因此執行下面的else
分支,然后執行for
循環,打印"This is the parent\n"
三次之后終止。 -
如果某個時刻子進程被調度執行了,從內核返回后就從
fork
函數返回,保存在變量pid
中的返回值是0,因此執行下面的if (pid == 0)
分支,然后執行for
循環,打印"This is the child\n"
六次之后終止。fork
調用把父進程的數據復制一份給子進程,但此后二者互不影響,在這個例子中,fork
調用之后父進程和子進程的變量message
和n
被賦予不同的值,互不影響。 -
父進程每打印一條消息就睡眠1秒,這時內核調度別的進程執行,在1秒這么長的間隙里(對于計算機來說1秒很長了)子進程很有可能被調度到。同樣地,子進程每打印一條消息就睡眠1秒,在這1秒期間父進程也很有可能被調度到。所以程序運行的結果基本上是父子進程交替打印,但這也不是一定的,取決于系統中其它進程的運行情況和內核的調度算法,如果系統中其它進程非常繁忙則有可能觀察到不同的結果。另外,讀者也可以把
sleep(1);
去掉看程序的運行結果如何。 -
這個程序是在Shell下運行的,因此Shell進程是父進程的父進程。父進程運行時Shell進程處于等待狀態( 第?3.3?節 “wait和waitpid函數” 會講到這種等待是怎么實現的),當父進程終止時Shell進程認為命令執行結束了,于是打印Shell提示符,而事實上子進程這時還沒結束,所以子進程的消息打印到了Shell提示符后面。最后光標停在
This is the child
的下一行,這時用戶仍然可以敲命令,即使命令不是緊跟在提示符后面,Shell也能正確讀取。
?
fork
函數的特點概括起來就是“
調用一次,返回兩次
”,在父進程中調用一次,在父進程和子進程中各返回一次。從上圖可以看出,一開始是一個控制流程,調用
fork
之后發生了分叉,變成兩個控制流程,這也就是“
fork
”(分叉)這個名字的由來了。子進程中
fork
的返回值是0,而父進程中
fork
的返回值則是子進程的id(從根本上說
fork
是從內核返回的,內核自有辦法讓父進程和子進程返回不同的值),這樣當
fork
函數返回后,程序員可以根據返回值的不同讓父進程和子進程執行不同的代碼。
fork
的返回值這樣規定是有道理的。
fork
在子進程中返回0,子進程仍可以調用
getpid
函數得到自己的進程id,也可以調用
getppid
函數得到父進程的id。在父進程中用
getpid
可以得到自己的進程id,然而要想得到子進程的id,只有將
fork
的返回值記錄下來,別無它法。
fork
的另一個特性是所有由父進程打開的描述符都被復制到子進程中。父、子進程中相同編號的文件描述符在內核中指向同一個
file
結構體,也就是說,
file
結構體的引用計數要增加。
用
gdb
調試多進程的程序會遇到困難,
gdb
只能跟蹤一個進程(默認是跟蹤父進程),而不能同時跟蹤多個進程,但可以設置
gdb
在
fork
之后跟蹤父進程還是子進程。以上面的程序為例:
$ gcc main.c -g $ gdb a.out GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu"... (gdb) l 2 #include <unistd.h> 3 #include <stdio.h> 4 #include <stdlib.h> 5 6 int main(void) 7 { 8 pid_t pid; 9 char *message; 10 int n; 11 pid = fork(); (gdb) 12 if(pid<0) { 13 perror("fork failed"); 14 exit(1); 15 } 16 if(pid==0) { 17 message = "This is the child\n"; 18 n = 6; 19 } else { 20 message = "This is the parent\n"; 21 n = 3; (gdb) b 17 Breakpoint 1 at 0x8048481: file main.c, line 17. (gdb) set follow-fork-mode child (gdb) r Starting program: /home/akaedu/a.out This is the parent [Switching to process 30725] Breakpoint 1, main () at main.c:17 17 message = "This is the child\n"; (gdb) This is the parent This is the parent
set follow-fork-mode child
命令設置
gdb
在
fork
之后跟蹤子進程(
set follow-fork-mode parent
則是跟蹤父進程),然后用
run
命令,看到的現象是父進程一直在運行,在
(gdb)
提示符下打印消息,而子進程被先前設的斷點打斷了。
3.2.?exec函數? 請點評
用
fork
創建子進程后執行的是和父進程相同的程序(但有可能執行不同的代碼分支),子進程往往要調用一種
exec
函數以執行另一個程序。當進程調用一種
exec
函數時,該進程的用戶空間代碼和數據完全被新程序替換,從新程序的啟動例程開始執行。調用
exec
并不創建新進程,所以調用
exec
前后該進程的id并未改變。
其實有六種以
exec
開頭的函數,統稱
exec
函數:
#include <unistd.h> 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[]);
這些函數如果調用成功則加載新的程序從啟動代碼開始執行,不再返回,如果調用出錯則返回-1,所以
exec
函數只有出錯的返回值而沒有成功的返回值。
這些函數原型看起來很容易混,但只要掌握了規律就很好記。不帶字母p(表示path)的
exec
函數第一個參數必須是程序的相對路徑或絕對路徑,例如
"/bin/ls"
或
"./a.out"
,而不能是
"ls"
或
"a.out"
。對于帶字母p的函數:
-
如果參數中包含/,則將其視為路徑名。
-
否則視為不帶路徑的程序名,在
PATH
環境變量的目錄列表中搜索這個程序。
帶有字母l(表示list)的
exec
函數要求將新程序的每個命令行參數都當作一個參數傳給它,命令行參數的個數是可變的,因此函數原型中有
...
,
...
中的最后一個可變參數應該是
NULL
,起sentinel的作用。對于帶有字母v(表示vector)的函數,則應該先構造一個指向各參數的指針數組,然后將該數組的首地址當作參數傳給它,數組中的最后一個指針也應該是
NULL
,就像
main
函數的
argv
參數或者環境變量表一樣。
對于以e(表示environment)結尾的
exec
函數,可以把一份新的環境變量表傳給它,其他
exec
函數仍使用當前的環境變量表執行新程序。
exec
調用舉例如下:
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL}; char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execv("/bin/ps", ps_argv); execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp); execve("/bin/ps", ps_argv, ps_envp); execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); execvp("ps", ps_argv);
事實上,只有
execve
是真正的系統調用,其它五個函數最終都調用
execve
,所以
execve
在man手冊第2節,其它函數在man手冊第3節。這些函數之間的關系如下圖所示。
一個完整的例子:
#include <unistd.h> #include <stdlib.h> int main(void) { execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL); perror("exec ps"); exit(1); }
執行此程序則得到:
$ ./a.out PID PPID PGRP SESS TPGID COMMAND 6614 6608 6614 6614 7199 bash 7199 6614 7199 6614 7199 ps
由于
exec
函數只有錯誤返回值,只要返回了一定是出錯了,所以不需要判斷它的返回值,直接在后面調用
perror
即可。注意在調用
execlp
時傳了兩個
"ps"
參數,第一個
"ps"
是程序名,
execlp
函數要在
PATH
環境變量中找到這個程序并執行它,而第二個
"ps"
是第一個命令行參數,
execlp
函數并不關心它的值,只是簡單地把它傳給
ps
程序,
ps
程序可以通過
main
函數的
argv[0]
取到這個參數。
調用
exec
后,原來打開的文件描述符仍然是打開的
[
37
]
。利用這一點可以實現I/O重定向。先看一個簡單的例子,把標準輸入轉成大寫然后打印到標準輸出:
例?30.4.?upper
/* upper.c */ #include <ctype.h> #include <stdio.h> int main(void) { int ch; while((ch = getchar()) != EOF) { putchar(toupper(ch)); } return 0; }
運行結果如下:
$ ./upper hello THERE HELLO THERE (按Ctrl-D表示EOF) $
使用Shell重定向:
$ cat file.txt this is the file, file.txt, it is all lower case. $ ./upper < file.txt THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.
如果希望把待轉換的文件名放在命令行參數中,而不是借助于輸入重定向,我們可以利用
upper
程序的現有功能,再寫一個包裝程序
wrapper
。
例?30.5.?wrapper
/* wrapper.c */ #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <fcntl.h> int main(int argc, char *argv[]) { int fd; if (argc != 2) { fputs("usage: wrapper file\n", stderr); exit(1); } fd = open(argv[1], O_RDONLY); if(fd<0) { perror("open"); exit(1); } dup2(fd, STDIN_FILENO); close(fd); execl("./upper", "upper", NULL); perror("exec ./upper"); exit(1); }
wrapper
程序將命令行參數當作文件名打開,將標準輸入重定向到這個文件,然后調用
exec
執行
upper
程序,這時原來打開的文件描述符仍然是打開的,
upper
程序只負責從標準輸入讀入字符轉成大寫,并不關心標準輸入對應的是文件還是終端。運行結果如下:
$ ./wrapper file.txt THIS IS THE FILE, FILE.TXT, IT IS ALL LOWER CASE.
3.3.?wait和waitpid函數? 請點評
一個進程在終止時會關閉所有文件描述符,釋放在用戶空間分配的內存,但它的PCB還保留著,內核在其中保存了一些信息:如果是正常終止則保存著退出狀態,如果是異常終止則保存著導致該進程終止的信號是哪個。這個進程的父進程可以調用
wait
或
waitpid
獲取這些信息,然后徹底清除掉這個進程。我們知道一個進程的退出狀態可以在Shell中用特殊變量
$?
查看,因為Shell是它的父進程,當它終止時Shell調用
wait
或
waitpid
得到它的退出狀態同時徹底清除掉這個進程。
如果一個進程已經終止,但是它的父進程尚未調用
wait
或
waitpid
對它進行清理,這時的進程狀態稱為僵尸(Zombie)
進程。任何進程在剛終止時都是僵尸進程,正常情況下,僵尸進程都立刻被父進程清理了,為了觀察到僵尸進程,我們自己寫一個不正常的程序,父進程
fork
出子進程,子進程終止,而父進程既不終止也不調用
wait
清理子進程:
#include <unistd.h> #include <stdlib.h> int main(void) { pid_t pid=fork(); if(pid<0) { perror("fork"); exit(1); } if(pid>0) { /* parent */ while(1); } /* child */ return 0; }
在后臺運行這個程序,然后用
ps
命令查看:
$ ./a.out & [1] 6130 $ ps u USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND akaedu 6016 0.0 0.3 5724 3140 pts/0 Ss 08:41 0:00 bash akaedu 6130 97.2 0.0 1536 284 pts/0 R 08:44 14:33 ./a.out akaedu 6131 0.0 0.0 0 0 pts/0 Z 08:44 0:00 [a.out] <defunct> akaedu 6163 0.0 0.0 2620 1000 pts/0 R+ 08:59 0:00 ps u
在
./a.out
命令后面加個
&
表示后臺運行,Shell不等待這個進程終止就立刻打印提示符并等待用戶輸命令。現在Shell是位于前臺的,用戶在終端的輸入會被Shell讀取,后臺進程是讀不到終端輸入的。第二條命令
ps u
是在前臺運行的,在此期間Shell進程和
./a.out
進程都在后臺運行,等到
ps u
命令結束時Shell進程又重新回到前臺。在
第?33?章?
信號
和
第?34?章?
終端、作業控制與守護進程
將會進一步解釋前臺(Foreground)
和后臺(Backgroud)
的概念。
父進程的pid是6130,子進程是僵尸進程,pid是6131,
ps
命令顯示僵尸進程的狀態為
Z
,在命令行一欄還顯示
<defunct>
。
如果一個父進程終止,而它的子進程還存在(這些子進程或者仍在運行,或者已經是僵尸進程了),則這些子進程的父進程改為
init
進程。
init
是系統中的一個特殊進程,通常程序文件是
/sbin/init
,進程id是1,在系統啟動時負責啟動各種系統服務,之后就負責清理子進程,只要有子進程終止,
init
就會調用
wait
函數清理它。
僵尸進程是不能用
kill
命令清除掉的,因為
kill
命令只是用來終止進程的,而僵尸進程已經終止了。思考一下,用什么辦法可以清除掉僵尸進程?
wait
和
waitpid
函數的原型是:
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t pid, int *status, int options);
若調用成功則返回清理掉的子進程id,若調用出錯則返回-1。父進程調用
wait
或
waitpid
時可能會:
-
阻塞(如果它的所有子進程都還在運行)。
-
帶子進程的終止信息立即返回(如果一個子進程已終止,正等待父進程讀取其終止信息)。
-
出錯立即返回(如果它沒有任何子進程)。
這兩個函數的區別是:
-
如果父進程的所有子進程都還在運行,調用
wait
將使父進程阻塞,而調用waitpid
時如果在options
參數中指定WNOHANG
可以使父進程不阻塞而立即返回0。 -
wait
等待第一個終止的子進程,而waitpid
可以通過pid
參數指定等待哪一個子進程。
可見,調用
wait
和
waitpid
不僅可以獲得子進程的終止信息,還可以使父進程阻塞等待子進程終止,起到進程間同步的作用。如果參數
status
不是空指針,則子進程的終止信息通過這個參數傳出,如果只是為了同步而不關心子進程的終止信息,可以將
status
參數指定為
NULL
。
例?30.6.?waitpid
#include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> int main(void) { pid_t pid; pid = fork(); if (pid < 0) { perror("fork failed"); exit(1); } if (pid == 0) { int i; for (i = 3; i > 0; i--) { printf("This is the child\n"); sleep(1); } exit(3); } else { int stat_val; waitpid(pid, &stat_val, 0); if (WIFEXITED(stat_val)) printf("Child exited with code %d\n", WEXITSTATUS(stat_val)); else if (WIFSIGNALED(stat_val)) printf("Child terminated abnormally, signal %d\n", WTERMSIG(stat_val)); } return 0; }
子進程的終止信息在一個
int
中包含了多個字段,用宏定義可以取出其中的每個字段:如果子進程是正常終止的,
WIFEXITED
取出的字段值非零,
WEXITSTATUS
取出的字段值就是子進程的退出狀態;如果子進程是收到信號而異常終止的,
WIFSIGNALED
取出的字段值非零,
WTERMSIG
取出的字段值就是信號的編號。作為練習,請讀者從頭文件里查一下這些宏做了什么運算,是如何取出字段值的。
4.?進程間通信? 請點評
每個進程各自有不同的用戶地址空間,任何一個進程的全局變量在另一個進程中都看不到,所以進程之間要交換數據必須通過內核,在內核中開辟一塊緩沖區,進程1把數據從用戶空間拷到內核緩沖區,進程2再從內核緩沖區把數據讀走,內核提供的這種機制稱為進程間通信(IPC,InterProcess Communication) 。如下圖所示。
4.1.?管道? 請點評
管道是一種最基本的IPC機制,由
pipe
函數創建:
#include <unistd.h> int pipe(int filedes[2]);
調用
pipe
函數時在內核中開辟一塊緩沖區(稱為管道)用于通信,它有一個讀端一個寫端,然后通過
filedes
參數傳出給用戶程序兩個文件描述符,
filedes[0]
指向管道的讀端,
filedes[1]
指向管道的寫端(很好記,就像0是標準輸入1是標準輸出一樣)。所以管道在用戶程序看起來就像一個打開的文件,通過
read(filedes[0]);
或者
write(filedes[1]);
向這個文件讀寫數據其實是在讀寫內核緩沖區。
pipe
函數調用成功返回0,調用失敗返回-1。
開辟了管道之后如何實現兩個進程間的通信呢?比如可以按下面的步驟通信。
-
父進程調用
pipe
開辟管道,得到兩個文件描述符指向管道的兩端。 -
父進程調用
fork
創建子進程,那么子進程也有兩個文件描述符指向同一管道。 -
父進程關閉管道讀端,子進程關閉管道寫端。父進程可以往管道里寫,子進程可以從管道里讀,管道是用環形隊列實現的,數據從寫端流入從讀端流出,這樣就實現了進程間通信。
例?30.7.?管道
#include <stdlib.h> #include <unistd.h> #define MAXLINE 80 int main(void) { int n; int fd[2]; pid_t pid; char line[MAXLINE]; if (pipe(fd) < 0) { perror("pipe"); exit(1); } if ((pid = fork()) < 0) { perror("fork"); exit(1); } if (pid > 0) { /* parent */ close(fd[0]); write(fd[1], "hello world\n", 12); wait(NULL); } else { /* child */ close(fd[1]); n = read(fd[0], line, MAXLINE); write(STDOUT_FILENO, line, n); } return 0; }
使用管道有一些限制:
-
兩個進程通過一個管道只能實現單向通信,比如上面的例子,父進程寫子進程讀,如果有時候也需要子進程寫父進程讀,就必須另開一個管道。請讀者思考,如果只開一個管道,但是父進程不關閉讀端,子進程也不關閉寫端,雙方都有讀端和寫端,為什么不能實現雙向通信?
-
管道的讀寫端通過打開的文件描述符來傳遞,因此要通信的兩個進程必須從它們的公共祖先那里繼承管道文件描述符。上面的例子是父進程把文件描述符傳給子進程之后父子進程之間通信,也可以父進程
fork
兩次,把文件描述符傳給兩個子進程,然后兩個子進程之間通信,總之需要通過fork
傳遞文件描述符使兩個進程都能訪問同一管道,它們才能通信。
使用管道需要注意以下4種特殊情況(假設都是阻塞I/O操作,沒有設置
O_NONBLOCK
標志):
-
如果所有指向管道寫端的文件描述符都關閉了(管道寫端的引用計數等于0),而仍然有進程從管道的讀端讀數據,那么管道中剩余的數據都被讀取后,再次
read
會返回0,就像讀到文件末尾一樣。 -
如果有指向管道寫端的文件描述符沒關閉(管道寫端的引用計數大于0),而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次
read
會阻塞,直到管道中有數據可讀了才讀取數據并返回。 -
如果所有指向管道讀端的文件描述符都關閉了(管道讀端的引用計數等于0),這時有進程向管道的寫端
write
,那么該進程會收到信號SIGPIPE
,通常會導致進程異常終止。在 第?33?章? 信號 會講到怎樣使SIGPIPE
信號不終止進程。 -
如果有指向管道讀端的文件描述符沒關閉(管道讀端的引用計數大于0),而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那么在管道被寫滿時再次
write
會阻塞,直到管道中有空位置了才寫入數據并返回。
管道的這四種特殊情況具有普遍意義。在 第?37?章? socket編程 要講的TCP socket也具有管道的這些特性。
習題? 請點評
1、在 例?30.7 “管道” 中,父進程只用到寫端,因而把讀端關閉,子進程只用到讀端,因而把寫端關閉,然后互相通信,不使用的讀端或寫端必須關閉,請讀者想一想如果不關閉會有什么問題。
2、請讀者修改 例?30.7 “管道” 的代碼和實驗條件,驗證我上面所說的四種特殊情況。
4.2.?其它IPC機制? 請點評
進程間通信必須通過內核提供的通道,而且必須有一種辦法在進程中標識內核提供的某個通道,上一節講的管道是用打開的文件描述符來標識的。如果要互相通信的幾個進程沒有從公共祖先那里繼承文件描述符,它們怎么通信呢?內核提供一條通道不成問題,問題是如何標識這條通道才能使各進程都可以訪問它?文件系統中的路徑名是全局的,各進程都可以訪問,因此可以用文件系統中的路徑名來標識一個IPC通道。
FIFO和UNIX Domain Socket這兩種IPC機制都是利用文件系統中的特殊文件來標識的。可以用
mkfifo
命令創建一個FIFO文件:
$ mkfifo hello $ ls -l hello prw-r--r-- 1 akaedu akaedu 0 2008-10-30 10:44 hello
FIFO文件在磁盤上沒有數據塊,僅用來標識內核中的一條通道,各進程可以打開這個文件進行
read
/
write
,實際上是在讀寫內核通道(根本原因在于這個
file
結構體所指向的
read
、
write
函數和常規文件不一樣),這樣就實現了進程間通信。UNIX Domain Socket和FIFO的原理類似,也需要一個特殊的socket文件來標識內核中的通道,例如
/var/run
目錄下有很多系統服務的socket文件:
$ ls -l /var/run/ total 52 srw-rw-rw- 1 root root 0 2008-10-30 00:24 acpid.socket ... srw-rw-rw- 1 root root 0 2008-10-30 00:25 gdm_socket ... srw-rw-rw- 1 root root 0 2008-10-30 00:24 sdp ... srwxr-xr-x 1 root root 0 2008-10-30 00:42 synaptic.socket
文件類型s表示socket,這些文件在磁盤上也沒有數據塊。UNIX Domain Socket是目前最廣泛使用的IPC機制,到后面講socket編程時再詳細介紹。
現在把進程之間傳遞信息的各種途徑(包括各種IPC機制)總結如下:
-
父進程通過
fork
可以將打開文件的描述符傳遞給子進程 -
子進程結束時,父進程調用
wait
可以得到子進程的終止信息 -
幾個進程可以在文件系統中讀寫某個共享文件,也可以通過給文件加鎖來實現進程間同步
-
進程之間互發信號,一般使用
SIGUSR1
和SIGUSR2
實現用戶自定義功能 -
管道
-
FIFO
-
mmap函數,幾個進程可以映射同一內存區
-
SYS V IPC,以前的SYS V UNIX系統實現的IPC機制,包括消息隊列、信號量和共享內存,現在已經基本廢棄
-
UNIX Domain Socket,目前最廣泛使用的IPC機制
5.?練習:實現簡單的Shell? 請點評
用講過的各種C函數實現一個簡單的交互式Shell,要求:
1、給出提示符,讓用戶輸入一行命令,識別程序名和參數并調用適當的
exec
函數執行程序,待執行完成后再次給出提示符。
2、識別和處理以下符號:
-
簡單的標準輸入輸出重定向(<和>):仿照 例?30.5 “wrapper” ,先
dup2
然后exec
。 -
管道(|):Shell進程先調用
pipe
創建一對管道描述符,然后fork
出兩個子進程,一個子進程關閉讀端,調用dup2
把寫端賦給標準輸出,另一個子進程關閉寫端,調用dup2
把讀端賦給標準輸入,兩個子進程分別調用exec
執行程序,而Shell進程把管道的兩端都關閉,調用wait
等待兩個子進程終止。
你的程序應該可以處理以下命令:
○ls△-l△-R○>○file1○
○cat○<○file1○|○wc△-c○>○file1○
○表示零個或多個空格,△表示一個或多個空格
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
