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共享管道原理
首先我們先了解管道的兩個特性:
- 隻能用于具有共同祖先的程序(具有親緣關系的程序)之間進行通信;通常,一個管道由一個程序建立,然後該程序調用fork,此後父、子程序之間就可應用該管道。
- 管道是半雙工的,也就是說管道隻能進行單向通信,即隻能一端寫一端讀,但我們調用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;
}
結果
上述程式子程序每次向管道寫入資訊,父程序從管道讀取并列印
<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函數建立并打開。
- 命名管道由mkfifo函數建立,打開用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;
}