進程間通信 System V系列: 共享內存,初識信號量
- 一.共享內存
- 1.引入
- 2.原理
- 3.系統調用接口
- 1.shmget
- 2.shmat和shmdt
- 3.shmctl
- 4.邊寫代碼邊了解共享內存的特性
- 1.ftok形成key,shmget創建與獲取共享內存
- 2.shm相關指令
- 3.shmat和shmdt掛接和取消掛接
- 4.shmctl獲取共享內存信息,釋放共享內存
- 5.開始通信
- 5.利用管道實現共享內存的協同機制
- 1.Sync(同步類)
- 2.讀寫端的修改
- 3.動圖演示
- 6.共享內存的優缺點
- 二.消息隊列
- 1.概念
- 2.接口,數據結構等等
- 三.信號量理論
- 1.信號量的原理
- 2.信號量的理論
- 1.從生活中的例子理解信號量
- 2.進程角度的信號量
- 3.信號量的細節
- 1.信號量必須要由OS提供并維護
- 2.信號量的基本操作
- 3.信號量的接口
- 1.semget
- 2.semctl
- 3.semop
- 四.System V系列的進程間通信的小總結
- 五.利用信號量實現共享內存的協同機制
- 1.思路
- 2.Server創建并獲取信號量,Client獲取信號量 -> ftok和semget
- 1.ftok
- 2.shmget
- 3.Server阻塞申請信號量資源 - semop
- 4.Client初始化信號量資源 - semctl
- 5.Server釋放信號量資源 - semctl
- 6.完整代碼
- 1.Common.hpp
- 2.sem.hpp
- 3.ShmServer.cpp
- 4.ShmClient.cpp
- 7.演示
我們不是都有管道了嗎?為什么還要有其他的進程間通信方式呢?
當時的年代,通信技術是一個非常火的點,就像現在人工智能和各種大模型一樣,類似于百家爭鳴的樣子,所以有很多進程間通信的方式
因為共享內存跟我們學的進程地址空間有密切聯系,所以我們重點學習
而信號量我們就先認識一下,學習一下理論即可
一.共享內存
1.引入
管道方便是方便,直接復用文件接口即可,但是想要使用管道是需要訪問內核的,而且管道的內核級緩沖區也是在內核當中的,因此會導致效率不是特別好(因為訪問內核本身就是一個比較大的消耗)
那么有沒有什么辦法能夠讓兩個進程無需訪問內核就能進行進程間通信呢?
2.原理
跟命名管道一樣,共享內存也是允許完全無關的兩個進程商量一下一起使用同一份資源,從而實現進程間通信的
看似很好懂,但是有幾個值得關注的點:
3.系統調用接口
shm就是shared_memory:共享內存
ipc: InterProcess Communication:進程間通信
1.shmget
第三個參數為什么要這么設計呢?
我們一起來分析一下
我們剛才還沒有說返回值
2.shmat和shmdt
分配完一個共享內存了,下面要做的事情就是把共享內存映射到進程的進程地址空間當中,并用頁表建立該映射
shmat:shmattach是負責建立映射的,也就是將共享內存和進程掛接起來
shmdt:shmdetach(detach是分離,拆卸的意思),也就是取消該共享內存跟進程的掛接關系
shmdt直接傳入shmat的返回值即可
shmat:如果掛接失敗,返回(void*)-1
3.shmctl
4.邊寫代碼邊了解共享內存的特性
1.ftok形成key,shmget創建與獲取共享內存
下面我們應該是要使用shmat和shmdt了,不過在此之前,我們還要介紹幾個指令
2.shm相關指令
如何釋放呢?
可以通過shmctl系統調用接口來釋放,也可以通過指令來釋放
我們先介紹指令釋放
這里的key顯示的是16進制,我們剛才打印的是10進制
因此我們改一下代碼,讓它以16進制打印
3.shmat和shmdt掛接和取消掛接
while :;do ipcs -m;sleep 1;done
這里反過濾掉了root創建的共享內存
while :;do ipcs -m | grep -v root;sleep 1;done
我們看到了掛接和取消掛接的全過程
4.shmctl獲取共享內存信息,釋放共享內存
我們實現了共享內存的創建/獲取,掛接,取消掛接和釋放
下面是時候開始讓這兩個進程開始通信了
在掛接之后,取消掛接之前開始通信
5.開始通信
我們剛才獲取信息只是為了告訴大家這個函數有這么個功能而已,因此我們就不調用這個獲取信息的函數了哦
通信成功
那么沒有協同機制怎么辦?
一個很好的方法是借助信號量來解決這一問題,但是因為信號量的接口太麻煩(比共享內存的這些接口還要麻煩很多),因此我們以后詳細介紹信號量的時候再去使用信號量的接口
要不然是像我剛才那樣通信雙方約定好一個暗號,讀端讀到暗號時意味著通信結束
而是那樣只能解決一部分情況下保證讀端讀取完所有的寫端數據時才退出
還是無法解決寫端還沒寫入你讀端就開始讀了啊
我們可以利用天然具有協同機制的管道啊!!
又因為我們這兩個進程是沒有血緣關系的,因此我們用一下命名管道吧
這里直接把我們之前寫的管理命名管道的代碼拿過來
#pragma once
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <cerrno>
#include <cstring>
#include <string>
using namespace std;
const char* path="./namedpipe";
#define MODE 0666
class Fifo
{
public:Fifo(const char* path):_path(path)//用成員變量保存路徑{int ret=mkfifo(_path.c_str(),MODE);if(ret==-1)//說明創建失敗{cerr<<"create namedpipe fail, errno: "<<errno<<" , strerror: "<<strerror(errno)<<endl;}else{cout<<"create namedpipe succeed"<<endl;}}~Fifo(){unlink(_path.c_str());}
private:string _path;
};
5.利用管道實現共享內存的協同機制
1.Sync(同步類)
2.讀寫端的修改
3.動圖演示
成功解決了寫端沒寫數據,讀端還讀的問題
6.共享內存的優缺點
下面我們趁熱打鐵快速了解一下消息隊列
二.消息隊列
1.概念
消息隊列的生命周期也是隨內核的,跟共享內存一樣
2.接口,數據結構等等
三.信號量理論
1.信號量的原理
這里介紹信號量的原理,
一方面是為了讓我們更好地理解信號量,一方面是先提出一些多線程當中的概念
2.信號量的理論
1.從生活中的例子理解信號量
2.進程角度的信號量
3.信號量的細節
1.信號量必須要由OS提供并維護
2.信號量的基本操作
3.信號量的接口
1.semget
2.semctl
3.semop
關于信號量的更多話題我們等到多線程的時候還會再說明的
四.System V系列的進程間通信的小總結
共享內存,消息隊列和信號量的很多接口都是相同的,它們的內核數據結構當中也都有一個一樣的結構體:ipc_perm,
它們都是主要應用于本地通信的,因此在目前的網絡時代當中并不常用(用的更多的還是網絡通信)
它們都屬于System V系列的進程間通信,OS為了管理它們搞了一個
ipc_ids結構體,ipc_id_ary結構體,kern_ipc_perm結構體實現了ipc資源的動態申請和釋放,并將對ipc資源的管理轉換為了對kern_ipc_perm的增刪查改和對ipc_id_ary的動態擴容
不過因為System V系列的進程間通信的結構和數據結構都是獨立設計的,跟文件是完全解耦的,因此不符合Linux一切皆文件的設計思想,這也是System V系列的進程間通信并不熱門的原因
如果OS能夠在struct file當中封裝一個ipc_perm的指針,把kern_ipc_perm關聯起來,并利用文件接口封裝ipc資源使用的接口,就能讓System V系列的進程間通信符合一切皆文件
那樣的話使用起來肯定也就更容易,肯定就能熱門了
五.利用信號量實現共享內存的協同機制
本來想寫完前4點就結束吧,不過心血來潮想用一下信號量,下面我們一起來用一下信號量吧
1.思路
依舊是回歸我們之前需要利用管道實現共享內存的協同機制的時候
我們的目的是讓讀端一開始阻塞等待,等到寫端準備要進行寫入的時候告訴讀端: 我要開始寫啦,你也開始讀吧
此時就能夠保證讀端不會在一開始的時候做無意義的讀取操作
大致流程分為:
- 讀端(Server)創建并獲取信號量
- Server阻塞申請信號量資源,此時讀端就是阻塞等待寫端進行寫入
- Client獲取讀端創建好的信號量
- 寫端(Client)準備寫入時初始化信號量資源
- Server成功申請信號量資源,開始進程間通信
- 最后Server釋放信號量資源
流程很清晰,
(那為什么我們一開始不用信號量呢? 因為信號量接口太麻煩了…,而且我用管道和信號量來解決這一共享內存的同步機制是為了學習熟悉這些接口)
之前有一些點我沒有注意到,寫代碼的時候屢屢碰壁,最后才搞過來了
2.Server創建并獲取信號量,Client獲取信號量 -> ftok和semget
1.ftok
- ftok里面傳入的路徑必須是我們Linux系統中的確存在的路徑!!!
- 我們申請的共享內存和信號量各自的key是不可以相同的(大家也能夠很好的理解,因為key才是ipc資源的唯一標識嘛)
我們就用這個產生sem的key
用這個產生shm的key
2.shmget
我們共享內存的資源就是一個整體,因此nsems傳入1
然后跟共享內存一樣,Server傳IPC_CREAT | IPC_EXCL | 0666, Client傳入IPC_CREAT即可
Server:
Client:
3.Server阻塞申請信號量資源 - semop
當sem_op<0即需要申請信號量時,如果信號量==0,那么該進程就會阻塞,等待信號量>0
而信號量在還沒有被進程設置之前默認值是0,因此我們可以這樣來玩
(注意:
- semget時傳入的nsems時你申請的這個信號量集當中的信號量數目,而不是信號量的初始值!!
- 初始值需要進程顯式傳入,而且默認值是0[我就是因為這點屢屢碰壁]
- sembuf是本來就有的,不需要我們顯式提供[我就是因為這點屢屢碰壁]
)
Server不設置信號量,在讀取之前申請信號量資源阻塞等待寫端進行寫入(我們起名為lock函數)
Client即將進行寫入之前初始化該信號量為1(我們起名為Unlock函數),此時Server等待成功,退出阻塞狀態,開始進行讀取操作
4.Client初始化信號量資源 - semctl
5.Server釋放信號量資源 - semctl
6.完整代碼
1.Common.hpp
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <cstring>
#include <sys/ipc.h>
#include <sys/shm.h>
using namespace std;const char* shm_path="/home/wzs/ubuntucode/process_ipc/semaphore";
const int shm_id=0x5678;
const int agreeSize=4096;key_t GetKey(const char* k_path,int proj_id)
{key_t ret=ftok(k_path,proj_id);if(ret==-1){cout<<"ftok fail"<<endl;exit(1);}return ret;
}class Shm
{
public:int GetShmid(int key,int size,int shmflg){int shmid=shmget(key,size,shmflg);if(shmid==-1){cout<<"shmget fail"<<endl;exit(1);}return shmid;}void* Attach(int shmid){void* addr=shmat(shmid,nullptr,0);if(addr==(void*)-1){cout<<"shmat fail"<<endl;exit(1);}return addr;}void Detach(void* addr){shmdt(addr);}void DelShm(int shmid){shmctl(shmid,IPC_RMID,nullptr);}
};
2.sem.hpp
#pragma once
#include <sys/sem.h>
const char* sem_path="/home/wzs/ubuntucode/process_ipc/pipe/namepipe/name_pipepool";
const int sem_id=0x3f45289;union semun { int val; /* 用于SETVAL */ struct semid_ds *buf; /* 用于IPC_STAT和IPC_SET */ unsigned short *array; /* 用于GETALL和SETALL */
};void ChangeCount(sembuf* buf,int val)
{buf->sem_num=0;buf->sem_op=val;buf->sem_flg=SEM_UNDO;
}class Sem
{
public:int GetSemid(key_t key,int nsems,int semflg){int semid=semget(key,nsems,semflg);if(semid==-1){cout<<"semget fail"<<endl;exit(1);}return semid;}void DelSem(int semid){semctl(semid,0,IPC_RMID);}void Change(int semid,sembuf* sops){cout<<"wait sem resource..."<<endl;semop(semid,sops,1);cout<<"wait success!"<<endl;}void GetInfo(int semid){int val=semctl(semid,0,GETVAL);cout<<val<<endl;}void Init(int semid,semun un){semctl(semid,0,SETVAL,un);}void Unlock(int semid){union semun sem_union;sem_union.val=1;//將信號量的初始值設置為1,此時相當于開鎖,讀端可以拿到信號量,開始讀取Init(semid,sem_union);}void lock(int semid){sembuf buf;ChangeCount(&buf,-1);Change(semid,&buf);//我想申請信號量,但是信號量默認是0,我需要阻塞等待}
};
3.ShmServer.cpp
#include "Common.hpp"
#include "sem.hpp"
int main()
{Shm shm;key_t sem_key=GetKey(shm_path,shm_id);//獲取Keyint shmid=shm.GetShmid(sem_key,agreeSize,IPC_CREAT | IPC_EXCL | 0666);//申請shmchar* addr=(char*)shm.Attach(shmid);//掛接shm//利用二元信號量(鎖)Sem sem;key_t k=GetKey(sem_path,sem_id);int semid=sem.GetSemid(k,1,IPC_CREAT | IPC_EXCL | 0666);//等待獲取鎖sem.lock(semid);cout<<"receive message begin########################################"<<endl;//開始讀取while(true){if(addr[0]=='q') break;cout<<"this is message:"<<addr<<"。"<<endl;sleep(1);}cout<<"receive message over#########################################"<<endl;cout<<"Server will detach shm now..."<<endl;shm.Detach(addr);//解除掛接shm.DelShm(shmid);//刪除shmsem.DelSem(semid);//刪除semreturn 0;
}
4.ShmClient.cpp
#include "Common.hpp"
#include "sem.hpp"
int main()
{Shm shm;key_t sem_key=GetKey(shm_path,shm_id);int shmid=shm.GetShmid(sem_key,agreeSize,IPC_CREAT);Sem sem;key_t k=GetKey(sem_path,sem_id);int semid=sem.GetSemid(k,1,IPC_CREAT);char* addr=(char*)shm.Attach(shmid);memset(addr,0,agreeSize);cout<<"send message begin########################################"<<endl;//開鎖sem.Unlock(semid);for(char c='A';c<='Z';c++){addr[c-'A']=c;sleep(1);}addr[0]='q';cout<<"send message over########################################"<<endl;cout<<"Client will detach shm now..."<<endl;shm.Detach(addr);return 0;
}
7.演示
以上就是Linux 進程間通信 System V系列: 共享內存,信號量,簡單介紹消息隊列的全部內容,希望能對大家有所幫助!!