POSIX提供了相關調用,能使我們將文件映射到內存中,借由此機制我們可以方便的從內存中讀取文件數據,也可以修改內存中的數據來改變文件內容,或實現父子進程間通信。
#include
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr-開發人員建議的映射文件的內存起始地址。一般來說傳NULL讓系統自行決議。
length-用來映射的內存大小,單位是字節。由於內存最小可尋址單位是頁,因此不足一頁的長度也會占用一頁。
prot-內存區域的訪問權限,有如下參數,可以或運算,注意要和打開文件的訪問權限一致:
注意如果文件的讀寫權限是只寫的,那麼無法保證共享內存的功能可用,因為PROT_WRITE隱含了PROT_READ。
flags-共享內存的行為,常見的有如下參數:
fd-要映射到內存中的文件描述符。
offset-文件描述符的偏移量,必須是頁大小的整數倍。
在調用成功後返回指向映射區的指針,失敗時返回MAP_FAILED,errno也會被設置,此處不再列舉錯誤碼類型。當程序試圖訪問無效的內存映射區域時,會收到SIGBUS信號;程序試圖寫入不可寫的區域時,收到SIGSEGV信號。
下圖顯示了信號的可能情況:
2200-4095的區間內寫入不能同步到文件,這段區域是為了保證頁對齊空余出來的范圍。4095-8191是映射的長度,訪問這段區域會收到SIGBUS信號,超過8192是未映射的區域,訪問這段區域會收到SIGSEGV信號。
前面說到offset參數必須指定為頁大小的整數倍(addr參數也一般由系統保證分配,也是頁面對齊的,下面相關的調用都是如此,不再贅述)。對於length,不足一頁的長度會占用一頁,讀取多占用部分獲得0,向多占用部分寫入也不會影響文件。
POSIX規定了通過sysconf調用可以獲得頁大小:
#include
long sysconf(int name);//返回name對應的值
long lPageSize = sysconf(_SC_PAGESIZE);//獲取頁大小,字節
Linux提供了系統調用,也能獲取頁大小:
#include
int getpagesize (void);
此外我們還可以使用宏來在編譯期獲取頁大小:
#include
int iPageSize = PAGE_SIZE;
為了二進制文件的移植性考慮,建議還是使用運行期獲取頁大小的方式,同時將offset設置為0,addr由系統分配。
在mmap()打開內存映射後,可以使用munmap()來關閉。
#include
int munmap (void *addr, size_t len);
關閉addr起始,長度是len個字節的映射區域。
當我們mmap()映射一個文件到內存時,該文件的引用計數會增加1,當我們的進程結束、執行exec()系列函數或者關閉內存映射時,文件的引用計數會減1。為了保證數據完整性,在munmap()之前需要調用msync()來寫入硬盤。
調用成功返回0,失敗返回-1。
對比read()/write()等系統調用,mmap()有以下優點:
read()/write()需要內核在用戶空間內存做讀寫操作,mmap()直接操作內核的頁緩沖,可以避免一次數據拷貝,對大文件操作來說優勢明顯 由於對映射的內存區域做操作,因此不像read()/write()那樣需要頻繁的系統調用 多個進程將同一個文件映射到內存可以實現進程間通信 在文件上移動讀寫位置只需要指針操作而不是lseek()映射的區域必須是頁的整數倍大小,對於映射小文件,很容易造成內存浪費。
使用mmap()來寫入文件時,需要提前知道寫入文件的大小,不像write()等系統調用能動態擴展文件大小。
#define _GNU_SOURCE
#include
#include
void * mremap (void *addr, size_t old_size, size_t new_size, unsigned long flags);
將addr對應的映射從old_size調整到new_size。flags的取值可以是0或者MREMAP_MAYMOVE,0代表不允許內核移動映射區域,MREMAP_MAYMOVE則表示內核可以根據實際情況移動映射區域以找到一個符合new_size大小要求的內存區域。len和prot參數都會被向上取整到頁大小的整數倍。調用成功返回映射的地址,失敗則返回MAP_FAILED。
#include
int mprotect (const void *addr, size_t len, int prot);
將len長度,addr起始對應的映射的訪問權限設置為prot。該調用在Linux上可以修改任意的內存段,而不僅僅是mmap()創建的,但是要保證addr是頁對齊的。prot的取值和mmap()提供的參數一致。
我們對可寫存儲映射的修改在顯式調用同步操作之前是不會保證立即同步到文件中的。
#include
int msync (void *addr, size_t len, int flags);
將以addr起始,長度為len的映射立即同步到內存中。flags取值含義如下:
調用成功返回0,失敗返回-1。
下面代碼演示如何將數據通過存儲映射的方式寫入硬盤:
#include
#include
#include
#include
#include
#define WIRTE_SIZE 50
int main ()
{
int fd = open("test.txt", O_CREAT|O_RDWR);
ftruncate (fd, WIRTE_SIZE);//新創建的文件必須要形成空洞文件,否則寫入的數據會被放棄
//映射的長度必須大於等於後面訪問的范圍,否則會收到SIGBUS信號
void * p =mmap(NULL, WIRTE_SIZE, PROT_WRITE|PROT_READ, MAP_SHARED, fd, 0);
if(p == MAP_FAILED)
{
perror("mmap");
return -1;
}
memcpy(p, "123", 3);
msync(p, WIRTE_SIZE, MS_SYNC);
return 0;
}
下面是一道練習題:使用mmap()實現命令cp
類似的功能:
//使用方式 a.out xxx xxx
#include
#include
#include
#include
#include
#include
#include
//使用存儲映射實現cp命令功能
int main(int argc,char *argv[])
{
if(argc < 3)
{
printf("please start as 'a.out srcfile destfile'\n");
return 0;
}
int fd = open(argv[1], O_RDONLY);
if(fd < 0)
{
perror("open");
return -1;
}
//文件存在則不覆蓋
/*if(access(argv[2], F_OK) != -1)
{
printf("please change a destfile name\n");
return 0;
}*/
struct stat buf;
fstat(fd, &buf);
printf("file size:%ld\n", buf.st_size);
//不能用creat,因為creat打開文件的模式是只寫,無法與mmap配合。前面說了mmap只能和有讀權限的文件描述符配合
//int fdDest = creat(argv[2], S_IRWXU);
int fdDest = open(argv[2], O_RDWR|O_CREAT, S_IRWXU);
if(fdDest < 0)
{
perror("creat");
}
if(ftruncate(fdDest, buf.st_size)<0)
{
perror("ftruncate");
return 0;
}
char* s = (char*)mmap(NULL, buf.st_size, PROT_WRITE|PROT_READ, MAP_PRIVATE, fd, 0);
if(s == MAP_FAILED)
{
perror("mmap1");
return 0;
}
char* d = (char*)mmap(NULL, buf.st_size, PROT_WRITE, MAP_SHARED, fdDest, 0);
if(d == MAP_FAILED)
{
perror("mmap2");
return 0;
}
memcpy(d, s, buf.st_size);
if(msync(d, buf.st_size, MS_ASYNC)<0)
{
perror("mmap");
return 0;
}
return 0;
}