進程間通信(一)管道

管道基礎

管道是一種最基本的IPC機制,作用於有血緣關係的進程之間(多用於父子進程間的通信),完成數據傳遞。調用pipe系統函數即可創建一個管道。有如下特質:

            1. 是一個僞文件(本質爲內核緩衝區)

            2. 由兩個文件描述符引用,一個表示讀端,一個表示寫端。

            3. 規定數據從管道的寫端流入管道,從讀端流出。

管道的原理: 管道實爲內核藉助內核緩衝區(4k)實現的環形隊列。

管道的侷限性:

            ① 數據自己讀不能自己寫。

            ② 數據一旦被讀走,便不在管道中存在,不可反覆讀取。

            ③ 由於管道採用半雙工通信方式。因此,數據只能在一個方向上流動(要想實現雙工就只能建立兩個管道)。

            ④ 只能在有公共祖先的進程間使用管道。

常見的通信模式有,單工通信、半雙工通信、全雙工通信。

  • 單工通信:是指消息只能單方向傳輸的工作方式。例如遙控、遙測(一部分),就是單工通信方式。
  • 半雙工通信:是指數據可以沿兩個方向傳送,但同一時刻一個信道只允許單方向傳送。例如:無線對講機就是一種半雙工設   備,在同一時間內只允許一方講話。
  • 全雙工:是指在通信的任意時刻,線路上可以同時存在A到B和B到A的雙向信號傳輸。電話就是典型的全雙工

 

如何用管道實現進程間通訊

        int pipe(int pipefd[2]);        成功:0;失敗:-1,設置errno

desc :  函數調用成功返回r/w兩個文件描述符。無需open,但需手動close。規定:fd[0] → r fd[1] → w,就像0對應標準輸入,1對應標準輸出一樣。向管道文件讀寫數據其實是在讀寫內核緩衝區

管道創建成功以後,創建該管道的進程(父進程)同時掌握着管道的讀端和寫端。如何實現父子進程間通信呢?通常可以採用如下步驟:

1. 父進程調用pipe函數創建管道,得到兩個文件描述符fd[0]、fd[1]指向管道的讀端和寫端。

2. 父進程調用fork創建子進程,那麼子進程也有兩個文件描述符指向同一管道。

3. 父進程關閉管道讀端,子進程關閉管道寫端。父進程可以向管道中寫入數據,子進程將管道中的數據讀出。由於管道是利用環形隊列實現的,數據從寫端流入管道,從讀端流出,這樣就實現了進程間通信。

demo:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>

int main()
{
	int fds[2], childPid, parentPid;
	int rst = 0, count;
	char buffer[1024];
	rst = pipe(fds);
	if (rst == -1) {
		perror("pipe error:");
		return -1;
	}

	rst = fork();
	if (rst == -1) {
		perror("fork failed: ");	
		return -1;
	}
	else if (rst == 0) {
		printf("This is parent:will read\n");
		close(fds[1]);
		while (1) {
			memset(buffer, 0, 1024);		
			count = read(fds[0], buffer, 1024);
			printf("\ncount == %d\n", count);
			write(STDOUT_FILENO, buffer, count);
		}
	}else {
		printf("This is child: will write\n");
		childPid = rst;
		close(fds[0]);
		while (1) {
			memset(buffer, 0, 1024);		
			memcpy(buffer, "abc123abc", 9);
			count = write(fds[1], buffer, 1024);
			sleep(5);
		}
	}
	return 0;
}

result :

NOTE: 大家對pipe的理解一定要充分, pipe能夠實現進程間通信的理論基礎是:fork之後父子進程共享打開的文件描述符(未見描述符是有內核空間的內存背書)。函數pipe()傳出一對文件描述符, 分別指向讀端和寫端。這其實就是pipe的全部, 至於developer怎麼利用這一點, 愛咋咋地。並不是說一定要父子進程分別關閉讀端和寫端, 也不是說就只能向一邊發數據(只不過是同一時間點不能夠雙向發, 只要時序安排得當, 照樣沒問題, 但是問題的關鍵是在現實中根本沒法安排時序, 下例中給出了雙向發消息), 我們知識爲了實現正確的進程間通信, 來給開發者添加一些潛規則, 避免不必要的麻煩和錯誤。

總結:

            1. 父子進程都不做任何關閉=====>>>>沒問題

             2. 認爲控制時序的雙向發消息=====>>>>沒問題

             3. 自己寫自己讀=====>>>>沒問題(除非閒的蛋疼了)

             4. 一個進程寫, 其他的親戚讀取============>>>>沒問題(要是你能夠控制好同步問題, 但是不要忘了,讀完可就沒了)

所以管道的單向通信只是我們選取了管道的諸多使用辦法中靠譜有用的一種而已, 好比黃瓜可以煎炒烹炸,我們卻只用它調涼菜, 因爲別的做法不好喫。至於放不放蔥花(是否做相應的關閉操作), 放那個牌子的生抽(誰讀誰寫), 放幾種調料(幾個讀,幾個寫), 全憑自己愛好。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>


int main()
{
	int fds[2], childPid, parentPid;
	int rst = 0, count;
	char buffer[1024];
	rst = pipe(fds);
	if (rst == -1) {
		perror("pipe error:");
		return -1;
	}

	rst = fork();
	if (rst == -1) {
		perror("fork failed: ");	
		return -1;
	}
	else if (rst == 0) {
		printf("parent = %d\n", getpid());
		sleep(1);
		while (1) {
			memset(buffer, 0, 1024);		
			count = read(fds[0], buffer, 1024);
			printf("%s, %d\n", buffer, getpid());
			sleep(3);
			memcpy(buffer, "123456789", 9);
			count = write(fds[1], buffer, 1024);
			sleep(3);
		}
	}else {
		printf("child = %d\n", getpid());
		while (1) {
			memset(buffer, 0, 1024);		
			memcpy(buffer, "abcdefghi", 9);
			count = write(fds[1], buffer, 1024);
			sleep(3);
			count = read(fds[0], buffer, 1024);
			printf("%s, %d\n", buffer, getpid());
			sleep(3);
		}
	}
	return 0;
}

管道讀寫行爲

使用管道需要注意以下4種特殊情況(假設都是阻塞I/O操作,沒有設置O_NONBLOCK標誌):

  • 1. 如果所有指向管道寫端的文件描述符都關閉了(管道寫端引用計數爲0),而仍然有進程從管道的讀端讀數據,那麼管道中剩餘的數據都被讀取後,再次read會返回0,就像讀到文件末尾一樣。
  • 2. 如果有指向管道寫端的文件描述符沒關閉(管道寫端引用計數大於0),而持有管道寫端的進程也沒有向管道中寫數據,這時有進程從管道讀端讀數據,那麼管道中剩餘的數據都被讀取後,再次read會阻塞,直到管道中有數據可讀了纔讀取數據並返回。
  • 3. 如果所有指向管道讀端的文件描述符都關閉了(管道讀端引用計數爲0),這時有進程向管道的寫端write,那麼該進程會收到信號SIGPIPE,通常會導致進程異常終止。當然也可以對SIGPIPE信號實施捕捉,不終止進程。具體方法信號章節詳細介紹。
  • 4. 如果有指向管道讀端的文件描述符沒關閉(管道讀端引用計數大於0),而持有管道讀端的進程也沒有從管道中讀數據,這時有進程向管道寫端寫數據,那麼在管道被寫滿時再次write會阻塞,直到管道中有空位置了才寫入數據並返回。

總結:

① 讀管道:

                   1. 管道中有數據,read返回實際讀到的字節數。

                   2. 管道中無數據:

                                (1) 管道寫端被全部關閉,read返回0 (好像讀到文件結尾)

                                (2) 寫端沒有全部被關閉,read阻塞等待(不久的將來可能有數據遞達,此時會讓出cpu)

② 寫管道:

                 1. 管道讀端全部被關閉,進程異常終止(也可使用捕捉SIGPIPE信號,使進程不終止)

                 2. 管道讀端沒有全部關閉:

                                (1) 管道已滿,write阻塞。
                                (2) 管道未滿,write將數據寫入,並返回實際寫入的字節數。

 

混淆的東西,管道的容量和管道的緩衝區大小

    管道的容量:指管道滿時裝的字節數,自2.6.11內核後,容量爲64k(65536)。管道滿了就會導致寫操作產生阻塞。

    管道緩衝區大小:由PIPE_BUF指定,指的是保證管道寫操作爲原子操作的最大值,如果一次寫入的內容超過這個值,那麼這次的寫操作就不是原子的。什麼意思呢?就是指,可能存在多個進程寫同一個管道,如果一次寫入的字節數大於緩衝區大小,則可能會出現A進程寫入的內容中插入了B進程寫入的內容。通常可以


   下面是manpage的解釋, 大家可以 man 7 pipe看

   Pipe capacity
       A pipe has a limited capacity.  If the pipe is full, then a write(2) will block or fail, depending on whether the O_NONBLOCK flag is set (see
       below).  Different implementations have different limits for the pipe capacity.  Applications should not rely on a  particular  capacity:  an
       application  should be designed so that a reading process consumes data as soon as it is available, so that a writing process does not remain
       blocked.

       In Linux versions before 2.6.11, the capacity of a pipe was the same as the system page size (e.g., 4096 bytes on i386).  Since Linux 2.6.11,
       the pipe capacity is 16 pages (i.e., 65,536 bytes in a system with a page size of 4096 bytes).  Since Linux 2.6.35, the default pipe capacity
       is 16 pages, but the capacity can be queried and set using the fcntl(2) F_GETPIPE_SZ and F_SETPIPE_SZ  operations.   See  fcntl(2)  for  more
       information.

 

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

#define MAXLINE     4096+100

int main(void)
{
    int n;
    int fd[2];
    pid_t pid;
    char line[MAXLINE];

    if (pipe(fd) < 0)
    {
        perror("pipe error");
    }
    
    if ((pid = fork()) < 0)
    {
        perror("fork error");
    }
    else if (pid > 0)          /* parent */
    {
        close(fd[1]);
        while ( 1 )
        {
            n = read(fd[0], line, MAXLINE);
            write(STDOUT_FILENO, line, n);
            write(STDOUT_FILENO, "\n\n\n", 3);
        }
    }
    else                      /* child */
    {
        if ((pid = fork()) < 0)
        {
            perror("fork error");
        }
        else if (pid > 0)
        {
            close(fd[0]);
            
            while (1)
            {
                memset(line, 'a',MAXLINE);
                write(fd[1], line, MAXLINE);
            }
        }
        else
        {
            close(fd[0]);
            
            while ( 1 )
            {
                memset(line, 'b',MAXLINE);
                write(fd[1], line, MAXLINE);
            }
        }
    }
    
    exit(0);
}

在 Linux 中,管道的實現並沒有使用專門的數據結構,而是藉助了文件系統的file結構和VFS的索引節點inode。通過將兩個 file 結構指向同一個臨時的 VFS 索引節點,而這個 VFS 索引節點又指向一個物理頁面而實現的。

有兩個 file 數據結構,但它們定義文件操作例程地址是不同的,其中一個是向管道中寫入數據的例程地址,而另一個是從管道中讀出數據的例程地址。這樣,用戶程序的系統調用仍然是通常的文件操作,而內核卻利用這種抽象機制實現了管道這一特殊操作。

//inode結點信息結構
struct inode {
...
    struct pipe_inode_info  *i_pipe;
... 
};
//管道緩衝區個數
#define PIPE_BUFFERS (16)
 
//管道緩存區對象結構
struct pipe_buffer {
    struct page *page; //管道緩衝區頁框的描述符地址
    unsigned int offset, len; //頁框內有效數據的當前位置,和有效數據的長度
    struct pipe_buf_operations *ops; //管道緩存區方法表的地址
};
 
//管道信息結構
struct pipe_inode_info {
    wait_queue_head_t wait; //管道等待隊列
    unsigned int nrbufs, curbuf; 
    //包含待讀數據的緩衝區數和包含待讀數據的第一個緩衝區的索引
    struct pipe_buffer bufs[PIPE_BUFFERS]; //管道緩衝區描述符數組
    struct page *tmp_page; //高速緩存區頁框指針
    unsigned int start;  //當前管道緩存區讀的位置
    unsigned int readers; //讀進程的標誌,或編號
    unsigned int writers; //寫進程的標誌,或編號
    unsigned int waiting_writers; //在等待隊列中睡眠的寫進程的個數
    unsigned int r_counter; //與readers類似,但當等待讀入FIFO的進程是使用
    unsigned int w_counter; //與writers類似,但當等待寫入FIFO的進程時使用
    struct fasync_struct *fasync_readers; //用於通過信號進行的異步I/O通知
    struct fasync_struct *fasync_writers; //用於通過信號的異步I/O通知

大家可以通過閱讀/usr/src/kernels/4.18.0-147.5.1.el8_1.x86_64/include/linux/pipe_fs_i.h 來獲取更多關於pipe的實現細節

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