Linux進程通信 - 無名管道與有名管道


無名管道(PIPE)和有名管道(FIFO)都是UNIX進程間通信(InterProcess Communication,簡稱IPC)的手段。

無名管道PIPE

管道特點

管道通常指無名管道,是IPC最古老的形式。管道有何特點?

  1. 半雙工通信,具有固定的讀端、寫端(單向傳輸數據);
  2. 管道只能在具有公共祖先的2個進程間使用,通常是父子進程;

管道的創建

管道由pipe函數創建,見pipe(2) — Linux manual page

#include <unistd.h>
/* fd返回2個文件描述符:fd[0]為讀而打開;fd[1]為寫而打開。fd[1]的輸出是fd[0]的輸入 */
int pipe(int fd[2]);

/* 提供位元選項可設置。flags=0時,pipe2同pipe。
 * 常用選項:O_NONBLOCK
 */
int pipe2(int fd[2], int flags);

通過fd[0]調用read()讀取數據的一端,叫讀端;通過fd[1]調用write()寫數據的一端進程,叫寫端。

通常的創建模型:

int pipefd[2];
ret = pipe(pipefd);
fork();

管道的狀態

同一個進程使用管道沒有意義,因為管道專門用於進程間通信。進程內的通信使用全局變量即可。要實現父子進程的通信,創建管道后,需要關閉一個讀端,一個寫端。比如一個從父進程到子進程的管道,父進程關閉fd[0],子進程關閉fd[1]。

單進程半雙工管道(無意義狀態)

fork子進程后的半雙工管道(創建后的初始狀態)

父進程到子進程的管道(理想狀態)

管道的使用規則

當管道的一端被關閉后,下面的規則起作用:

  1. 讀端和寫端都可以對應多個進程,但通常一個管道只有一個讀進程和一個寫進程;
  2. 當讀一個寫端已關閉的管道時,在所有數據都被讀取后,read返回0,表示文件結束(只要還有一個寫端還有進程,就不會產生文件的結束);
  3. 如果寫一個讀端已關閉的管道,會產生信號SIGPIPE。如果忽略該信號,或者捕捉該信號從並從其處理程序返回,則write返回-1,errnor=EPIPE。

查看打開的管道文件

管道是文件的一種,/proc/PID/fd(這里PID是打開管道的進程id)下可以查看進程打開的管道文件:

$ ll /proc/2889/fd
...

管道的內存區域大小

write寫管道時,要求寫的字節數 <= PIPE_BUF (內核的管道緩沖區大小)。多個進程同時寫一個管道,可能造成寫的字節數超過PIPE_BUF,所寫數據可能會與其他進程所寫數據相互交叉。
調用fcntl,pathconf或fpathconf函數,可確定PIPE_BUF的值。

// 獲取管道大小
pipe_capacity = fcntl(fd, ?F_GETPIPE_SZ);
// 設置管道的大小
ret = fcntl(fd, ?F_SET_PIPE_SZ, size);

管道內存區域的大小必須在頁面大小(PAGE)和上限值之間。上限值記錄在/proc/sys/fs/pipe-max-size,特權用戶可修改該上限值

$ cat /proc/sys/fs/pipe-max-size
1048576

示例

創建父進程到子進程的管道,父進程通過管道向子進程傳送數據。

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <string.h>
#include <sys/wait.h>

#define MAXLINE    80
int main() {
    pid_t pid ;
    int fd[2];
    char buf[MAXLINE];

    int ret = pipe(fd);
    if (ret < 0) {
        perror("pipe error\n");
        exit(1);
    }

    pid = fork();
    if (pid < 0) {
        perror("fork error");
        exit(1);
    }
    else if (pid == 0) { // child
        close(fd[1]);
        
        int n = read(fd[0], buf, MAXLINE);
        write(STDOUT_FILENO, buf, n);
    }
    else { // parent
        close(fd[0]);

        char *s = "hello world\n";
        write(fd[1], s, strlen(s));

        waitpid(pid, NULL, 0);
        exit(0);
    }

    return 0;
}

有名管道FIFO

有名管道特點

有名管道也叫命名管道,可以解決無名管道只能適用於兩個有共同祖先的進程的問題,即使兩個不相關的進程也能通過FIFO交換數據。有名管道是一種文件類型,存在於文件系統中,通過stat結構的st_mode成員的編碼
有名管道和無名管道本質是一樣的,最大的區別是:有名管道有關聯的實體文件,而無名管道沒有。正因為有關聯實體文件,沒有親緣關系的任意兩個進程之間,可以通過有名管道進行通信。

創建FIFO文件

使用有名管道前,需要創建FIFO文件,調用mkfifo(C程序)或mkfifo命令(shell環境)

#include <sys/types.h>
#include <sys/stat.h>

/* 創建fifo文件
 * pathname 指定文件名(路徑)
 * mode 類似於open,指定FIFO文件的讀寫執行權利
 */
int mkfifo(const char *pathname, mode_t mode);

第二個參數指定FIFO文件讀寫執行權利,但真實的權限還需要按當前進程umask掩碼得到:

real_mode = (mode & ~umask);  // 當前進程的umask掩碼,可調用umask()獲取

打開FIFO文件

通過調用open打開文件,close關閉文件。
不能以O_RDWR(讀、寫)模式打開FIFO文件,O_RDWR模式打開FIFO文件行為未定義。
使用FIFO文件的方法是:一個進程以只讀模式(O_RDONLY)打開FIFO文件,另一個進程以只寫模式(O_WRONLY)打開FIFO文件。負責寫入FIFO的進程的寫入內容,就可以被負責讀出FIFO的進程讀取到,從而實現通信的目的。

O_NONBLOCK
沒有指定O_NONBLOCK模式時,以O_RDONLY只讀打開FIFO文件不能返回(處於阻塞狀態),等寫打開;同樣以O_WRONLY只寫打開FIFO文件也不能返回,等讀打開。O_RDONLY和O_WRONLY請求同時達到FIFO文件時,兩個進程的打開請求就都能返回了。
如果指定了O_NONBLOCK,讀打開請求在沒有寫進程的時候,可以成功返回;但是沒有讀進程的寫打開請求,返回-1(失敗),errno = ENXIO。
原因:FIFO只有讀取端,沒有寫入端,沒有明顯的危害,嘗試讀取FIFO數據的操作不會返回任何數據。相反,如果允許只存在寫入端不存在讀取端,那么open之后,所有向FIFO文件的寫入操作,都會導致產生SIGPIPE信號,以及調用write返回EPIPE的錯誤。因此,在源頭上堵住(open返回失敗)該問題反而合理。

添加O_NONBLOCK選項標志
對於無名管道,可以通過pipe2(fd, O_NONBLOCK);來指定O_NONBLOCK選項。
如果忘記為FIFO文件設置O_NONBLOCK標志位,可以通過fcntl:

// 添加O_NONBLOCK標志位
int flags = fcntl(fd, F_GETFL);
flags |= O_NONBLOCK;
fcntl(fd, F_SETFL, flags);

// 清除O_NONBLOCK標志位
int flags = fcntl(fd, F_GETFL);
flags &= ~O_NONBLOCK;
fcntl(fd, F_SETFL, flags);

讀寫FIFO文件

同普通文件操作,通過調用read讀文件,write寫文件。
寫文件的數據量不能超過PIPE_BUF個字節(limits.h),否則不能保證是原子的(內容連續)。多個進程同時寫入,寫入的內容也不會被其他進程寫入內容打斷。
PIPE_BUF最少512byte,對Linux是4096(一個頁面大小)。

參考Linux進程間通信(一)之無名管道(PIPE)和有名管道(FIFO)
《APUE》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM