共享內存方式

最快的IPC形式,這樣的內存區域映射到共享它的進程的地址空間,這些進程的數據傳輸就不再涉及內核(進程不再通過任何進入內核的系統調用來彼此傳遞數據,內核必須允許各個進程共享內存區域的內存映射關系然后一直處理該內存區域),但是在共享內存中存放或讀取信息需要進程間的同步方式。
客戶——服務器交互信息的步驟

使用共享內存方式:(共享內存區對象同時出現在客戶和服務器的地址空間中)
- 服務器使用一個信號量來獲取某個共享內存對象的權利
- 服務器將數據從輸入文件讀到共享內存區對象,read的第二個參數指定的數據緩沖區地址指向這個共享內存區對象
- 服務區器讀入完畢使用一個信號量通知客戶
- 客戶將這些數據從該共享內存區對象寫到輸出文件中

具有至少隨內核持續特性
內存映射文件
由open函數打開,由mmap函數把得到的描述符映射到調用進程地址空間的文件。
mmap函數把一個文件或Posix共享內存區對象映射到調用進程的地址空間。使得進程之間通過映射同一個普通文件實現共享內存。使用該函數的三個目的:
- 使用普通文件以提供內存映射I/O
- 使用特殊文件以提供匿名內存映射
- 使用shm_open以提供無親緣關系間的進程Posix共享內存區
內存映射文件(普通文件):在open之后調用mmap把他映射到調用進程地址空間的某個文件。使用內存映射文件的一個特性是:所有的I/O都在內核的掩蓋下完成,我們只需編寫存取內存映射區中各個值的代碼(就是不調用read和write執行I/O時那樣有內核直接參與I/O的完成,而是有內核在背后通過操縱頁表等方式間接參與,這樣用戶進程看來I/O不再涉及系統調用),絕不再使用read,write,lseek。但是不是所有的文件都能進行內存映射的(終端或套接字映射到內存嗎就會導致mmap返回一個錯誤,這些類型的描述符必須使用read和write)。
在無親緣關系的進程間共享內存區(特殊文件匿名映射):所映射文件的實際內容成了被共享內存區的初始內容,而且這些進城對該共享內存區所做的任何變動都復制會映射文件(提供隨文件系統的持續性,要使用MAP_SHARED)。
不能對終端或套接字描述符進行mmap,這類描述符必須使用read、write來訪問。
#include <sys/mman.h> void *mmap(void *addr,size_t len,int port,int flags,int fd,off_t offset); //返回值:成功返回被映射區的起始地址,成功調用后fd可關閉,該操作對mmap建立映射關系無影響,失敗返回MAP_FAILED,[其值為(void *)-1],munmap返回-1。
- addr:指定描述fd被映射到的進程內空間的起始地址,通常為空指針,讓內核自己去選擇起始地址,無論哪種情況下返回值都是描述符fd所映射到內存的起始地址
- fd:有效的文件描述詞。一般是由open()函數返回,其值也可以設置為-1,此時需要指定flags參數中的MAP_ANON,表明進行的是匿名映射。
- len:映射到調用進程地址空間的字節數,他被映射文件開頭起第offset個字節處開始算,offest通常為0
- port:內存映射保護;PROT_READ:數據可讀;PROT_WEITE:數據可寫;PROT_EXEC:數據可執行;PROT_NONE:數據不可訪問
- flags:
1>MAP_PRIVATE:變動自私的,調用進程對被映射數據所修改只對改進程可見,而不改變其底層支持對象(或是一個文件對象或是一個共享內存區對象);
2>MAP_SHARED:變動共享的,調用進程對被映射數據所修改對於共享該對象所有的進程可見,改變其底層支持對象(或是一個文件對象或是一個共享內存區對象)以上這二者必須指定一個;
3>MAP_FIXED:從移植方面來講不應該指定該標志,但addr不是一個空指針,那么addr如何處置取決於實現,不為空的addr值通常被當作有關該內存區應是如何具體定位的線索,可移植性應該把addr指定一個空指針而且不指定該標志,使用指定的映射起始地址,如果由start和len參數指定的內存區重疊於現存的映射空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。
4> MAP_NORESERVE :不要為這個映射保留交換空間。當交換空間被保留,對映射區修改的可能會得到保證。當交換空間不被保留,同時內存不足,對映射區的修改會引起段違例信號。
5>MAP_LOCKED:鎖定映射區的頁面,從而防止頁面被交換出內存。
6>MAP_GROWSDOWN:用於堆棧,告訴內核VM系統,映射區可以向下擴展。
7>MAP_ANONYMOUS:匿名映射,映射區不與任何文件關聯。
8>MAP_ANON:MAP_ANONYMOUS的別稱,不再被使用。
9>MAP_FILE:兼容標志,被忽略。
10>MAP_32BIT:將映射區放在進程地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標志只在x86-64平台上得到支持。
11>MAP_POPULATE:為文件映射通過預讀的方式准備好頁表。隨后對映射區的訪問不會被頁違例阻塞
12>MAP_NONBLOCK:僅和MAP_POPULATE一起使用時才有意義。不執行預讀,只為已存在於內存中的頁面建立頁表入口。
函數返回值,errno被設為以下的某個值
- EACCES:訪問出錯
- EAGAIN:文件已被鎖定,或者太多的內存已被鎖定
- EBADF:fd不是有效的文件描述詞
- EINVAL:一個或者多個參數無效
- ENFILE:已達到系統對打開文件的限制
- ENODEV:指定文件所在的文件系統不支持內存映射
- ENOMEM:內存不足,或者進程已超出最大內存映射數量
- EPERM:權能不足,操作不允許
- ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標志
- SIGSEGV:試着向只讀區寫入
- SIGBUS:試着訪問不屬於進程的內存區
在fork之前可以先指定MAP_SHARED調用mmap。mmap成功返回后可關閉fd,該操作對mmap建立的映射關系無影響。
munmap
#include <sys/mman.h> int munmap(void *addr,size_t len); //返回值:成功返回0,出錯返回-1
- addr:由mmap返回的地址
- len:映射區大小
- 再次訪問這些地址將會導致SIGSEGV信號,如果被映射區域是MAP_PRIVATE標志,那么調用進程對他的所有變動都會被丟棄
msync
內核的虛擬內存算法保持映射文件(一般在硬盤上)與內存映射區(一般在內存中)的同步,前提他是一個MAP_SHARED內存區,也就是我們改了處於內存映射到某個文件的內存區中某個位置的內容,那么內核將在稍后某個時刻相應的更新文件,然而我們需要確信硬盤上的文件內容與內存區中的內容一致,於是調用該函數來執行同步
#include <sys/mman.h> int msync(void *addr,size_t len,int flags); //返回值:成功返回0,出錯返回-1
flags:
- MS_ASYNC:執行異步寫,一旦寫操作已由內核排入隊列,該標志指定的函數立即返回
- MS_SYNC:執行同步寫(與ASYNC選其一,不能同時指定),該標志是等到寫操作完成后再返回
- MS_INVALIDATE:使高速緩存的數據丟失,與其最終副本不一致的文件數據的所有內存中副本都失效,后續的引用將從文件中取得數據
信號量父子進程中各有一份
/************************************************************************* > File Name: mmap.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月16日 星期二 16時53分40秒 ************************************************************************/ #include <iostream> #include <sys/mman.h> #include <sys/types.h> #include <semaphore.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <error.h> using namespace std; string name("/tmp"); #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) const int loop=10; int main(int argc,char **argv) { //信號量在內核中 sem_t *sem=sem_open(name.c_str(),O_CREAT|O_EXCL,FILE_MODE,1); if(sem==SEM_FAILED) { cerr<<strerror(errno)<<endl; exit(-1); } sem_unlink(name.c_str()); //int value; //sem_getvalue(sem,&value); //cout<<value<<endl; int fd=open("1.txt",O_RDWR|O_CREAT,FILE_MODE); int zero=0; write(fd,&zero,sizeof(int)); //子父進程都有自己的ptr副本,每個副本都指向共享內存區域的一個整數 int *ptr=(int *)mmap(nullptr,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(ptr==MAP_FAILED) { cerr<<strerror(errno)<<endl; exit(1); } close(fd); setbuf(stdout,nullptr); if((fork()==0))//子進程 { cout<<"child: "<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(sem); cout<<(*ptr)++<<endl; sem_post(sem); } exit(0); } cout<<"parent :"<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(sem); cout<<(*ptr)++<<endl; sem_post(sem); } exit(0); }

信號量在共享內存中
/************************************************************************* > File Name: mmap.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月16日 星期二 16時53分40秒 ************************************************************************/ #include <iostream> #include <sys/mman.h> #include <sys/types.h> #include <semaphore.h> #include <unistd.h> #include <fcntl.h> #include <string.h> #include <error.h> using namespace std; typedef struct Shared { int count; sem_t sem; }Shared; const int loop=10; #define FILE_MODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) int main(int argc,char **argv) { Shared s; int fd=open("1.txt",O_RDWR|O_CREAT,FILE_MODE); write(fd,&s,sizeof(Shared)); //子父進程都有自己的ptr副本,每個副本都指向共享內存區域的一個整數 Shared *ptr=(Shared *)mmap(nullptr,sizeof(Shared),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); if(ptr==MAP_FAILED) { cerr<<strerror(errno)<<endl; exit(1); } close(fd); //此時信號量在共享內存中 int res=sem_init(&ptr->sem,1,1); if(res==-1) { cerr<<strerror(errno)<<endl; exit(1); } //取消緩沖 setbuf(stdout,nullptr); if((fork()==0))//子進程 { cout<<"child: "<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(&ptr->sem); cout<<(*ptr).count++<<endl; sem_post(&ptr->sem); } exit(0); } cout<<"parent :"<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(&ptr->sem); cout<<(*ptr).count++<<endl; sem_post(&ptr->sem); } exit(0); }

匿名內存映射
/************************************************************************* > File Name: anon.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月16日 星期二 20時19分50秒 ************************************************************************/ #include <iostream> #include <sys/mman.h> #include <unistd.h>//fork #include <fcntl.h>//O_RDWR #include <semaphore.h> #include <string.h>//strerror #include <error.h> using namespace std; string name("/tmp2"); const int loop=10; int main() { sem_t *sem=sem_open(name.c_str(),O_CREAT|O_EXCL,1); if(sem==SEM_FAILED) { cerr<<strerror(errno)<<endl; exit(1); } //內存區初始化為0 int *ptr=(int *)mmap(nullptr,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0); if(ptr==MAP_FAILED) { cerr<<strerror(errno)<<endl; exit(1); } if(fork()==0) { cout<<"child: "<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(sem); cout<<(*ptr)++<<endl; sem_post(sem); } sem_unlink(name.c_str()); sem_close(sem); exit(0);//不顯示的調用 } cout<<"parent: "<<getpid()<<" "<<getppid()<<endl; for(int i=0;i<loop;++i) { sem_wait(sem); cout<<(*ptr)++<<endl; sem_post(sem); } sem_unlink(name.c_str()); sem_close(sem); munmap(ptr,sizeof(int)); exit(0); }
使用dev/zero映射
/************************************************************************* > File Name: dev.cpp > Author: Chen Tianzeng > Mail: 971859774@qq.com > Created Time: 2019年04月16日 星期二 21時40分03秒 ************************************************************************/ #include <iostream> #include <fcntl.h> #include <unistd.h> #include <semaphore.h> #include <sys/mman.h> using namespace std; int main() { int fd=open("dev/zero",O_RDWR); int *ptr=(int *)mmap(nullptr,sizeof(int),PROT_READ|PROT_WRITE,MAP_SHARED,fd,0); close(fd); return 0; }
代碼 github連接:https://github.com/tianzengBlog/test/tree/master/ipc2/mmap
訪問內存映射區對象
文件大小等於內存映射區大小,但這個大小不是頁面大小的整數倍。比如內存映射區大小我5000,頁面的大小為4096,在程序中仍能訪問第二頁(4096-8191),但訪問第三頁引發SIGESGV信號。所以內核允許讀寫最后一頁中映射區以遠的部分(內核的內存保護以頁為單位),但向這部分擴展區寫的任何內容都不會寫到文件中,
共享內存區對象
由shm_open打開一個IPC的名字(也許是在文件系統中的某個路徑),所返回的描述符由mmap函數映射到當前進程的地址空間。
- 指定一個參數名字調用shm_open,以創建一個新的共享內存區對象或打開一個已存在的共享內存區對象
- 調用mmap把這個共享內存區映射到調用進程的地址空間,傳遞給shm_open的名字參數隨后由希望共享該內存區的任何其他進程調用
#include <sys/mman.h> int shm_open(const char *name,int oflag,mode_t mode); //成功返回非負描述符,若出則為-1 int shm_unlink(const char *name); //成功返回0,出錯返回-1
- olfag:必須或者含有O_RDONLY,或者含有O_RDWR,還可以指定O_CREAT,O_EXCL,或O_TRUNC。如果隨O_RDWR指定O_TRUNC,且所需的共享內存已存在,那么長度將會被截為0
- mode:在指定O_CREAT標志前提下使用,如果沒有使用O_CREAT該參數置0,該標志必須指定。
- 刪除一個共享內存區的對象名,跟其他所有unlink一樣(刪除文件系統的一個路徑名,刪除一個posix消息隊列的mq_unlink,刪除一個有名信號量sem_unlink)一樣,刪除一個名字不會影響對其底層支持對象的現有引用,直到對該對象的全部引用關閉為止,刪除一個名字僅僅為防止后續的open,mq_oepn,sem_open調用取得成功
- 共享內存在不同的進程中可以出現不同的地址
- 新創建的共享內存區對象大小應該為0,可用ftrucate改變共享內存對象大小,用fstat查看共享內存對象大小
- 共享內存對象名不應該和消息隊列名相同
代碼連接:https://github.com/tianzengBlog/test/tree/master/ipc2/mmap/shm
ftruncate
普通文件或共享內存區的大小都可以通過該函數修改,並且調用fstat獲得對象的信息。
#include <unistd.h> int ftruncate(int fd,off_t leght); //成功返回0失敗返回-1
- 對於一個普通文件,如果該文件的大小大於lenght參數,額外的數據就會被丟棄,如果該文件的大小小於lenght,那么該文件是否修改及其大小是否增長是未加說明的。實際上對於普通文件來說,把他的大小擴展到length字節的可移植方法是:先lseek到偏移字節lenght-1處,然后write 1個字節的數據,如果大小被擴展,那么擴展的部分顯得好像已用0填寫過
- 對於一個共享內存區對象:ftruncate把該對象的大小設置為length字節,如果共享內存區被擴展,那么擴展部分全為0
共享內存區對象與內存映射文件區別
- 內存映射文件的數據載體是物理文件。
- 共享內存區對象,也就是共享的數據載體是物理內存。
