linux 的多进程运行机制,Linux 多进程-2
揭秘文件描述符的本質(zhì)
1. 文件描述符的本質(zhì)是數(shù)組元素的下標(biāo)
右側(cè)的表稱為 i 節(jié)點表,在整個系統(tǒng)中只有1張。該表可以視為結(jié)構(gòu)體數(shù)組,該數(shù)組的一個元素對應(yīng)于一個物理文件。
中間的表稱為文件表,在整個系統(tǒng)中只有1張。該表可以視為結(jié)構(gòu)體數(shù)組,一個結(jié)構(gòu)體中有很多字段,其中有3個字段比較重要:
file status flags:用于記錄文件被打開來讀的,還是寫的。其實記錄的就是 open 調(diào)用中用戶指定的第2個參數(shù)
current file offset:用于記錄文件的當(dāng)前讀寫位置(指針)。正是由于此字段的存在,使得一個文件被打開并讀取后,下一次讀取將從上一次讀取的字符后開始讀取
v-node ptr:該字段是指針,指向右側(cè)表的一個元素,從而關(guān)聯(lián)了物理文件。左側(cè)的表稱為文件描述符表,每個進(jìn)程有且僅有1張。該表可以視為指針數(shù)組,數(shù)組的元素指向文件表的一個元素。最重要的是:數(shù)組元素的下標(biāo)就是大名鼎鼎的文件描述符。
open 系統(tǒng)調(diào)用執(zhí)行的操作:新建一個 i 節(jié)點表元素,讓其對應(yīng)打開的物理文件(如果對應(yīng)于該物理文件的 i 節(jié)點元素已經(jīng)建立,就不做任何操作);
新建一個文件表的元素,根據(jù) open 的第2個參數(shù)設(shè)置 file status flags 字段,將 current file offset 字段置0,將 v-node ptr 指向剛建立的 i 節(jié)點表元素;
在文件描述符表中,尋找1個尚未使用的元素,在該元素中填入一個指針值,讓其指向剛建立的文件表元素。最重要的是:將該元素的下標(biāo)作為 open 的返回值返回。
這樣一來,當(dāng)調(diào)用 read(write) 時,根據(jù)傳入的文件描述符,OS 就可以找到對應(yīng)的文件描述符表元素,進(jìn)而找到文件表的元素,進(jìn)而找到 i 節(jié)點表元素,從而完成對物理文件的讀寫。
2. fork 對文件描述符的影響
fork 會導(dǎo)致子進(jìn)程繼承父進(jìn)程打開的文件描述符,其本質(zhì)是將父進(jìn)程的整個文件描述符表復(fù)制一份,放到子進(jìn)程的 PCB 中。因此父、子進(jìn)程中相同文件描述符(文件描述符為整數(shù))指向的是同一個文件表元素,這將導(dǎo)致父(子)進(jìn)程讀取文件后,子(父)進(jìn)程將讀取同一文件的后續(xù)內(nèi)容。
案例分析(forkfd.c):
1 #include
2 #include
3 #include
4 #include
5 #include
6 #include
7
8 int main(void)
9 {
10 int fd, pid, status;
11 char buf[10];
12 if ((fd = open("./test.txt", O_RDONLY)) < 0) {
13 perror("open"); exit(-1);
14 }
15 if ((pid = fork()) < 0) {
16 perror("fork"); exit(-1);
17 } else if (pid == 0) { //child
18 read(fd, buf, 2);
19 write(STDOUT_FILENO, buf, 2);
20 } else { //parent
21 sleep(2);
23 lseek(fd, SEEK_CUR, 1);
24 read(fd, buf, 3);
25 write(STDOUT_FILENO, buf, 3);
26 write(STDOUT_FILENO, "\n", 1);
27 }
28 return 0;
29 }
假設(shè)./test.txt 的內(nèi)容是 abcdefg,那么子進(jìn)程的18行將讀到字符 ab;由于父、子進(jìn)程的文件描述符 fd 都指向同一個文件表元素,因此當(dāng)父進(jìn)程執(zhí)行23行時,fd 對應(yīng)的文件的讀寫指針將移動到字符 d,而不是字符 b,從而24行讀到的是字符 def ,而不是字符 bcd 。程序運(yùn)行的最終結(jié)果是打印 abdef,而不是 abbcd。
相對應(yīng)的,如果是兩個進(jìn)程獨立調(diào)用 open 去打開同一個物理文件,就會有2個文件表元素被創(chuàng)建,并且他們都指向同一個 i 節(jié)點表元素。兩個文件表元素都有自己獨立的 current file offset 字段,這將導(dǎo)致2個進(jìn)程獨立的對同一個物理文件進(jìn)行讀寫,因此第1個進(jìn)程讀取到文件的第1個字符后,第2個進(jìn)程再去讀取該文件時,仍然是讀到的是文件的第1個字符,而不是第1個字符的后續(xù)字符。
對應(yīng)用程序員而言,最重要結(jié)論是: 如果子進(jìn)程不打算使用父進(jìn)程打開的文件,那么應(yīng)該在 fork 返回后立即調(diào)用 close 關(guān)閉該文件。
父子進(jìn)程同步的功臣— wait
1. wait 的作用
在 forkbase.c 中,fork 出子進(jìn)程后,為了保證子進(jìn)程先于父進(jìn)程運(yùn)行,在父進(jìn)程中使用了 sleep(2) 的方式讓父進(jìn)程睡眠2秒。但實際上這樣做,并不能100%保證子進(jìn)程先于父進(jìn)程運(yùn)行,因為在負(fù)荷非常重的系統(tǒng)中,有可能在父進(jìn)程睡眠2秒期間,OS 并沒有調(diào)度到子進(jìn)程運(yùn)行,并且當(dāng)父進(jìn)程睡醒后,首先調(diào)度到父進(jìn)程運(yùn)行。那么,如何才能100%保證父、子進(jìn)程完全按程序員的安排來進(jìn)行同步呢?答案是:系統(tǒng)調(diào)用 wait!
需要包含的頭文件: 、
函數(shù)原型:pid_t wait(int * status)
功能:等待進(jìn)程結(jié)束。
返回值:若成功則為子進(jìn)程ID號,若出錯則為-1.
參數(shù)說明: status:用于存放進(jìn)程結(jié)束狀態(tài)。
wait 函數(shù)用于使父進(jìn)程阻塞,直到一個子進(jìn)程結(jié)束。父進(jìn)程調(diào)用 wait,該父進(jìn)程可能會:
阻塞(如果其所有子進(jìn)程都還在運(yùn)行)。
帶子進(jìn)程的終止?fàn)顟B(tài)立即返回(如果一個子進(jìn)程已終止,正等待父進(jìn)程存取其終止?fàn)顟B(tài))。
出錯立即返回(如果它沒有任何子進(jìn)程)。
2. 調(diào)用 wait 的實例
wait.c
1 #include
2 #include
3 #include
4 #include
5 #include
6 void pr_exit(intstatus);
7 int main(void)
8 {
9 pid_t pid;
10 int status;
11 if ( (pid = fork()) < 0)
12 { perror("fork");exit(-1); }
13 else if (pid == 0) { /* child */
14 sleep(1);
15 printf("inchild\n");
16 exit(101);
17 }
18 if (wait(&status) != pid) /* wait for child */
19 { perror("wait");exit(-2); }
20 printf("in parent\n");
21 pr_exit(status); /* and print itsstatus */
22 if ( (pid = fork()) < 0)
23 { perror("fork");exit(-1); }
24 else if (pid == 0) /*child */
25 abort(); /* generates SIGABRT */
26 if (wait(&status) != pid) /* wait for child */
27 { perror("wait");exit(-2); }
28 pr_exit(status); /* and printits status */
29 if ( (pid = fork()) < 0)
30 { perror("fork");exit(-1); }
31 else if (pid == 0) /*child */
32 status /= 0; /* divide by 0 generates SIGFPE */
33 if (wait(&status) != pid) /* wait for child */
34 { perror("wait");exit(-1); }
35 pr_exit(status); /* and printits status */
36 exit(0);
37 }
38 void pr_exit(int status) {
39 if (WIFEXITED(status))
40 printf("normallytermination, low-order 8 bit of exit status = %d\n", WEXITSTATUS(status));
41 else if(WIFSIGNALED(status))
42 printf("abnormallytermination, singal number = %d\n", WTERMSIG(status));
43 }
運(yùn)行結(jié)果分析:
11行創(chuàng)建了一個子進(jìn)程,13行根據(jù) fork 的返回值區(qū)分父、子進(jìn)程。 我們先看父進(jìn)程,父進(jìn)程從18行運(yùn)行,這里調(diào)用了 wait 函數(shù)等待子進(jìn)程結(jié)束,并將子進(jìn)程結(jié)束的狀態(tài)保存在 status 中。這時,父進(jìn)程就阻塞在 wait 這里了,這樣就保證了子進(jìn)程先運(yùn)行。子進(jìn)程從13行開始運(yùn)行,然后 sleep 1秒,打印出“in child”后,調(diào)用exit函數(shù)退出進(jìn)程。這里exit中有個參數(shù)101,表示退出的值是101。.子進(jìn)程退出后,父進(jìn)程 wait 到了子進(jìn)程的狀態(tài),并把狀態(tài)保存到了 status 中。后面的 pr_exit 函數(shù)是用來對進(jìn)程的退出狀態(tài)進(jìn)行打印。接下來,父進(jìn)程又創(chuàng)建一個子進(jìn)程,然后又一次調(diào)用 wait 函數(shù)等待子進(jìn)程結(jié)束,父進(jìn)程這時候阻塞在了 wait 這里。子進(jìn)程開始執(zhí)行,子進(jìn)程里面只有一句話:abort(),abort 會結(jié)束子進(jìn)程并發(fā)送一個 SIGABORT 信號,喚醒父進(jìn)程。所以父進(jìn)程會接受到一個 SIGABRT 信號,并將子進(jìn)程的退出狀態(tài)保存到 status 中。然后調(diào)用 pr_exit 函數(shù)打印出子進(jìn)程結(jié)束的狀態(tài)。然后父進(jìn)程再次創(chuàng)建了一個子進(jìn)程,依然用 wait 函數(shù)等待子進(jìn)程結(jié)束并獲取子進(jìn)程退出時的狀態(tài)。子進(jìn)程里面就一句 status/= 0,這里用0做了除數(shù),所以子進(jìn)程會終止,并發(fā)送一個 SIGFPE 信號,這個信號是用來表示浮點運(yùn)算異常,比如運(yùn)算溢出,除數(shù)不能為0等。這時候父進(jìn)程 wait 函數(shù)會捕捉到子進(jìn)程的退出狀態(tài),然后調(diào)用 pr_exit 處理。 pr_exit 函數(shù)將 status 狀態(tài)傳入,然后判斷該狀態(tài)是不是正常退出,如果是正常退出會打印出退出值;不是正常退出會打印出退出時的異常信號。這里用到了幾個宏,簡單解釋如下:
WIFEXITED: 這個宏是用來判斷子進(jìn)程的返回狀態(tài)是不是為正常,如果是正常退出,這個宏返回真。
WEXITSTATUS: 用來返回子進(jìn)程正常退出的狀態(tài)值。
WIFSIGNALED: 用來判斷子進(jìn)程的退出狀態(tài)是否是非正常退出,若非正常退出時發(fā)送信號,則該宏返回真。
WTERMSIG: 用來返回非正常退出狀態(tài)的信號 number。 所以這段代碼的結(jié)果是分別打印出了三個子進(jìn)程的退出狀態(tài)和異常結(jié)束的信號編號。
進(jìn)程控制地字第1號系統(tǒng)調(diào)用 — exec
當(dāng)一個程序調(diào)用 fork 產(chǎn)生子進(jìn)程,通常是為了讓子進(jìn)程去完成不同于父進(jìn)程的某項任務(wù),因此含有 fork 的程序,通常的編程模板如下:
if ((pid = fork()) == 0) {
dosomething in child process;
exit(0);
}
do something in parent process;
這樣的編程模板使得父、子進(jìn)程各自執(zhí)行同一個二進(jìn)制文件中的不同代碼段,完成不同的任務(wù)。這樣的編程模板在大多數(shù)情況下都能勝任,但仔細(xì)觀察這種編程模板,你會發(fā)現(xiàn)它要求程序員在編寫源代碼的時候,就要預(yù)先知道子進(jìn)程要完成的任務(wù)是什么。這本不是什么過分的要求,但在某些情況下,這樣的前提要求卻得不到滿足,最典型的例子就是 Linux 的基礎(chǔ)應(yīng)用程序 —— shell。你想一想,在編寫 shell 的源代碼期間,程序員是不可能知道當(dāng) shell 運(yùn)行時,用戶輸入的命令是 ls 還是 cp,難道你要在 shell 的源代碼中使用 if--elseif--else if--else if …… 結(jié)構(gòu),并拷貝 ls、cp 等等外部命令的源代碼到 shell 源代碼中嗎?退一萬步講,即使這種弱智的處理方式被接受的話,你仍然會遇到無法解決的難題。想一想,如果用戶自己編寫了一個源程序,并將其編譯為二進(jìn)制程序 test,然后再在 shell 命令提示符下輸入./test,對于采用前述弱智方法編寫的 shell,它將情何以堪?
看來天字1號雖然很牛,但亦難以獨木擎天,必要情況下,也需要地字1號予以協(xié)作,啊,偉大的團(tuán)隊精神!
1. exec 的機(jī)制和用法
下面就詳細(xì)介紹一下進(jìn)程控制地字第1號系統(tǒng)調(diào)用——exec 的機(jī)制和用法。
(1)exec 的機(jī)制:
在用 fork 函數(shù)創(chuàng)建子進(jìn)程后,子進(jìn)程往往要調(diào)用 exec 函數(shù)以執(zhí)行另一個程序。 當(dāng)子進(jìn)程調(diào)用 exec 函數(shù)時,會將一個二進(jìn)制可執(zhí)行程序的全路徑名作為參數(shù)傳給exec,exec 會用新程序代換子進(jìn)程原來全部進(jìn)程空間的內(nèi)容,而新程序則從其 main 函數(shù)開始執(zhí)行,這樣子進(jìn)程要完成的任務(wù)就變成了新程序要完成的任務(wù)了。 因為調(diào)用exec并不創(chuàng)建新進(jìn)程,所以前后的進(jìn)程 ID 并未改變。exec 只是用另一個新程序替換了當(dāng)前進(jìn)程的正文、數(shù)據(jù)、堆和棧段。進(jìn)程還是那個進(jìn)程,但實質(zhì)內(nèi)容已經(jīng)完全改變。呵呵,這是不是和中國A股的借殼上市有異曲同工之妙? 順便說一下,新程序的 bss 段清0這個操作,以及命令行參數(shù)和環(huán)境變量的指定,也是由 exec 完成的。
(2)exec 的用法:
函數(shù)原型: int execle(const char * pathname,const char * arg0, ... (char *)0, char * const envp [] )
返回值: exec 執(zhí)行失敗返回-1,成功將永不返回(想想為什么?)。哎,牛人就是有脾氣,天字1號是調(diào)用1次,返回2次;地字1號,干脆就不返回了,你能奈我何?
參數(shù):
pathname: 新程序的二進(jìn)制文件的全路徑名
arg0:新程序的第1個命令行參數(shù) argv[0],之后是新程序的第2、3、4……個命令行參數(shù),以 (char*)0 表示命令行參數(shù)的結(jié)束
envp:新程序的環(huán)境變量
2. exec 的使用實例
echoall.c
1 #include
2 #include
3 #include
4
5 int main(int argc, char*argv[])
6 {
7 int i;
8 char **ptr;
9 extern char **environ;
10 for (i = 0; i < argc; i++) /* echo all command-line args */
11 printf("argv[%d]:%s\n", i, argv[i]);
12 for (ptr = environ; *ptr != 0;ptr++) /* and all env strings */
13 printf("%s\n",*ptr);
21 }
將此程序進(jìn)行編譯,生成二進(jìn)制文件命名為 echoall,放在當(dāng)前目錄下。很容易看出,此程序運(yùn)行將打印進(jìn)程的所有命令行參數(shù)和環(huán)境變量。
!源文件過長,請直接查看源代碼 exec.c
程序運(yùn)行結(jié)果:
1 argv[0]: echoall
2 argv[1]: myarg1
3 argv[2]: MY ARG2
4 USER=unknown
5 PATH=/tmp
6 argv[0]: echoall
7 argv[1]: only 1 arg
8 ORBIT_SOCKETDIR=/tmp/orbit-dennis
9 SSH_AGENT_PID=1792
10 TERM=xterm
11 SHELL=/bin/bash
12 XDG_SESSION_COOKIE=0a13eccc45d521c3eb847f7b4bf75275-1320116445.669339
13 GTK_RC_FILES=/etc/gtk/gtkrc:/home/dennis/.gtkrc-1.2-gnome2
14 WINDOWID=62919986
15 GTK_MODULES=canberra-gtk-module
16 USER=dennis
.......
運(yùn)行結(jié)果分析: 1-5行是第1個子進(jìn)程14行運(yùn)行新程序 echoall 的結(jié)果,其中:1-3行打印的是命令行參數(shù);4、5行打印的是環(huán)境變量。 6行之后是第2個子進(jìn)程23行運(yùn)行新程序 echoall 的結(jié)果,其中:6、7行打印的是命令行參數(shù);8行之后打印的是環(huán)境變量。之所以第2個子進(jìn)程的環(huán)境變量那么多,是因為程序23行調(diào)用 execlp 時,沒有給出環(huán)境變量參數(shù),因此子進(jìn)程就會繼承父進(jìn)程的全部環(huán)境變量。
總結(jié)
以上是生活随笔為你收集整理的linux 的多进程运行机制,Linux 多进程-2的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux内核优化哪些参数,linux内
- 下一篇: MySQL 逻辑架构与常用的存储引擎