參考博客:https://blog.csdn.net/sjsjnsjnn/article/details/125864580
一、進程間通訊介紹
1.1 進程間通訊的概念
進程通信(Interprocess communication),簡稱:IPC
- 本來進程之間是相互獨立的。但是由于不同的進程之間可能要共享某些信息,所以就必須要有通訊來實現進程間的互斥和同步。比如說共享同一塊內存、管道、消息隊列、信號量等等就是實現這一過程的手段,相當于移動公司在打電話的作用。
1.2 進程間通訊的目的
- 數據傳輸:一個進程需要將它的數據發送給另一個進程
- 資源共享:多個進程之間共享同樣的資源。
- 通知事件:一個進程需要向另一個或一組進程發送消息,通知它(它們)發生了某種事件(如進程終止時要通知父進程)。
- 進程控制:有些進程希望完全控制另一個進程的執行(如Debug進程),此時控制進程希望能夠攔截另一個進程的所有陷入和異常,并能夠及時知道它的狀態改變
1.3 進程間通信的前提
- 進程間通信的前提本質:由操作系統參與,提供一份所有通信進行都能看到的公共資源;
- 兩個或多個進程相互通信,必須先看到一份公共的資源,這里的所謂的資源是屬于操作系統的,就是一段內存(可能以文件的方式提供、可能以隊列的方式提供,也有可能提供的就是原始內存塊),這也就是通信方式有很多種的原因;
1.4 進程間通信的分類
管道
- 匿名管道pipe
- 命名管道
System V IPC
- System V 消息隊列
- System V 共享內存
- System V 信號量
POSIX IPC
- 消息隊列
- 共享內存
- 信號量
- 互斥量
- 條件變量
- 讀寫鎖
二、管道通訊
2.1 管道的概念
- 管道是Unix中最古老的進程間通信的形式。
- 我們把從一個進程連接到另一個進程的一個數據流稱為一個“管道”
比如下面的命令,我們通過管道連接了cat test.c
和wc -l
兩個命令,本質是兩個進程
cat test.c | wc -l
- 運行的結果如下,統計了
test.c
文件的行數(wc
),并且將對應的結果輸出了出來(cat
) - 這里執行的順序為從右到左,先執行
wc -l
2.2 匿名管道
2.2.1 基本原理
- 匿名管道用于進程間通信,且僅限于父子進程之間的通信。
-
我們知道進程的PCB中包含了一個指針數組 struct file_struct,它是用來描述并組織文件的。父進程和子進程均有這個指針數組,因為子進程是父進程的模板,其代碼和數據是一樣的;
-
打開一個文件時,其實是將文件加載到內核中,內核將會以結構體(struct file)的形式將文件的相關屬性、文件操作的指針集合(即對應的底層IO設備的調用方法)等;
-
當父進程進行數據寫入時(例如:寫入“hello Linux”),數據是先被寫入到用戶級緩沖區,經由系統調用函數,又寫入到了內核緩沖區,在進程結束或其他的操作下才被寫到了對應的設備中;
-
如果數據在寫入設備之前,“hello Linux”是在內核緩沖區的,因為子進程和父進程是同時指向這個文件的,所以子進程是能夠看到這個數據的,并且可以對其操作;
-
簡單來說,父進程向文件寫入數據時,不直接寫入對應的設備中,而是將數據暫存在內核緩沖區中,交給子進程來處理;
所以這種基于文件的方式就叫做管道;
2.2.2 管道的創建步驟
- 在創建匿名管道實現父子進程間通信的過程中,需要pipe函數和fork函數搭配使用,具體步驟如下:
- 匿名管道屬于單向通信,意味著父子進程只有一個端是打開的,實現父子通信的時候就需要根據自己的想要實現的情況,關閉對應的文件描述符;
pipe函數
#include <unistd.h>
int pipe(int pipefd[2]);
函數的參數是兩個文件的描述符,是輸出型參數:
pipefd[0]
:讀管道 — 對應的文件描述符是3pipefd[1]
:寫管道 — 對應的文件描述符是4
返回值:成功返回0,失敗返回-1;
2.2.3 匿名管道通訊
- 下面的代碼通過使用
fork
和pipe
函數實現父子進程之間的通訊 - 其中,父進程用于讀取數據,子進程用于寫入數據
- 由于管道是單向通訊的,因此需要關閉管道的另一端,即父進程關閉寫端,子進程關閉讀端
void test1(){int pipe_fd[2];memset(pipe_fd,0,sizeof(pipe_fd));int ret = pipe(pipe_fd);if(ret < 0 ){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if(ret == 0){ //子進程close(pipe_fd[0]); //關閉子進程讀端for(int i =1;i<=10;++i){std::string msg = "hello from child process :" + std::to_string(i) +" times";write(pipe_fd[1],msg.c_str(),msg.size());sleep(1);}exit(0);}close(pipe_fd[1]); //關閉父進程寫端char buffer[1024];memset(buffer,0,sizeof(buffer));while(1){ssize_t s = read(pipe_fd[0],buffer,sizeof(buffer));if(s <= 0){std::cout << "read finished !" << std::endl;break; }else{buffer[s] = '\0';std::cout << "read from child process : " << buffer << std::endl;}}
}
- 可以發現,管道的讀寫端的文件描述符為
3
,4
,其中0
,1
,2
通常是輸入流、輸出流和錯誤流 - 通過打印結果,可以發現父子進程成功通訊了
2.2.4 匿名管道通訊的特點
五個特點
- 管道僅限父子通訊,只能單向通訊
- 管道提供流式服務
- 管道自帶同步與互斥機制
- 進程退出,管道隨之釋放,因此管道的生命周期隨進程
- 如果需要雙向通訊,則需要建立兩個管道
四個情況
- 讀端不讀或者讀得慢,寫端要等待讀端
- 讀端關閉,寫端收到
SIGPIPE
信號后終止 - 寫端不寫或者寫得慢,讀端要等待寫端
- 寫端關閉,讀端讀到
EOF
后退出
2.2.5 字節流通訊
- 字節流的特征就是沒有邊界,每次讀取指定的字節
- 我們發送數據的時候是先把數據寫到內核緩沖區中,讀取的時候也是從內核緩沖區讀取指定的字節
- 因此,如果寫端慢了,那么讀取的數據會重合在一起,如下面的程序所示
void test1(){int pipe_fd[2];memset(pipe_fd,0,sizeof(pipe_fd));int ret = pipe(pipe_fd);if(ret < 0 ){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if(ret == 0){ //子進程close(pipe_fd[0]); //關閉子進程讀端for(int i =1;i<=10;++i){std::string msg = "hello from child process :" + std::to_string(i) +" times";write(pipe_fd[1],msg.c_str(),msg.size());sleep(1);}exit(0);}close(pipe_fd[1]); //關閉父進程寫端char buffer[1024];memset(buffer,0,sizeof(buffer));while(1){sleep(10);ssize_t s = read(pipe_fd[0],buffer,sizeof(buffer));if(s <= 0){std::cout << "read finished !" << std::endl;break; }else{buffer[s] = '\0';std::cout << "read from child process : " << buffer << std::endl;}}
}
- 可以發現,讀取的數據全都合并在一起了,因為我們指定讀取的字節數較大
2.2.6 同步機制
- 內核的緩沖區是有大小限制的,下面我們不斷發送數據到內核緩沖區,到了
65536
字節后,就發送不了了,此時阻塞了進程
void test2(){int pipe_fd[2];memset(pipe_fd,0,sizeof(pipe_fd));int ret = pipe(pipe_fd);if(ret < 0 ){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if(ret == 0){ //子進程close(pipe_fd[0]); //關閉子進程讀端int writedBytes = 0;for(int i =1;i<=10000000;++i){write(pipe_fd[1],"a",1);writedBytes ++;std::cout << "child process send msg: " << "a" <<",writed Bytes = " << writedBytes<< std::endl;}exit(0);}close(pipe_fd[1]); //關閉父進程寫端char buffer[1024];memset(buffer,0,sizeof(buffer));while(1){sleep(1);}
}
- 管道通訊自帶同步機制和互斥機制,也就是發送端和接收端看到的數據是一致的,并且同時只有一段可以讀或者寫
- 下面的程序在內核緩沖區寫滿了以后,嘗試讀取數據,發現只有讀取了一些數據之后,才能繼續往內核寫入數據,而不是讀取一個字節可以寫入一個字節
void test3()
{int pipe_fd[2];memset(pipe_fd,0,sizeof(pipe_fd));int ret = pipe(pipe_fd);if(ret < 0 ){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if(ret == 0){ //子進程close(pipe_fd[0]); //關閉子進程讀端int writedBytes = 0;for(int i =1;i<=10000000;++i){write(pipe_fd[1],"a",1);writedBytes ++;std::cout << "child process send msg: " << "a" <<",writed Bytes = " << writedBytes<< std::endl;}exit(0);}close(pipe_fd[1]); //關閉父進程寫端sleep(5);while(1){char c = 0;read(pipe_fd[0],&c,1);std::cout << "read :" << c << std::endl;sleep(1);}
}
- 讀取部分數據后,才會繼續寫入
- 讀端太慢,會導致寫端等待讀端
void test3()
{int pipe_fd[2];memset(pipe_fd,0,sizeof(pipe_fd));int ret = pipe(pipe_fd);if(ret < 0 ){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if(ret == 0){ //子進程close(pipe_fd[0]); //關閉子進程讀端int writedBytes = 0;for(int i =1;i<=10000000;++i){write(pipe_fd[1],"a",1);writedBytes ++;std::cout << "child process send msg: " << "a" <<",writed Bytes = " << writedBytes<< std::endl;}exit(0);}close(pipe_fd[1]); //關閉父進程寫端sleep(5);char buffer[1024];memset(buffer,0,sizeof(buffer));int readBytes = 0;while(1){char c = 0;ssize_t s = read(pipe_fd[0],buffer,sizeof(buffer));buffer[s] = '\0';std::cout << "read :" << buffer << std::endl;std::cout << "read bytes = " << readBytes << std::endl;sleep(1);readBytes += s;}
}
2.2.7 寫端關閉
- 寫端關閉,那么讀端會讀到
EOF
后自動退出 - 比如下面的程序,我們讓讀進程先休眠一會,然后寫進程寫了一些數據后退出,那么讀進程讀到
EOF
后也就退出了
void test4()
{int pipe_fd[2];memset(pipe_fd, 0, sizeof(pipe_fd));int ret = pipe(pipe_fd);if (ret < 0){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if (ret == 0){ // 子進程close(pipe_fd[0]); // 關閉子進程讀端for (int i = 1; i <= 10; ++i){write(pipe_fd[1], "abcdefg", 7);}exit(0);}sleep(5);close(pipe_fd[1]); // 關閉父進程寫端char buffer[1024];memset(buffer, 0, sizeof(buffer));int readBytes = 0;while (1){char c = 0;ssize_t s = read(pipe_fd[0], buffer, sizeof(buffer));if (s <= 0){std::cout << "read finished !" << std::endl;break;}buffer[s] = '\0';readBytes += s;std::cout << "read :" << buffer << std::endl;std::cout << "read bytes = " << readBytes << std::endl;sleep(1);}
}
2.2.8 讀端關閉
- 讀端關閉,寫段會收到
SIGPIPE
信號,然后中斷進程 - 當我們的讀端關閉,寫端還在寫入,在操作系統的層面上,嚴重不合理;這本質上就是在浪費操作系統的資源,所以操作系統在遇到這樣的情況下,會將子進程殺掉(發送13號信號—SIGPIPE)
下面的shell
腳本用于持續跟蹤測試進程
while :; do ps axj | grep pipe_process | grep -v grep; sleep 1; echo "####################";
done;
- 可以發現,子進程退出后,父進程隨之也退出了
- 這里我們添加上父進程等待子進程,也就是
waitpid
,然后輸出對應的信號值 - 可以發現,退出后的信號值為
13
,對應的是SIGPIPE
void test5()
{int pipe_fd[2];memset(pipe_fd, 0, sizeof(pipe_fd));int ret = pipe(pipe_fd);if (ret < 0){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if (ret == 0){ // 子進程close(pipe_fd[0]); // 關閉子進程讀端for (int i = 1; i <= 10; ++i){write(pipe_fd[1], "abcdefg", 7);sleep(1);}exit(0);}close(pipe_fd[1]); // 關閉父進程寫端char buffer[7];memset(buffer, 0, sizeof(buffer));int readBytes = 0;while (1){char c = 0;ssize_t s = read(pipe_fd[0], buffer, sizeof(buffer));if (s <= 0){std::cout << "read finished !" << std::endl;break;}buffer[s] = '\0';std::cout << "read :" << buffer << std::endl;readBytes += s;std::cout << "read bytes = " << readBytes << std::endl;sleep(2);close(pipe_fd[0]);int status = 0;waitpid(-1, &status, 0);printf("exit code: %d\n",(status >> 8)& 0xFF);printf("exit signal: %d\n",status& 0x7F);}
}
- 查詢對應的信號,符合預期
kill -l
2.2.9 非阻塞管道
int pipe2(int pipefd[2], int flags);
- 可以通過
pip2
函數,設置管道通訊的阻塞與非阻塞 - 可以通過設置
O_NONBLOCK
標志為非阻塞,默認為阻塞,或者傳入0
當沒有數據可讀時
- O_NONBLOCK disable:read調用阻塞,即進程暫停執行,一直等到有數據來到為止。
- O_NONBLOCK enable:read調用返回-1,errno值為EAGAIN。
當管道滿的時候
- O_NONBLOCK disable: write調用阻塞,直到有進程讀走數據
- O_NONBLOCK enable:調用返回-1,errno值為EAGAIN
- 如果所有管道寫端對應的文件描述符被關閉,則read返回0
- 如果所有管道讀端對應的文件描述符被關閉,則write操作會產生信號SIGPIPE,進而可能導致write進程退出
- 當要寫入的數據量不大于PIPE_BUF時,linux將保證寫入的原子性。
- 當要寫入的數據量大于PIPE_BUF時,linux將不再保證寫入的原子性。
2.2.9.1 非阻塞寫入滿了
- 下面的程序演示非阻塞管道寫端,在內核寫入滿了以后的返回值
void test6(){int pipe_fd[2];memset(pipe_fd, 0, sizeof(pipe_fd));int ret = pipe2(pipe_fd,O_NONBLOCK);if (ret < 0){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if (ret == 0){ // 子進程close(pipe_fd[0]); // 關閉子進程讀端int writedBytes = 0;for (int i = 1; i <= 10000000; ++i){int ret = write(pipe_fd[1], "a", 1);if(ret == -1 && errno == EAGAIN){std::cout << "errno: EAGAIN !" << std::endl;sleep(1);continue;}writedBytes++;std::cout << "child process send msg: " << "a" << ",writed Bytes = " << writedBytes << std::endl;}exit(0);}close(pipe_fd[1]); // 關閉父進程寫端int readBytes = 0;while (1){sleep(1);}
}
2.2.9.2 非阻塞無數據可讀
- 下面的程序演示非阻塞管道讀取,無數據可讀的返回值
void test7(){int pipe_fd[2];memset(pipe_fd, 0, sizeof(pipe_fd));int ret = pipe2(pipe_fd,O_NONBLOCK);if (ret < 0){std::cout << "error:" << strerror(ret) << std::endl;return;}std::cout << "pipe_fd[0] = " << pipe_fd[0] << std::endl;std::cout << "pipe_fd[1] = " << pipe_fd[1] << std::endl;ret = fork();if (ret == 0){ // 子進程close(pipe_fd[0]); // 關閉子進程讀端sleep(60);}close(pipe_fd[1]); // 關閉父進程寫端int readBytes = 0;char buffer[1024] = {0};while (1){ssize_t s = read(pipe_fd[0],buffer, sizeof(buffer));if(s == -1 && errno == EAGAIN){std::cout << "errno = " << "EAGAIN !" << std::endl;sleep(1);continue;}}
}
2.3 命名管道
2.3.1 命名管道的概念
- 匿名管道應用的一個限制就是只能在具有共同祖先(具有親緣關系)的進程間通信。
- 如果我們想在不相關的進程之間交換數據,可以使用FIFO文件來做這項工作,它經常被稱為命名管道。 命名管道是一種特殊類型的文件
2.3.2 命名管道的創建
2.3.2.1 命令行創建
可以通過命令行創建命名管道,使用mkfifo
指令
創建管道之后,可以使用cat
指令讀取數據,使用echo
指令寫入數據
2.3.2.2 代碼創建
- 使用
mkfifo
函數可以創建一個命名管道
函數原型
int mkfifo(const char *pathname, mode_t mode);
pathname:表示你要創建的命名管道文件
- 如果pathname是以文件的方式給出,默認在當前的路徑下創建;
- 如果pathname是以某個路徑的方式給出,將會在這個路徑下創建;
mode:表示給創建的命名管道設置權限
- 我們在設置權限時,例如0666權限,它會受到系統的umask(文件默認掩碼)的影響,實際創建出來是(mode & ~umask)0664;
- 所以想要正確的得到自己設置的權限(0666),我們需要將文件默認掩碼設置為0;
返回值:命名管道創建成功返回0,失敗返回-1
#define MY_FIFO "myfifo"int main(int argc,char* argv[])
{umask(0);int ret = mkfifo(MY_FIFO,0666);if(ret < 0){perror("mkfifo");return 1;}return 0;
}
2.3.3 使用命名管道通訊
- 我們使用CS模型,在服務端創建命名管道,同時服務端不斷讀取數據,客戶端發送數據
- 使用阻塞管道,當沒有數據可讀,服務端持續阻塞等待客戶端的數據
server
#include<iostream>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<cstring>#define MY_FIFO "fifo"
int main(){umask(0);int ret = mkfifo(MY_FIFO,0666);if(ret < 0){perror("mkfifo");return 1;}int fd = open(MY_FIFO,O_RDONLY);//只讀模式if(fd < 0){perror("open");return 1;}while(1){char buffer[1024];memset(buffer,0,sizeof(buffer));ssize_t len = read(fd,buffer,sizeof(buffer) - 1);if(len == 0){std::cout << "read fifo finished !" << std::endl;break;}else if(len > 0){buffer[len] = '\0';std::cout << "read from client : " << buffer << std::endl;}else{perror("open");break;}}close(fd);return 0;
}
client
#include<iostream>
#include<sys/stat.h>
#include<unistd.h>
#include<fcntl.h>
#include<cstring>#define MY_FIFO "fifo"
int main(){int fd = open(MY_FIFO,O_WRONLY);//寫入模式if(fd < 0){perror("open");return 1;}while(1){std::string str;std::cout << "Please enter message :";std::cin >> str;ssize_t len = write(fd,str.c_str(),str.size());if(len <= 0){perror("write");break;}}close(fd);return 0;
}
2.4 管道的總結
管道:
-
管道分為匿名管道和命名管道;
-
管道通信方式的中間介質是文件,通常稱這種文件為管道文件;
-
匿名管道:管道是半雙工的,數據只能單向通信;需要雙方通信時,需要建立起兩個管道;只能用于父子進程或者兄弟進程之間(具有親緣關系的進程)。
-
命名管道:不同于匿名管道之處在于它提供一個路徑名與之關聯,以FIFO的文件形式存在于文件系統中。這樣,即使與FIFO的創建進程不存在親緣關系的進程,只要可以訪問該路徑,就能夠彼此通過FIFO相互通信
-
利用系統調用pipe()創建一個無名管道文件,通常稱為無名管道或PIPE;利用系統調用mkfifo()創建一個命名管道文件,通常稱為有名管道或FIFO。
-
PIPE是一種非永久性的管道通信機構,當它訪問的進程全部終止時,它也將隨之被撤消。
-
FIFO是一種永久的管道通信機構,它可以彌補PIPE的不足。管道文件被創建后,使用open()將文件進行打開,然后便可對它進行讀寫操作,通過系統調用write()和read()來實現。通信完畢后,可使用close()將管道文件關閉。
-
匿名管道的文件是內存中的特殊文件,而且是不可見的,命名管道的文件是硬盤上的設備文件,是可見的。
更多資料:https://github.com/0voice