文章目錄
- 1. 行程間通信目的
- 2. 行程通信的發展
- 3. 管道
- 3.1 匿名管道
- 3.2 管道讀寫規則
- 3.2.1 寫端不關閉檔案描述符,不寫入,(管道為空)讀取條件不就緒,讀端就可能就會長時間阻塞
- 3.2.2 讀端不關閉檔案描述符,不讀,(管道滿了)寫條件不就緒,寫端就會被阻塞
- 3.2.3 寫端關閉檔案描述符,讀端讀取完之后,讀到檔案結尾
- 3.2.4 讀端關閉檔案描述符,寫端可能會被殺掉
- 3.3 站在內核理解管道
- 3.4 管道特點
- 3.5 命名管道
- 4. 管道總結
- 5. 共享記憶體
- 5.1 共享記憶體的函式,系統呼叫,bash命令
- 5.2 庫函式ftok
- 5.3 系統呼叫shmget
- 5.4 系統呼叫shmctl
- 5.4 系統呼叫shmat與shmdt
- 5.5 bash命令
- 6.共享記憶體的通信
1. 行程間通信目的
- 資料傳輸:一個行程需要將他的資料發送給另一個行程
- 資源共享:多個行程需要共享同樣的資源
- 通知事件:一個行程需要向一個或一組行程發送訊息,通知他們發生
- 行程控制:有些行程希望完全控制另一個行程的執行,此時控制行程希望能夠攔截另一個行程的所有陷入和例外,并能夠及時知道他的狀態改變,
2. 行程通信的發展
- 管道
- System V行程間通信
- POSIX行程通信
其中管道是用檔案實作的,而System V與POSIX是兩套標準,繞過檔案來通信,
3. 管道
當行程在記憶體中執行這一步步代碼時,并不是立即把檔案內容寫到硬碟上的檔案,而是先寫到檔案快取區,行程結束由作業系統重繪至硬碟上對應的檔案,

行程間通信的本質就是讓兩個行程看到同一份資源,即這個檔案記憶體緩沖區,
當fork創建子行程,子行程會繼承父行程的檔案描述符等,通過struct file就能找到檔案緩沖區

3.1 匿名管道
先寫一個makefile檔案

現在我們對于這個命令就要有這深入的理解,bash創建子行程,子行程execl(ls)行程替換,使用dup2,將顯示的檔案重定向到makfile里,
創建,

int pipefd[2],是一個大小為2的形引陣列,他是一個輸出型引數,也就是說在這個函式內會改變這個陣列,我們在外面會拿到他,
pipe這個系統呼叫創建匿名管道,但為什么要打開兩次檔案,分配兩個檔案描述符,而且一個以讀方式打開檔案,一個以寫方式打開檔案,
- 不能open一次,給他只讀或只寫,因為子行程會拷貝那么子行程對那個檔案也就只讀或只寫了)
- 不能open一次,同時給他讀和寫,因為只能父行程寫,子行程讀,或者子行程寫,父行程讀,而這樣一個行程(無論父子),一個檔案描述符,同時擁有兩種權限,是無法控制的)
- open兩次,拿兩個檔案描述符,一個描述符有讀權限,一個描述符有寫權限,雖然這個行程(無論父子)對檔案也可以進行讀和寫,但是我們可以手動控制,例如:父行程讀,子行程寫,就把父行程寫,子行程讀的檔案描述符關了,
上面講到父行程必須以讀寫方式打開檔案,pipe這個函式呼叫封裝了這種操作方式,而回傳的陣列里,pipefd[0]表示讀端,pipefd[1]表示寫端,
下面代碼表示,子行程寫,父行程讀,而且子行程關閉了讀,父行程關閉了寫,保證管道的單向資料傳輸,
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include <sys/stat.h>
4 #include <fcntl.h>
5 #include<unistd.h>
6 #include<string.h>
7 int main()
8 {
9 int pipefd[2]={0};
10 pipe(pipefd);
11 pid_t id= fork();
12 if(id==0)
13 {
14 const char* str="i am child ,child writing";
15 close(pipefd[0]);
16 while(1)
17 {
18 //子行程寫
19 write(pipefd[1],str,strlen(str));
20 sleep(1);
21 }
22 }
23 else if(id>0){
24 close(pipefd[1]);
25 char buf[64];
26 while(1)
27 {
28 //第三個引數為你期望讀多少,回傳值為實際讀了多少位元組
29 ssize_t s=read(pipefd[0],buf,sizeof(buf)-1);
30 if(s>0)
31 {
32 buf[s]=0;
33 }
34 printf("father get message:%s\n",buf);
35
36 }
37 }
38 return 0;
39 }

這里有四個特征,需要進一步深入挖掘,
3.2 管道讀寫規則
3.2.1 寫端不關閉檔案描述符,不寫入,(管道為空)讀取條件不就緒,讀端就可能就會長時間阻塞
讓子行程(寫端)sleep5s,父行程(讀端)不變,那么就會發現子行程寫一次,停頓的時候,父行程也在停,也就是說父行程以子行程節奏為主,子行程不動,父行程也就在阻塞式等待,就是將自己pcb由R狀態設定為S狀態,從運行佇列挪到等待佇列,那么假如寫段不關閉檔案描述符,而且一直不往里面寫資料,那么讀端就可能會一直堵塞(等待),
3.2.2 讀端不關閉檔案描述符,不讀,(管道滿了)寫條件不就緒,寫端就會被阻塞
讓父行程(讀端)sleep5s,子行程(寫端),就會發現寫端一次性寫滿了管道,讀端讀一次,可能過一會寫端才會往管道里在寫資料,
3.2.3 寫端關閉檔案描述符,讀端讀取完之后,讀到檔案結尾
假如當子行程(寫端)寫了5次,關閉掉寫段檔案描述符,最后read會回傳0,即讀到檔案結尾
3.2.4 讀端關閉檔案描述符,寫端可能會被殺掉
假如當子行程(寫端)一次寫滿,父行程(讀端)讀3次關掉檔案描述符,寫端行程(子行程)會被作業系統通過信號直接殺掉,又由于我們代碼中父行程死回圈,沒有回收,所以子行程僵尸了,
3.3 站在內核理解管道
管道也是一種檔案,他也有自己的inode,在struct file中存在path,path中有一個dentry,里面存著目錄的inode,inode里面有和block的映射,就找到了檔案名和inodeid,在找到檔案的inode,
3.4 管道特點
- 匿名管道只能用于具有共同祖先的行程(具有親緣關系的行程)之間進行通信;通常,一個管道由一個行程創建,然后該行程呼叫fork,此后父、子行程之間就可應用該管道,
- 管道提供流式服務
- 一般而言,行程退出,管道釋放,所以管道的生命周期隨行程
- 一般而言,內核會對管道操作進行同步與互斥
- 管道是半雙工的,資料只能向一個方向流動;需要雙方通信時,需要建立起兩個管道
當我們敲下這個命令的時候就可以理解他的原理
who | wc - l
首先bash命令列,呼叫pipe()創建匿名管道,在創建兩個子行程,execl行程替換為who和wc-l,兩個行程都拷貝了bash的檔案描述符,bash關閉兩個檔案描述符,who為寫端,wc-l為讀端,關掉who的讀端檔案描述符,關掉wc-l的寫端檔案描述符,who本來要往顯示幕上列印,結果要寫到管道檔案里,dup2(pipefd[1],1,嚴謹一點關掉pipefd[1],wc-l原本讀的是鍵盤,結果要讀管道檔案,所以dup2(pipefd[0],0),然后關掉pipefd[0],
3.5 命名管道

mkfifo為庫函式,創建一個命名管道
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/stat.h>
4 int main()
5 {
6 mkfifo("./fifo",0666);
7 return 0;
8 }

p為管道檔案

匿名管道由pipe系統呼叫創建,我們并不關心他叫什么名字,只用他的檔案記憶體緩沖區,常用于有親緣關系的行程,子行程繼承了檔案資訊,但兩個沒有關系的行程怎么通信呢?
mkfifo是一個庫函式,作用為創建一個命名管道檔案,

下面來寫一個客戶端與服務端,客戶端發送,服務端接收并列印出來,
首先服務端創建管道,只讀的方式open,回傳fd,然后呼叫read方法讀取到一個陣列buf,最后列印出來
1 #include<stdio.h>
2 #include<sys/types.h>
3 #include<sys/stat.h>
4 #include<fcntl.h>
5 #include<unistd.h>
6 int main()
7 {
8 if(-1==mkfifo("./fifo",0644))
9 {
10 perror("err");
11 }
12 int fd=open("./fifo",O_RDONLY);
13 if(fd>=0)
14 {
15 char buf[64];
16 while(1)
17 {
18 ssize_t s=read(fd,buf,sizeof(buf)-1);
19 if(s>0)
20 {
21 buf[s]=0;
22 printf("client#: %s",buf);
23 }
24 else if(s==0)
25 {
26 //當實際讀到位元組為0時
printf("client quit \n");
break;
27 }
28 else{
29 perror("read err");
30 break;
31 }
32 return 0;
33 }
34 }
35
客戶端負責寫,讀端已經創建管道,所以寫端不必在創建,把什么寫進管道呢,我們重定向,從鍵盤輸入進管道,這樣服務端就可以接受到了,
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/types.h>
4 #include<sys/stat.h>
5 #include<fcntl.h>
6 int main()
7 {
8 int fd=open("./fifo",O_WRONLY);
9 if(fd>=0)
10 {
11 char buf[64];
12 while(1)
13 {
14 //從鍵盤輸入的字符到buf里
15
16 printf("please write memsage# \n");
17
18 ssize_t s=read(0,buf,sizeof(buf)-1);
19 if(s>0)
20 {
21 buf[s]=0;
22 //往fd里寫buf,想發s個位元組
23 write(fd,buf,s);
24 }
25 }
26 }
27 return 0;
28 }
客戶端寫

服務端讀,當客戶端終止,服務端讀到檔案尾,結束

命名管道通過檔案名+檔案描述符來實作通信,

管道檔案始終大小為0,這就是最開始提到的,作業系統直接創建檔案,加載到記憶體中,但是只需要用到它的檔案記憶體緩沖區,所以不需要把資訊寫入到硬碟中的fifo中,
而對于管道的4條規則,命名管道也同樣適用,
4. 管道總結
-
匿名管道,pipe(int pipefd[2])系統呼叫,在記憶體中創建了一個檔案,形參輸出型引數,會回傳兩個檔案描述符,用于操作,fork之后,子行程會繼承這兩個檔案描述符,一個行程有兩個檔案描述符,不要那個就關閉那個,它在記憶體中創建了一個匿名管道檔案,親緣行程繼承了檔案資訊可以看見他,從而使用它的檔案記憶體緩沖區,
-
命名管道通過mkfifo庫函式在硬碟上創建檔案,加載進記憶體,任意行程通過檔案名和檔案描述符來看見他,也是只使用了它的檔案記憶體緩沖區,實際上資訊并未重繪至硬碟,
-
管道的4個規則,
5. 共享記憶體
system V標準共享記憶體
行程間通信本質是讓兩個行程看到同一塊資源,對于管道我們借助了pipe檔案的檔案記憶體緩沖區,那么共享記憶體就是繞過檔案,直接在物理記憶體開辟一段空間,通過某種映射,讓兩個行程與這段空間關聯起來,這樣一個行程修改,另一個行程就能直接看見他,因為這段空間是兩者共享的,

寫端寫到管道的檔案記憶體緩沖區,讀端從管道的檔案記憶體緩沖區讀,用戶到內核,內核到用戶,共享記憶體只需要一次寫入,所以它幾乎是最快的IPC方式,
系統中存在這大量行程無時無刻不在通信,所以我們對行程間通信也要先描述在組織,通信的本質在于看到同一塊資源,而管道,共享記憶體都是資源,只是取決于作業系統通過檔案系統還是記憶體管理來分配資源,
所以我們需要創建共享記憶體,關聯共享記憶體,取消關聯,洗掉共享記憶體,
共享記憶體的資料結構

- ipc_perm:用戶標識資訊
- shm_segsz:記憶體大小
- shm_atime:最后一次掛接時間
- shm_dtime:最后一次取消掛接時間
- shm_ctime:最后一次改變時間
用戶標識資訊的結構體
struct kern_ipc_perm {
spinlock_t lock;
bool deleted;
int id;
key_t key;
kuid_t uid;
kgid_t gid;
kuid_t cuid;
kgid_t cgid;
umode_t mode;
unsigned long seq;
void *security;
struct rhash_head khtnode;
struct rcu_head rcu;
refcount_t refcount;
} ____cacheline_aligned_in_smp __randomize_layout;
其中key值代表這塊共享記憶體的唯一值,作業系統把它分配給新創建的共享記憶體,
5.1 共享記憶體的函式,系統呼叫,bash命令
5.2 庫函式ftok
先認識一個庫函式

他用來創建一個唯一的key值,之后我們就可以把它分配給創建共享記憶體的函式,填到共享記憶體的ipc_perm里,來讓共享記憶體有個唯一的標識,
5.3 系統呼叫shmget


key值是給作業系統看的,而在這個回傳值是給用戶看的,
key_t key = ftok(PATHNAME,PROJ_ID);
int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
形參IPC_CREAT|IPC_EXECL,代表共享記憶體不存在則創建,存在則出錯回傳,

第一次創建成功,第二次失敗,說明共享記憶體的生命周期和管道不一樣,是隨內核的,
5.4 系統呼叫shmctl

這段代碼是創建了共享記憶體,5s之后使用系統呼叫刪掉,而實際上我們要知道作業系統分配資源,使用一個struct shmid_ds的結構體來描述他,
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/ipc.h>
4 #include<sys/types.h>
5 #include<sys/shm.h>
6 #include"commnd.h"
7 int main()
8 {
9 key_t key = ftok(PATHNAME,PROJ_ID);
10 int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
11 sleep(5);
12 if(shmid<0)
13 {
14 perror("err");
15 return 1;
16 }
17 sleep(5);
18 shmctl(shmid,IPC_RMID,NULL);
19 return 0;
20 }
while :; do ipcs -m; sleep 1;echo "####";done
使用腳本監控一下

5.4 系統呼叫shmat與shmdt

回傳值是虛擬地址空間的虛擬地址,

引數是shmat的回傳值
所以可以寫一個完整的共享記憶體的生命周期,先創建(同時作業系統分配資源,struct shmid_ds結構體描述),5s后此行程與記憶體相關聯,nattch變成1,5s后又變成0,5s后被洗掉
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/ipc.h>
4 #include<sys/types.h>
5 #include<sys/shm.h>
6 #include"commnd.h"
7 int main()
8 {
9 key_t key = ftok(PATHNAME,PROJ_ID);
10 int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
11 sleep(5);
12 if(shmid<0)
13 {
14 perror("err");
15 return 1;
16 }
17 char* str= (char*)shmat(shmid,NULL,0);
18 sleep(5);
19 shmdt(str);
20 sleep(5);
21 shmctl(shmid,IPC_RMID,NULL);
22 return 0;
23 }
開始

過5s

在過5s

5.5 bash命令
- 查看共享記憶體
ipcs-m

perms:權限,由于剛才代碼沒有|權限這里顯示0
shmid:創建共享記憶體函式的回傳值給用戶用的,
bytes:分配記憶體的大小,頁的整數倍,假如大小設定為4097,分配的其實是兩頁,但是你只能用4097,
nattch:掛接個數 - 洗掉共享記憶體
ipcrm -m shmid

6.共享記憶體的通信
和管道一樣寫一個服務器端,客戶端是最好的驗證,
服務器端,創建共享記憶體,直接可以看到資料
srever.c
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/ipc.h>
4 #include<sys/types.h>
5 #include<sys/shm.h>
6 #include"commnd.h"
7 int main()
8 {
9 key_t key = ftok(PATHNAME,PROJ_ID);
10 int shmid=shmget(key,SIZE,IPC_CREAT|IPC_EXCL|0666);
11 sleep(5);
12 if(shmid<0)
13 {
14 perror("err");
15 return 1;
16 }
17 char* str= (char*)shmat(shmid,NULL,0);
18 while(1)
19 {
20
21 //str為首元素地址,直接可以用它列印字串,就是列印共享區里的內容
22 printf("%s\n",str);
23 sleep(1);
24 }
25 shmdt(str);
26 shmctl(shmid,IPC_RMID,NULL);
27 return 0;
28 }
client.c
客戶端,ftok函式的路徑與服務器端保持一致,這樣才能拿到key,key是給作業系統用的,shmget不需要任何權限了,因為服務器端已經創建了共享記憶體,通過key值他們看到了同一塊資源,回傳值shmid供用戶進行操作(掛接,洗掉等),注意!!客戶端不能在洗掉共享記憶體因為在服務器端已經洗掉過了
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<sys/ipc.h>
4 #include<sys/types.h>
5 #include<sys/shm.h>
6 #include"commnd.h"
7 int main()
8 {
9 //形參一樣得到的key值一樣
10 key_t key = ftok(PATHNAME,PROJ_ID);
11 //去掉權限,因為在server端已經創建共享記憶體,
12 int shmid=shmget(key,SIZE,0);
13 sleep(5);
14 if(shmid<0)
15 {
16 perror("err");
17 return 1;
18 }
19 //掛接,拿到地址
20 char* str= (char*)shmat(shmid,NULL,0);
21 char s='a';
22 for(;s<='z';s++)
23 {
24 str[s-'a']=s;
25 //5s寫一次
26 sleep(5);
27 }
28 shmdt(str);
29 //shmctl(shmid,IPC_RMID,NULL);
30 return 0;
31 }

在客戶端,5s往共享記憶體寫一次,但是服務器端在客戶端不寫的那4s,還是把東西重復列印出來了,而在管道中假如寫端不寫,讀端是會阻塞等待,直到你在寫,得出結論,共享記憶體不提供任何同步與互斥機制,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/275474.html
標籤:其他
上一篇:Java2021春招實習面經整理
