[Linux]管道

羡鱼OvO發表於2024-12-03

管道

程序間通訊

通訊目的

  • 資料傳輸:一個程序需要將它的資料傳送給另一個程序。
  • 資源共享:多個程序之間共享同樣的資源。
  • 通知事件:一個程序需要向另一個或一組程序傳送訊息,通知它們發生了某種事件(如程序終止時要通知父程序)。
  • 程序控制:有些程序希望完全控制另一個程序的執行,此時控制程序希望能夠攔截另一個程序的所有陷入和異常,並能夠及時知道它的狀態改變。

通訊的本質

  1. 作業系統需要直接或者間接給通訊雙方的程序提供“記憶體空間”。
  2. 要通訊的程序必須看到一份共同的資源。

可以說,不同的通訊種類本質就是作業系統提供資源的模組不同。

管道

管道是基於檔案系統的,分為匿名管道和命名管道。它是一種單向的、先進先出的資料通道,資料只能沿著一個方向流動,從管道的寫入端流入,從讀取端流出。

匿名管道

匿名管道沒有具體的檔名,只能用於具有親緣關係的程序之間通訊(通常是父子程序)。

匿名管道的建立

使用pipe()系統呼叫來建立匿名管道。

  • 函式原型:int pipe(int pipefd[2]);
  • 引數:pipefd[2]是一個輸出型引數,用於儲存建立的管道的兩個檔案描述符。pipefd[0]用於從管道讀取資料,pipefd[1]用於向管道寫入資料。
  • 返回值:成功返回0,失敗返回-1。

匿名管道的使用

前面說過,程序間通訊就是讓兩個程序看到同一份資源。匿名管道是透過建立子程序的方式來實現的。當fork()建立子程序後,子程序會以父程序為模板來生成,此時子程序會和父程序有一張相同的檔案描述符表。此時就可以實現讓兩個程序看到同一份檔案。管道檔案是一種記憶體級檔案,它裡面的內容不需要重新整理到磁碟。

[Linux]管道

而又由於管道只能單向資料通訊,所以父子程序都要關閉不用的描述符。例如父程序寫資料,子程序讀資料,此時父程序就要關閉讀端,子程序就要關閉寫端(推薦關閉,不關閉的話也沒關係,只要不使用就行)。

讀寫的不同情況

我們透過下面這段程式碼來討論讀寫的幾種不同情況

#include <iostream>
#include <cassert>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    //建立管道
    int fds[2];
    int n = pipe(fds);
    assert(n == 0);

    pid_t id = fork();
    assert(id >= 0);
    if (id == 0)
    {
        //子程序進行寫入
        close(fds[0]);//關閉子程序的讀端

        const char *str = "我是子程序,此時正在給你發訊息";
        int cnt = 0;
        while (true)
        {
            cnt++;
            char buffer[1024];
            snprintf(buffer, sizeof buffer, "chaild to parent : %s[%d][%d]", str, cnt, getpid());
            //將資料寫入管道
            write(fds[1], buffer, strlen(buffer));
            //每隔一秒寫一次
            sleep(1);
        }

        close(fds[1]);
        std::cout << "子程序關閉寫端" << std::endl;
        exit(0);
    }
    //父程序進行讀取
    close(fds[1]);//關閉父程序的寫端
    while (true)
    {
        char buffer[1024];
        ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;//新增'\0'
        }
        std::cout << "get message: " << buffer << " | my pid: " << getpid() << std::endl;
    }
    n = waitpid(id, nullptr, 0);
    assert(n == id);
    close(fds[0]);
	return 0;
}
  1. 寫慢,讀快

    從上面的執行結果你會發現,當子程序每隔1秒才寫入一條訊息時,父程序會跟隨著子程序每隔一秒讀一條訊息,也就是說父程序的讀速度和子程序的一樣。

    當子程序的休眠時間改為5秒的時候,同樣父程序也會以5秒為時間間隔進行讀取。也就是說,當子程序寫入完成後進行5秒的休眠,父程序讀取完子程序寫的歷史訊息後,進入阻塞等待的狀態,直到子程序再次向管道中進行寫入操作。

    綜上,當管道中的資料都被讀取完時,父程序會在read()操作這裡阻塞。

  2. 寫快,讀慢

    這次,讓父程序每隔5秒讀一次,子程序不進行休眠。

       //父程序每隔五秒讀一次
    while (true)
    {
    	char buffer[1024];
    	ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
    	if (s > 0)
    	{
    		buffer[s] = 0;//新增'\0'
    	}
    	std::cout << "get message: " << buffer << " | my pid: " << getpid() << std::endl;
    	sleep(5);
    }
    
    //子程序不休眠
    while (true)
    {
    	cnt++;
    	char buffer[1024];
    	snprintf(buffer, sizeof buffer, "chaild to parent : %s[%d][%d]", str, cnt, getpid());
    	//將資料寫入管道
    	write(fds[1], buffer, strlen(buffer));
    }
    

    此時你會發現,父程序一下會讀取一大批的資料(這批資料並不是按行顯示的,因為它讀取的時候是按照位元組流的方式進行讀取)。若是再將父程序讀取的時間間隔繼續增加,你會發現管道可能會被寫滿,寫滿後,子程序就會被阻塞,直到父程序從管道中讀取資料。

    也就是說,管道是一個有著固定大小的緩衝區,當緩衝區滿時,寫操作就要被阻塞。

  3. 寫關閉,讀到0

    這次讓子程序寫一條資料後就退出,父程序每隔一秒讀一次。

    //父程序每隔一秒讀一次
    while (true)
    {
    	char buffer[1024];
    	ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
    	if (s > 0)
    	{
    		buffer[s] = 0;//新增'\0'
    		std::cout << "get message: " << buffer << " | my pid: " << getpid() << std::endl;
    	}
    	else if (s == 0)
    	{
    	std::cout << "read: " << s << std::endl;
    	break;
    	}
    	// else break;
    	sleep(1);
    }
    
    //子程序寫一條資料後就關閉
    while (true)
    {
    	cnt++;
    	char buffer[1024];
    	snprintf(buffer, sizeof buffer, "chaild to parent : %s[%d][%d]", str, cnt, getpid());
    	//將資料寫入管道
    	write(fds[1], buffer, strlen(buffer));
    	break;
    }
    
    close(fds[1]);
    std::cout << "子程序關閉寫端" << std::endl;
    exit(0);
    

    如果是這種情況,父程序遲早會將管道的資料讀完。最終讀到0後,管道中沒有資料了,父程序也會退出。

  4. 讀關閉,寫

    由於管道是單向通訊,當讀關閉後,寫就沒有意義了,此時作業系統就會發出訊號終止寫程序。

    //父程序讀一次後就關閉    
    while (true)
    {
        char buffer[1024];
        ssize_t s = read(fds[0], buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;//新增'\0'
            std::cout << "get message: " << buffer << " | my pid: " << getpid() << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "read: " << s << std::endl;
            break;
        }
        break;
    }
    close(fds[0]);
    std::cout << "父程序關閉讀端" << std::endl;
    
    int status = 0;
    n = waitpid(id, &status, 0);
    assert(n == id);
    //獲取退出訊號
    std::cout << "pid->" << n << ":" << WTERMSIG(status) << std::endl;
    
    //子程序一直寫
    while (true)
    {
        cnt++;
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "chaild to parent : %s[%d][%d]", str, cnt, getpid());
        //將資料寫入管道
        write(fds[1], buffer, strlen(buffer));
    }
    
    close(fds[1]);
    std::cout << "子程序關閉寫端" << std::endl;
    exit(0);
    
    [Linux]管道

命名管道

命名管道有一個特定的檔名,它可以在不相關的程序之間進行通訊,只要這些程序知道管道的檔名即可訪問它。

命名管道的建立

命名管道又兩種建立方式。

  1. 直接透過命令列建立:mkfifo [檔名]

    [Linux]管道
  2. 透過mkfifo()系統呼叫建立。

    • 函式原型:int mkfifo(const char *filename, mode_t mode);
    • 引數:第一個引數是檔名,第二個引數是建立檔案時指定許可權。

匿名管道是透過建立子程序的方式讓父子程序看到同一份資源,那命名管道又是如何讓沒有親緣關係的兩個程序看到同一份資源呢?我們知道,在作業系統中以檔名加檔案路徑的方式就可以標定一個檔案的唯一性。所以讓不同的程序開啟指定名稱(檔名+檔案路徑)的檔案就可以讓不同的程序看到同一份資源了。

與匿名管道的區別

  • 匿名管道由pipe函式建立並開啟。
  • 命名管道由mkfifo函式建立,開啟用open

命名管道與匿名管道之間唯一的區別在它們建立與開啟的方式不同,一但這些工作完成之後,它們具有相同的語義。

總結

  1. 管道的生命週期隨著程序。
  2. 匿名管道可以用來在具有親緣關係的程序間通訊。
  3. 命名管道可以用來在不相關的程序之間通訊。
  4. 管道是面向位元組流的。
  5. 管道是以半雙工單向的方式進行通訊。
  6. 管道具有同步與互斥機制。

相關文章