Linux行程通信——匿名管道、命名管道、管道的特性和共享記憶體
- 一、管道
- 1.1 什么是管道?
- 1.2 匿名管道
- <1> 匿名管道引數說明
- <2> fork共享管道原理
- <3> 站在檔案描述符角度-深度理解管道
- <4> 管道讀寫規則
- <5> 管道的特性與特點總結
- 1.3 命名管道
- <1> 創建一個命名管道
- <2> 匿名管道與命名管道的區別
- <3> 命名管道的打開規則
- <4> 用命名管道實作server&clinet通信
- 二、共享記憶體
- 2.1 共享記憶體示意圖
- 2.2 共享記憶體函式
- <1> shmget函式(創建共享記憶體)
- <2> shmat函式(關聯共享記憶體)
- <3> shmdt函式(取消關聯)
- <4> shmctl函式(洗掉共享記憶體)
- 2.3 用共享記憶體實作server&client通信
- <1> Makefile
- <2> comm.h
- <3> server.c
- <4> client.c
- <5> 結果
一、管道
1.1 什么是管道?
- 管道是Unix中最古老的行程間通信的形式,
- 我們把從一個行程連接到另一個行程的一個資料流稱為一個“管道”
- 管道的本質就是一塊緩沖區

1.2 匿名管道
<1> 匿名管道引數說明

#include <unistd.h>
int pipe(int fd[2]);
-
功能:創建一無名管道原型
-
引數
fd:檔案描述符陣列其中fd[0]表示讀端, fd[1]表示寫端(我們可以將0看作一張嘴代表讀,1看作一支筆代表寫)
-
回傳值:成功回傳0,失敗回傳錯誤代碼

上圖可以更好的幫助理解pipe函式的功能,當呼叫pipe函式時,向系統傳遞一個fd檔案描述符陣列,其中fd[1]對應寫端,將資料塞入管道,fd[0]代表讀端,從管道中讀取資料
#include<stdio.h> #include<unistd.h> int main() { int fd[2] = {0}; int ret = pipe(fd); printf("ret:%d\n",ret); printf("fd[0]:%d\n",fd[0]); printf("fd[1]:%d\n",fd[1]); return 0; }
<2> fork共享管道原理
首先我們先了解管道的兩個特性:
1. 只能用于具有共同祖先的行程(**具有親緣關系的行程**)之間進行通信;通常,一個管道由一個行程創
建,然后該行程呼叫fork,此后父、子行程之間就可應用該管道,
2. **管道是半雙工的,也就是說管道只能進行單向通信,即只能一端寫一端讀**,但我們呼叫fork函式時,如果父行程寫入i am father,那么父行程就需要關閉讀端,而子行程讀取父行程寫入的資訊時,子行程就需要關閉寫端,如下圖所示

代碼實體
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
int main()
{
int pipefd[2] = {0};
int ret = pipe(pipefd);
if(ret==-1)
{
perror("pipe");
return 1;
}
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
//父行程讀取
if(id > 0)
{
//父行程關閉寫檔案描述符
close(pipefd[1]);
char buf[64];
while(1)
{
ssize_t s = read(pipefd[0],buf,sizeof(buf)-1);
if(s > 0)
{
buf[s] = 0;
printf("father get a msg:%s\n",buf);
sleep(1);
}
}
}
//子行程寫入i am child
if(id == 0)
{
//子行程關閉讀檔案描述符
close(pipefd[0]);
const char *msg = "i am child";
while(1)
{
write(pipefd[1],msg,strlen(msg));
sleep(1);
}
}
return 0;
}
結果
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZLlzXFd6-1617778502369)(https://gitee.com/Kyrie-leon/blog-image/raw/master/Linux//20210406092524.png
]
上述程式子行程每次向管道寫入資訊,父行程從管道讀取并列印
<3> 站在檔案描述符角度-深度理解管道
-
父行程呼叫pipe()創建管道,假設系統分配檔案描述符3給fd[0]用于讀,檔案描述符4給fd[1]用于寫

-
父行程呼叫fork()函式創建子行程,子行程具有和父行程同樣的資料,檔案描述符的指向也是相同的

-
但是管道具有半雙工特征,即只能一端讀一端寫,因此父子行程需要關閉各自不需要的檔案描述符,假設父行程寫子行程讀,那么父行程關閉fd[0]讀端,子行程關閉fd[1]寫端

<4> 管道讀寫規則
-
當沒有資料可讀時
-
O_NONBLOCK disable:read呼叫阻塞,即行程暫停執行,一直等到有資料來到為止,
-
O_NONBLOCK enable:read呼叫回傳-1,errno值為EAGAIN,
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> int main() { int pipefd[2] = {0}; int ret = pipe(pipefd); if(ret==-1) { perror("pipe"); return 1; } pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } //父行程讀取 if(id > 0) { //父行程關閉寫檔案描述符 close(pipefd[1]); char buf[64]; while(1) { ssize_t s = read(pipefd[0],buf,sizeof(buf)-1); if(s > 0) { buf[s] = 0; printf("father get a msg:%s\n",buf); } } } //子行程寫入i am child if(id == 0) { //子行程關閉讀檔案描述符 close(pipefd[0]); const char *msg = "i am child"; int count = 0; while(1) { write(pipefd[1],msg,strlen(msg)); printf("write a msg:%d\n",count++); sleep(5); } } return 0; }
-
-
當管道滿的時候
-
O_NONBLOCK disable: write呼叫阻塞,直到有行程讀走資料
-
O_NONBLOCK enable:呼叫回傳-1,errno值為EAGAIN
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> int main() { int pipefd[2] = {0}; int ret = pipe(pipefd); if(ret==-1) { perror("pipe"); return 1; } pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } //父行程讀取 if(id > 0) { //父行程關閉寫檔案描述符 close(pipefd[1]); char buf[64]; while(1) { ssize_t s = read(pipefd[0],buf,sizeof(buf)-1); if(s > 0) { buf[s] = 0; printf("father get a msg:%s\n",buf); sleep(5); } } } //子行程寫入i am child if(id == 0) { //子行程關閉讀檔案描述符 close(pipefd[0]); const char *msg = "i am child"; int count = 0; while(1) { write(pipefd[1],msg,strlen(msg)); printf("write a msg:%d\n",count++); } } return 0; }
-
-
如果所有管道寫端對應的檔案描述符被關閉,則read回傳0
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<unistd.h> int main() { int pipefd[2] = {0}; int ret = pipe(pipefd); if(ret==-1) { perror("pipe"); return 1; } pid_t id = fork(); if(id < 0) { perror("fork"); return 1; } //父行程讀取 else if(id > 0) { //父行程關閉寫檔案描述符 close(pipefd[1]); char buf[64]; while(1) { ssize_t s = read(pipefd[0],buf,sizeof(buf)-1); if(s > 0) { buf[s] = 0; printf("father get a msg:%s\n",buf); sleep(1); } printf("father get a msg:%d\n",s); sleep(1); } } //子行程寫入i am child else { //子行程關閉讀檔案描述符 close(pipefd[0]); const char *msg = "i am child\n"; int count = 0; while(1) { write(pipefd[1],msg,strlen(msg)); printf("write a msg:%d\n",count++); //讀10次后關閉寫端 if(count == 10) { close(pipefd[1]); break; } } exit(2); } return 0; }
-
如果所有管道讀端對應的檔案描述符被關閉,則write操作會產生信號SIGPIPE,進而可能導致write行程 退出

-
當要寫入的資料量不大于PIPE_BUF時,linux將保證寫入的原子性,
-
當要寫入的資料量大于PIPE_BUF時,linux將不再保證寫入的原子性,
<5> 管道的特性與特點總結
管道道的四個特性:
- 如果寫端不關閉檔案描述符且不寫入,讀端可能需要長時間阻塞;讀取條件不滿足時,讀取端就要被阻塞(管道為空);
- 當我們實際再進行寫入時,如果寫入條件不滿足我們寫入端就要進行阻塞(管道滿了);
- 如果寫端不但不寫入還關閉檔案描述符,讀端讀取完資料后就會讀到檔案結尾;
- 如果讀端關閉,寫端行程可能在后續會被行程殺掉,
管道特點:
-
只能用于具有共同祖先的行程(具有親緣關系的行程)之間進行通信;通常,一個管道由一個行程創
建,然后該行程呼叫fork,此后父、子行程之間就可應用該管道, -
管道提供流式服務
-
一般而言,行程退出,管道釋放,所以管道的生命周期隨行程,管道檔案只是標識,洗掉后依然可以通信
-
一般而言,內核會對管道操作進行同步(沒有資料讀阻塞,緩沖區寫滿寫阻塞)與互斥
-
管道是半雙工的,資料只能向一個方向流動;需要雙方通信時,需要建立起兩個管道
1.3 命名管道
-
管道應用的一個限制就是只能在具有共同祖先(具有親緣關系)的行程間通信,
-
如果我們想在不相關的行程之間交換資料,可以使用FIFO檔案來做這項作業,它經常被稱為命名管道,
-
命名管道是一種特殊型別的檔案
<1> 創建一個命名管道
命名管道可以從命令列上創建,命令列方法是使用下面這個命令:
mkfifo filename
命名管道也可以從程式里創建,相關函式有:
int mkfifo(const char *filename, mode_t mode);

<2> 匿名管道與命名管道的區別
-
匿名管道由pipe函式創建并打開,
-
命名管道由mk?fo函式創建,打開用open
-
FIFO(命名管道)與pipe(匿名管道)之間唯一的區別在它們創建與打開的方式不同,一但這些作業完 成之后,它們具有相同的語意,
<3> 命名管道的打開規則
- 如果當前打開操作是為讀而打開FIFO時
- O_NONBLOCK disable:阻塞直到有相應行程為寫而打開該FIFO
- O_NONBLOCK enable:立刻回傳成功
- 如果當前打開操作是為寫而打開FIFO時
- O_NONBLOCK disable:阻塞直到有相應行程為讀而打開該FIFO
- O_NONBLOCK enable:立刻回傳失敗,錯誤碼為ENXIO
<4> 用命名管道實作server&clinet通信
-
構建Makefile檔案
.PHONY:all all:server client client:client.c gcc -o $@ $^ server:server.c gcc -o $@ $^ .PHONY:clean clean: rm client server fifo -
服務器代碼
#include<stdio.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define FIFO_FILE "./fifo" int main() { umask(0); if(-1 == mkfifo(FIFO_FILE,0666)) { perror("mkfifo"); return 1; } int fd = open(FIFO_FILE, O_RDONLY); if(fd < 0) { perror("open"); return 1; } else { while(1) { char buf[1024]; ssize_t s = read(fd,buf,sizeof(buf)-1); if(s > 0) { buf[s] = 0; printf("#############################\n"); printf("client#:%s\n",buf); } else { close(fd); printf("server offline!\n"); break; } } } return 0; } -
客戶端代碼
#include<stdio.h> #include<string.h> #include<unistd.h> #include<sys/types.h> #include<sys/stat.h> #include<fcntl.h> #define FIFO_FILE "./fifo" int main() { int fd = open(FIFO_FILE,O_WRONLY); if(fd < 0) { perror("open"); return 1; } else { while(1) { printf("Please Input Your Message:"); fflush(stdout); char msg[1024]; //從鍵盤讀取資訊 ssize_t s = read(0,msg,sizeof(msg)-1); if(s > 0) { msg[s] = 0; write(fd,msg,strlen(msg)); } } } return 0; }
通過命名管道可以發現管道的本質就是一塊快取

二、共享記憶體
共享記憶體區是最快的IPC形式,一旦這樣的記憶體映射到共享它的行程的地址空間,這些行程間資料傳遞不再涉及到 內核,換句話說是行程不再通過執行進入內核的系統呼叫來傳遞彼此的資料,
2.1 共享記憶體示意圖

行程間能夠實作通信必然需要看到同一份資源,而共享記憶體就是通過讓行程A和B能夠同時看到同一塊物理記憶體而實作的行程間通信,而共享記憶體是將同一塊物理記憶體映射到各個行程虛擬地址空間,可以直接通過虛擬地址訪問,相較于其它方式少了兩步內核態與用戶態之間的資料拷貝因此速度最快,對于一份資料想要通過A傳遞給B,只要拷貝到行程A的地址空間,共享記憶體再將這份資源拷貝過來,然后再拷貝給行程B,這樣減少了諸多的步驟就可以做到高速高效了,
2.2 共享記憶體函式

<1> shmget函式(創建共享記憶體)

<2> shmat函式(關聯共享記憶體)

-
說明

<3> shmdt函式(取消關聯)

<4> shmctl函式(洗掉共享記憶體)


2.3 用共享記憶體實作server&client通信
<1> Makefile
.PHONY:all
all:client server
clinet:client.c
gcc -o $@ $^
server:server.c
gcc -o $@ $^
.PHONY:clean
clean:
rm client server
<2> comm.h
#pragma once
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
#define PATHNAME "."
#define PROJ_ID 0x6666
#define SIZE 4096
<3> server.c
#include"comm.h"
int main()
{
//獲取一個唯一標識記憶體的key值
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
//創建共享記憶體
int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
//關聯共享記憶體
char * addr = shmat(shmid, NULL, 0);
sleep(2);
int count = 0;
while(count++ < 26)
{
printf("client#%s\n",addr);
sleep(1);
}
//取消共享記憶體
shmdt(addr);
sleep(5);
//洗掉
shmctl(shmid,IPC_RMID,NULL);
return 0;
}
<4> client.c
#include"comm.h"
int main()
{
//獲取一個唯一標識記憶體的key值
key_t key = ftok(PATHNAME, PROJ_ID);
if(key < 0)
{
perror("ftok");
return 1;
}
//創建共享記憶體
int shmid = shmget(key,SIZE,0);
//關聯共享記憶體
char * addr = shmat(shmid, NULL, 0);
int i = 0;
while(i < 26)
{
addr[i] = 'A' + i;
i++;
addr[i] = 0;
sleep(1);
}
//取消共享記憶體
shmdt(addr);
sleep(5);
return 0;
}
<5> 結果

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/273634.html
標籤:其他
