Linux程序間通訊分類 以及 pipe的原理實現
一個大型的應用系統,往往需要眾多程序協作,程序(Linux程序概念見附1)間通訊的重要性顯而易見。本系列文章闡述了Linux環境下的幾種主要程序間通訊手段,並針對每個通訊手段關鍵技術環節給出詳細例項。為達到闡明問題的目的,本文還對某些通訊手段的內部實現機制進行了分析。
linux下的程序通訊手段基本上是從Unix平臺上的程序通訊手段繼承而來的。而對Unix發展做出重大貢獻的兩大主力AT&T的貝爾實驗室及BSD(加州大學伯克利分校的伯克利軟體釋出中心)在程序間通訊方面的側重點有所不同。前者對Unix早期的程序間通訊手段進行了系統的改進和擴充,形成了“system V IPC”,通訊程序侷限在單個計算機內;後者則跳過了該限制,形成了基於套介面(socket)的程序間通訊機制。Linux則把兩者繼承了下來,如圖示:
其中,最初Unix IPC包括:管道、FIFO、訊號;System V IPC包括:System V訊息佇列、System V訊號燈、System V共享記憶體區;Posix IPC包括: Posix訊息佇列、Posix訊號燈、Posix共享記憶體區。有兩點需要簡單說明一下:1)由於Unix版本的多樣性,電子電氣工程協會(IEEE)開發了一個獨立的Unix標準,這個新的ANSI Unix標準被稱為計算機環境的可移植性作業系統介面(PSOIX)。現有大部分Unix和流行版本都是遵循POSIX標準的,而Linux從一開始就遵循POSIX標準;2)BSD並不是沒有涉足單機內的程序間通訊(socket本身就可以用於單機內的程序間通訊)。事實上,很多Unix版本的單機IPC留有BSD的痕跡,如4.4BSD支援的匿名記憶體對映、4.3+BSD對可靠訊號語義的實現等等。
圖一給出了linux 所支援的各種IPC手段,在本文接下來的討論中,為了避免概念上的混淆,在儘可能少提及Unix的各個版本的情況下,所有問題的討論最終都會歸結到Linux環境下的程序間通訊上來。並且,對於Linux所支援通訊手段的不同實現版本(如對於共享記憶體來說,有Posix共享記憶體區以及System V共享記憶體區兩個實現版本),將主要介紹Posix API。
linux下程序間通訊的幾種主要手段簡介:
- 管道(Pipe)及有名管道(named pipe):管道可用於具有親緣關係程序間的通訊,有名管道克服了管道沒有名字的限制,因此,除具有管道所具有的功能外,它還允許無親緣關係程序間的通訊;
- 訊號(Signal):訊號是比較複雜的通訊方式,用於通知接受程序有某種事件發生,除了用於程序間通訊外,程序還可以傳送訊號給程序本身;linux除了支援Unix早期訊號語義函式sigal外,還支援語義符合Posix.1標準的訊號函式sigaction(實際上,該函式是基於BSD的,BSD為了實現可靠訊號機制,又能夠統一對外介面,用sigaction函式重新實現了signal函式);
- 報文(Message)佇列(訊息佇列):訊息佇列是訊息的連結表,包括Posix訊息佇列system V訊息佇列。有足夠許可權的程序可以向佇列中新增訊息,被賦予讀許可權的程序則可以讀走佇列中的訊息。訊息佇列克服了訊號承載資訊量少,管道只能承載無格式位元組流以及緩衝區大小受限等缺點。
- 共享記憶體:使得多個程序可以訪問同一塊記憶體空間,是最快的可用IPC形式。是針對其他通訊機制執行效率較低而設計的。往往與其它通訊機制,如訊號量結合使用,來達到程序間的同步及互斥。
- 訊號量(semaphore):主要作為程序間以及同一程序不同執行緒之間的同步手段。
- 套介面(Socket):更為一般的程序間通訊機制,可用於不同機器之間的程序間通訊。起初是由Unix系統的BSD分支開發出來的,但現在一般可以移植到其它類Unix系統上:Linux和System V的變種都支援套接字。
簡介:本文主要介紹了管道(pipe)的基本概念和用途;分析了環形緩衝區的儲存、訪問及其實現方法;分析併發訪問可能引發的問題,並給出解決方法;分析了linux2.6.29核心中pipe的讀寫函式。
1、管道(pipe)
管道是程序間通訊的主要手段之一。一個管道實際上就是個只存在於記憶體中的檔案,對這個檔案的操作要通過兩個已經開啟檔案進行,它們分別代表管道的兩端。管道是一種特殊的檔案,它不屬於某一種檔案系統,而是一種獨立的檔案系統,有其自己的資料結構。根據管道的適用範圍將其分為:無名管道和命名管道。
● 無名管道
主要用於父程序與子程序之間,或者兩個兄弟程序之間。在linux系統中可以通過系統呼叫建立起一個單向的通訊管道,且這種關係只能由父程序來建立。因此,每個管道都是單向的,當需要雙向通訊時就需要建立起兩個管道。管道兩端的程序均將該管道看做一個檔案,一個程序負責往管道中寫內容,而另一個從管道中讀取。這種傳輸遵循“先入先出”(FIFO)的規則。
● 命名管道
命名管道是為了解決無名管道只能用於近親程序之間通訊的缺陷而設計的。命名管道是建立在實際的磁碟介質或檔案系統(而不是隻存在於記憶體中)上有自己名字的檔案,任何程序可以在任何時間通過檔名或路徑名與該檔案建立聯絡。為了實現命名管道,引入了一種新的檔案型別——FIFO檔案(遵循先進先出的原則)。實現一個命名管道實際上就是實現一個FIFO檔案。命名管道一旦建立,之後它的讀、寫以及關閉操作都與普通管道完全相同。雖然FIFO檔案的inode節點在磁碟上,但是僅是一個節點而已,檔案的資料還是存在於記憶體緩衝頁面中,和普通管道相同。
2、環形緩衝區
每個管道只有一個頁面作為緩衝區,該頁面是按照環形緩衝區的方式來使用的。這種訪問方式是典型的“生產者——消費者”模型。當“生產者”程序有大量的資料需要寫時,而且每當寫滿一個頁面就需要進行睡眠等待,等待“消費者”從管道中讀走一些資料,為其騰出一些空間。相應的,如果管道中沒有可讀資料,“消費者”程序就要睡眠等待,具體過程如下圖所示。
圖1 生產者——消費者關係圖
2.1環形緩衝區實現原理
環形緩衝區是嵌入式系統中一個常用的重要資料結構。一般採用陣列形式進行儲存,即在記憶體中申請一塊連續的線性空間,可以在初始化的時候把儲存空間一次性分配好。只是要模擬環形,必須在邏輯上把陣列的頭尾相連線。只要對陣列最後一個元素進行特殊的處理——訪問尾部元素的下一元素時,重新回到頭部元素。對於從尾部回到頭部只需模緩衝長度即可(假設maxlen為環形緩衝的長度,當讀指標read指向尾部元素時,只需執行read=read%maxlen即可使read回到頭部元素)。
圖2 環形緩衝區圖
2.2讀寫操作
環形緩衝區要維護寫端(write)和讀端(read)兩個索引。寫入資料時,必須先確保緩衝區沒有滿,然後才能將資料寫入,最後將write指標指向下一個元素;讀取資料時,首先要確保緩衝區不為空,然後返回read指標對應得元素,最後使read指向下一個元素的位置。讀寫操作虛擬碼:
2.3判斷“滿”和“空”
當read和write指向同一個位置時環形緩衝區為空或滿。為了區別環滿和空,當read和write重疊的時候環空;而當write比read快,追到距離read還有一個元素間隔的時候,就認為環已經滿了。環形緩衝區原理圖如圖3所示。
圖3 環形緩衝區實現原理圖
3 併發訪問
考慮到在不同環境下,任務可能對環形緩衝區的訪問情況不同,需要對併發訪問的情況進行分析。
在單任務環境下,只存在一個讀任務和一個寫任務,只要保證寫任務可以順利的完成將資料寫入,而讀任務可以及時的將資料讀出即可。如果有競爭發生,可能會出現如下情況:
Case1:假如寫任務在“寫指標加1,指向下一個可寫空位置”執行完成時被打斷,如圖3所示,此時寫指標write指向非法位置。當系統排程讀任務執行時,如果讀任務需要讀多個數據,那麼不但應該讀出的資料被讀出,而且當讀指標被調整為0是,會將以前已經讀出的資料重複讀出。
圖4 寫指標非法
Case2:假設讀任務進行讀操作,在“讀指標加1”執行完時被打斷,如圖4所示,此時read所處的位置是非法的。當系統排程寫任務執行時,如果寫任務要寫多個數據,那麼當寫指標指到尾部時,本來緩衝區應該為滿狀態,不能再寫,但是由於讀指標處於非法位置,在讀任務執行前,寫任務會任務緩衝區為空,繼續進行寫操作,將覆蓋還沒有來的及讀出的資料。
圖5 讀指標非法
為了避免上述錯誤的發生,必須保證讀寫指標操作是原子性的,讀寫指標的值要麼是沒有修改的,要麼是修改正確的。可以引入訊號量,有效的保護臨界區程式碼,就可以避免這些問題。在單任務環境下,也可以通過採取適當的措施來避免訊號量的使用,從而提高程式的執行效率。
4.linux核心中pipe的讀寫實現
Linux核心中採用struct pipe_inode_info結構體來描述一個管道。
其中,當pipe為空/滿時,採用等待佇列,該佇列使用自旋鎖進行保護。
用struct Pipe_buffer資料結構描述pipe的緩衝(buffer)
本文重點針對pipe實現中對環形緩衝區的操作方法,目的是借鑑學習其互斥訪問方法。因此,著重分析pipe_read和pipe_write方法。
●Pipe_read(fs/pipe.c)
訪問pipe對應的inode必須獲得相應的互斥鎖,防止併發訪問。
資料的讀出放在一個死迴圈中,整個for迴圈中的程式碼均屬於臨界區,需要互斥鎖進行保護。
有以下幾種情況才會退出:
▲ 完成資料的讀出;
▲ Pipe沒有writer程序
▲ 程序設定了O_NONBLOCK標誌
325行將buffer中的資料讀出。完成後,緊接著調整buffer中指標的位置
其中,348行設定標誌,do_wakeup為1,說明buffer中已經有空位置可以寫入資料,這時,可以喚醒等待佇列中的睡眠的寫程序。
如果沒有退出,或者成功讀取資料,讀程序會主動呼叫pipe_wait函式進行睡眠等待,直到有writer程序寫入資料並將其喚醒。
當程序從臨界區中退出後會釋放互斥鎖。
最後,為了防止reader程序是因為收到訊號量而退出,再給睡眠的writer程序一次機會,檢查do_wakeup,如果為1就喚醒睡眠的writer程序。
● pipe_write(fs/pipe.c)
首先,與pipe_read相同,pipe_write採用互斥鎖對臨界區進行保護。寫操作也放在死迴圈中,退出條件也與read相同。
與pipe_read不同,writer程序不總是睡眠等待,在呼叫pipe_wait進行睡眠後,如果有read程序讀走某些資料,write程序會隨時進行寫操作。
轉載自:http://blog.csdn.net/sunmenggmail/article/details/7888746