無錫網(wǎng)絡(luò)營(yíng)銷推廣軟件蘭州seo
目錄
一、進(jìn)程間通信介紹
二、管道
1.什么是管道(pipe)
2.重定向和管道
(1)為什么要有管道的存在
(2)重定向和管道的區(qū)別
3.匿名管道
(1)匿名管道原理?
(2)站在文件描述符角度理解匿名管道
(3)創(chuàng)建匿名管道
(4)匿名管道讀寫規(guī)則
(5)匿名管道特點(diǎn)
(6)匿名管道4種特殊情況
(7)匿名管道大小
4.命名管道
(1)命名管道原理?
(2)創(chuàng)建命名管道
(3)命名管道的數(shù)據(jù)不會(huì)刷新到磁盤
5.匿名管道和命名管道的區(qū)別
三、System V IPC
1.System V標(biāo)準(zhǔn)
2.共享內(nèi)存
(1)原理?
(2)步驟
(3)函數(shù)?
shmget
shmctl
shmat
shmdt
(4)使用
3.共享內(nèi)存和管道區(qū)別?
四、消息隊(duì)列?
1.原理?
2.數(shù)據(jù)結(jié)構(gòu)
3.步驟
4.函數(shù)?
(1)msgget
(2)msgctl
(3)msgsnd
(4)msgrcv
五、信號(hào)量
1.原理?
2.數(shù)據(jù)結(jié)構(gòu)
3.函數(shù)
(1)semget
(2)semctl
(3)semop
六、System V IPC總結(jié)
一、進(jìn)程間通信介紹
? ? ? ? 之前學(xué)習(xí)的進(jìn)程,都是各自運(yùn)行,互不干擾,進(jìn)程之間沒有協(xié)同。然而有許多場(chǎng)景下是需要進(jìn)程之間相互協(xié)同的,由于進(jìn)程是程序員寫的,因此進(jìn)程之間的協(xié)同本質(zhì)上就是程序員之間的協(xié)同,比如一個(gè)程序員從數(shù)據(jù)庫(kù)里面拿數(shù)據(jù),另一個(gè)程序員要把從數(shù)據(jù)庫(kù)里面拿到的數(shù)據(jù)進(jìn)行格式化,寫成特定格式,還有一個(gè)程序員根據(jù)格式化的數(shù)據(jù)進(jìn)行統(tǒng)計(jì),如果把這些工作量當(dāng)成意見工作去處理的話,如果其中這三個(gè)環(huán)節(jié)有任何一個(gè)環(huán)節(jié)出錯(cuò)了,那么這個(gè)工作就進(jìn)行不下去了,需要逐一去排查到底是哪個(gè)環(huán)節(jié)出錯(cuò)了,耗時(shí)久且效率低。
? ? ? ? 因此把這個(gè)工作可以分為3個(gè)部分,分別讓3個(gè)不同的進(jìn)程去做:1個(gè)進(jìn)程從數(shù)據(jù)庫(kù)拿數(shù)據(jù),1個(gè)進(jìn)程做數(shù)據(jù)格式化,1個(gè)進(jìn)程做數(shù)據(jù)分析。這就做到了在業(yè)務(wù)層面上用進(jìn)程進(jìn)行解耦。一旦拿數(shù)據(jù)有問題就去找拿數(shù)據(jù)的進(jìn)程,一旦格式化有問題就去找格式化的進(jìn)程,一旦數(shù)據(jù)分析有問題就去找數(shù)據(jù)分析的進(jìn)程。業(yè)務(wù)層面上的解耦能夠增加代碼的可維護(hù)性,這就是進(jìn)程之間的協(xié)同。比如過濾出文件中含字母'i'的行:
cat fdProcess.c運(yùn)行起來就是一個(gè)進(jìn)程,核心工作只是打印數(shù)據(jù),用grep來過濾含有字母'i'的行,數(shù)據(jù)源是從上一個(gè)進(jìn)程cat fdProcess.c通過管道來交給grep的。這就叫做協(xié)同。
就算是父子進(jìn)程,共享了進(jìn)程的代碼和數(shù)據(jù),寫的時(shí)候都必須分開,用寫時(shí)拷貝來寫。兩個(gè)相互獨(dú)立的進(jìn)程,交互數(shù)據(jù),成本很高,各自連對(duì)方保存數(shù)據(jù)的地址空間都看不到,因?yàn)楠?dú)立的進(jìn)程使用獨(dú)立的進(jìn)程地址空間,頁(yè)表映射到不同的物理內(nèi)存,所以看不到對(duì)方的數(shù)據(jù),因此要完成進(jìn)程間通信,不能只在應(yīng)用層解決,必須也要操作系統(tǒng)參與進(jìn)來,要讓操作系統(tǒng)設(shè)計(jì)通信方式。
通信的本質(zhì)就是傳遞數(shù)據(jù),這些數(shù)據(jù)需要一個(gè)進(jìn)程向公共資源里面去放,另一個(gè)進(jìn)程從公共資源向外拿,而公共資源還需要有暫存數(shù)據(jù)的能力。這個(gè)公共資源肯定不屬于這兩個(gè)進(jìn)程,因?yàn)檫M(jìn)程具有獨(dú)立性,如果這個(gè)公共資源是進(jìn)程A的,那么進(jìn)程B是看不到的:
從上圖可以看出進(jìn)程間通信有以下3種方式,目的是為了讓不同的進(jìn)程看到同一份資源:
- 管道
- System V進(jìn)程間通信
- POSIX進(jìn)程間通信?
同時(shí)需要先了解以下概念:
數(shù)據(jù)傳輸:一個(gè)進(jìn)程需要將它的數(shù)據(jù)發(fā)送給另一個(gè)進(jìn)程
資源共享:多個(gè)進(jìn)程之間共享同樣的資源。
通知事件:一個(gè)進(jìn)程需要向另一個(gè)或一組進(jìn)程發(fā)送消息,通知它(它們)發(fā)生了某種事件(如進(jìn)程終止 時(shí)要通知父進(jìn)程)。
進(jìn)程控制:有些進(jìn)程希望完全控制另一個(gè)進(jìn)程的執(zhí)行(如Debug進(jìn)程),此時(shí)控制進(jìn)程希望能夠攔截另 一個(gè)進(jìn)程的所有陷入和異常,并能夠及時(shí)知道它的狀態(tài)改變
二、管道
1.什么是管道(pipe)
管道是Unix最古老的進(jìn)程間通信的形式。把從一個(gè)進(jìn)程連接到另一個(gè)進(jìn)程的數(shù)據(jù)流稱為管道,Linux 管道使用豎線'|'連接多個(gè)命令,這被豎線'|'稱為管道符。
當(dāng)在兩個(gè)命令之間設(shè)置管道時(shí),管道符'|'左邊命令的輸出就變成了右邊命令的輸入。只要第一個(gè)命令向標(biāo)準(zhǔn)輸出寫入,而第二個(gè)命令是從標(biāo)準(zhǔn)輸入讀取,那么這兩個(gè)命令就可以形成一個(gè)管道。大部分的 Linux 命令都可以用來形成管道。
如下所示,對(duì)于命令cat fdProcess.c|grep -i 'i',管道符'|'之前的進(jìn)程cat fdProcess.c是標(biāo)準(zhǔn)輸入進(jìn)程,管道符'|'之后的進(jìn)程grep -i 'i'是標(biāo)準(zhǔn)輸出進(jìn)程,第一個(gè)命令向標(biāo)準(zhǔn)輸出寫入,第二個(gè)命令是從標(biāo)準(zhǔn)輸入讀取,這兩個(gè)命令形成了管道,管道作用于內(nèi)核。
如果沒有管道,那么這兩條命令就得分兩次執(zhí)行。因此用管道執(zhí)行也能達(dá)到同樣的效果。對(duì)于一些備份壓縮復(fù)制需求的命令就可以避免創(chuàng)建臨時(shí)文件。?
管道特點(diǎn):
- 命令的語(yǔ)法緊湊并且使用簡(jiǎn)單。
- 管道將多個(gè)命令串聯(lián)到一起完成復(fù)雜任務(wù)。
- 從管道輸出的標(biāo)準(zhǔn)錯(cuò)誤會(huì)混合到一起。
2.重定向和管道
(1)為什么要有管道的存在
既然有重定向,為什么還要有管道呢?比如如下命令使用重定向?qū)⒖蓤?zhí)行程序process1的輸出都放入file中:
process1 > file
但是如果想讓可執(zhí)行程序process1 的輸出傳遞到可執(zhí)行程序process2呢?需要:
process1 > temp && process2 < temp
這個(gè)命令做了3步:
- 運(yùn)行名為process1
?
- 將輸出保存到名為temp的文件中
- 運(yùn)行名為的程序process2,假裝用戶在鍵盤上輸入temp的內(nèi)容。
有沒有發(fā)現(xiàn)這樣做很麻煩,既要?jiǎng)?chuàng)建臨時(shí)文件,又要用戶在鍵盤上輸入呢,但是管道就很簡(jiǎn)單呀:
process1 | process2
的效果和命令process1 > temp && process2 < temp的作用是一樣的。
(2)重定向和管道的區(qū)別
管道也有重定向的作用,因?yàn)樗淖兞藬?shù)據(jù)的輸入輸出方向。沖重定向使用">"將文件和命令連接起來,用文件來接收命令的輸出,而管道使用"I"將命令和命令連接起來,用第二個(gè)命令來接收第一個(gè)命令的輸出。
使用重定向一定要小心一些,如果連續(xù)鍵入如下兩條命令:
cd /usr/bin
ls > less
第一條命令將當(dāng)前目錄切換到了大多數(shù)程序所存放的目錄,第二條命令是告訴 Shell 用 ls 命令的輸出重寫文件 less。因?yàn)?/usr/bin 目錄已經(jīng)包含了名稱為 less的文件,第二條命令用 ls 輸出的文本重寫了 less 程序,因此破壞了文件系統(tǒng)中的 less 程序,這就破壞了less文件。這是使用重定向操作符錯(cuò)誤重寫文件的一個(gè)教訓(xùn),所以在使用重定向時(shí)要謹(jǐn)慎。
管道分為匿名管道和命名管道。
3.匿名管道
(1)匿名管道原理?
匿名管道僅限于本地父子進(jìn)程之間通信,不支持跨網(wǎng)絡(luò)之間的兩個(gè)進(jìn)程之間的通信。
進(jìn)程在操作文件時(shí),通過文件描述符找到文件,如果需要讀文件就直接執(zhí)行讀方法。使用fork創(chuàng)建子進(jìn)程之后,那么子進(jìn)程就擁有了自己的PCB,父進(jìn)程指向的struct file文件描述符表結(jié)構(gòu)也需要給子進(jìn)程拷貝一份。
?這是因?yàn)?#xff1a;
- file_struct結(jié)構(gòu)是屬于進(jìn)程的,因?yàn)閒ile_struct能夠讓進(jìn)程看到已經(jīng)打開了多少個(gè)文件以及文件之間的關(guān)系,因此file_struct是屬于進(jìn)程的。file_struct屬于進(jìn)程,那么它一定屬于父進(jìn)程,在創(chuàng)建子進(jìn)程的時(shí)候,也必須為子進(jìn)程復(fù)制這份file_struct結(jié)構(gòu)。因?yàn)檫M(jìn)程具有獨(dú)立性,所以內(nèi)核數(shù)據(jù)結(jié)構(gòu)也必須保持獨(dú)立。
- 如果讓子進(jìn)程也看到了父進(jìn)程的文件了,那么父進(jìn)程的文件進(jìn)行讀寫時(shí),緩沖區(qū)也被子進(jìn)程看到了,這就沒有做好進(jìn)程獨(dú)立性。
因此操作系統(tǒng)會(huì)將這個(gè)結(jié)構(gòu)給子進(jìn)程也拷貝一份:
基于文件的通信方式就叫做管道。進(jìn)程、struct_file、緩沖區(qū)、操作方法等都是操作系統(tǒng)提供的,文件不屬于進(jìn)程,屬于操作系統(tǒng)。父進(jìn)程先打開文件,讓子進(jìn)程繼承,雖然結(jié)構(gòu)上互相獨(dú)立,但它們指向同一個(gè)文件,一個(gè)向文件寫,另一個(gè)從文件讀,兩個(gè)進(jìn)程看到了同一份公共資源,這就滿足了進(jìn)程通信的前提。
(2)站在文件描述符角度理解匿名管道
- 父進(jìn)程創(chuàng)建管道
管道可以看做文件的內(nèi)核緩沖區(qū),父進(jìn)程創(chuàng)建管道時(shí),分別以讀方式和寫方式打開同一文件:
- ?父進(jìn)程fork出子進(jìn)程
?當(dāng)父進(jìn)程創(chuàng)建出子進(jìn)程后,父進(jìn)程的所有文件描述符表信息會(huì)被子進(jìn)程繼承,雖然父子進(jìn)程各自擁有獨(dú)立的文件描述符,但是內(nèi)容是一樣的,所以父子進(jìn)程都可以看到曾經(jīng)打開的讀端和寫端進(jìn)行讀寫,不過管道只能單向通信,只能有一個(gè)讀端,一個(gè)寫端。
所以父進(jìn)程一開始就有兩個(gè)文件描述符,一個(gè)讀端,一個(gè)寫端,這樣子進(jìn)程繼承復(fù)制了父進(jìn)程的文件描述符后,也有讀端和寫端。否則如果父進(jìn)程一開始只有讀端,沒有寫端,那么子進(jìn)程也只有讀端,沒有寫端,那么兩個(gè)讀端是不能進(jìn)行讀寫的。
- ?父進(jìn)程關(guān)閉讀端(寫端),子進(jìn)程關(guān)閉寫端(讀端)
?至于父子進(jìn)程誰(shuí)關(guān)閉讀端,誰(shuí)關(guān)閉寫端,取決于父進(jìn)程讀還是子進(jìn)程讀,現(xiàn)在來看一下父進(jìn)程寫,子進(jìn)程讀的情況,現(xiàn)在關(guān)閉父進(jìn)程的讀端和子進(jìn)程的寫端:
(3)創(chuàng)建匿名管道
第一步:父進(jìn)程使用pipe函數(shù)來創(chuàng)建管道
#include <unistd.h>int pipe(int pipefd[2]);
參數(shù):pipefd文件描述符數(shù)組,元素個(gè)數(shù)為2,是輸出型參數(shù),通過這個(gè)參數(shù)讀取到打開的兩個(gè)文件描述符。其中pipefd[0]為讀操作,pipefd[1]為寫操作,且順序不能顛倒。
返回值:成功返回0,失敗返回-1。
現(xiàn)在來創(chuàng)建一個(gè)管道:
#include<stdio.h>
#include<unistd.h>int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗{perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);return 0;
}
執(zhí)行結(jié)果如下:
?可以看到文件描述符分別為3和4,因?yàn)?、1、2都被標(biāo)準(zhǔn)輸入、標(biāo)準(zhǔn)輸出、標(biāo)準(zhǔn)錯(cuò)誤占用了:
第二步:父進(jìn)程fork出子進(jìn)程
#include<stdio.h>
#include<unistd.h>int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{}//父進(jìn)程return 0;
}
第3步:創(chuàng)建單向信道
現(xiàn)在如果想讓父進(jìn)程讀,子進(jìn)程寫,那么就要關(guān)閉父進(jìn)程的寫端和子進(jìn)程的讀端,即關(guān)閉父進(jìn)程的寫文件描述符和子進(jìn)程的讀文件描述符。為了讓子進(jìn)程關(guān)閉讀文件描述符后不要繼續(xù)向后執(zhí)行,使用eixt函數(shù)來終止。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{close(pipefd[0]);//子進(jìn)程關(guān)閉讀文件描述符exit(0);}//父進(jìn)程close(pipefd[1]);//父進(jìn)程關(guān)閉寫文件描述符return 0;
}
現(xiàn)在已經(jīng)建立了父子進(jìn)程,并且父子進(jìn)程都看到了同一份資源?,F(xiàn)在讓子進(jìn)程寫入,需要調(diào)用write方法,讓父進(jìn)程讀取,需要調(diào)用read方法,write和read方法的使用請(qǐng)參考文章【Linux】-- 基礎(chǔ)IO和動(dòng)靜態(tài)庫(kù)第一章節(jié)第1節(jié)的內(nèi)容?第一章節(jié)第1節(jié)的內(nèi)容。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{close(pipefd[0]);//子進(jìn)程關(guān)閉讀文件描述符const char *string_write = "lunch ";while(1){write(pipefd[1],string_write,strlen(string_write));//子進(jìn)程向文件緩沖區(qū)寫,pipe只要有緩沖區(qū)就一直寫入}close(pipefd[1]);exit(0);}//父進(jìn)程close(pipefd[1]);//父進(jìn)程關(guān)閉寫文件描述符while(1){sleep(1);char string_read[64] ={0};size_t readLength = read(pipefd[0],string_read,sizeof(string_read));//父進(jìn)程從文件緩沖區(qū)讀,pipe只要有緩沖區(qū)就一直讀if(readLength == 0)//讀到內(nèi)容為空{(diào)printf("child quit...\n");break;}else if(readLength > 0)//讀到了正常內(nèi)容{string_read[readLength] = 0;printf("child write# %s\n",string_read);}else//讀出錯(cuò){printf("read error...\n");break;}close(pipefd[0]);}return 0;
}
?可以看到執(zhí)行結(jié)果如下,子進(jìn)程寫入,父進(jìn)程讀取:
?對(duì)于字節(jié)流,只要緩沖區(qū)有數(shù)據(jù),就把緩沖區(qū)的所有數(shù)據(jù)全部讀出來,一次讀取一個(gè)字節(jié)。
(4)匿名管道讀寫規(guī)則
pipe2函數(shù)與pipe函數(shù)類似,也是用于創(chuàng)建匿名管道,其函數(shù)原型如下:
int pipe2(int pipefd[2], int flags);
對(duì)于flags:?
- 當(dāng)沒有數(shù)據(jù)可讀時(shí)
O_NONBLOCK disable:read調(diào)用阻塞,即進(jìn)程暫停執(zhí)行,一直等到有數(shù)據(jù)來到為止。
O_NONBLOCK enable:read調(diào)用返回-1,errno值為EAGAIN。
- ?當(dāng)管道滿的時(shí)候
O_NONBLOCK disable: write調(diào)用阻塞,直到有進(jìn)程讀走數(shù)據(jù)
O_NONBLOCK enable:調(diào)用返回-1,errno值為EAGAIN
- 如果所有管道寫端對(duì)應(yīng)的文件描述符被關(guān)閉,則read返回0
- 如果所有管道讀端對(duì)應(yīng)的文件描述符被關(guān)閉,則write操作會(huì)產(chǎn)生信號(hào)SIGPIPE,進(jìn)而可能導(dǎo)致write進(jìn)程退出
- 當(dāng)要寫入的數(shù)據(jù)量<=PIPE_BUF時(shí),linux將保證寫入的原子性。
- 當(dāng)要寫入的數(shù)據(jù)量>PIPE_BUF時(shí),linux將不再保證寫入的原子性。
(5)匿名管道特點(diǎn)
- 只能用于具有共同祖先的進(jìn)程之間進(jìn)行通信;通常,一個(gè)管道由一個(gè)進(jìn)程創(chuàng)建,然后該進(jìn)程調(diào)用fork,父、子進(jìn)程之間就可用該管道通信(具有親緣關(guān)系的進(jìn)程,祖孫進(jìn)程也可以)
- 管道提供流式服務(wù),原子性寫入(讀端讀取的數(shù)據(jù)是任意的,底層沒有對(duì)數(shù)據(jù)做明確分割,報(bào)文段不定,因此是流式服務(wù))
- 父子進(jìn)程退出,管道文件釋放,所以管道的生命周期隨進(jìn)程
- 內(nèi)核會(huì)對(duì)管道操作進(jìn)行同步與互斥
- 管道是半雙工的,數(shù)據(jù)只能向一個(gè)方向流動(dòng);需要雙方通信時(shí),需要建立起兩個(gè)管道
(6)匿名管道4種特殊情況
- 讀端不讀或者讀的慢,寫端要等讀端
- 讀端關(guān)閉,寫端收到SIGPIPE信號(hào)直接終止
- 寫端不寫或?qū)懙穆?#xff0c;讀端要等寫端
- 寫端關(guān)閉,讀端讀完pipe內(nèi)部的數(shù)據(jù)然后再讀,會(huì)讀到0,表明讀到文件結(jié)尾
(7)匿名管道大小
?如果讓子進(jìn)程無限循環(huán)每次往管道里寫一個(gè)字符,并且計(jì)數(shù),父進(jìn)程從管道里面不讀取數(shù)據(jù),當(dāng)計(jì)數(shù)不再增長(zhǎng)時(shí),計(jì)數(shù)值就為管道的大小:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{close(pipefd[0]);//子進(jìn)程關(guān)閉讀文件描述符int count = 0;while(1){write(pipefd[1],"a",1);count++;printf("count:%d\n",count);}close(pipefd[1]);exit(0);}//父進(jìn)程close(pipefd[1]);//父進(jìn)程關(guān)閉寫文件描述符while(1)//父進(jìn)程不讀取{sleep(1);}return 0;
}
?運(yùn)行結(jié)果如下,從1打印到65536:
?這說明管道大小為65536B=64KB。這也說明了如果寫端向管道寫滿數(shù)據(jù)以后,那么寫端就不寫了,等待讀端讀;同理,如果讀端把管道數(shù)據(jù)讀完了,管道沒數(shù)據(jù),那么讀端就不讀了,等待寫端寫。
?管道在被寫端寫滿以后,讀端要拿走數(shù)據(jù),如果一次拿走4KB,寫端才會(huì)寫,否則不會(huì)觸發(fā)寫端去寫,為什么是4KB呢?讓父進(jìn)程讀取的時(shí)候,存放數(shù)據(jù)的數(shù)組大小從1KB開始向上遞增到4KB的時(shí)候,寫端才寫:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main()
{int pipefd[2] = {0};if(pipe(pipefd) != 0)//匿名管道創(chuàng)建失敗了{(lán)perror("pipe error!");return 1;}printf("pipefd[0]:%d\n",pipefd[0]);printf("pipefd[1]:%d\n",pipefd[1]);if(fork() == 0)//子進(jìn)程{close(pipefd[0]);//子進(jìn)程關(guān)閉讀文件描述符const char *string_write = "lunch ";int count = 0;while(1){//write(pipefd[1],string_write,strlen(string_write));//子進(jìn)程向文件緩沖區(qū)寫,pipe只要有緩沖區(qū)就一直寫入write(pipefd[1],"a",1);count++;printf("count:%d\n",count);}close(pipefd[1]);exit(0);}//父進(jìn)程close(pipefd[1]);//父進(jìn)程關(guān)閉寫文件描述符while(1){sleep(3);char string_read[1024*4+1] ={0};//按照1024*1 1024*2 1024*3 1024*4向上遞增size_t readLength = read(pipefd[0],string_read,sizeof(string_read));//父進(jìn)程從文件緩沖區(qū)讀,pipe只要有緩沖區(qū)就一直讀printf("readLength = %d\n",readLength);string_read[readLength] = 0;printf("father take:%c\n",string_read[0]);}return 0;
}
可以看到,管道寫入字符的計(jì)數(shù)一開始增加到了65536B,父進(jìn)程讀走4KB之后,子進(jìn)程繼續(xù)寫,每寫一次,count計(jì)數(shù)就會(huì)++ :
?為什么讀走4KB的時(shí)候,寫端才寫,而讀走1KB 2KB 3KB時(shí)不寫呢?這是因?yàn)橐WC寫入和讀取的原子性:假如還沒讀夠4KB,就把寫端喚醒了,那么寫端就要來寫了,這就變成了,寫端在寫的同時(shí),讀端要來讀,這就違背了管道半雙工通信,不能同時(shí)讀寫的原則。同理,如果寫端寫的特別慢,讀端讀的特別快,當(dāng)緩沖區(qū)沒有數(shù)據(jù)時(shí),會(huì)等待數(shù)據(jù)寫入進(jìn)去后,讀端再讀 。因此要保證同步。
4.命名管道
(1)命名管道原理?
?匿名管道用于有血緣關(guān)系的進(jìn)程間通信,那么對(duì)于沒有血緣關(guān)系的進(jìn)程,他們之間如何通信呢?這就要用到命名管道,命名管道是一種特殊的文件,使用FIFO(First In First Out)來進(jìn)行通信。
?如何讓兩個(gè)沒有血緣關(guān)系的不相干的進(jìn)程看到操作系統(tǒng)提供的同一份資源?對(duì)于文件系統(tǒng)來說當(dāng)進(jìn)程A把磁盤文件打開,向磁盤里面寫數(shù)據(jù),寫完之后關(guān)閉這個(gè)磁盤文件,進(jìn)程B再把這個(gè)磁盤文件打開并讀取數(shù)據(jù):
但是這樣做有點(diǎn)慢,因?yàn)檫M(jìn)程A再內(nèi)存中打開這個(gè)文件,為這個(gè)文件建立內(nèi)存相關(guān)的數(shù)據(jù)結(jié)構(gòu)和緩沖區(qū),進(jìn)程B也在內(nèi)存中打開同一個(gè)文件,這樣就是一個(gè)通過讀的方式打開,一個(gè)通過寫的方式打開,進(jìn)程可以向這個(gè)內(nèi)存文件寫,進(jìn)程B可以從這個(gè)內(nèi)存文件讀,暫時(shí)先不把數(shù)據(jù)刷新到磁盤,否則效率會(huì)降低,這是基于內(nèi)存進(jìn)行數(shù)據(jù)之間的通信,那么A進(jìn)程和B進(jìn)程就可以通過這個(gè)內(nèi)存文件進(jìn)行不相關(guān)的進(jìn)程間的通信。
不相關(guān)的A進(jìn)程和B進(jìn)程是如何看到同一份資源的呢?路徑+文件名能唯一指定一個(gè)文件,這樣就能讓進(jìn)程A和進(jìn)程B打開同一份文件?,F(xiàn)在需要1個(gè)文件,同時(shí)滿足:
- 文件被打開時(shí),數(shù)據(jù)不要被刷新到磁盤上,而是保存臨時(shí)數(shù)據(jù)
- 這個(gè)文件也必須在磁盤上也有對(duì)應(yīng)的文件名
符合這些條件的只有命名管道。而且這個(gè)文件是有名字的,通過路徑+文件名確定唯一性來做到的:
(2)創(chuàng)建命名管道
命名管道有兩種創(chuàng)建方式:?
- 通過mkfifo命令創(chuàng)建
mkfifo name
如創(chuàng)建一個(gè)名為testFifo的管道文件:
?可以看到文件類型為p,p表明這是一個(gè)管道文件。創(chuàng)建了命名管道文件后,就可以通信了:
?echo和cat是兩個(gè)不同的指令,但是運(yùn)行起來是兩個(gè)進(jìn)程,左側(cè)的消息打印到了右側(cè)的屏幕上,一個(gè)進(jìn)程把自己的內(nèi)容寫入到了命名管道文件中,通過命名管道文件把數(shù)據(jù)傳遞給另一個(gè)進(jìn)程。
- 通過mkfifo函數(shù)創(chuàng)建
mkfifo函數(shù)的作用是生成一個(gè)FIFO的特殊文件,即命名管道?
#include <sys/types.h>#include <sys/stat.h>int mkfifo(const char *pathname, mode_t mode);
pathname:文件名
mode:管道的默認(rèn)權(quán)限,可用過umask來設(shè)置
返回值:成功返回0,失敗返回-1
現(xiàn)在使用mkfifo函數(shù)創(chuàng)建命名管道,server.c創(chuàng)建管道文件,并給管道文件分配權(quán)限:
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>#define fifo_file ./fifo_fileint main()
{if(mkfifo(fifo_file,0666) < 0)//創(chuàng)建一個(gè)命名管道{perror("mkfifo");return 1;}return 0;
}
client.c暫時(shí)什么都不做:
#include<stdio.h>int main()
{return 0;
}
Makefile一次生成兩個(gè)可執(zhí)行文件:
.PHONY:all
all:client serverclient:client.cgcc -o $@ $^server:server.cgcc -o $@ $^.PHONY:clean
clean:rm -rf client server fifo_file
編譯后,生成兩個(gè)可執(zhí)行程序:
現(xiàn)在通信想讓client和server可執(zhí)行程序互相傳遞詳細(xì),那么?client和server可執(zhí)行程序運(yùn)行起來就是兩個(gè)進(jìn)程,而且是兩個(gè)毫不相干的進(jìn)程,沒有血緣關(guān)系。
執(zhí)行srver課執(zhí)行程序后,生成fifo_file命名管道,文件類型是p,但是權(quán)限是644,并不是666:
?這是因?yàn)閒ifo文件的參數(shù)mode受系統(tǒng)umask影響,可以查看到Umask的值是2:
?那么可以看出mode = mode & ~umask(666&~002),如果修改umask的值,比如創(chuàng)建命名管道文件時(shí)將umask清0:
server.c
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#define fifo_file "./fifo_file"int main()
{umask(0);//將umask清0if(mkfifo(fifo_file,0666) < 0)//創(chuàng)建一個(gè)命名管道{perror("mkfifo");return 1;}int fd = open(fifo_file,O_RDONLY);if(fd < 0){perror("open");return 2;}while(1){char buffer[64] = {0};ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);if(read_length > 0)//讀取成功{buffer[read_length-1] = 0;printf("client # %s\n",buffer);}else if(read_length == 0){printf("client quit\n");}else{perror("read");break;}}close(fd);return 0;
}
這時(shí)可以看到命名管道文件的權(quán)限變成了666:
?對(duì)于client和server進(jìn)程,想讓server讀,client寫,不推薦用c/c++接口,有緩沖區(qū),而系統(tǒng)調(diào)用沒有緩沖區(qū),推薦使用系統(tǒng)調(diào)用接口,client使用系統(tǒng)調(diào)用接收標(biāo)準(zhǔn)輸入并寫入到命名管道文件中:
client.c?
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>#define fifo_file "./fifo_file"//client不需要?jiǎng)?chuàng)建命名管道文件,只需要獲取就可以了
int main()
{int fd = open(fifo_file,O_WRONLY);if(fd < 0){perror("open");return 1;}while(1){printf("請(qǐng)輸入# ");//client的輸入提示fflush(stdout);//刷新一下標(biāo)準(zhǔn)輸出char buffer[64] = {0};//先把數(shù)據(jù)從標(biāo)準(zhǔn)輸入拿到client進(jìn)程內(nèi)部ssize_t read_length = read(0,buffer,sizeof(buffer)-1);if(read_length > 0){buffer[read_length-1] = 0;//拿到了數(shù)據(jù)write(fd,buffer,strlen(buffer));}}close(fd);return 0;
}
?現(xiàn)在運(yùn)行,得先讓server跑起來創(chuàng)建一個(gè)命名管道,然后再運(yùn)行client端,就可以再client端寫入數(shù)據(jù)了:
?從以上就可以看出,對(duì)于兩個(gè)不想管的進(jìn)程,通過命名管道,一個(gè)進(jìn)程把消息發(fā)給了另外一個(gè)進(jìn)程。因此一旦有了命名管道,只需要讓通信雙方進(jìn)程按照文件操作即可。由于命名管道也是基于字節(jié)流的,因此實(shí)際上,信息傳遞的時(shí)候,需要通信雙方定制“協(xié)議”。
現(xiàn)在讓client控制server,讓server去執(zhí)行任務(wù)??梢宰宻erver執(zhí)行程序替換,比如當(dāng)client接收標(biāo)準(zhǔn)輸入寫入到命名管道文件中的字符串為"show"時(shí),就會(huì)執(zhí)行l(wèi)s命令:
server.c
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>
#include<string.h>
#include<sys/wait.h>#define fifo_file "./fifo_file"int main()
{umask(0);if(mkfifo(fifo_file,0666) < 0)//創(chuàng)建一個(gè)命名管道{perror("mkfifo");return 1;}int fd = open(fifo_file,O_RDONLY);if(fd < 0){perror("open");return 2;}//業(yè)務(wù)邏輯,進(jìn)行讀寫while(1){char buffer[64] = {0};ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);if(read_length > 0)//讀取成功{buffer[read_length] = 0;if(strcmp(buffer,"show") == 0){printf("the string is show\n");if(fork() == 0){execl("/usr/bin/ls","ls","-l",NULL);//程序替換exit(1);}waitpid(-1,NULL,0);}else{printf("client # %s\n",buffer);}}else if(read_length == 0){printf("client quit\n");}else{perror("read");break;}}close(fd);return 0;
}
client.c
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>#define fifo_file "./fifo_file"//client不需要?jiǎng)?chuàng)建命名管道文件,只需要獲取就可以了
int main()
{int fd = open(fifo_file,O_WRONLY);if(fd < 0){perror("open");return 1;}while(1){printf("請(qǐng)輸入# ");//client的輸入提示fflush(stdout);//刷新一下標(biāo)準(zhǔn)輸出char buffer[64] = {0};//先把數(shù)據(jù)從標(biāo)準(zhǔn)輸入拿到client進(jìn)程內(nèi)部ssize_t read_length = read(0,buffer,sizeof(buffer)-1);if(read_length > 0){buffer[read_length - 1] = 0;//拿到了數(shù)據(jù)write(fd,buffer,strlen(buffer));}}close(fd);return 0;
}
現(xiàn)在運(yùn)行,得先讓server跑起來創(chuàng)建一個(gè)命名管道,然后再運(yùn)行client端,就可以再client端寫入數(shù)據(jù)了,在client輸入"show"之后,server就將ls的內(nèi)容展示出來了:
?可以看到通過命名管道把數(shù)據(jù)從一個(gè)進(jìn)程傳遞給另外一個(gè)進(jìn)程,并且也實(shí)現(xiàn)了讓一個(gè)進(jìn)程控制了另外一個(gè)進(jìn)程去執(zhí)行任務(wù),達(dá)到了進(jìn)程間通信的目的。
(3)命名管道的數(shù)據(jù)不會(huì)刷新到磁盤
假如讓server進(jìn)程每隔20秒讀一次,而client不斷往管道發(fā)消息,那么數(shù)據(jù)只能在管道文件:
server.c
#include<stdio.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdlib.h>
#include<wait.h>
#include<string.h>
#include<sys/wait.h>#define fifo_file "./fifo_file"int main()
{umask(0);if(mkfifo(fifo_file,0666) < 0)//創(chuàng)建一個(gè)命名管道{perror("mkfifo");return 1;}int fd = open(fifo_file,O_RDONLY);if(fd < 0){perror("open");return 2;}//業(yè)務(wù)邏輯,進(jìn)行讀寫while(1){char buffer[64] = {0};sleep(20);//等待20秒再讀ssize_t read_length = read(fd,buffer,sizeof(buffer)-1);if(read_length > 0)//讀取成功{buffer[read_length] = 0;if(strcmp(buffer,"show") == 0){printf("the string is show\n");if(fork() == 0){execl("/usr/bin/ls","ls","-l",NULL);//程序替換exit(1);}waitpid(-1,NULL,0);}else{printf("client # %s\n",buffer);}}else if(read_length == 0){printf("client quit\n");}else{perror("read");break;}}close(fd);return 0;
}
client.c不用修改:
#include<stdio.h>
#include<unistd.h>
#include<sys/stat.h>
#include<sys/types.h>
#include<fcntl.h>
#include<string.h>#define fifo_file "./fifo_file"//client不需要?jiǎng)?chuàng)建命名管道文件,只需要獲取就可以了
int main()
{int fd = open(fifo_file,O_WRONLY);if(fd < 0){perror("open");return 1;}while(1){printf("請(qǐng)輸入# ");//client的輸入提示fflush(stdout);//刷新一下標(biāo)準(zhǔn)輸出char buffer[64] = {0};//先把數(shù)據(jù)從標(biāo)準(zhǔn)輸入拿到client進(jìn)程內(nèi)部ssize_t read_length = read(0,buffer,sizeof(buffer)-1);if(read_length > 0){buffer[read_length - 1] = 0;//拿到了數(shù)據(jù)write(fd,buffer,strlen(buffer));}}close(fd);return 0;
}
這20秒內(nèi),client向命名管道寫,但是server沒有從命名管道讀,按理來說,命名管道里面有內(nèi)容,大小不為0,但是在這20秒之內(nèi)發(fā)現(xiàn)命名管道的fifo_file的大小為0,這就說明了命名管道的數(shù)據(jù),由于效率問題,不會(huì)刷新到磁盤。
5.匿名管道和命名管道的區(qū)別
創(chuàng)建與打開的方式不同:
- 匿名管道由pipe函數(shù)創(chuàng)建并打開
- 命名管道由mkfifo函數(shù)創(chuàng)建,由open函數(shù)打開
后面就有相同的語(yǔ)義了
三、System V IPC
1.System V標(biāo)準(zhǔn)
System V是一種用于在操作系統(tǒng)層面上進(jìn)行進(jìn)程間通信的標(biāo)準(zhǔn),system V標(biāo)準(zhǔn)給用戶提供了系統(tǒng)調(diào)用接口,只要用戶使用它所提供的系統(tǒng)調(diào)用就可以完成進(jìn)程間通信。IPC(Inter-Process Communication)是進(jìn)程間通信。System V IPC不用基于文件進(jìn)行通信。
如何把系統(tǒng)調(diào)用接口提供給用戶使用呢?System V是操作系統(tǒng)內(nèi)核的一部分,是為操作系統(tǒng)中多進(jìn)程提供的一種通信方案。但是操作系統(tǒng)不相信任何用戶,采用系統(tǒng)調(diào)用為用戶提供功能。所以System V進(jìn)程間通信,存在專門用來通信的接口:System call(系統(tǒng)調(diào)用)
這就需要制定一套標(biāo)準(zhǔn)用來在同一主機(jī)內(nèi)進(jìn)行進(jìn)程間通信:System V。System V進(jìn)程間通信分為3種:
- System V消息隊(duì)列
- System V共享內(nèi)存
- System V信號(hào)量
消息隊(duì)列模型通過在協(xié)作進(jìn)程間交換消息來實(shí)現(xiàn)通信。共享內(nèi)存模型會(huì)建立起一塊供協(xié)作進(jìn)程共享的內(nèi)存區(qū)域,進(jìn)程通過向此共享區(qū)域讀出或?qū)懭霐?shù)據(jù)來交換信息。以下是消息隊(duì)列和共享內(nèi)存的通信模型:
?消息隊(duì)列的實(shí)現(xiàn)經(jīng)常采用系統(tǒng)調(diào)用,因此需要消耗更多時(shí)間使內(nèi)核介入,但是共享內(nèi)存只在建立共享內(nèi)存區(qū)域時(shí)需要系統(tǒng)調(diào)用,一旦建立共享內(nèi)存,所有訪問都是常規(guī)內(nèi)存訪問,不需要借助內(nèi)核。
由于消息隊(duì)列和共享內(nèi)存用來傳遞消息,信號(hào)量用來實(shí)現(xiàn)進(jìn)程間同步和互斥。因此主要來看看進(jìn)程間通信方式中效率較高的共享內(nèi)存。
2.共享內(nèi)存
(1)原理?
把申請(qǐng)的共享內(nèi)存映射到不同進(jìn)程的地址空間當(dāng)中。有進(jìn)程A和進(jìn)程B,進(jìn)程A通過頁(yè)表映射找到進(jìn)程A的代碼和數(shù)據(jù),同樣,進(jìn)程B也通過頁(yè)表映射找到進(jìn)程B的代碼和數(shù)據(jù),由于兩個(gè)進(jìn)程的數(shù)據(jù)結(jié)構(gòu)相互獨(dú)立,且物理內(nèi)存當(dāng)中的代碼和數(shù)據(jù)也相互獨(dú)立,因此兩個(gè)進(jìn)程不會(huì)互相干擾。
在物理內(nèi)存開辟一塊共享內(nèi)存空間后,需要通過系統(tǒng)調(diào)用把開辟的內(nèi)存空間經(jīng)過頁(yè)表映射到進(jìn)程地址空間,那么共享內(nèi)存在進(jìn)程地址空間也有了虛擬地址,叫做共享存儲(chǔ)器映射區(qū),再把共享存儲(chǔ)器映射區(qū)的虛擬地址填到頁(yè)表當(dāng)中,這樣共享內(nèi)存的虛擬地址和物理地址就建立起了對(duì)應(yīng)關(guān)系,而且各個(gè)進(jìn)程也就看到了共享內(nèi)存同一份資源。
?以上的過程也是讓進(jìn)程掛接到共享內(nèi)存空間上的過程。操作系統(tǒng)內(nèi)可能存在多個(gè)共享內(nèi)存,那么操作系統(tǒng)需要管理這些共享內(nèi)存,管理還是先描述再組織。
?如何保證能夠讓多個(gè)進(jìn)程看到同一個(gè)共享內(nèi)存呢?
?共享內(nèi)存一定要有唯一標(biāo)識(shí)ID,就能讓不同進(jìn)程識(shí)別到同一個(gè)共享內(nèi)存資源。那么這個(gè)ID一定在描述共享內(nèi)存的數(shù)據(jù)結(jié)構(gòu)中。
(2)步驟
可以總結(jié)出使用共享內(nèi)存的過程:
- 創(chuàng)建共享內(nèi)存
- 關(guān)聯(lián)(掛接)
- 去關(guān)聯(lián)(去掛接)
- 釋放共享內(nèi)存
(3)函數(shù)?
shmget
使用shmget函數(shù)創(chuàng)建共享內(nèi)存,來申請(qǐng)一塊共享內(nèi)存空間:
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
key | 通過ftok函數(shù)生成 |
size | 建議為4KB的整數(shù)倍,操作系統(tǒng)為了提高內(nèi)存和硬盤的數(shù)據(jù)交換的速度,以4KB為單位 |
shmflg | hmflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了 |
返回值 | 成功就返回共享內(nèi)存地址,失敗就返回-1 |
?其中,shmget第一個(gè)參數(shù)key是通過ftok函數(shù)生成的:
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
pathname | 自定義的文件路徑名 |
proj_id | 序號(hào),低8位被使用,非0 |
返回值 | 返回key,會(huì)被設(shè)置進(jìn)共享內(nèi)存在內(nèi)核的數(shù)據(jù)結(jié)構(gòu)里面 |
shmget第三個(gè)參數(shù)shmflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了:
?創(chuàng)建共享內(nèi)存后,如何查看共享內(nèi)存呢?ipcs命令用于報(bào)告進(jìn)程間通信設(shè)施狀況,其中:
ipcs -m //查看共享內(nèi)存(Shared Memory Segments)
ipcs -q //查看消息隊(duì)列(Message Queue)
ipcs -s //查看信號(hào)量(Semaphore Arrays)
shmctl
使用完共享內(nèi)存后,如果不刪除的話,共享內(nèi)存會(huì)一直存在,直到系統(tǒng)重啟。如何刪除呢?有兩種刪除方式,一種是命令刪除:
ipcrm -m shmid
key只是用來在系統(tǒng)層面進(jìn)行唯一標(biāo)識(shí),不能用來管理共享內(nèi)存。而shmid是操作系統(tǒng)給用戶返回的id,用來在用戶層進(jìn)行共享內(nèi)存管理,所以ipcrm是用戶層的命令。 以上是命令刪除,那么如何在代碼中刪除共享內(nèi)存呢?
?因此另外一種刪除共享內(nèi)存的方式就是使用shmctl函數(shù)控制共享內(nèi)存:
#include <sys/ipc.h>
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);
shmid | 操作系統(tǒng)給用戶返回的id |
cmd | 選項(xiàng),有多個(gè) |
buf | data structure數(shù)據(jù)結(jié)構(gòu)類型指針 |
返回值 | 刪除成功返回0,失敗返回-1 |
其中cmd選項(xiàng)有多個(gè):
IPC_STAT | 將shmid的內(nèi)核數(shù)據(jù)結(jié)構(gòu)拷貝到buf指向的shmid_ds結(jié)構(gòu)中 |
IPC_SET | 將buf指向的shmid_ds結(jié)構(gòu)的一些成員的值寫入與此共享內(nèi)存段相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),同時(shí)更新其shm_ctime成員 |
IPC_RMID | 刪除共享內(nèi)存 |
第三個(gè)參數(shù)???
?其中,shmid_ds數(shù)據(jù)結(jié)構(gòu)如下:
struct shmid_ds
{struct ipc_perm shm_perm; /* Ownership and permissions */size_t shm_segsz; /* Size of segment (bytes) */time_t shm_atime; /* Last attach time */time_t shm_dtime; /* Last detach time */time_t shm_ctime; /* Last change time */pid_t shm_cpid; /* PID of creator */pid_t shm_lpid; /* PID of last shmat(2)/shmdt(2) */shmatt_t shm_nattch; /* No. of current attaches */...
};
shmat
?使用shmat把共享內(nèi)存映射到調(diào)用進(jìn)程的地址空間(關(guān)聯(lián):增加共享內(nèi)存和進(jìn)程地址空間映射關(guān)系的頁(yè)表項(xiàng))
#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
shmid | 操作系統(tǒng)給用戶返回的id |
shmaddr | 表明把共享內(nèi)存掛接到進(jìn)程地址空間的哪些范圍中? |
shmflg | 有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了,同shmget函數(shù)的shmflg標(biāo)志,這里設(shè)置為0就可以了 |
返回值 | 返回共享內(nèi)存掛接到進(jìn)程地址空間的虛擬地址,同申請(qǐng)堆空間的malloc返回值是一樣的 |
shmdt
?shmdt用來斷開共享內(nèi)存和進(jìn)程地址空間的映射(去關(guān)聯(lián):刪除共享內(nèi)存和進(jìn)程地址空間映射關(guān)系的頁(yè)表項(xiàng),而不是釋放共享內(nèi)存)
#include <sys/types.h>
#include <sys/shm.h>int shmdt(const void *shmaddr);
shmaddr | 要斷開映射的共享內(nèi)存地址,且必須和shmat的參數(shù)shmaddr相同 |
返回值 | 成功斷開返回0,失敗返回-1 |
(4)使用
兩個(gè)進(jìn)程使用共享內(nèi)存通信,需要進(jìn)行創(chuàng)建、關(guān)聯(lián)、去關(guān)聯(lián)、刪除的步驟,現(xiàn)在使用上面的函數(shù)來進(jìn)行server和client兩個(gè)進(jìn)程間的通信。
comm.h來包含頭文件
#include<sys/ipc.h>
#include<sys/shm.h>
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>#define PATH_NAME "/home/delia/linux/20230627-sharedMemory/shared/server.c" //ftok的路徑
#define PROJ_ID 0x6666
#define SIZE 4097
server端需要生成唯一ID,創(chuàng)建共享內(nèi)存,關(guān)聯(lián)共享內(nèi)存,去關(guān)聯(lián)共享內(nèi)存,刪除共享內(nèi)存:
server.c
#include "comm.h"int main()
{key_t key = ftok(PATH_NAME,PROJ_ID);//生成唯一ID保證在統(tǒng)一系統(tǒng)當(dāng)中
找到共享內(nèi)存if(key < 0){perror("fork");return 1;}//1.創(chuàng)建共享內(nèi)存int shmid = shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);//共享內(nèi)存不存在就創(chuàng)建,權(quán)限為666,共享內(nèi)存可以用文件權(quán)限來約束if(shmid < 0){perror("shmget");return 2;}printf("key = %u,shmid = %d\n",key,shmid);sleep(1);//2.關(guān)聯(lián)char *mem = shmat(shmid,NULL,0);printf("attaches shm success\n");sleep(15);//通信邏輯while(1){sleep(1);//printf("%s\n",mem);}//3.去關(guān)聯(lián)shmdt(mem);printf("detaches shm success\n");//4.刪除共享內(nèi)存shmctl(shmid,IPC_RMID,NULL);sleep(5);printf("key = %u,shmid = %d after shmctl\n",key,shmid);return 0;
}
client端需要和客戶端一樣生成同一個(gè)唯一ID,創(chuàng)建使用同一個(gè)共享內(nèi)存,關(guān)聯(lián)共享內(nèi)存,去關(guān)聯(lián)共享內(nèi)存,不需要?jiǎng)h除共享內(nèi)存,因?yàn)閟erver端已經(jīng)刪除了:
client.c
#include "comm.h"int main()
{key_t key = ftok(PATH_NAME,PROJ_ID);//生成唯一ID保證在統(tǒng)一系統(tǒng)當(dāng)中
找到共享內(nèi)存if(key < 0){perror("ftok");return 1;}printf("%u\n",key);//1.創(chuàng)建共享內(nèi)存int shmid = shmget(key,SIZE,IPC_CREAT);//共享內(nèi)存已存在就返回已存在共享內(nèi)存if(shmid < 0){perror("shmget");return 2;}//2.關(guān)聯(lián)char *mem = shmat(shmid,NULL,0);sleep(5);printf("client process attaches success\n");//通信邏輯char c = 'A';while(c <= 'G'){mem[c - 'A'] = c;c++;mem[c - 'A'] = 0;sleep(2);}//3.去關(guān)聯(lián)shmdt(mem);printf("client process detaches success\n");return 0;
}
?Makefile
.PHONY:all
all:server clientserver:server.cgcc -o $@ $^
client:client.cgcc -o $@ $^.PHONY:clean
clean:rm -f server client
make之后,使用命令
while :; do ipcs -m;sleep 1;echo "#################"; done
來查看共享內(nèi)存的掛接進(jìn)程的數(shù)量變化:當(dāng)server端和client端進(jìn)程都沒有開啟時(shí),看到共享內(nèi)存信息的nattch的個(gè)數(shù)為0,當(dāng)server和client端都運(yùn)行起來之后,發(fā)現(xiàn)nattch的個(gè)數(shù)變成了2,client所寫的消息就會(huì)被server讀取,當(dāng)client端去關(guān)聯(lián)之后,nattch變成了1,最后當(dāng)server端退出時(shí),共享內(nèi)存被刪除,nattch又變成了0:
key | 系統(tǒng)區(qū)別各個(gè)共享內(nèi)存的唯一標(biāo)識(shí) |
shmid | 共享內(nèi)存的用戶層id(句柄) |
owner | 共享內(nèi)存的擁有者 |
perms | 共享內(nèi)存的權(quán)限 |
bytes | 共享內(nèi)存的大小 |
nattch | 關(guān)聯(lián)共享內(nèi)存的進(jìn)程數(shù) |
status | 共享內(nèi)存的狀態(tài) |
從以上可以看出,共享內(nèi)存有以下特點(diǎn):
- 共享內(nèi)存一旦建立好并映射進(jìn)自己進(jìn)程的地址空間,該進(jìn)程就可以看到該共享內(nèi)存,就像malloc的空間一樣,不需要任何系統(tǒng)調(diào)用接口(比如read、write會(huì)將數(shù)據(jù)從內(nèi)核拷貝到用戶或從用戶拷貝到內(nèi)核)。
- 共享內(nèi)存是所有進(jìn)程間通信中速度最快的,這是因?yàn)閷⒁粔K共享內(nèi)存映射到不同的進(jìn)程地址空間,共享內(nèi)存地址對(duì)應(yīng)在內(nèi)存上的空間就拿到了,所以server和Client有任何一方寫了,另一方馬上就看到了。
- 生命周期隨內(nèi)核,而且不提供同步互斥機(jī)制,需要程序員自行保證數(shù)據(jù)的安全。
3.共享內(nèi)存和管道區(qū)別?
從共享內(nèi)存的特點(diǎn)可以看出:
(1)創(chuàng)建好共享內(nèi)存后,就不需要再調(diào)用系統(tǒng)接口進(jìn)行通信了, 而管道創(chuàng)建好后還需要調(diào)用read、write等系統(tǒng)接口進(jìn)行通信。
(2) 共享內(nèi)存沒有同步互斥機(jī)制,但是管道有同步互斥機(jī)制。
(3)共享內(nèi)存是所有進(jìn)程間通信方式中速度最快的,將數(shù)據(jù)從一個(gè)進(jìn)程傳輸帶另一個(gè)進(jìn)程,管道需要進(jìn)行4次拷貝,共享內(nèi)存需要進(jìn)行2次拷貝,共享內(nèi)存需要的拷貝次數(shù)少。
使用管道,將文件從一個(gè)進(jìn)程傳到另一個(gè)進(jìn)程需要4次拷貝:
- 服務(wù)端把信息從輸入文件復(fù)制到服務(wù)端的臨時(shí)緩沖區(qū)
- 把服務(wù)端的臨時(shí)緩沖區(qū)信息復(fù)制到管道中
- 客戶端把信息從管道復(fù)制到客戶端的緩沖區(qū)
- 把客戶端臨時(shí)緩沖區(qū)的信息復(fù)制到輸出文件中
?使用共享內(nèi)存,將文件從一個(gè)進(jìn)程傳到另一個(gè)進(jìn)程需要2次拷貝:
- 將信息從輸入文件拷貝到共享內(nèi)存
- 將信息從共享內(nèi)存拷貝到輸出文件
四、消息隊(duì)列?
1.原理?
消息隊(duì)列是一個(gè)消息的鏈表,可以把消息看作一個(gè)記錄,具有特定的格式以及特定的優(yōu)先級(jí)。對(duì)消息隊(duì)列有寫權(quán)限的進(jìn)程可以向消息隊(duì)列中按照一定的規(guī)則添加新消息,對(duì)消息隊(duì)列有讀權(quán)限的進(jìn)程則可以從消息隊(duì)列中讀走消息。消息隊(duì)列的生命周期是隨內(nèi)核的。?
隊(duì)列的每個(gè)成員都是數(shù)據(jù)塊,每個(gè)數(shù)據(jù)塊包含類型和信息兩部分。這個(gè)隊(duì)列也遵循先進(jìn)先出,即從隊(duì)頭讀取消息,向隊(duì)尾寫入消息:
每個(gè)數(shù)據(jù)塊都有類型,這就說明,各個(gè)數(shù)據(jù)塊的類型可以不同,因此,接收者進(jìn)程接收的數(shù)據(jù)塊可以有不同的類型值。消息隊(duì)列的資源必須手動(dòng)刪除,因?yàn)閟ystem V IPC資源的生命周期是隨內(nèi)核的。
2.數(shù)據(jù)結(jié)構(gòu)
?消息對(duì)中的數(shù)據(jù)塊如何管理呢?還是先描述,再組織。使用命令:
cat /usr/include/linux/msg.h
就能夠看到消息隊(duì)列的數(shù)據(jù)結(jié)構(gòu)如下:?
struct msqid_ds
{ struct ipc_perm msg_perm; /* Ownership and permissions */time_t msg_stime; /* Time of last msgsnd(2) */time_t msg_rtime; /* Time of last msgrcv(2) */time_t msg_ctime; /* Time of last change */unsigned long __msg_cbytes; /* Current number of bytes inqueue (nonstandard) */msgqnum_t msg_qnum; /* Current number of messagesin queue */msglen_t msg_qbytes; /* Maximum number of bytesallowed in queue */pid_t msg_lspid; /* PID of last msgsnd(2) */pid_t msg_lrpid; /* PID of last msgrcv(2) */
};
?第一個(gè)ipc_perm?結(jié)構(gòu)體是不是有點(diǎn)熟悉呢?它和shm_perm是同類型的結(jié)構(gòu)體,使用命令:
cat /usr/include/linux/ipc.h
就能夠看到ipc_perm 的結(jié)構(gòu)體定義如下:
struct ipc_perm
{key_t __key; /* Key supplied to msgget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions */unsigned short __seq; /* Sequence number */
};
3.步驟
消息隊(duì)列使用過程如下:?
- 創(chuàng)建
- 發(fā)送
- 接收
- 釋放
4.函數(shù)?
(1)msgget
使用msgget來創(chuàng)建消息隊(duì)列:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgget(key_t key, int msgflg);
key | 通過ftok函數(shù)生成 |
msgflg | msgflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了 |
返回值 | 創(chuàng)建成功就返回消息隊(duì)列標(biāo)識(shí)符,失敗就返回-1 |
同shmget一樣,msgget第一個(gè)參數(shù)key是通過ftok函數(shù)生成的:
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
pathname | 自定義的文件路徑名 |
proj_id | 序號(hào),低8位被使用,非0 |
返回值 | 返回key,會(huì)被設(shè)置進(jìn)共享內(nèi)存在內(nèi)核的數(shù)據(jù)結(jié)構(gòu)里面 |
msgget第三個(gè)參數(shù)msgflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了:
(2)msgctl
使用msgctl來釋放消息隊(duì)列:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgctl(int msqid, int cmd, struct msqid_ds *buf);
使用完消息隊(duì)列后,如果不刪除的話,消息隊(duì)列會(huì)一直存在,直到系統(tǒng)重啟。如何刪除呢?有兩種刪除方式,一種是命令刪除:
ipcrm -q msqid
那么如何在代碼中刪除共享內(nèi)存呢?因此另外一種刪除消息隊(duì)列的方式就是使用msgctl函數(shù)控制消息隊(duì)列:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgctl(int msqid, int cmd, struct msqid_ds *buf);
msqid | 消息隊(duì)列的用戶層id |
cmd | 選項(xiàng),有多個(gè) |
buf | data structure數(shù)據(jù)結(jié)構(gòu)類型指針 |
返回值 | 刪除成功返回0,失敗返回-1 |
其中cmd選項(xiàng)有多個(gè):?
IPC_STAT | 將msqid的內(nèi)核數(shù)據(jù)結(jié)構(gòu)拷貝到buf指向的msqid_ds結(jié)構(gòu)中 |
IPC_SET | 將buf指向的msqid_ds結(jié)構(gòu)的一些成員的值寫入與此共享內(nèi)存段相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu),同時(shí)更新其msq_ctime成員 |
IPC_RMID | 刪除共享內(nèi)存 |
(3)msgsnd
使用msgsnd向消息隊(duì)列發(fā)送數(shù)據(jù):
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
msqid | 操作系統(tǒng)給用戶返回的id |
msgp | 待發(fā)送數(shù)據(jù)塊 |
msgsz | 待發(fā)送數(shù)據(jù)塊大小 |
msgflg | 發(fā)送數(shù)據(jù)塊的方式,一般為0 |
返回值 | 0表示調(diào)用成功,-1表示調(diào)用失敗 |
其中第二個(gè)參數(shù)msgp的結(jié)構(gòu)為:
struct msgbuf{long mtype; /* message type, must be > 0 */char mtext[1]; /* message data */
};
其中mutex為待發(fā)送的信息,mutex大小可以由我們自己指定。
(4)msgrcv
使用msgrcv從消息隊(duì)列獲取消息:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
msqid | 操作系統(tǒng)給用戶返回的id |
msgp | 獲取到的數(shù)據(jù)塊 |
msgsz | 獲取到的數(shù)據(jù)塊大小 |
msgtyp | 獲取到的數(shù)據(jù)塊的類型 |
msgflg | 獲取數(shù)據(jù)塊的方式,一般為0 |
返回值 | >0表示實(shí)際獲取的字節(jié)數(shù),-1表示調(diào)用失敗 |
?
五、信號(hào)量
1.原理?
前面的管道、共享內(nèi)存、消息隊(duì)列都以傳輸數(shù)據(jù)為目的,但是信號(hào)量不以傳輸數(shù)據(jù)為目的,通過共享資源的方式,來達(dá)到多個(gè)進(jìn)程同步互斥的目的。?
信號(hào)量,有時(shí)被稱為信號(hào)燈,是在多線程環(huán)境下使用的一種設(shè)施,是可以用來保證兩個(gè)或多個(gè)關(guān)鍵代碼段不被并發(fā)調(diào)用。在進(jìn)入一個(gè)關(guān)鍵代碼段之前,線程必須獲取一個(gè)信號(hào)量;一旦該關(guān)鍵代碼段完成了,那么該線程必須釋放信號(hào)量。其它想進(jìn)入該關(guān)鍵代碼段的線程必須等待直到第一個(gè)線程釋放信號(hào)量。本質(zhì)就是一個(gè)計(jì)數(shù)器,衡量臨界資源中的資源數(shù)。
這就像坐火車一樣,并不是因?yàn)樽谧簧?#xff0c;這個(gè)作為才屬于某一個(gè)人,而是買了票的時(shí)候,這個(gè)作為就已經(jīng)屬于買票的人了,因此買票的本質(zhì)就是對(duì)臨界資源的預(yù)訂,票的數(shù)量就是信號(hào)量。如以下代碼:
?
信號(hào)量相關(guān)概念:
- 臨界資源:被多個(gè)執(zhí)行流同時(shí)訪問的資源,一次只允許一個(gè)進(jìn)程使用。比如管道、共享內(nèi)存、消息隊(duì)列、信號(hào)量。
- 臨界區(qū):進(jìn)程中訪問臨界資源的代碼(和臨界資源配套)為了保護(hù)數(shù)據(jù)安全,就要把臨界區(qū)保護(hù)起來,就有了信號(hào)量。
- 原子性:一件事情要么做完,要么不做,沒有中間狀態(tài)。
- IPC資源必須刪除,否則不會(huì)自動(dòng)清除,除非重啟,所以system V IPC資源的生命周期隨內(nèi)核。
?信號(hào)量本質(zhì)是對(duì)臨界資源的統(tǒng)計(jì),更是操作系統(tǒng)對(duì)臨界資源的預(yù)定機(jī)制,信號(hào)量要誒預(yù)訂,所有線程要訪問臨界資源,得先申請(qǐng)信號(hào)量,那么所有的進(jìn)程就得先看到信號(hào)量,信號(hào)量就是臨界資源,要保護(hù)信號(hào)量這個(gè)臨界資源,信號(hào)量的常見操作即PV操作就必須保證原子性。
2.數(shù)據(jù)結(jié)構(gòu)
使用命令:
cat /usr/include/linux/sem.h
就能夠看到信號(hào)量的數(shù)據(jù)結(jié)構(gòu)如下:?
struct semid_ds
{struct ipc_perm sem_perm; /* permissions .. see ipc.h */__kernel_time_t sem_otime; /* last semop time */__kernel_time_t sem_ctime; /* last change time */struct sem *sem_base; /* ptr to first semaphore in array */struct sem_queue *sem_pending; /* pending operations to be processed */struct sem_queue **sem_pending_last; /* last pending operation */struct sem_undo *undo; /* undo requests on this array */unsigned short sem_nsems; /* no. of semaphores in array */
};
?第一個(gè)ipc_perm?結(jié)構(gòu)體是不是有點(diǎn)熟悉呢?它和shm_perm、msg_perm是同類型的結(jié)構(gòu)體,使用命令:
cat /usr/include/linux/ipc.h
就能夠看到ipc_perm 的結(jié)構(gòu)體定義如下:
struct ipc_perm
{key_t __key; /* Key supplied to msgget(2) */uid_t uid; /* Effective UID of owner */gid_t gid; /* Effective GID of owner */uid_t cuid; /* Effective UID of creator */gid_t cgid; /* Effective GID of creator */unsigned short mode; /* Permissions */unsigned short __seq; /* Sequence number */
};
3.函數(shù)
(1)semget
?使用semget創(chuàng)建信號(hào)量:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semget(key_t key, int nsems, int semflg);
key | 操作系統(tǒng)給用戶返回的id |
nsems | 創(chuàng)建的信號(hào)量的個(gè)數(shù) |
semflg | semflg標(biāo)志有多個(gè),先了解最常用的兩個(gè)標(biāo)志IPC_CREAT和IPC_EXCL就可以了 |
返回值 | 創(chuàng)建成功就返回信號(hào)量標(biāo)識(shí)符,-1表示創(chuàng)建失敗 |
(2)semctl
?使用semctl刪除信號(hào)量:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semctl(int semid, int semnum, int cmd, ...);
semid | 信號(hào)量的用戶層id |
semnum | 信號(hào)量序號(hào) |
cmd | 信號(hào)量的控制操作標(biāo)識(shí) |
返回值 | 創(chuàng)建成功就返回信號(hào)量標(biāo)識(shí)符,-1表示創(chuàng)建失敗 |
(3)semop
使用semop來進(jìn)行信號(hào)量的PV操作:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>int semop(int semid, struct sembuf *sops, unsigned nsops);
semid | 信號(hào)量的用戶層id |
sops | 是sembuf類型的操作指針 |
nsops | 單個(gè)信號(hào)量的操作 |
返回值 | 創(chuàng)建成功就返回信號(hào)量標(biāo)識(shí)符,-1表示創(chuàng)建失敗 |
使用命令:
cat /usr/include/linux/sem.h
可以看到sembuf結(jié)構(gòu)體:
struct sembuf
{unsigned short sem_num; /* semaphore index in array */short sem_op; /* semaphore operation */short sem_flg; /* operation flags */
};
sem_num | 指定要操作的信號(hào)量,0表示第一個(gè)信號(hào)量,1表示第二個(gè)信號(hào)量,…… |
sem_op | 信號(hào)量操作 |
sem_flg | 操作標(biāo)識(shí) |
六、System V IPC總結(jié)
?從以上內(nèi)容可以看出,共享內(nèi)存、消息隊(duì)列、信號(hào)量,雖然屬性和實(shí)現(xiàn)起來有差別,但是他們維護(hù)的數(shù)據(jù)結(jié)構(gòu)的成員卻是一樣的,即ipc_perm結(jié)構(gòu)體,這樣每次要申請(qǐng)System V IPC時(shí),無論是共享內(nèi)存、消息隊(duì)列、信號(hào)量,都會(huì)在數(shù)組中開辟ipc_perm這樣的結(jié)構(gòu):
?那么內(nèi)核可以分配一個(gè)ipc_perm數(shù)組,用來指向每一個(gè)IPC資源。
?
?
?