UNIX網(wǎng)絡編程第二卷進程間通信對mmap函數(shù)進行了說明。該函數(shù)主要用途有三個:
1、將一個普通文件映射到內存中,通常在需要對文件進行頻繁讀寫時使用,這樣用內存讀寫取代I/O讀寫,以獲得較高的性能;
2、將特殊文件進行匿名內存映射,可以為關聯(lián)進程提供共享內存空間;
3、為無關聯(lián)的進程提供共享內存空間,一般也是將一個普通文件映射到內存中。
函數(shù):void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
參數(shù)start:指向欲映射的內存起始地址,通常設為 NULL,代表讓系統(tǒng)自動選定地址,映射成功后返回該地址。
參數(shù)length:代表將文件中多大的部分映射到內存。
參數(shù)prot:映射區(qū)域的保護方式。可以為以下幾種方式的組合:
PROT_EXEC 映射區(qū)域可被執(zhí)行
PROT_READ 映射區(qū)域可被讀取
PROT_WRITE 映射區(qū)域可被寫入
PROT_NONE 映射區(qū)域不能存取
參數(shù)flags:影響映射區(qū)域的各種特性。在調用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果參數(shù)start所指的地址無法成功建立映射時,則放棄映射,不對地址做修正。通常不鼓勵用此旗標。
MAP_SHARED對映射區(qū)域的寫入數(shù)據(jù)會復制回文件內,而且允許其他映射該文件的進程共享。
MAP_PRIVATE 對映射區(qū)域的寫入操作會產(chǎn)生一個映射文件的復制,即私人的“寫入時復制”(copy on write)對此區(qū)域作的任何修改都不會寫回原來的文件內容。
MAP_ANONYMOUS建立匿名映射。此時會忽略參數(shù)fd,不涉及文件,而且映射區(qū)域無法和其他進程共享。
MAP_DENYWRITE只允許對映射區(qū)域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
MAP_LOCKED 將映射區(qū)域鎖定住,這表示該區(qū)域不會被置換(swap)。
參數(shù)fd:要映射到內存中的文件描述符。如果使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設為-1。有些系統(tǒng)不支持匿名內存映射,則可以使用fopen打開/dev/zero文件,然后對該文件進行映射,可以同樣達到匿名內存映射的效果。
參數(shù)offset:文件映射的偏移量,通常設置為0,代表從文件最前方開始對應,offset必須是分頁大小的整數(shù)倍。
返回值:
若映射成功則返回映射區(qū)的內存起始地址,否則返回MAP_FAILED(-1),錯誤原因存于errno 中。
錯誤代碼:
EBADF 參數(shù)fd 不是有效的文件描述詞
EACCES 存取權限有誤。如果是MAP_PRIVATE 情況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
EINVAL 參數(shù)start、length 或offset有一個不合法。
EAGAIN 文件被鎖住,或是有太多內存被鎖住。
ENOMEM 內存不足。
系統(tǒng)調用mmap()用于共享內存的兩種方式:
(1)使用普通文件提供的內存映射:
適用于任何進程之間。此時,需要打開或創(chuàng)建一個文件,然后再調用mmap()
典型調用代碼如下:
fd=open(name, flag, mode); if(fd<0) ...
ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
通過mmap()實現(xiàn)共享內存的通信方式有許多特點和要注意的地方,可以參看UNIX網(wǎng)絡編程第二卷。
(2)使用特殊文件提供匿名內存映射:
適用于具有親緣關系的進程之間。由于父子進程特殊的親緣關系,在父進程中先調用mmap(),然后調用 fork()。那么在調用fork()之后,子進程繼承父進程匿名映射后的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進程就可以通過映射區(qū) 域進行通信了。注意,這里不是一般的繼承關系。一般來說,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。 對于具有親緣關系的進程實現(xiàn)共享內存最好的方式應該是采用匿名內存映射的方式。此時,不必指定具體的文件,只要設置相應的標志即可。
Unix/Linux的內存映射
共享內存可以說 是最有用的進程間通信方式,也是最快的IPC形式;兩個不同的進程A和B共享內存的意思就是:同一塊物理內存即被映射到進程A的地址空間中又內映射到進程 B的地址空間中.進程A可以實時地看到進程B對共享內存中數(shù)據(jù)的更新,反之,進程B也可以實時地看到進程A對共享內存的更新;由于多個進程同時訪問同一塊 共享內存區(qū)域,那就需要某種同步機制來保證多個不同進程對共享內存的訪問,互斥鎖、信號量/信號燈、信號量集都可以;
采用共享內存來實現(xiàn)進程間通信的一個很明顯的好處就是:進程可以直接讀寫內存,基本上不需要任何額外的數(shù)據(jù)拷貝. 而對于像管道、消息隊列之類的IPC方式,則需要在內核空間和用戶空間之間進行四次數(shù)據(jù)拷貝,而共享內存則只需要兩次拷貝:一次是從輸入文件拷貝到共享內 存區(qū),另外一次是從共享內存區(qū)拷貝到輸出文件中.實際上,進程之間在共享內存時,并不總是讀寫少量數(shù)據(jù)后就解除映射,有新的通信時,再重新建立共享內存區(qū) 域,而是保持共享區(qū)域,直到通信完畢為止;這樣,數(shù)據(jù)內容一直保存在共享內存中,并沒有寫回文件.共享內存中的數(shù)據(jù)內容往往是在解除映射時才寫回文件的. 因此,采用共享內存的通信方式是非常有效的;
Linux的2.2.x以后的內核版本支持多種共享內存方式,比如:內存映射mmap、POSIX共享內存、System V共享內存;
一、內核怎樣保證各個進程尋址到同一塊共享內存區(qū)域的內存頁面:
1、page cache及swap cache中頁面的區(qū)分:一個被訪問文件的物理頁面都駐留在page cache或swap cache中,一個頁面的所有信息由struct page來描述,struct page結構中有一個字段為指針mapping,它指向一個struct address_space類型的結構.page cache或swap cache中的所有頁面就是根據(jù)struct address_space結構以及一個偏移量來區(qū)分的;
2、文件與struct address_space結構的對應:一個具體的文件被打開之后,內核會在內存中為之建立一個struct inode結構類型的節(jié)點,其中的i_mapping字段指向一個struct address_space類型的結構,這樣,一個文件就對應一個struct address_space結構,一個struct address_space和一個偏移量就可以確定一個page cache或swap cache中的一個頁面.因此,當要尋址某個數(shù)據(jù)的時候,很容易根據(jù)給定的文件及數(shù)據(jù)在文件內的偏移量范圍之內找到對應的頁面;
3、進程調用mmap()時,只是在進程的地址空間中新增加了一塊相應大小的緩沖區(qū),并設置另外相應的訪問標識,但是并沒有建立進程地址空間到物理頁面的映射.所以,第一次訪問該空間時,會引發(fā)一個缺頁異常;
4、對于共享內存的情況,缺頁異常處理程序首先在swap cache中尋找目標頁(符合struct address_space以及偏移量的物理頁),如果找到,則直接返回該頁的地址;如果沒找到,則判斷該頁是否在交換分區(qū)(swap area)中存在,如果存在,則執(zhí)行一個換入操作;如果上述兩種情況都不滿足,則缺頁處理程序將分配新的物理頁,并把它插入到page cache中.進程最終將更新進程頁表;
注意:對于映射普通文件(非共享映射)的情況,缺頁處理程序首先會在page cache中根據(jù)struct address_space和偏移量尋找相應頁面.如果沒找到,則說明文件數(shù)據(jù)還沒有讀入內存,處理程序會從磁盤讀入相應的文件頁面,并返回相應的頁面地址,同時,進程頁表也會被更新;
換句話說,對于共享內存來說,缺頁處理程序是在swap cache和swap area中尋找相應的頁面,而對于非共享映射(映射普通文件)來說,則是在page cache來尋找對應的頁面;
5、所有進程在映射同一塊共享內存區(qū)域時,情況都一樣,在建立線性地址與物理地址之間的映射之后,不論進程各自返回的地址如何,實際上訪問的必然都是同一塊共享內存區(qū)域對應的物理頁面.
注意:一塊共享區(qū)域可以看作是特殊文件系統(tǒng)shm中的一個特殊文件,shm的安裝點在交換分區(qū)上;
二、內存映射 :實際上,內存映射機制并不是完全為了共享內存的目的而設計的,它本身提供了不同于一般普通文件的訪問方式,進程可以像訪問內存一樣對普通文件進程操作.而POSIX或System V共享內存IPC則純粹是用于共享內存的目的.當然內存映射實現(xiàn)共享內存,也是內存映射的應用之一;內存映射機制的用途:A、以訪問內存的方式讀寫文件; B、實現(xiàn)共享內存;
三、mmap()系統(tǒng)調用: mmap()系統(tǒng)調用使得進程之間通過映射同一個普通文件而實現(xiàn)共享內存的目的.普通文件被映射到進程的地址空間之后,進程就可以像訪問普通內存一樣對文件進行訪問,不必再調用read()、write()等系統(tǒng)調用操作. mmap()系統(tǒng)調用介紹: void* mmap(void* addr, size_t len, int prot, int flags, int fd, off_t offset); 該函數(shù)在進程的地址空間與文件對象或共享內存對象之間建立一種映射關系; addr :該參數(shù)指定文件應該被映射到進程地址空間的起始地址,一般被指定為一個空指針,此時,程序把選擇起始地址的任務留給內核來完成了.這個地址是進程地址空間中需要映射到文件中的內存區(qū)域的首地址;也就是說,在進程地址空間中用于文件映射的內存區(qū)域的首地址; len :文件被映射到調用進程的地址空間中的字節(jié)數(shù),它從被映射文件開頭offset個字節(jié)處開始算起,取len個字節(jié),把文件中的這len個字節(jié)的文件空間映射到進程的地址空間中; port :指定文件被映射到內存中之后的訪問權限.可取的值有:PORT_READ(可讀)、PORT_WRITE(可寫)、PORT_EXEC(可執(zhí)行)、PORT_NONE(不可訪問); flags :映射標記;取值如下:MAP_SHARED、MAP_PRIVATE、MAP_FIXED,其中,MAP_SHARED和MAP_PRIVATE必選其一,而MAP_FIXED則不推薦使用; fd :即將被映射到進程地址空間中的文件的描述符.一般由系統(tǒng)調用open()返回;同時,fd可以指定為-1,此時,必須指定flags參數(shù)中的 MAP_ANON,表明進程的是匿名映射(不涉及具體的文件名,避免了文件的創(chuàng)建及打開,很顯然,只能用于具有親屬關系的進程之間的通信). offset:從文件開頭計算offset個字節(jié)處開始映射;也就是,文件中需要被映射的文件內容的起始地址,這個起始地址的計算是以文件開頭為參照的;這個參數(shù)一般取值為0,表示從文件開頭處開始映射; 返回值:文件最終映射到進程地址空間中的起始地址;進程可直接以該地址為有效的起始地址進行操作;也就是文件中開始映射的起始字節(jié)點到進程中對應映射內存區(qū)的起始地址點處的一個映射;換句話就是說,在進程地址空間中用于文件映射的內存區(qū)域的首地址;
四、系統(tǒng)調用mmap()用于共享內存的兩種方式: A、使用普通文件提供的內存映射/共享內存:適用于任何進程之間;此時,需要使用系統(tǒng)調用open()事先打開或創(chuàng)建一個文件,然后再調用mmap(): fd = open(filename, flag, mode); ...... ptr = mmap(NULL, len, PORT_READ|PORT_WRITE, MAP_SHARED, fd, 0); 使用特殊文件提供匿名內存映射: 適用于具有親屬關系的進程之間;由于父子進程之間的這種特殊的父子關系,在父進程中先調用mmap(),然后調用fork(),那么,在調用fork() 之后,子進程繼承了父進程的所有資源,當然也包括匿名映射后的地址空間和mmap()返回的地址,這樣,父子進程就可以通過映射區(qū)域進行通信了; 注意:這里不是一般的繼承關系.一般來說,子進程單獨維護從父進程繼承下來的一些變量,而mmap()返回的地址卻是由父子進程共同維護的;對于具有親屬關系的進程之間實現(xiàn)共享內存的最好方式應該是采用匿名映射的方式.此時,不必指定具體的條件,只要設置相應的標志即可.
五、解除內存映射關系:
當進程間通信結束時,需要解除文件頁面空間到進程地址空間之間的映射關系;也就說,進程通信結束時,需要把掛載到進程地址空間上的文件卸載下來;這個任務由系統(tǒng)調用munmap();
int munmap(void* addr, size_t len);
該系統(tǒng)調用用于在進程地址空間中結束映射關系;
addr:是調用mmap()返回的進程地址空間中用于文件映射的內存區(qū)域的首地址;
len :進程地址空間中映射區(qū)域的大小,單位:字節(jié);
當映射關系解除之后,對原來映射地址的訪問將導致段錯誤發(fā)生;
返回值: -1:失敗; 0:成功;
六、內存映射的同步:
一般來說,進程在映射空間中對共享內容的修改并不會直接寫回到磁盤文件中,往往在調用munmap()之后才會同步輸出到磁盤文件中.那么,在程序運行過 程中,在調用munmap()之前,可以通過調用msync()來實現(xiàn)磁盤上文件內容與共享內存區(qū)中的內容與一致;或者是把對共享內存區(qū)的修改同步輸出到 磁盤文件中;
注意:
1、最終被映射文件內容的長度不會超過文件本身的初始大小,即:內存映射操作不能改變文件的大小;
2、可以用于進程間通信的得有效地址空間大小大體上受限于被映射文件的大小,但是并不完全受限于文件大小.
在Linux中,內存的保護機制是以內存頁為單位的,即使被映射的文件只有一個字節(jié)的大小,內核也會為這個文件的映射分配一個頁面大小的內存空間.當被映 射文件的大小小于一個頁面大小時,進程可以對mmap()返回地址開始的一個頁面大小進行訪問,而不會出錯;但是,如果對一個頁面之外的地址空間進行訪 問,則導致錯誤發(fā)生.因此,可用于進程間通信的有效地址空間的大小不會超過被映射文件大小與一個頁面大小的和;
3、文件一旦被映射之后,調用mmap()的進程對返回地址空間的訪問就是對某一內存區(qū)域進行訪問,暫時脫離了磁盤上文件的影響.所有對mmap()返回 地址空間的操作只在內存范圍內有意義,只有在調用了munmap()或msync()之后,才會把映射內存中的相應內容寫回到磁盤文件中,所寫內容的大小 仍然不會超過被映射文件的大小;
七、對mmap()返回的地址空間的訪問:
Linux采用的是頁式管理機制.對于用mmap()映射普通文件來說,進程會在自己的地址空間中新增加一塊空間,空間的大小由mmap()的len參數(shù) 指定,注意:進程并不一定能夠對新增加的全部空間都進行有效的訪問.進程能夠訪問的有效地址空間的大小取決于文件中被映射部分的大小.簡單地說,能夠容納 文件中被映射部分大小的最少頁面?zhèn)€數(shù)決定了進程從mmap()返回的地址開始,能夠訪問的有效地址空間大小.超過這個空間大小,內核會根據(jù)超過的嚴重程度 返回發(fā)送不同的信號給進程.
注意:決定進程能夠訪問的有效地址空間大小的因素是文件中被映射的部分,而不是整個文件;另外,如果指定了文件的偏移部分,一定要注意為頁面大小的整數(shù)倍;
總之:采用內存映射機制mmap()來實現(xiàn)進程間通信是很方便的,在應用層上,調用接口非常簡單,內部實現(xiàn)機制涉及到了Linux的存儲管理以及文件系統(tǒng)等方面的內用;