文章目錄
- 一、共享內存的原理
- 二、信道的建立
- 1.創建共享內存
- 1.key的作用
- 2.key的選取
- 3.shmid的作用
- 4.key和shmid的區別
- 5.內存設定的特性
- 6.shmflg的設定
- 2.綁定共享內存
- 3.代碼示例
- 三、利用共享內存通信
- 1.通信
- 2.解除綁定
- 3.銷毀共享內存
- 1.命令行銷毀
- 2.程序中銷毀
- 四、共享內存的生命周期
- 五、數據安全問題
- 六、源碼
- Fifo.hpp
- Comm.hpp
- Shm.hpp
- server.cc
- client.cc
一、共享內存的原理
共享內存是通過在物理內存上開辟一塊空間,然后讓需要通信的進程都映射到這一塊空間,這樣就使它們看到同一塊資源了。
共享內存區是最快的IPC形式。
?旦這樣的內存映射到共享它的進程的地址空間,這些進程間數據傳遞
不再涉及到內核,換句話說是進程不再通過執?進?內核的系統調?來傳遞彼此的數據
共享內存通信是雙向的,
也就是說一個進程可以既讀又寫,使用起來就和C語言的malloc申請到的內存差不多。這種通信方式存在著數據安全問題,會在下文細說。
二、信道的建立
1.創建共享內存
創建共享內存使用shmget函數,它的作用是創建或獲取共享內存段的系統調用。
對于shmget的使用來說,雖然操作起來相對簡單,但要完全理解其各種參數的設定則較為困難。不過接下來我會進行詳細講解。
shmget聲明如下:
#include <sys/ipc.h>
#include <sys/shm.h>int shmget(key_t key, size_t size, int shmflg);
- 參數key:用戶設定任意一個數,用于區分不同共享內存,通常由ftok生成。
- 參數size:設定共享內存的大小。
- 參數shmflg:標志位,用于指定共享內存段的創建方式和權限。常見的標志包括:
IPC_CREAT :如果共享內存段不存在,則創建它。
IPC_EXCL :與 IPC_CREAT 一起使用,確保創建的共享內存段是新的。
權限標志:如 0666,表示所有用戶都有讀寫權限。 - 返回值: 成功時返回共享內存段的標識符 (shmid) 。 失敗時返回 -1,并設置 errno 以指示錯誤類型。
1.key的作用
- 思考1:在用戶層面如何讓兩個獨立進程共享同一塊內存?
- 思考2:在匿名管道和命名管道中,用戶層面是如何讓兩個進程確定同一個資源的?
問題2很顯然,管道的本質是文件,用戶通過讓兩個程序打開同一個文件名來實現看到同一個資源。
因此,共享內存同樣需要一個key來充當類似文件名的功能。
2.key的選取
key參數本質是一個int類型,我們可以直接指定一個數值傳入,當然,為了更規范,更專業,我們通常都會使用ftok來生成。
#include <sys/types.h>
#include <sys/ipc.h>key_t ftok(const char *pathname, int proj_id);
- 參數pathnme:一個存在的文件路徑(例如 /tmp/myfile),文件必須存在,否則 ftok 會失敗。
- 參數proj_id:一個整數,用于進一步區分不同的 IPC 對象。
- 返回值:
成功:返回生成的 key_t 鍵值。
失敗:返回 -1,并設置 errno 以指示錯誤原因。
3.shmid的作用
shmid是一個int類型,由shmget返回,在作用上和物理意義上與文件系統中的fd類似。
它的作用主要是讓用戶找到指定的共享內存。
在操作系統內核中,shmid 的物理意義如下:
內核維護一個 共享內存段表(如 struct shmid_kernel),每個表項對應一塊共享內存。
shmid 是該表的索引,通過它找到對應的共享內存段(含物理頁、權限、掛載進程等信息)。
共享內存最終映射到 實際的物理頁幀(通過頁表機制)。
物理本質是內存頁的映射:多個進程的虛擬地址映射到同一組物理頁。
4.key和shmid的區別
key最終成為系統層
區分不同IPC的標志,而shmid則是用戶層
用來區分不同IPC的標志。
5.內存設定的特性
這里的內存設定指的是shmget函數中的參數size。
當傳入的內存不足4096字節(4KB)的倍數時,會擴到4096倍數。但是只會提供size大小的使用空間。這樣做可以規避掉一些因為共享內存過多帶來的問題。
6.shmflg的設定
對于共享內存,我們可以將程序簡單分為創建端和使用端,它們的shmflg設定通常是:
- 創建端:IPC_CREAT | IPC_EXCL | 0666
- 使用端:IPC_CREAT
創建端要保證IPC是最新的,所以需要加IPC_EXCL,然后還需要設定權限。
使用端只需要獲取共享內存段的系統調用,因此只用一個IPC_CREAT即可。
2.綁定共享內存
以上我們完成的只是共享內存的創建,接下來還需要把進程綁定到共享內存,使用函數shmat
,其中at指的是單詞attach。
#include <sys/types.h>
#include <sys/shm.h>void *shmat(int shmid, const void *shmaddr, int shmflg);
- 參數shmid:傳入從shmget中返回的shmid來指定共享內存。
- 參數shmaddr:指定共享內存段附加到進程地址空間的位置,通常設為nullptr,系統會自動選擇一個合適的地址。
- 參數shmflg:讀寫方式,常用的有:
SHM_RDONLY:以只讀方式附加共享內存段。
0:以讀寫方式附加共享內存段。 - 返回值:
成功時,返回共享內存段附加到進程地址空間的起始地址。
失敗時,返回 (void *) -1,并設置 errno。
3.代碼示例
創建端程序:
int main()
{//生成一個keyint key = ftok(".", 48);//創建共享內存int shmid = shmget(key, 4069, IPC_CREAT | IPC_EXCL | 0666);//連接到共享內存void* p = shmat(shmid,nullptr,0);//使用共享內存//... ...return 0;
}
使用端程序:
int main()
{//生成一個相同keyint key = ftok(".", 48);//獲取到共享內存的系統調用int shmid = shmget(key, 4069, IPC_CREAT);//連接到共享內存void* p = shmat(shmid,nullptr,0);//使用共享內存//... ...return 0;
}
注意:為了簡潔和方便說明問題,以上代碼省略了頭文件的包含和返回值有效性的判斷等等,在實際開發中可不敢省略。
三、利用共享內存通信
1.通信
上文我們只是完成了信道的建立,接下來我們進行通信,通過上面的操作,我們已經獲取到共享內存的起始地址。
它的用法與C語言的malloc申請的內存用法相同,只是共享內存可以同時被兩個進程訪問。
如下寫端:
int main()
{int key = ftok(".", 48);int shmid = shmget(key, 4069, IPC_CREAT | IPC_EXCL | 0666);void* p = shmat(shmid,nullptr,0);//使用共享內存char* chp = (char*)p;for(int i='a';i<='z';i++){sleep(1);*chp=i;chp++;}return 0;
}
讀端:
int main()
{int key = ftok(".", 48);int shmid = shmget(key, 4069, IPC_CREAT);void* p = shmat(shmid,nullptr,0);//使用共享內存char* chp = (char*)p;while(true){sleep(1);cout<<chp<<endl;}return 0;
}
注意:為了獲取到同一個共享內存,我們設定的key必須一致。
2.解除綁定
如果進程退出時沒有解除綁定,共享內存段仍然會保留在系統的共享內存資源中,直到顯式刪除(通過 shmctl 或系統重啟)。
使用shmdt
來解除綁定,其中dt代表單詞delete。
int shmdt(const void *shmaddr);
-
參數shmaddr:需要斷開連接的共享內存的起始地址。
-
返回值:
成功:返回0。
失敗:返回-1,并設置errno以指示錯誤原因。
一個共享內存,與它綁定的程序的個數是由一個引用計數機制進行維護的,當shmdt成功,引用計數減1。
3.銷毀共享內存
共享內存不會隨程序的結束而銷毀,它是隨內核的 ,因此需要顯式地進行銷毀,可以使用shmctl函數。或在命令行中使用指令進行銷毀。
1.命令行銷毀
查看共享內存信息
ipcs -m
如下:
nattch信息:它表示與該 共享內存連接的程序個數。
銷毀共享內存
ipcrm -m 2
這里需要填入shmid(即這里的2)來指定共享內存。
2.程序中銷毀
在程序中銷毀我們使用函數shmctl
,其中ctl代表單詞control。
#include <sys/ipc.h>
#include <sys/shm.h>int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 參數shmid:傳入從shmget返回的shmid來指定需要銷毀的共享內存。
- 參數cmd:需要傳入一個操作選項,操作選項很多,而IPC_RMID就是用來銷毀共享內存的。
- 參數shmid_ds:這是一個輸出型參數,如果你需要獲取共享內存的信息,則傳入一個shmid_ds類型的指針來接收,如果不是通常傳入nullptr即可。
- 返回值:
成功時返回 0。
失敗時返回 -1,并設置 errno 以指示錯誤類型。
注:命令行銷毀和程序中銷毀效果是一樣的,因為命令行銷毀底層還是調用了shmctl函數。
四、共享內存的生命周期
共享內存的生命周期是不隨進程的,而是隨內核,如果沒有顯示刪除它就會一直存在,盡管相關的進程已經退出。直到重裝系統才得以釋放。
使用shmctl釋放共享內存存在的情況
1.正常釋放
當nattach(引用計數)為0時,即沒有進程與它綁定,被正常釋放。
2.共享內存段被標記為已刪除,但仍有進程附加(shmat)
共享內存段已經被標記為已刪除(不能附加到新的進程),但之前仍有一些進程附加到該共享內存段并正在使用。所以共享內存段不會被立即釋放。只有當所有附加的進程都調用 shmdt 分離后,系統才會釋放資源。
五、數據安全問題
共享內存最大的優點就是快
, 相比使用管道技術,它減少了中間復雜轉化和拷貝工作,而是直接對物理內存進行訪問。
但它也有一個致命的缺點,相比管道技術,共享內存它的讀端和寫端是不帶有同步機制
的,這就很容易使得數據混亂,也就是造成數據不一致問題。
比如我們寫端寫入“hello world”,而讀端讀到的可能是“he”,“ll”,“o wor”,“ld”等等無法預測的奇葩數據。 讀端一個勁地讀,不會管寫端這句話是否已經說完,而且也無法知道。
當我們不了解鎖的情況下想要解決這個問題,可以利用命名管道來解決,因為命名管道帶有同步機制,我們用它的write和read函數來保護數據的安全,當然write和read并不用寫或讀什么有意義的數據。
六、源碼
Fifo.hpp
#pragma once
#include <iostream>
#include <cstdio>
#include <sys/types.h>
#include <sys/stat.h>
#include <string>
#include <fcntl.h>
#include <unistd.h>
using namespace std;#include "Comm.hpp"#define PATH "."
#define FILENAME "fifo"//#define FIFO_FILE "fifo"class NamedFifo
{
public:NamedFifo(const string& path,const string& name):_path(path),_name(name){_fifoname =_path + "/" + _name;umask(0);//創建管道int n = mkfifo(_fifoname.c_str(), 0666);if(n < 0){cout << "mkdir fifo error" << endl;ERR_EXIT("makefifo");}else{cout << "fifo success" << endl;}}~NamedFifo(){//刪除管道文件int n = unlink(_fifoname.c_str());if(n == 0){cout << "unlink fifo" << endl;}else{// perror("remove fifo fail");// exit(1);ERR_EXIT("unlink");}}
private:string _path;string _name;string _fifoname;
};class FileOper
{
public:FileOper(const string& path,const string& name):_path(path), _name(name), _fd(-1){_fifoname = _path + "/" + _name;}~FileOper(){}void OpenForRead(){// 打開, write 方沒有執行open的時候,read方,就要在open內部進行阻塞// 直到管道文件打開了,open才會返回!_fd = open(_fifoname.c_str(), O_RDONLY);if(_fd < 0){ERR_EXIT("open");}cout << "open fifo success" << endl;}void OpenForWrite(){//寫_fd = open(_fifoname.c_str(), O_WRONLY);if(_fd < 0){ERR_EXIT("open");}cout << "open fifo success" << endl;}void Wakeup(){//寫操作char c = ' ';int n = write(_fd, &c, 1);printf("嘗試喚醒: %d\n", n);}bool Wait(){//讀操作char c;int n = read(_fd, &c, 1);if(n > 0){printf("喚醒成功: %d\n", n);return true;}else{return false;}}void Close(){if(_fd > 0){close(_fd);}}private:string _path;string _name;string _fifoname;int _fd;
};
Comm.hpp
#pragma once#include <cstdio>
#include <cstdlib>#define ERR_EXIT(m) \
do\
{\perror(m);\exit(EXIT_FAILURE);\
}while(0)
Shm.hpp
#pragma once#include <iostream>
#include <cstdio>
#include <string>
#include <sys/types.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <unistd.h>
using namespace std;#include "Comm.hpp"const int gdefaultid = -1;
const int gsize = 4096;
const string pathname = ".";
const int projid = 0x66;
const int gmode = 0666;#define CREATER "creater"
#define USER "user"class Shm
{
public:Shm(const string& pathname,int projid,const string& usertype):_shmid(gdefaultid),_size(gsize),_start_mem(nullptr),_usertype(usertype){_key = ftok(pathname.c_str(), projid);if(_key < 0){ERR_EXIT("fotk");}if(_usertype == CREATER){ //創建共享內存Create();}else if(_usertype == USER){//得到共享內存Get();}else{}//鏈接共享內存Attach();}void* VirtualAddr(){printf("VirtualAddr: %p\n", _start_mem);return _start_mem;}int Size(){return _size;}void Attr(){struct shmid_ds ds;//ds輸出型參數int n = shmctl(_shmid, IPC_STAT, &ds);printf("shm_segsz: %ld\n", ds.shm_segsz);printf("key: 0x%x\n", ds.shm_perm.__key);}~Shm(){cout << _usertype << endl;if(_usertype == CREATER){Destroy();}}private://創建新的共享內存void CreateHelper(int flg){printf("key: 0x%x\n", _key);//共享內存的生命周期,跟隨內核_shmid = shmget(_key, _size, flg);if(_shmid < 0){ERR_EXIT("shmget");}printf("shmid: %d\n", _shmid);}void Create(){CreateHelper(IPC_CREAT | IPC_EXCL | gmode);}void Attach(){_start_mem = shmat(_shmid, nullptr, 0);if((long long)_start_mem < 0){ERR_EXIT("shmat");}printf("attach success\n");}void Detach(){int n = shmdt(_start_mem);if(n == 0){printf("detach success\n");}}void Get(){CreateHelper(IPC_CREAT);}void Destroy(){if(_shmid == gdefaultid){return;}Detach();if(_usertype == CREATER){int n = shmctl(_shmid, IPC_RMID,nullptr);if(n > 0){printf("shmctl delete shm: %d success!\n", n);}else{ERR_EXIT("shmctl");}}}private:int _shmid;key_t _key;int _size;void* _start_mem;string _usertype;
};
server.cc
#include "Shm.hpp"
#include "Fifo.hpp"int main()
{Shm shm(pathname, projid, CREATER);sleep(5);shm.Attr();NamedFifo fifo(PATH, FILENAME);// 文件操作FileOper readerfile(PATH, FILENAME);readerfile.OpenForRead();char* mem = (char*)shm.VirtualAddr();//讀寫共享內存,沒有使用系統調用while(true){if(readerfile.Wait()){printf("%s\n", mem);}else{break;}}readerfile.Close();cout << "server end normal!" << endl;return 0;
}
client.cc
#include "Shm.hpp"
#include "Fifo.hpp"int main()
{FileOper writerfile(PATH, FILENAME);writerfile.OpenForWrite();Shm shm(pathname, projid, USER);char* mem = (char*)shm.VirtualAddr();int index = 0;for(char c = 'A';c <= 'Z';c++,index += 2){sleep(2);mem[index] = c;mem[index + 1] = c;mem[index + 2] = 0;writerfile.Wakeup();sleep(1);}writerfile.Close();return 0;
}