Linux系統程式設計:mmap使用技巧

劉近光發表於2018-07-28

簡介

本文將介紹了mmap的基本概念,並重點介紹mmap使用中常遇到的問題。

mmap是什麼

mmap函式把一個檔案或一個Poxis共享記憶體區物件對映到呼叫程式的地址空間,以使用普通檔案提供記憶體對映I/O,或使用特殊檔案以提供匿名記憶體對映,或使用shm_open以提供無親緣關係程式間的Posix共享記憶體區。

使用記憶體對映檔案所得到的奇妙特性是,所有的I/O都在核心的掩蓋下完成,只需編寫存取記憶體對映區中各個值的程式碼,而不用呼叫read、write或lseek,系統會自動負責將髒頁面回寫到檔案中。

#include <sys/mman.h>

void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);

實際對映關係如下圖所示:

mmap使用細節

1、使用mmap需要注意的一個關鍵點是,mmap對映區域大小必須是物理頁大小(page_size)的整倍數(32位系統中通常是4k位元組)。原因是,記憶體的最小粒度是頁,而程式虛擬地址空間和記憶體的對映也是以頁為單位。為了匹配記憶體的操作,mmap從磁碟到虛擬地址空間的對映也必須是頁。使用函式sysconf(_SC_PAGE_SIZE)可以獲取系統頁的大小。

2、核心可以跟蹤被記憶體對映的底層物件(檔案)的大小,程式可以合法的訪問在當前檔案大小以內又在記憶體對映區以內的那些位元組,否則,程式會丟擲某種異常。也就是說,如果檔案的大小一直在擴張,只要在對映區域範圍內的資料,程式都可以合法得到,這和對映建立時檔案的大小無關。具體情形參見“情形三”。

manual中對於異常訊號到描述:

       Use of a mapped region can result in these signals:

       SIGSEGV
              Attempted write into a region mapped as read-only.

       SIGBUS 
              Attempted access to a portion of the buffer that does not correspond 
to the file (for example, beyond the end of the file, including the case where 
another process  has  truncated the file).

3、對映建立之後,即使檔案關閉,對映依然存在。因為對映的是磁碟的地址,不是檔案本身,和檔案控制程式碼無關。同時可用於程式間通訊的有效地址空間不完全受限於被對映檔案的大小,因為是按頁對映。

在上面的知識前提下,我們下面看看如果大小不是頁的整倍數的具體情況:

情形一:一個檔案的大小是5000位元組,mmap函式從一個檔案的起始位置開始,對映5000位元組到虛擬記憶體中。

分析:因為單位物理頁面的大小是4096位元組,雖然被對映的檔案只有5000位元組,但是對應到程式虛擬地址區域的大小需要滿足整頁大小,因此mmap函式執行後,實際對映到虛擬記憶體區域8192個 位元組,5000~8191的位元組部分用零填充。對映後的對應關係如下圖所示:

此時:

(1)讀/寫前5000個位元組(0~4999),會返回操作檔案內容。

(2)讀位元組5000~8191時,結果全為0。寫5000~8191時,程式不會報錯,但是所寫的內容不會寫入原檔案中 。

(3)讀/寫8192以外的磁碟部分,會返回一個SIGSECV錯誤。

情形二:一個檔案的大小是5000位元組,mmap函式從一個檔案的起始位置開始,對映15000位元組到虛擬記憶體中,即對映大小超過了原始檔案的大小。

分析:由於檔案的大小是5000位元組,和情形一一樣,其對應的兩個物理頁。那麼這兩個物理頁都是合法可以讀寫的,只是超出5000的部分不會體現在原檔案中。由於程式要求對映15000位元組,而檔案只佔兩個物理頁,因此8192位元組~15000位元組都不能讀寫,操作時會返回異常。如下圖所示:

此時:

(1)程式可以正常讀/寫被對映的前5000位元組(0~4999),寫操作的改動會在一定時間後反映在原檔案中。

(2)對於5000~8191位元組,程式可以進行讀寫過程,不會報錯。但是內容在寫入前均為0,另外,寫入後不會反映在檔案中。

(3)對於8192~14999位元組,程式不能對其進行讀寫,會報SIGBUS錯誤。

(4)對於15000以外的位元組,程式不能對其讀寫,會引發SIGSEGV錯誤。

情形三:一個檔案初始大小為0,使用mmap操作對映了1000*4K的大小,即1000個物理頁大約4M位元組空間,mmap返回指標ptr。

分析:如果在對映建立之初,就對檔案進行讀寫操作,由於檔案大小為0,並沒有合法的物理頁對應,如同情形二一樣,會返回SIGBUS錯誤。

但是如果,每次操作ptr讀寫前,先增加檔案的大小(posix_allocate函式),那麼ptr在檔案大小內部的操作就是合法的。例如,檔案擴充4096位元組,ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要檔案擴充的範圍在1000個物理頁(對映範圍)內,ptr都可以對應操作相同的大小。

這樣,方便隨時擴充檔案空間,隨時寫入檔案,不造成空間浪費。

另外一個一般資料中很少提及的,是關於稀疏檔案的mmap問題,可以參考上面3鍾情形的分析,來解決對應的SIGSEGV和SIGBUS異常問題。

參考資料

1. UNIX網路程式設計 卷2 程式間通訊 第12章 共享記憶體區介紹

2. https://www.cnblogs.com/huxiao-tee/p/4660352.html

相關文章