piple

簡介:本文主要介紹了管道(pipe)的基本概念和用途;分析了環形緩衝區的存儲、訪問及其實現方法;分析併發訪問可能引發的問題,並給出解決方法;分析了linux2.6.29內核中pipe的讀寫函數。

1、管道(pipe

管道是進程間通信的主要手段之一。一個管道實際上就是個只存在於內存中的文件,對這個文件的操作要通過兩個已經打開文件進行,它們分別代表管道的兩端。管道是一種特殊的文件,它不屬於某一種文件系統,而是一種獨立的文件系統,有其自己的數據結構。根據管道的適用範圍將其分爲:無名管道和命名管道。

     無名管道

主要用於父進程與子進程之間,或者兩個兄弟進程之間。在linux系統中可以通過系統調用建立起一個單向的通信管道,且這種關係只能由父進程來建立。因此,每個管道都是單向的,當需要雙向通信時就需要建立起兩個管道。管道兩端的進程均將該管道看做一個文件,一個進程負責往管道中寫內容,而另一個從管道中讀取。這種傳輸遵循“先入先出”(FIFO)的規則。

     命名管道

命名管道是爲了解決無名管道只能用於近親進程之間通信的缺陷而設計的。命名管道是建立在實際的磁盤介質或文件系統(而不是隻存在於內存中)上有自己名字的文件,任何進程可以在任何時間通過文件名或路徑名與該文件建立聯繫。爲了實現命名管道,引入了一種新的文件類型——FIFO文件(遵循先進先出的原則)。實現一個命名管道實際上就是實現一個FIFO文件。命名管道一旦建立,之後它的讀、寫以及關閉操作都與普通管道完全相同。雖然FIFO文件的inode節點在磁盤上,但是僅是一個節點而已,文件的數據還是存在於內存緩衝頁面中,和普通管道相同。

2、環形緩衝區

每個管道只有一個頁面作爲緩衝區,該頁面是按照環形緩衝區的方式來使用的。這種訪問方式是典型的“生產者——消費者”模型。當“生產者”進程有大量的數據需要寫時,而且每當寫滿一個頁面就需要進行睡眠等待,等待“消費者”從管道中讀走一些數據,爲其騰出一些空間。相應的,如果管道中沒有可讀數據,“消費者”進程就要睡眠等待,具體過程如下圖所示。

 

生產者——消費者關係圖

2.1環形緩衝區實現原理

環形緩衝區是嵌入式系統中一個常用的重要數據結構。一般採用數組形式進行存儲,即在內存中申請一塊連續的線性空間,可以在初始化的時候把存儲空間一次性分配好。只是要模擬環形,必須在邏輯上把數組的頭尾相連接。只要對數組最後一個元素進行特殊的處理——訪問尾部元素的下一元素時,重新回到頭部元素。對於從尾部回到頭部只需模緩衝長度即可(假設maxlen爲環形緩衝的長度,當讀指針read指向尾部元素時,只需執行read=read%maxlen即可使read回到頭部元素)。

環形緩衝區圖

2.2讀寫操作

環形緩衝區要維護寫端(write)和讀端(read)兩個索引。寫入數據時,必須先確保緩衝區沒有滿,然後才能將數據寫入,最後將write指針指向下一個元素;讀取數據時,首先要確保緩衝區不爲空,然後返回read指針對應得元素,最後使read指向下一個元素的位置。讀寫操作僞代碼:

2.3判斷“滿”和“空”

readwrite指向同一個位置時環形緩衝區爲空或滿。爲了區別環滿和空,當readwrite重疊的時候環空;而當writeread快,追到距離read還有一個元素間隔的時候,就認爲環已經滿了。環形緩衝區原理圖如圖3所示。

環形緩衝區實現原理圖

併發訪問

考慮到在不同環境下,任務可能對環形緩衝區的訪問情況不同,需要對併發訪問的情況進行分析。

在單任務環境下,只存在一個讀任務和一個寫任務,只要保證寫任務可以順利的完成將數據寫入,而讀任務可以及時的將數據讀出即可。如果有競爭發生,可能會出現如下情況:

Case1:假如寫任務在“寫指針加1,指向下一個可寫空位置”執行完成時被打斷,如圖3所示,此時寫指針write指向非法位置。當系統調度讀任務執行時,如果讀任務需要讀多個數據,那麼不但應該讀出的數據被讀出,而且當讀指針被調整爲0是,會將以前已經讀出的數據重複讀出。

寫指針非法

Case2:假設讀任務進行讀操作,在“讀指針加1”執行完時被打斷,如圖4所示,此時read所處的位置是非法的。當系統調度寫任務執行時,如果寫任務要寫多個數據,那麼當寫指針指到尾部時,本來緩衝區應該爲滿狀態,不能再寫,但是由於讀指針處於非法位置,在讀任務執行前,寫任務會任務緩衝區爲空,繼續進行寫操作,將覆蓋還沒有來的及讀出的數據。

讀指針非法

爲了避免上述錯誤的發生,必須保證讀寫指針操作是原子性的,讀寫指針的值要麼是沒有修改的,要麼是修改正確的。可以引入信號量,有效的保護臨界區代碼,就可以避免這些問題。在單任務環境下,也可以通過採取適當的

 

4.linux內核中pipe的讀寫實現

Linux內核中採用struct pipe_inode_info結構體來描述一個管道。

其中,當pipe爲空/滿時,採用等待隊列,該隊列使用自旋鎖進行保護。

struct Pipe_buffer數據結構描述pipe的緩衝(buffer

本文重點針對pipe實現中對環形緩衝區的操作方法,目的是借鑑學習其互斥訪問方法。因此,着重分析pipe_readpipe_write方法。

Pipe_read(fs/pipe.c)

訪問pipe對應的inode必須獲得相應的互斥鎖,防止併發訪問。

數據的讀出放在一個死循環中,整個for循環中的代碼均屬於臨界區,需要互斥鎖進行保護。

有以下幾種情況纔會退出:

     完成數據的讀出;

     Pipe沒有writer進程

     進程設置了O_NONBLOCK標誌

325行將buffer中的數據讀出。完成後,緊接着調整buffer中指針的位置

其中,348行設置標誌,do_wakeup1,說明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進程會隨時進行寫操作。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章