亚洲免费在线-亚洲免费在线播放-亚洲免费在线观看-亚洲免费在线观看视频-亚洲免费在线看-亚洲免费在线视频

Linux系統編程--進程

系統 1841 0

進程? 請點評

目錄

1. 引言
2. 環境變量
3. 進程控制
3.1. fork函數
3.2. exec函數
3.3. wait和waitpid函數
4. 進程間通信
4.1. 管道
4.2. 其它IPC機制
5. 練習:實現簡單的Shell

1.?引言? 請點評

我們知道,每個進程在內核中都有一個進程控制塊(PCB)來維護進程相關的信息,Linux內核的進程控制塊是 task_struct 結構體。現在我們全面了解一下其中都有哪些信息。

  • 進程id。系統中每個進程有唯一的id,在C語言中用 pid_t 類型表示,其實就是一個非負整數。

  • 進程的狀態,有運行、掛起、停止、僵尸等狀態。

  • 進程切換時需要保存和恢復的一些CPU寄存器。

  • 描述虛擬地址空間的信息。

  • 描述控制終端的信息。

  • 當前工作目錄(Current Working Directory)

  • umask 掩碼。

  • 文件描述符表,包含很多指向 file 結構體的指針。

  • 和信號相關的信息。

  • 用戶id和組id。

  • 控制終端、Session和進程組。

  • 進程可以使用的資源上限(Resource Limit)

目前讀者并不需要理解這些信息的細節,在隨后幾章中講到某一項時會再次提醒讀者它是保存在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 ,如下圖所示。

圖?30.1.?fork/exec

?

第?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 函數,它們在整個進程地址空間中的位置如下圖所示。

圖?30.2.?進程地址空間

?

和命令行參數 argv 類似,環境變量表也是一組字符串,如下圖所示。

圖?30.3.?環境變量

?

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
      

這個程序的運行過程如下圖所示。

圖?30.4.?fork


  1. 父進程初始化。

  2. 父進程調用 fork ,這是一個系統調用,因此進入內核。

  3. 內核根據父進程復制出一個子進程,父進程和子進程的PCB信息相同,用戶態代碼和數據也相同。因此, 子進程現在的狀態看起來和父進程一樣,做完了初始化,剛調用了 fork 進入內核,還沒有從內核返回

  4. 現在有兩個一模一樣的進程看起來都調用了 fork 進入內核等待從內核返回(實際上 fork 只調用了一次),此外系統中還有很多別的進程也等待從內核返回。是父進程先返回還是子進程先返回,還是這兩個進程都等待,先去調度執行別的進程,這都不一定,取決于內核的調度算法。

  5. 如果某個時刻父進程被調度執行了,從內核返回后就從 fork 函數返回,保存在變量 pid 中的返回值是子進程的id,是一個大于0的整數,因此執行下面的 else 分支,然后執行 for 循環,打印 "This is the parent\n" 三次之后終止。

  6. 如果某個時刻子進程被調度執行了,從內核返回后就從 fork 函數返回,保存在變量 pid 中的返回值是0,因此執行下面的 if (pid == 0) 分支,然后執行 for 循環,打印 "This is the child\n" 六次之后終止。 fork 調用把父進程的數據復制一份給子進程,但此后二者互不影響,在這個例子中, fork 調用之后父進程和子進程的變量 message n 被賦予不同的值,互不影響。

  7. 父進程每打印一條消息就睡眠1秒,這時內核調度別的進程執行,在1秒這么長的間隙里(對于計算機來說1秒很長了)子進程很有可能被調度到。同樣地,子進程每打印一條消息就睡眠1秒,在這1秒期間父進程也很有可能被調度到。所以程序運行的結果基本上是父子進程交替打印,但這也不是一定的,取決于系統中其它進程的運行情況和內核的調度算法,如果系統中其它進程非常繁忙則有可能觀察到不同的結果。另外,讀者也可以把 sleep(1); 去掉看程序的運行結果如何。

  8. 這個程序是在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節。這些函數之間的關系如下圖所示。

圖?30.5.?exec函數族


一個完整的例子:

        #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) 。如下圖所示。

圖?30.6.?進程間通信


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。

開辟了管道之后如何實現兩個進程間的通信呢?比如可以按下面的步驟通信。

圖?30.7.?管道


  1. 父進程調用 pipe 開辟管道,得到兩個文件描述符指向管道的兩端。

  2. 父進程調用 fork 創建子進程,那么子進程也有兩個文件描述符指向同一管道。

  3. 父進程關閉管道讀端,子進程關閉管道寫端。父進程可以往管道里寫,子進程可以從管道里讀,管道是用環形隊列實現的,數據從寫端流入從讀端流出,這樣就實現了進程間通信。

例?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 標志):

  1. 如果所有指向管道寫端的文件描述符都關閉了(管道寫端的引用計數等于0),而仍然有進程從管道的讀端讀數據,那么管道中剩余的數據都被讀取后,再次 read 會返回0,就像讀到文件末尾一樣。

  2. 如果有指向管道寫端的文件描述符沒關閉(管道寫端的引用計數大于0),而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那么管道中剩余的數據都被讀取后,再次 read 會阻塞,直到管道中有數據可讀了才讀取數據并返回。

  3. 如果所有指向管道讀端的文件描述符都關閉了(管道讀端的引用計數等于0),這時有進程向管道的寫端 write ,那么該進程會收到信號 SIGPIPE ,通常會導致進程異常終止。在 第?33?章? 信號 會講到怎樣使 SIGPIPE 信號不終止進程。

  4. 如果有指向管道讀端的文件描述符沒關閉(管道讀端的引用計數大于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○
○表示零個或多個空格,△表示一個或多個空格

Linux系統編程--進程


更多文章、技術交流、商務合作、聯系博主

微信掃碼或搜索:z360901061

微信掃一掃加我為好友

QQ號聯系: 360901061

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

【本文對您有幫助就好】

您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描上面二維碼支持博主2元、5元、10元、自定義金額等您想捐的金額吧,站長會非常 感謝您的哦!!!

發表我的評論
最新評論 總共0條評論
主站蜘蛛池模板: 精品哟哟哟国产在线观看不卡 | 欧美一级午夜免费视频你懂的 | 国产精品一区在线免费观看 | 久草综合在线视频 | 天天干天天操 | 激情一区二区三区 | h片在线免费观看 | 亚洲精品视频在线播放 | 国产亚洲精品成人a在线 | 欧美69xx| 正在播放一区二区 | 国产精品视频一区二区三区 | 一级特黄a视频 | 99精品国产在热久久 | 日本xoxo | 国产在线观看自拍 | 超级碰碰青草免费视频92 | 免费爱爱| 亚洲国产欧美在线观看 | 一级看片 | 久久免费观看视频 | 在线欧美精品国产综合五月 | 韩国亚洲伊人久久综合影院 | 曰批免费视频播放在线看片一 | 国产日产久久 | 欧美线人一区二区三区 | 久久婷婷久久一区二区三区 | 欧美成人性视频播放 | 亚洲成aⅴ人片在线观 | 国产中日韩一区二区三区 | 美女在线看永久免费网址 | 国产精品柳州莫菁身材四 | 一级毛片卡 | 国产伦精品一区三区视频 | 97视频免费播放观看在线视频 | 国产一区亚洲二区三区 | 在线 色 | 天天躁日日2018躁狠狠躁 | 人人爱天天做夜夜爽88 | 久久久伊香蕉网站 | 999热精品这里在线观看 |