Linux進程間通信---命名管道

概述

Linux進程間通信—管道(無名管道、pipe)一文中介紹了匿名管道的使用,但是其中有一個明顯的缺陷,匿名管道只能用於有親緣關係的進程之間通信,命名管道則解決了這個缺陷,可以在沒有親緣關係的2個進程之間進行通信。

命名管道(named pipe) 也被成爲FIFO文件,是一種特殊類型的文件。在文件系統中可以找到實在的文件與之對應,這樣進程可以像訪問文件一樣對管道進行訪問,大部分文件訪問的API(open、read、write)也可以用於命名管道的訪問。

命名管道的行爲和匿名管道基本一致,也存在如下特點:

  1. 阻塞方式下,read會因管道空阻塞,和write會因管道滿阻塞,直到可讀或者可寫;
  2. 阻塞方式下,open操作也會阻塞,直到另一端也能夠打開的時候,open才能正常返回;
  3. 非阻塞方式下,以O_RDONLY打開管道,可以正常打開;以O_WRONLY打開管道,會返回失敗,並設置ENXIO(6)錯誤碼;
  4. 在所有讀端關閉的情況下,進行寫入時會拋出SIGPIPE信號;
  5. 以O_RDWR方式打開命名管道,其行爲是未定義的;(參考POSIX)

API

#include <sys/types.h>
#include <sys/stat.h>
/* 創建fifo文件
 * @filename: 文件名
 * @mode: 文件的讀寫權限
 * return: 成功返回0, 失敗返回-1, 並正確設置errno
 */
int mkfifo(const char *filename, mode_t mode);

#include <sys/stat.h> 
#include <fcntl.h>
/* 打開管道文件
 * @filename: 文件名
 * @oflag: 打開方式,O_WRONLY, O_RDONLY,O_NONBLOCK等
 * return: 成功返回非負的文件描述符, 失敗返回-1, 並正確設置errno
 */
int open(const char *path, int oflag, ... );

#include <unistd.h>
/* 讀取管道文件
 * @fildes: 文件描述符
 * @buf: 存儲結果的內存
 * @nbyte: 最大讀取的字節數
 * return: 成功返回非負的讀取的字節數, 失敗返回-1, 並正確設置errno
 */
ssize_t read(int fildes, void *buf, size_t nbyte);

#include <unistd.h>
/* 向管道文件寫入數據
 * @fildes: 文件描述符
 * @buf: 需要寫入的數據
 * @nbyte: 最大寫入的字節數
 * return: 成功返回非負的讀取的字節數, 失敗返回-1, 並正確設置errno
 */
ssize_t write(int fildes, void *buf, size_t nbyte);

mkfifo創建一個FIFO文件,值得注意的是,這是一個***真實的文件系統中的文件***。用open打開一個管道,並通過readwrite進行文件讀寫。這些操作和直接操作文件基本沒有任何區別。

訪問命名管道

同匿名管道相比,訪問命名管道多了一個open的操作。直接看例子:

pipe_read.c: 每隔2s,從管道中讀取128個字節,pipe_write.c:每隔1s從管道中讀取128字節。

// pipe_read.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>

#define err_print(fmt, args...) do { print_now(); printf("[E_NO: %d, %s]", errno, strerror(errno)); printf(fmt, ##args ); printf("\n");} while(0)
#define dbg_print(fmt, args...) do { print_now(); printf("[file: %s, line: %d]", __FILE__, __LINE__); printf(fmt, ##args ); printf("\n"); } while(0)

void print_now()
{
    time_t rawtime;
    struct tm * timeinfo;
    char *t;

    time( &rawtime );
    timeinfo = localtime( &rawtime );
    t = asctime(timeinfo);
    t[strlen(t) - 1] = 0;
    printf("[%s] ", t);
}

int main(int argc, char *argv[])
{
    int fd;
    int ret;
    int len;

    ret = mkfifo("my_fifo", 0666);
    if(ret != 0)
    {
        err_print("mkfifo");
        if(EEXIST != errno)
            exit(-1);
    }

    // 只讀並指定阻塞方式打開(默認)
    fd = open("my_fifo", O_RDONLY);
    if(fd<0)
    {
        err_print("open fifo");
        exit(-1);
    }

    dbg_print("open fifo ok.");

    while(1)
    {
        char recv[129] = {0};

        // 緩衝區空的情況下,會阻塞
        len = read(fd, recv, sizeof(recv) - 1);
        if (len <= 0)
        {
            dbg_print("read from my_fifo buf=%d [%s]",len, recv);
            continue;
        }

        recv[len] = '\0';
        dbg_print("read from my_fifo buf=%d [%s]",len, recv);
        sleep(2);
    }

    return 0;
}
// pipe_write.c
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <time.h>

#define err_print(fmt, args...) do { print_now(); printf("[E_NO: %d, %s]", errno, strerror(errno)); printf(fmt, ##args ); printf("\n");} while(0)
#define dbg_print(fmt, args...) do { print_now(); printf("[file: %s, line: %d]", __FILE__, __LINE__); printf(fmt, ##args ); printf("\n"); } while(0)

static void sig_handler(int signo)
{
    dbg_print("get signal %d", signo);
}

void print_now()
{
    time_t rawtime;
    struct tm * timeinfo;
    char *t;

    time( &rawtime );
    timeinfo = localtime( &rawtime );
    t = asctime(timeinfo);
    t[strlen(t) - 1] = 0;
    printf("[%s] ", t);
}

int main(int argc, char *argv[])
{
    int fd;
    int ret;
    int len;
    int times;

    struct sigaction psa;

    psa.sa_handler = sig_handler;
    // 默認情況下,SIGPIPE會導致進程退出,在這裏我們捕獲並輸出該信號
    sigaction(SIGPIPE, &psa, NULL);

    // 創建命名管道
    ret = mkfifo("my_fifo", 0666);
    if(ret != 0)
    {
        err_print("mkfifo");
        if(EEXIST != errno)
            exit(-1);
    }

    // 只寫並指定阻塞方式打開(默認)
    fd = open("my_fifo", O_WRONLY);
    if(fd<0)
    {
        err_print("open fifo");
        exit(-1);
    }
    dbg_print("open fifo ok.");

    char send[129];
    send[sizeof(send) - 1] = '\0';
    times = 0;
    len = 0;
    while(1)
    {
        // 緩衝區滿的情況下,會阻塞
        len = write(fd, send, sizeof(send) - 1);
        if (len < 0)
        {
            err_print("write error");
        }
        dbg_print("%d write to my_fifo buf len: %d",times, len);
        sleep(1);
        times ++;
    }

    return 0;
}
# 分別編譯
$ gcc pipe_read.c -o pipe_read
$ gcc pipe_read.c -o pipe_read
# 在終端1中執行write動作
$ date; ./pipe_write
2019年 03月 15日 星期五 10:57:08 CST
[Fri Mar 15 10:57:08 2019] [E_NO: 17, File exists]mkfifo
# 等待6s以後纔有open成功的輸出,這個時間恰好是pipe_read執行的時間,說明open阻塞到了現在;
# 先執行pipe_read再執行pipe_write的效果是一樣的
[Fri Mar 15 10:57:14 2019] [file: pipe_write.c, line: 61]open fifo ok.
[Fri Mar 15 10:57:14 2019] [file: pipe_write.c, line: 75]0 write to my_fifo buf len: 128
[Fri Mar 15 10:57:15 2019] [file: pipe_write.c, line: 75]1 write to my_fifo buf len: 128
[Fri Mar 15 10:57:16 2019] [file: pipe_write.c, line: 75]2 write to my_fifo buf len: 128
[Fri Mar 15 10:57:17 2019] [file: pipe_write.c, line: 75]3 write to my_fifo buf len: 128
[Fri Mar 15 10:57:18 2019] [file: pipe_write.c, line: 75]4 write to my_fifo buf len: 128
[Fri Mar 15 10:57:19 2019] [file: pipe_write.c, line: 75]5 write to my_fifo buf len: 128
# 因pipe_read關閉, 繼續寫入時收到了SIGPIPE(13)信號,此處如果我們不捕獲該信號,默認進程退出
[Fri Mar 15 10:57:20 2019] [file: pipe_write.c, line: 16]get signal 13
# 此時write函數返回了-1,並設置了EPIPE(32)錯誤
[Fri Mar 15 10:57:20 2019] [E_NO: 32, Broken pipe]write error
[Fri Mar 15 10:57:20 2019] [file: pipe_write.c, line: 75]6 write to my_fifo buf len: -1
[Fri Mar 15 10:57:21 2019] [file: pipe_write.c, line: 16]get signal 13
[Fri Mar 15 10:57:21 2019] [E_NO: 32, Broken pipe]write error
[Fri Mar 15 10:57:21 2019] [file: pipe_write.c, line: 75]7 write to my_fifo buf len: -1

# 在終端2中執行read動作
$ date; ./pipe_read
# 執行時間比pipe_read晚6s
2019年 03月 15日 星期五 10:57:14 CST
[Fri Mar 15 10:57:14 2019] [E_NO: 17, File exists]mkfifo
[Fri Mar 15 10:57:14 2019] [file: pipe_read.c, line: 48]open fifo ok.
# 此時已經有數據可讀,read會立馬返回
[Fri Mar 15 10:57:14 2019] [file: pipe_read.c, line: 58]read from my_fifo buf=128 []
[Fri Mar 15 10:57:16 2019] [file: pipe_read.c, line: 58]read from my_fifo buf=128 []
[Fri Mar 15 10:57:18 2019] [file: pipe_read.c, line: 58]read from my_fifo buf=128 []
# 此時通過Ctrl + C 強制結束進程
^C
# 此時可以發現有個文件存在於文件夾下,首字符p表示這是一個管道文件
$ ls -l my_fifo
prw-rw-r-- 1 qingru.meng qingru.meng 0 3月  15 11:43 my_fifo

以阻塞方式open時,讀、寫必須都在open時才能同時返回,否則會被阻塞:

  • 如以O_WRONLY打開時,會阻塞,直到另外有以O_RDONLY、O_REWR方式open的操作時纔會返回;
  • 如以O_RDWR打開時, open不會阻塞;

非阻塞方式時:

  • 以O_WRONLY打開,會直接返回負值,並設置錯誤爲ENXIO(6);
  • 以O_RDONLY和O_RDWR可以正常打開;

以阻塞方式read和write時,如果不可讀或者不可寫時,會阻塞,直到可讀或可寫;以非阻塞方式read和write時,如果不可讀或不可寫,則直接返回-1。

安全性

命名管道的設計是爲了方便不同進程之間的信息傳遞,一個進程向管道中寫入信息,另外一個進程從中讀取信息,假設有多個進程同時向管道中寫入信息,是否會發生信息的交錯?

在POSIX.1中有關於pipe的描述,通過man指令看一下:

$ man 7 pipe
PIPE_BUF
    POSIX.1 says that write(2)s of less than PIPE_BUF bytes must be atomic: the output data is written to the pipe as a contiguous sequence.  Writes of more than PIPE_BUF bytes  may  be nonatomic:  the  kernel  may  interleave  the  data  with data written by other processes. POSIX.1 requires PIPE_BUF to be at least 512 bytes.  (On Linux, PIPE_BUF is  4096  bytes.) The  precise  semantics depend on whether the file descriptor is nonblocking (O_NONBLOCK), whether there are multiple writers to the pipe, and on n, the number of bytes to be  written:
    O_NONBLOCK disabled, n <= PIPE_BUF 
    All  n  bytes are written atomically; write(2) may block if there is not room for n bytes to be written immediately
    O_NONBLOCK enabled, n <= PIPE_BUF
    If there is room to write n bytes to the pipe, then write(2) succeeds  immediately, writing all n bytes; otherwise write(2) fails, with errno set to EAGAIN.
    O_NONBLOCK disabled, n > PIPE_BUF
    The  write  is  nonatomic:  the  data  given  to  write(2)  may be interleaved with write(2)s by other process; the write(2) blocks until n bytes have been written.
    O_NONBLOCK enabled, n > PIPE_BUF
    If the pipe is full, then write(2) fails, with errno  set  to  EAGAIN.   Otherwise, from  1  to  n  bytes may be written (i.e., a "partial write" may occur; the caller should check the return value from write(2) to see how  many  bytes  were  actually written), and these bytes may be interleaved with writes by other processes.

意思就是規定了對pipe的實現時需要依據PIPE_BUF的大小來決定pipe的寫入行爲,而寫入小於PIPE_BUF大小的數據必須是原子的

  1. 阻塞時,寫入n <= PIPE_BUF字節

    寫入n字節是原子的,不會發生交錯,如果沒有n個字節的空間,write會阻塞;

  2. 非阻塞時,寫入n <= PIPE_BUF字節

    如果空間足夠,則成功寫入,否則返回-1,並設置EAGAIN;

  3. 阻塞時,寫入n > PIPE_BUF字節

    寫入是非原子的,數據可能發生交錯,直到n字節全部寫完,write纔會返回;

  4. 非阻塞時,寫入n > PIPE_BUF字節

    如果管道滿,write返回-1,並設置EAGAIN,否則可能寫入任意1到n字節數據,調用者需要自己檢查write的返回值,以確定是否全部寫完。

PIPE_BUF的大小依據Linux內核的不同而有所不同(最小512字節,一般爲4K),最好事先確定:

$ ulimit -a
...
# 可以發現PIPE_BUF爲512*8=4K
pipe size            (512 bytes, -p) 8
...

參考資料

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