Linux:程序間通訊(匿名管道命名管道)(共享記憶體,訊息佇列,訊號量)
目錄
程序間通訊的介紹
程序間通訊:程序之間的溝通交流
因為程序的獨立性,所以導致程序間的資料通訊將變得非常麻煩。
在實際工作中往往會出現一個系統中好幾個程序協同工作,那麼這些程序就需要交流溝通,完成協作。
作業系統不得不提供方法來使程序間能夠通訊。
因此就產生了程序間通訊方式,來解決如何進行程序間通訊的問題。
為什麼要程序通訊:
程序之間的協作(事件通知,資料傳輸,程序控制)
因為通訊的公共介質有所不同,所以通訊的方式不止一種。
程序間通訊方式:
- 管道(匿名管道/命名管道)
- 共享記憶體(System v IPC)
- 訊息佇列(System v IPC)
- 訊號量(System v IPC)
管道
我們把從一個程序連線到另一個程序的一個數據流成為一個“管道”
傳輸資料資源
原理:作業系統為程序提供一個雙方都能訪問核心的一塊緩衝區,作業系統提供的管道操作介面就是基礎io介面
半雙工:單向通訊(所以必須確定方向)
匿名管道
匿名管道:
不可見 於檔案系統,建立的緩衝區是沒有名字的僅僅適用於具有親緣關係的程序間通訊,因為匿名管道其他程序根本找到不到,
所以無法通訊, 只能通過子程序複製父程序的方法,讓自己從能夠訪問到相同的管道,來實現通訊。
(管道操作:io操作---檔案描述符)
半雙工:單向通訊(所以必須確定方向)
原理:
匿名管道的原理其實就是一個建立一個子程序,子程序複製了父程序的描述符表,因此也有兩個描述符,並且它們指向同一個管道,這時候它們兩個都能訪問到這個管道,因此可以通訊。
只不過因為管道是半雙工單向通行,因此在通行之前需要確定資料流向,
#include<unistd.h>
功能:建立無名管道
原型:int pipe(int fd[2]);
引數:檔案描述符陣列。其中fd[0]表示讀端,fd[1]表示寫端
返回值:成功返回0,失敗返回錯誤程式碼
程式碼實現
//這是一個匿名管道的demo
//父程序寫子程序讀
//就能用於具有親緣關係的程序間通訊
//建立匿名管道必須在建立子程序之前
//否則子程序將無法複製
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
int main()
{
pid_t pid;
int pipefd[2] = {0};
if(pipe(pipefd) < 0)
{
perror("pipe");
return -1;
}
pid = fork();
if(pid < 0)
{
perror("error");
return -1;
}
else if(pid == 0)
{
sleep(1);
close(pipefd[1]);
char buff[1024] = {0};
read(pipefd[0],buff,1024);
printf("child:%s\n",buff);
close(pipefd[0]);
}
else
{
close(pipefd[0]);
char *ptr = "wjf";
write(pipefd[1],ptr,strlen(ptr));
close(pipefd[1]);
}
return 0;
}
匿名管道特性
- 只能用於具有親緣關係的程序間通訊
- 管道是半雙工的單向通訊
- 管道的生命週期隨程序(開啟管道的所用程序退出,管道釋放)
- 管道是面向位元組流傳輸資料的(面向位元組流:資料無規則,無明顯邊界,收發靈活,可能會粘連)
- 自帶同步與互斥
同步:保證一個操作的訪問時序性(我操作完了你在操作)
互斥:對臨界資源同一時間的唯一訪問性,保護臨界資源的安全(我操作時你不能操作)
實現管道符 |
| 為匿名管道
//這是一個實現管道符的demo
//命令:ps -ef | grep ssh
//一個程序執行ps程式,一個程序執行grep程式
//ps程式就需要將結果通過管道傳遞給grep程式
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
int main()
{
pid_t pid = -1;
int pipefd[2] = {0};
if(pipe(pipefd) < 0)
{
perror("pipe");
return -1;
}
pid = fork();
if(pid < 0)
{
perror("fork");
return -1;
}
else if(pid == 0)
{
//子程序執行grep程式處理ps的結果(從管道讀資料)
// sleep(3);
dup2(pipefd[0],0);//重定向:從管道中獲取
close(pipefd[1]);
execl("/bin/grep","grep","ssh",NULL);
close(pipefd[0]);
}
else
{
//父程序執行ps程式,將結果寫入管道
dup2(pipefd[1],1);//重定向:本身將資料寫入標準輸出顯示器。重定向後寫入管道
close(pipefd[0]);
execl("/bin/ps","ps","-ef",NULL);
close(pipefd[1]);
}
return 0;
}
命名管道
命名管道:檔案系統可見,是一個特殊(管道型別)型別檔案(其他程序都能看見,所以任意程序都可以開啟)
命名管道可以應用於同一主機上的所有程序間通訊
建立:
命名建立:mkfifo pipe_filename
程式碼建立: int mkfifo(const char*pathname, mode_t mode )
標頭檔案: #include <sys/types.h>
#include <sys/stat.h>
這兩個標頭檔案已經在#include<unistd.h>中包含過了
p開標頭檔案為管道檔案
命名管道特性
- 不僅具有匿名管道的讀寫特性,並且還有自己的開啟特性
- 匿名管道是直接開啟的,pipe直接返回的檔案描述符
- 命名管道建立之後,並不會直接開啟,需要使用者自己open開啟,後續通過檔案描述符操作
- //開啟管道檔案:
- //如果以只讀開啟命名管道,那麼open函式將阻塞等待
- //直到其他程序以寫的方式開啟這個命名管道
- //如果以只寫的開啟命名管道,那麼open函式將阻塞等待
- //直到有其它程序以讀的方式開啟這個祕密管道
- //如果命名管道以讀寫的方式開啟,則不會阻塞
程式碼實現
:命名管道實現程序間交流
至少一寫一讀所以有兩份程式碼:
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
int main()
{
char *file = "./test.fifo";
umask(0);//僅僅在當前程序有效
if(mkfifo(file,0664) < 0)
{
if(errno == EEXIST)
printf("fifo exit\n");
else
{
perror("mkfifo");
return -1;
}
}
int fd =open(file,O_RDONLY);//只能在mkfifo處建立,不能使用O_CREAST
if(fd < 0)
{
perror("open");
return -1;
}
printf("open fifo success!\n");
while(1)
{
char buff[1024] = {0};//如果不初始化直接定義,結尾可能不是'\0',都是亂碼 (memset(buff,0x00,1024)也可以)
int ret = read(fd,buff,1024);
if(ret > 0 )
printf("wjf say:%s\n",buff);
}
return 0;
}
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<fcntl.h>
#include<string.h>
int main()
{
char *file = "./test.fifo";
umask(0);//僅僅在當前程序有效
if(mkfifo(file,0664) < 0)
{
if(errno == EEXIST)
printf("fifo exit\n");
else
{
perror("mkfifo");
return -1;
}
}
int fd =open(file,O_WRONLY);//只能在mkfifo處建立,不能使用O_CREAST
if(fd < 0)
{
perror("open");
return -1;
}
printf("open fifo success!\n");
while(1)
{
fflush(stdout);
char buff[1024] = {0};
printf("input: ");
scanf("%s",buff);
write(fd,buff,strlen(buff));
}
return 0;
管道讀寫規則
1:管道無資料:讀取
————如果描述符是預設的阻塞屬性,讀取將會阻塞掛起等待,直到管道有資料
————如果描述符被設定為非阻塞屬性,讀取操作符將不具備條件,直接報錯返回EAGAIN
2:管道資料存滿
————如果描述符是預設的阻塞屬性,寫入操作將會阻塞掛起等待,直到有資料被程序取走
————如果描述符被設定為非阻塞屬性,寫入操作將不具備條件,直接報錯返回EAGAIN
3: 如果寫端全部被關閉,這時候如果讀取資料,讀取完管道中的資料,然後返回0。
4: 如果讀端全部被關閉,這時候如果寫入資料,則會觸發異常,作業系統會給程序傳送SIGPIPE信 號,程序收到這個訊號將會退出
5: 當寫入的資料大小超過PIPE_BUF,則這個操作是一個非原子操作,有可能被打斷
6:當寫入的資料大小小於PIPE_BUF,管道保證讀寫的原子性(操作不會被打斷,造成資料混亂)
作業系統中ipc的相關命令
ipcs:檢視ipc資訊
-q:檢視訊息佇列
-m: 檢視共享記憶體
-s:檢視訊號量
ipcrm 刪除ipc
共享記憶體(重點)
一般情況,寫入資料將資料從使用者空間拷貝到核心空間,讀取資料時將資料從核心空間拷貝到使用者空間,效率低下
共享記憶體是最快的IPC形式,一旦這個月的記憶體對映到共享它的程序空間,這些程序間的資料不在涉及到核心
換句話說程序不在通過執行進入核心的系統呼叫來傳遞彼此的資料
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget(key_t key, size_t size, int shmflg);
#include <sys/types.h>
#include <sys/shm.h>
void *shmat(int shmid, const void *shmaddr, int shmflg);
int shmdt(const void *shmaddr);
程序間通訊最快的一種:多個程序將同一塊實體記憶體對映到自己的虛擬地址空間,以這種方式’操作這個虛擬地址
相較於其他通訊方式,少了兩步使用者空間和核心空間的拷貝過程因此速度最快
- 建立/開啟一塊共享記憶體
- 將這塊共享記憶體對映到自己的虛擬地址空間
- 各種記憶體的操作
- 解除對映關係
- 刪除共享記憶體 ipcs -m 、ipcs -m shmid
生命週期:
刪除一個共享記憶體的時候,如果這個共享記憶體依然有其他程序對映連結,這時候這個共享記憶體不會被直接刪除,而是等到所有程序都與共享記憶體解除對映之後才刪除
程式碼實現
//這是一個共享記憶體的demo 共享資料
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
int main()
{
shmget(IPC_KEY,333,IPC_CREAT | 0664);
return 0;
}
程式碼實現獲取資料
//這是一個共享記憶體的demo 共享資料
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
int main()
{
int shmid = -1;
shmid = shmget(IPC_KEY,333,IPC_CREAT | 0664);
if(shmid < 0)
{
printf("shmget error\n");
return -1;
}
void *shm_start = shmat(shmid,NULL,0);
if(shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
int i = 0;
while(1)
{
printf("魔鏡魔鏡誰最帥? %s",shm_start);
sleep(1);
}
return 0;
}
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<sys/shm.h>
#define IPC_KEY 0x123456
int main()
{
int shmid = -1;
// int shmget(key_t key, size_t size, int shmflg);
// key:共享記憶體在系統上的標識
// ftok這個介面可以通過一個檔案計算出一個key值
//size:共享記憶體的大小
//shmflg:IPC_CREAT 建立 | 許可權
//返回值:共享記憶體的操作控制代碼
shmid = shmget(IPC_KEY,333,IPC_CREAT | 0664);
if(shmid < 0)
{
printf("shmget error\n");
return -1;
}
//建立的這個共享記憶體無法直接操作,因為我們只能操作虛擬地址空間中的地址 //因此第二步就是將共享記憶體對映到虛擬地址空間,讓我們能夠通過虛擬地址來訪>問這塊記憶體 // void *shmat(int shmid, const void *shmaddr, int shmflg);
//shmid:共享記憶體控制代碼
//shmaddr:對映首地址(通常置空,交給作業系統取判斷)
//shmflg: SHM_RDONLY 只讀否則可讀可寫
//返回:對映到虛擬地址空間的首地址,失敗:(void*) -1
void *shm_start = shmat(shmid,NULL,0);
if(shm_start == (void*)-1)
{
perror("shmat error");
return -1;
}
int i = 0;
while(1)
{
sprintf(shm_start,"%s----%d\n","of course 王佳飛",i++);
sleep(1);
}
return 0;
訊息佇列
是核心為我們建立的一個佇列,通過這個佇列的識別符號key,每一個程序都可以開啟這個佇列,每一個程序都可以通過這個佇列中插入一個節點或者插入一個節點來完成不同程序間的通訊。這個佇列中的節點有一個型別。)(有型別的資料塊)
ipcs -q(檢視訊息佇列)
- 在核心中建立一個訊息佇列,其他的所有程序都可以通過相同的IPC_KEY開啟訊息佇列
- 這時候可以向佇列中新增資料,也可以拿資料(全雙工)
- 但是可能存在拿錯的問題,所以能夠放的資料是有型別的資料塊
- 並且讀寫的時候只能按訊息塊來發送或接受
ipcs -q 通過這個命令和選項來檢視作業系統上的訊息佇列
ipcrm -q msgid 通過這個命令刪除指定佇列訊息
- 建立訊息佇列:msgget :建立
- 傳送資料/接受資料:msgsnd:傳送 msgrcv:接受
- 釋放訊息佇列: msgctl:控制
- 訊息佇列生命週期隨核心
- msgrcv:msgtype
- msgtype:取佇列中第一個節點,不分型別
- msgtype>0:取指定型別資料塊的一個節點
- msgtype<0:取小於msgtype絕對值型別的第一個節點
訊號量
訊號量是程序間的通訊方式之一,但是並不是用來進行資料傳輸,而是用來程序控制(程序間的同步與互斥)
保證程序間對臨界資源的安全有序訪問,同步保證有序,互斥保證安全。(具有等待佇列的計數器)
- 同步:保證對臨界資源訪問的時序可控性
- 互斥:對臨界資源同一時間的唯一訪問性
多個程序同時操作一個臨界資源的時候就需要通過同步與互斥的機制來實現臨界資源的安全訪問。
- 本質:具有一個等待佇列的計數器!(代表現在還有沒有資源使用)
當計數器不大於0時,代表沒有資源可用時,需要阻塞等待。 - 同步:
只有訊號量資源計數從0轉變為1時,會通知別人中斷阻塞等待
在去操作臨界資源。也就是說資源釋放(+1)之後其他程序才能獲取資 源 ···(-1),然後進行操作。
可以理解為停車場的可用車位計數牌,走一輛車多一個空位所以+1,反之-1 - 互斥:
訊號量如果想要實現互斥,那麼它的計數器只能是0或1(一元訊號量)
我獲取計數器的資源,那麼別人就無法獲取。
訊號量的操作:
semget 建立訊號量
semctl SETVAL SETALL 設定訊號量的計數器初始值
在對臨界資源操作前先獲取訊號量 -1操作
對臨界資源操作完畢之後需要釋放訊號量+1操作
semop
semctl
訊號量作為程序間通訊方式,意味著大家都能夠訪問到訊號量,訊號量實際上也是一個臨界資源,當然訊號量的這個臨界資源是不會出問題的,為什麼?
因為訊號量的操作是一個原子操作
訊號量的操作:
semget 建立訊號量
semctl SETVAL SETALL 設定訊號量的計數器初始值
在對臨界資源操作前先獲取訊號量 -1操作
對臨界資源操作完畢之後需要釋放訊號量+1操作
semop
1:在對臨界資源操作前先獲取訊號量 -1操作
2:對臨界資源操作完畢之後需要釋放訊號量+1操作
semctl IPC_RMID
:刪除