管道(pipe)
管道可用于具有親緣關(guān)系進程間的通信,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關(guān)系進程間的通信。
實現(xiàn)機制:
管道是由內(nèi)核管理的一個緩沖區(qū),相當(dāng)于我們放入內(nèi)存中的一個紙條。管道的一端連接一個進程的輸出。這個進程會向管道中放入信息。
管道的另一端連接一個進程的輸入,這個進程取出被放入管道的信息。
一個緩沖區(qū)不需要很大,它被設(shè)計成為環(huán)形的數(shù)據(jù)結(jié)構(gòu),以便管道可以被循環(huán)利用。
當(dāng)管道中沒有信息的話,從管道中讀取的進程會等待,直到另一端的進程放入信息。
當(dāng)管道被放滿信息的時候,嘗試放入信息的進程會等待,直到另一端的進程取出信息。當(dāng)兩個進程都終結(jié)的時候,管道也自動消失。
從原理上,管道利用fork機制建立,從而讓兩個進程可以連接到同一個PIPE上。
最開始的時候,上面的兩個箭頭都連接在同一個進程Process 1上(連接在Process 1上的兩個箭頭)。
當(dāng)fork復(fù)制進程的時候,會將這兩個連接也復(fù)制到新的進程(Process 2)。
隨后,每個進程關(guān)閉自己不需要的一個連接 (兩個黑色的箭頭被關(guān)閉; Process 1關(guān)閉從PIPE來的輸入連接,Process 2關(guān)閉輸出到PIPE的連接),這樣,剩下的紅色連接就構(gòu)成了如上圖的PIPE。
實現(xiàn)細(xì)節(jié):
在 Linux 中,管道的實現(xiàn)并沒有使用專門的數(shù)據(jù)結(jié)構(gòu),而是借助了文件系統(tǒng)的file結(jié)構(gòu)和VFS的索引節(jié)點inode。
通過將兩個 file 結(jié)構(gòu)指向同一個臨時的 VFS 索引節(jié)點,而這個 VFS 索引節(jié)點又指向一個物理頁面而實現(xiàn)的。如下圖
有兩個 file 數(shù)據(jù)結(jié)構(gòu),但它們定義文件操作例程地址是不同的,其中一個是向管道中寫入數(shù)據(jù)的例程地址,而另一個是從管道中讀出數(shù)據(jù)的例程地址。
這樣,用戶程序的系統(tǒng)調(diào)用仍然是通常的文件操作,而內(nèi)核卻利用這種抽象機制實現(xiàn)了管道這一特殊操作。
關(guān)于管道的讀寫
管道實現(xiàn)的源代碼在fs/pipe.c中,在pipe.c中有很多函數(shù),其中有兩個函數(shù)比較重要,即管道讀函數(shù)pipe_read()和管道寫函數(shù)pipe_wrtie()。
管道寫函數(shù)通過將字節(jié)復(fù)制到 VFS 索引節(jié)點指向的物理內(nèi)存而寫入數(shù)據(jù),而管道讀函數(shù)則通過復(fù)制物理內(nèi)存中的字節(jié)而讀出數(shù)據(jù)。
當(dāng)然,內(nèi)核必須利用一定的機制同步對管道的訪問,為此,內(nèi)核使用了鎖、等待隊列和信號。
當(dāng)寫進程向管道中寫入時,它利用標(biāo)準(zhǔn)的庫函數(shù)write(),系統(tǒng)根據(jù)庫函數(shù)傳遞的文件描述符,可找到該文件的 file 結(jié)構(gòu)。
file 結(jié)構(gòu)中指定了用來進行寫操作的函數(shù)(即寫入函數(shù))地址,
于是,內(nèi)核調(diào)用該函數(shù)完成寫操作。寫入函數(shù)在向內(nèi)存中寫入數(shù)據(jù)之前,必須首先檢查 VFS 索引節(jié)點中的信息,同時滿足如下條件時,才能進行實際的內(nèi)存復(fù)制工作:
·內(nèi)存中有足夠的空間可容納所有要寫入的數(shù)據(jù);
·內(nèi)存沒有被讀程序鎖定。
如果同時滿足上述條件,寫入函數(shù)首先鎖定內(nèi)存,然后從寫進程的地址空間中復(fù)制數(shù)據(jù)到內(nèi)存。
否則,寫入進程就休眠在 VFS 索引節(jié)點的等待隊列中,接下來,內(nèi)核將調(diào)用調(diào)度程序,而調(diào)度程序會選擇其他進程運行。
寫入進程實際處于可中斷的等待狀態(tài),當(dāng)內(nèi)存中有足夠的空間可以容納寫入數(shù)據(jù),或內(nèi)存被解鎖時,讀取進程會喚醒寫入進程,這時,寫入進程將接收到信號。
當(dāng)數(shù)據(jù)寫入內(nèi)存之后,內(nèi)存被解鎖,而所有休眠在索引節(jié)點的讀取進程會被喚醒。
管道的讀取過程和寫入過程類似。但是,進程可以在沒有數(shù)據(jù)或內(nèi)存被鎖定時立即返回錯誤信息,而不是阻塞該進程,這依賴于文件或管道的打開模式。
反之,進程可以休眠在索引節(jié)點的等待隊列中等待寫入進程寫入數(shù)據(jù)。當(dāng)所有的進程完成了管道操作之后,管道的索引節(jié)點被丟棄,而共享數(shù)據(jù)頁也被釋放。
Linux函數(shù)原型
#include
filedes[0]用于讀出數(shù)據(jù),讀取時必須關(guān)閉寫入端,即close(filedes[1]);
filedes[1]用于寫入數(shù)據(jù),寫入時必須關(guān)閉讀取端,即close(filedes[0])。
程序?qū)嵗?/p>
int main(void){ int n; int fd[2]; pid_t pid; char line[MAXLINE]; if(pipe(fd) 0){ /* 先建立管道得到一對文件描述符 */ exit(0); } if((pid = fork()) 0) /* 父進程把文件描述符復(fù)制給子進程 */ exit(1); else if(pid > 0){ /* 父進程寫 */ close(fd[0]); /* 關(guān)閉讀描述符 */ write(fd[1], " hello world ", 14); } else{ /* 子進程讀 */ close(fd[1]); /* 關(guān)閉寫端 */ n = read(fd[0], line, MAXLINE); write(STDOUT_FILENO, line, n); } exit(0);}
命名管道(named PIPE)
由于基于fork機制,所以管道只能用于父進程和子進程之間,或者擁有相同祖先的兩個子進程之間 (有親緣關(guān)系的進程之間)。
為了解決這一問題,Linux提供了FIFO方式連接進程。FIFO又叫做命名管道(named PIPE)。
FIFO (First in, First out)為一種特殊的文件類型,它在文件系統(tǒng)中有對應(yīng)的路徑。
當(dāng)一個進程以讀(r)的方式打開該文件,而另一個進程以寫(w)的方式打開該文件,那么內(nèi)核就會在這兩個進程之間建立管道,所以FIFO實際上也由內(nèi)核管理,不與硬盤打交道。
之所以叫FIFO,是因為管道本質(zhì)上是一個先進先出的隊列數(shù)據(jù)結(jié)構(gòu),最早放入的數(shù)據(jù)被最先讀出來,從而保證信息交流的順序。
FIFO只是借用了文件系統(tǒng)(file system,命名管道是一種特殊類型的文件,因為Linux中所有事物都是文件,它在文件系統(tǒng)中以文件名的形式存在。)來為管道命名。
寫模式的進程向FIFO文件中寫入,而讀模式的進程從FIFO文件中讀出。
當(dāng)刪除FIFO文件時,管道連接也隨之消失。
FIFO的好處在于我們可以通過文件的路徑來識別管道,從而讓沒有親緣關(guān)系的進程之間建立連接
函數(shù)原型:
#include
其中pathname是被創(chuàng)建的文件名稱,mode表示將在該文件上設(shè)置的權(quán)限位和將被創(chuàng)建的文件類型(在此情況下為S_IFIFO),dev是當(dāng)創(chuàng)建設(shè)備特殊文件時使用的一個值。
因此,對于先進先出文件它的值為0。
程序?qū)嵗?/p>
#include
編譯這個程序:
gcc –o fifo1.c fifo
運行這個程序:
$ fifo1
用ls命令查看所創(chuàng)建的管道
$ls-lF/tmp/my_fifoprwxr-xr-x 1 root root 0 05-08 20:10 /tmp/my_fifo|
注意:ls命令的輸出結(jié)果中的第一個字符為p,表示這是一個管道。
最后的|符號是由ls命令的-F選項添加的,它也表示是這是一個管道。
FIFO讀寫規(guī)則
1.從FIFO中讀取數(shù)據(jù):約定:如果一個進程為了從FIFO中讀取數(shù)據(jù)而阻塞打開了FIFO,那么稱該進程內(nèi)的讀操作為設(shè)置了阻塞標(biāo)志的讀操作
2.從FIFO中寫入數(shù)據(jù):約定:如果一個進程為了向FIFO中寫入數(shù)據(jù)而阻塞打開FIFO,那么稱該進程內(nèi)的寫操作為設(shè)置了阻塞標(biāo)志的寫操作。
詳見:http://blog.csdn.net/MONKEY_D_MENG/article/details/5570468
信號(Signal)
信號是比較復(fù)雜的通信方式,用于通知接受進程有某種事件發(fā)生,除了用于進程間通信外,進程還可以發(fā)送信號給進程本身;
Linux除了支持Unix早期信號語義函數(shù)sigal外,還支持語義符合Posix.1標(biāo)準(zhǔn)的信號函數(shù)sigaction(實際上,該函數(shù)是基于BSD的,BSD為了實現(xiàn)可靠信號機制,又能夠統(tǒng)一對外接口,用sigaction函數(shù)重新實現(xiàn)了signal函數(shù))
信號種類
每種信號類型都有對應(yīng)的信號處理程序(也叫信號的操作),就好像每個中斷都有一個中斷服務(wù)例程一樣。
大多數(shù)信號的默認(rèn)操作是結(jié)束接收信號的進程;
然而,一個進程通常可以請求系統(tǒng)采取某些代替的操作,各種代替操作是:
忽略信號。隨著這一選項的設(shè)置,進程將忽略信號的出現(xiàn)。
有兩個信號 不可以被忽略:SIGKILL,它將結(jié)束進程;
SIGSTOP,它是作業(yè)控制機制的一部分,將掛起作業(yè)的執(zhí)行。
恢復(fù)信號的默認(rèn)操作。
執(zhí)行一個預(yù)先安排的信號處理函數(shù)。
進程可以登記特殊的信號處理函數(shù)。
當(dāng)進程收到信號時,信號處理函數(shù)將像中斷服務(wù)例程一樣被調(diào)用,當(dāng)從該信號處理函數(shù)返回時,控制被返回給主程序,并且繼續(xù)正常執(zhí)行。
但是,信號和中斷有所不同。
中斷的響應(yīng)和處理都發(fā)生在內(nèi)核空間,而信號的響應(yīng)發(fā)生在內(nèi)核空間,信號處理程序的執(zhí)行卻發(fā)生在用戶空間。
那么,什么時候檢測和響應(yīng)信號呢?通常發(fā)生在兩種情況下:
當(dāng)前進程由于系統(tǒng)調(diào)用、中斷或異常而進入內(nèi)核空間以后,從內(nèi)核空間返回到用戶空間前夕;
當(dāng)前進程在內(nèi)核中進入睡眠以后剛被喚醒的時候,由于檢測到信號的存在而提前返回到用戶空間。
函數(shù)原型等詳見:
http://www.cnblogs.com/biyeymyhjob/archive/2012/08/04/2622265.html
信號本質(zhì)
信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。
信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。
信號是進程間通信機制中唯一的異步通信機制,可以看作是異步通知,通知接收信號的進程有哪些事情發(fā)生了。
信號機制經(jīng)過POSIX實時擴展后,功能更加強大,除了基本通知功能外,還可以傳遞附加信息。
信號來源
信號事件的發(fā)生有兩個來源:
硬件來源(比如我們按下了鍵盤或者其它硬件故障);
軟件來源,最常用發(fā)送信號的系統(tǒng)函數(shù)是kill, raise, alarm和setitimer以及sigqueue函數(shù),軟件來源還包括一些非法運算等操作。
關(guān)于信號處理機制的原理(內(nèi)核角度)
內(nèi)核給一個進程發(fā)送軟中斷信號的方法,是在進程所在的進程表項的信號域設(shè)置對應(yīng)于該信號的位。
這里要補充的是,
如果信號發(fā)送給一個正在睡眠的進程,那么要 看該進程進入睡眠的優(yōu)先級,如果進程睡眠在可被中斷的優(yōu)先級上,則喚醒進程;
否則僅設(shè)置進程表中信號域相應(yīng)的位,而不喚醒進程。
這一點比較重要,因為進程檢查是否收到信號的時機是:一個進程在即將從內(nèi)核態(tài)返回到用戶態(tài)時;
或者,在一個進程要進入或離開一個適當(dāng)?shù)牡驼{(diào)度優(yōu)先級睡眠狀態(tài)時。
內(nèi)核處理一個進程收到的信號的時機是在一個進程從內(nèi)核態(tài)返回用戶態(tài)時。
所以,當(dāng)一個進程在內(nèi)核態(tài)下運行時,
軟中斷信號并不立即起作用,要等到將返回用戶態(tài)時才處理。
進程只有處理完信號才會返回用戶態(tài)(上面的例子程序中,在步驟5中,解除阻塞后,先打印caught SIGQUIT,再打印SIGQUIT unblocked,即在sigprocmask返回前,信號處理程序先執(zhí)行),進程在用戶態(tài)下不會有未處理完的信號。
內(nèi)核處理一個進程收到的軟中斷信號是在該進程的上下文中,因此,進程必須處于運行狀態(tài)。
如果進程收到一個要捕捉的信號,那么進程從內(nèi)核態(tài)返回用戶態(tài)時執(zhí)行用戶定義的函數(shù)。
而且執(zhí)行用戶定義的函數(shù)的方法很巧妙,內(nèi)核是在用戶棧上創(chuàng)建一個新的層,該層中將返回地址的值設(shè)置成用戶定義的處理函數(shù)的地址,
這樣進程從內(nèi)核返回彈出棧頂時就返回到用戶定義的函數(shù)處,從函數(shù)返回再彈出棧頂時,才返回原先進入內(nèi)核的地方,接著原來的地方繼續(xù)運行。
這樣做的原因是用戶定義的處理函數(shù)不能且不允許在內(nèi)核態(tài)下執(zhí)行(如果用戶定義的函數(shù)在內(nèi)核態(tài)下運行的話,用戶就可以獲得任何權(quán)限)。
在信號的處理方法中有幾點特別要引起注意。
第一,在一些系統(tǒng)中,當(dāng)一個進程處理完中斷信號返回用戶態(tài)之前,內(nèi)核清除用戶區(qū)中設(shè)定的對該信號的處理例程的地址,
即下一次進程對該信號的處理方法又改為默認(rèn)值,除非在下一次信號到來之前再次使用signal系統(tǒng)調(diào)用。
這可能會使得進程在調(diào)用signal之前又得 到該信號而導(dǎo)致退出。在BSD中,內(nèi)核不再清除該地址。
但不清除該地址可能使得進程因為過多過快的得到某個信號而導(dǎo)致堆棧溢出。為了避免出現(xiàn)上述情況。
在 BSD系統(tǒng)中,內(nèi)核模擬了對硬件中斷的處理方法,即在處理某個中斷時,阻止接收新的該類中斷。
第二個要引起注意的是,如果要捕捉的信號發(fā)生于進程正在一個系統(tǒng)調(diào)用中時,并且該進程睡眠在可中斷的優(yōu)先級上(若系統(tǒng)調(diào)用未睡眠而是在運行,根據(jù)上面的分 析,等該系統(tǒng)調(diào)用運行完畢后再處理信號),
這時該信號引起進程作一次longjmp,跳出睡眠狀態(tài),返回用戶態(tài)并執(zhí)行信號處理例程。
當(dāng)從信號處理例程返回 時,進程就象從系統(tǒng)調(diào)用返回一樣,但返回了一個錯誤如-1,并將errno設(shè)置為EINTR,指出該次系統(tǒng)調(diào)用曾經(jīng)被中斷。
這要注意的是,BSD系統(tǒng)中內(nèi) 核可以自動地重新開始系統(tǒng)調(diào)用,或者手如上面所述手動設(shè)置重啟。
第三個要注意的地方:若進程睡眠在可中斷的優(yōu)先級上,則當(dāng)它收到一個要忽略的信號時,該進程被喚醒,但不做longjmp,一般是繼續(xù)睡眠。
但用戶感覺不 到進程曾經(jīng)被喚醒,而是象沒有發(fā)生過該信號一樣。
所以能夠使pause、sleep等函數(shù)從掛起態(tài)返回的信號必須要有信號處理函數(shù),如果沒有什么動作,可以將處理函數(shù)設(shè)為空。
第四個要注意的地方:內(nèi)核對子進程終止(SIGCLD)信號的處理方法與其他信號有所區(qū)別。
當(dāng)進程正常或異常終止時,內(nèi)核都向其父進程發(fā)一個SIGCLD 信號,缺省情況下,父進程忽略該信號,就象沒有收到該信號似的,
如果父進程希望獲得子進程終止的狀態(tài),則應(yīng)該事先用signal函數(shù)為SIGCLD信號設(shè) 置信號處理程序,在信號處理程序中調(diào)用wait。
SIGCLD信號的作用是喚醒一個睡眠在可被中斷優(yōu)先級上的進程。
如果該進程捕捉了這個信號,就象普通信號處理一樣轉(zhuǎn)到處理例程。
如果進程忽略該信號,則 什么也不做。
其實wait不一定放在信號處理函數(shù)中,但這樣的話因為不知道子進程何時終止,在子進程終止前,wait將使父進程掛起休眠。
信號生命周期
責(zé)任編輯:lq
-
Linux
+關(guān)注
關(guān)注
87文章
11342瀏覽量
210140 -
管道
+關(guān)注
關(guān)注
3文章
145瀏覽量
18000 -
數(shù)據(jù)結(jié)構(gòu)
+關(guān)注
關(guān)注
3文章
573瀏覽量
40190 -
signal
+關(guān)注
關(guān)注
0文章
110瀏覽量
24949
原文標(biāo)題:Linux 進程間通信之管道(pipe)、命名管道(FIFO)與信號(Signal)
文章出處:【微信號:LinuxHub,微信公眾號:Linux愛好者】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論