首頁 > 軟體

Linux管道和系統呼叫pipe()

2020-06-16 17:03:15

一、管道通訊

    父進程和子進程之間,或者兩個兄弟進程之間,可以通過系統呼叫建立起一個單向的通訊管道。但是這種管道只能由父進程開建立,對於子進程來說是靜態的,與生俱來的。管道兩端的進程各自都將該管道視作一個檔案。一個進程寫,另一個進程讀。並且,通過管道傳遞的內容遵循“先入先出”(FIFO)的原則。每個管道都是單向的,需要雙向通訊時就要建立兩個管道。

二、系統呼叫pipe()

    管道機制的主體是系統呼叫pipe(),但是由pipe()所建立的管道的兩端都在同一進程中,所以必須在fork的配合下,才能在父子進程之間或者兩個子進程之間建立起進程間的通訊管道。由於管道兩端都是以(已開啟)檔案的形式出現在相關的進程中,在具體實現上也是作為匿名檔案來實現的。所以pipe()的程式碼與檔案系統密切相關。

asmlinkage int sys_pipe(unsigned long * fildes){
    int fd[2];
    int error;
    error = do_pipe(fd);
    if(!error){
        if(copy_to_user(fildes, fd, 2 * sizeof(int)))
            error = -EFAULT; 
    }
    return error;               
}

    這裡由do_pipe()建立起一個管道,通過作為呼叫引數的陣列fd[]返回代表著管道兩端的兩個已經開啟檔案號,,再由copy_to_user()將陣列fd[]複製到使用者空間。顯然,do_pipe是這個系統呼叫的主題。

三、管道兩端是否可以共用同一個file資料結構

    在檔案系統中,進程對每個已經開啟檔案的操作都是通過一個file資料結構進行的,只有在  由同一進程按照相同模式開啟同一檔案  時才共用同一個資料結構。一個管道實際上就是一個無形(只存在於記憶體中)的檔案,對這個檔案的操作要通過兩個已經開啟的檔案進行,分別代表該管道的兩端。雖然最初建立時一個管道的兩端都在同一進程中,但是在實際使用時卻總是分別在兩個不同的進程。,所以,管道的兩端不能共用同一個file資料結構。而要為止各分配一個file資料結構。

四、管道為什麼需要inode結構?

    每個檔案都是有一個inode資料結構代表的。雖然一個管道實際上是一個無形的檔案。但是也得有一個inode資料結構。由於這個檔案在建立管道之前並不存在,所以需要在建立管道時臨時建立一個inode結構(呼叫get_pipe_inode()函數)。

五、關於pipe_new()函數

    pipe_new()函數,先分配一個記憶體頁面用做管道的緩衝區,再分配一個緩衝區用作pipe_inode_info資料結構。為什麼要這麼做?用來實現管道的檔案是無形的,它並不出現在磁碟或者其他的檔案系統儲存媒介上,而只存在於記憶體空間,其他進程也無法“開啟”或者存取這個檔案。所以,這個所謂檔案實質上只是一個用作緩衝區的記憶體頁面,只是把它納入了檔案系統的機制,借用了檔案系統的各種資料結構和操作加以管理。

六、對於管道的操作

    inode資料結構中有個重要的成分i_fop,是指向一個file_operations資料結構的指標。這個資料結構中給出了用於該檔案的每種操作的函數指標。對於管道來說,這個資料結構是rdwr_pipe_fops:

struct file_operations rdwr_pipe_fops = {
    llseek: pipe_lseek,
    read:  pipe_read,
    write:  pipe_write,
    poll:  pipe_poll,
    ioctl:  pipe_ioctl,
    open:  pipe_rdwr_open,
    release:pipe_rdwr_release,
};

    函數get_pipe_inode()中分配了inode結構以後,進行了一些初始化操作。但是,程式碼中並沒有設定inode結構中的inode_operations結構指標i_op,所以該指標為0.可見,對於用來實現管道的inode並不允許對這裡的inode進行常規操作,只有當inode代表著“有形”的檔案時才使用。

    對於管道是否必須有目錄項的問題:在正常情況下,每個檔案都至少有一個“目錄項”,代表著這個檔案的路徑名;而每個目錄項則只描述一個檔案,在denty資料結構中有個指標指向相應的inode結構。因此,在file資料結構中有個指標f_dentry指向做開啟檔案的目錄項dentry資料結構,這樣,從file結構開始就可以一路通道檔案inode結構。但是,對於管道,檔案是無形的,本來並非得有個目錄項不可。可是,在file資料結構中並沒有直接指向相應inode結構的指標,一定要經過一個目錄項轉換一下才行。然而inode結構又是各種檔案操作的樞紐,所以對於管道也得有一個目錄項了。故呼叫d_alloc()函數分配一個目錄項,然後通過d_add()使已經分配的inode結構,與這個函數掛上鉤,並且讓兩個已經開啟檔案結構中的f_entry()指標都指向這個目錄。另外,對於管道的目錄項的操作只允許刪除操作。

    對於管道的兩端來說,管道是單向的,所以一端設定成唯讀,另一端設定成只寫。同時兩端的檔案操作也分別設定成read_pipe_fops,write_pipe_fops,結構如下:

struct file_operations read_pipe_fops = {
    llseek: pipe_lseek,
    read:  pipe_read,
    write:  bad_pipe_w,//pipe_write,
    poll:  pipe_poll,
    ioctl:  pipe_ioctl,
    open:  pipe_read_open,
    release:pipe_read_release,
};
struct file_operations write_pipe_fops = {
    llseek: pipe_lseek,
    read:  bad_pipe_r,//pipe_read,
    write:  pipe_write,
    poll:  pipe_poll,
    ioctl:  pipe_ioctl,
    open:  pipe_write_open,
    release:pipe_write_release,
};

其中,bad_pipe_w()bad_pipe_r()函數分別用來返回錯誤程式碼。是的檔案只支援讀或者寫。

6.1怎麼做到進程間通訊?

    管道的兩端在建立之初都在同一個進程中,顯然起不到通訊的作用。怎樣才能將管道用於進程間通訊呢

    1)進程A建立一個管道,建立完成時代表管道兩端的兩個已開啟檔案都在進程A中(下圖所示:父進程建立管道)

     

    2)進程A通過fork()建立出進程B,在fork()的過程中進程A大開啟檔案表按照原樣拷貝到進程B中(下圖所示:父子進程共用管道)

       

    3)A關閉管道的讀端,而B關閉進程的寫端。管道的寫端在進程A中,而進程的讀端在進程B中,成為父子進程之間的通訊管道(下圖:父子管道通過管道單向通訊)。             

 

    4)進程A又通過fork()建立進程C,然後關閉其管道寫端而與管道脫離關係。使得管道的寫端在進程C中,讀端在進程B中。成為兩個兄弟進程之間的管道(如下圖所示,兄弟進程之間通過管道單向通訊)、

   

    5)進程C和B各自通過exec()執行各自的目標程式,並通過管道進行單向通訊、

6.2命名管道

    由於管道是一種無形,無名的檔案,它就只能通過fork()的過程建立在“近親”的進程之間,而不可能成為可以在任意兩個進程之間建立通訊的機制,更不可能成為一般的,通用的進程間通訊模型。同時,管道機制的這種缺點本身就強烈地暗示著人們,只要用有名,有形的檔案來實現管道,就能克服這種缺點。這裡所謂的有名是指這樣一個檔案應該有檔名,使得任何進程都可以通過檔名或者路徑名與這個檔案掛上鉤;在這裡,“有形”是指檔案的inode應該存在於磁碟或者其他檔案系統上,使得任何進程在任意時間(不僅僅是在fork()時)都可以建立(或斷開)與這個檔案的聯絡。所以命名管道的出現時必然的。

    為了實現“命名管道”,在“普通檔案”,“塊裝置檔案”,“字元裝置檔案”之外由設立了一種檔案型別,稱為“FIFO”檔案。對於這種檔案的存取,嚴格遵循先進先出的原則,而不允許有在檔案內移動讀寫指標位置的lseek()操作。這樣一來,就可以像在磁碟上建立一個檔案一樣地建立一個命名管道,具體可以使用mkmod命令建立:

    $mkmod mypipe p

    這裡的引數p表示所建立的節點(也就是特殊檔案)的型別為命名管道。

    建立了這樣的節點以後,有關進程就可以想開啟一個檔案一樣“開啟”與這個命名管道的聯絡。對於FIFO檔案上的操作可以通過下列幾個file_operations資料結構確定:

struct file_operations read_fifo_fops = {
    llseek: pipe_lseek,
    read:  pipe_read,
    write:  bad_pipe_w,//pipe_write,
    poll:  fifo_poll,
    ioctl:  pipe_ioctl,
    open:  pipe_read_open,
    release:pipe_read_release,
};
struct file_operations write_fifo_fops = {
    llseek: pipe_lseek,
    read:  bad_pipe_r,//pipe_read,
    write:  pipe_write,
    poll:  fifo_poll,
    ioctl:  pipe_ioctl,
    open:  pipe_write_open,
    release:pipe_write_release,
};

struct file_operations rdwr_fifo_fops = {
    llseek: pipe_lseek,
    read:  pipe_read,
    write:  pipe_write,
    poll:  fifo_poll,
    ioctl:  pipe_ioctl,
    open:  pipe_rdwr_open,
    release:pipe_rdwr_release,
};

    從上面程式碼,對照一下用於普通管道的資料結構read_pipe_fops,write_pipe_fops和rdwr_pipe_fops就可以看出它們幾乎是完全一樣的。fifo_poll和pipe_poll()都用於select()系統呼叫,與通訊機制本身沒有多大關係,這裡我們並不關心。所不同的是,對於普通管道雖然也定義了相當於open()的操作pipe_read_open(),pipe_write和pipe_rdwr_open(),但是這些函數實際上在典型的應用中是不使用的。普通管道是通過do_pipe()建立,通過fork()的過程延伸到兩個進程之間的。對於父進程,在系統呼叫fork()以後就已經開啟,而對於子進程來說則是與生俱來的,所以都不需要再開啟。然而,命名管道就不同了參加通訊的進程確實要呼叫這些函數來“開啟通向已經建立在檔案系統中的FIFO檔案的通道。

    既然普通管道和命名管道的不同之處僅僅在於“”開啟“”的過程,那麼我們來分析一下,一個進程是怎樣通過open()系統呼叫來建立與一個已經建立的FIFO檔案之間的聯絡的。在檔案系統中,進程在核心中由sys_open()進入file_open(),然後在open_namei()中呼叫一個函數path_walk(),根據檔案的路徑名在檔案系統中找到代表這個檔案的inode,在將磁碟上inode讀入記憶體時,要根據檔案的型別(FIFO檔案的S_FIFO標誌位1),將inode中的i_op和i_fop指標設定成指向相應的inode_operations資料結構和file_operations資料結構,但是對於FIFO這樣的特殊檔案則呼叫init_special_inode()來加以初始化。

 

6.3對FIFO檔案開啟的三種模式

    FIFO檔案可以按照三種不同的模式開啟,就是“唯讀”,“只寫”,“讀寫”。同時,在系統呼叫open()中還有個引數flags。如果flags的標誌位O_NONBLOCK為1,就表示在開啟的過程中即使某些條件得不到滿足也不需要等待,而應立即返回。

    在典型的應用中,就相對普通管道一樣,一個進程按照唯讀模式開啟命名通道,成為“消費者”;另一個進程按照只寫模式開啟命名管道,成為“生產者”。可是在普通管道的情況下,管道的兩端是由同一進程do_pipe()中同時開啟的,而在命名管道的情況下則管道的兩端通常分別由兩個進程先後開啟,這就有了個“同步”的問題。除此之外,還有個不同,就是普通管道既然是“無名”,“無形”的,一般就不會由另一個進程也來開啟這個管道。而在命名管道的情況下,任意一個進程都可以通過相同的路徑名開啟同一個FIFO檔案。這些因素都使建立命名管道的過程比建立普通管道的過程要複雜一些。

    先來看命名管道的“讀端”,也就是按照“唯讀”模式開啟一個FIFO檔案時的幾種情況:

    1)如果管道的寫端已經開啟,那麼現在讀端的開啟就完成了命名管道的建立過程,在這種情況下,寫端的進程,也就是“生產者”進程一般都是正在睡眠中,等待著命名管道建立過程的完成,所以要將其喚醒。然後,兩個進程差不多同時返回各自的使用者空間,然後就可以通過這個命名管道進行通訊了。

    2)如果命名管道的寫端尚未開啟,而flags中的O_NONBLOCK標誌位為1,表示不應該等待。此時讀端雖已開啟,但是命名管道只是部分建立了(寫端未開啟)。而標誌的使用又要求系統呼叫不加等待立即返回,所以不做等待。

    3)如果命名管道的寫端尚未開啟,而flags中的O_NONBLOCK標誌位為0,。在這種情況下,讀端的開啟只是完成了命名管道建立過程的一半,所以消費者進程要通過wait_for_parter()進入睡眠,等待某個生產者進程來開啟命名管道的寫端已完成其建立過程。

    相應的,命名管道的寫端的開啟也有以下幾種不同的情況(只寫):

    1)如果命名管道的讀端已經開啟,那麼寫端的開啟就完成了命名管道的建立過程。在這種情況下,命名管道讀端的進程(“消費者進程”)有可能正在睡眠中等待,所以,如果當前進程是第一次開啟該管道寫端的進程,就要負責將其喚醒。
    2)如果命名管道的讀端尚未開啟,而flags中的O_NONBLOCKED標誌位為0。在這種情況下,生產者進程要睡眠等待至消費者進程開啟命名管道的讀端才可以返回。

    3)如果命名管道的讀端尚未開啟,而flags中的O_NONBLOCKED標誌位為1。此時對命名管道寫端開啟失敗,所以要釋放已經分配的各種資源並且返回-1.

    對命名管道檔案“讀寫”開啟

    此時相當於有一個進程同時開啟了命名管道的兩端,所以不管怎樣都不需要等待。但是還有可能已經有某個進程已經開啟了寫端或者讀端而正在睡眠等待,所以只要有任意一端是第一次開啟,也就喚醒了,正在睡眠等待的進程。

    命名管道一經建立們以後的讀寫以及關閉操作就和普通進程完全相同了。注意雖然FIFO檔案的inode節點在磁碟上,但那只是一個節點,而檔案的資料則只存在於記憶體緩衝頁面中,與普通管道一樣。

本文永久更新連結地址http://www.linuxidc.com/Linux/2017-11/148216.htm


IT145.com E-mail:sddin#qq.com