mmap()函數用法詳解

mmap將一個文件或者其它對象映射進內存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最後一個頁不被使用的空間將會清零。mmap在用戶空間映射調用系統中作用很大。
頭文件 <sys/mman.h>
函數原型
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

int munmap(void* start,size_t length);

mmap()[1] 必須以PAGE_SIZE爲單位進行映射,而內存也只能以頁爲單位進行映射,若要映射非PAGE_SIZE整數倍的地址範圍,要先進行內存對齊,強行以PAGE_SIZE的倍數大小進行映射。

用法:

下面說一下內存映射的步驟:
用open系統調用打開文件, 並返回描述符fd.
用mmap建立內存映射, 並返回映射首地址指針start.
對映射(文件)進行各種操作, 顯示(printf), 修改(sprintf).
用munmap(void *start, size_t lenght)關閉內存映射.
用close系統調用關閉文件fd.

UNIX網絡編程第二捲進程間通信對mmap函數進行了說明。該函數主要用途有三個:
1、將一個普通文件映射到內存中,通常在需要對文件進行頻繁讀寫時使用,這樣用內存讀寫取代I/O讀寫,以獲得較高的性能;
2、將特殊文件進行匿名內存映射,可以爲關聯進程提供共享內存空間;
3、爲無關聯的進程提供共享內存空間,一般也是將一個普通文件映射到內存中。

函數:void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize); 
參數start:指向欲映射的內存起始地址,通常設爲 NULL,代表讓系統自動選定地址,映射成功後返回該地址。

參數length:代表將文件中多大的部分映射到內存。

參數prot:映射區域的保護方式。可以爲以下幾種方式的組合:
PROT_EXEC 映射區域可被執行
PROT_READ 映射區域可被讀取
PROT_WRITE 映射區域可被寫入
PROT_NONE 映射區域不能存取

參數flags:影響映射區域的各種特性。在調用mmap()時必須要指定MAP_SHARED 或MAP_PRIVATE。
MAP_FIXED 如果參數start所指的地址無法成功建立映射時,則放棄映射,不對地址做修正。通常不鼓勵用此旗標。
MAP_SHARED對映射區域的寫入數據會複製迴文件內,而且允許其他映射該文件的進程共享。
MAP_PRIVATE 對映射區域的寫入操作會產生一個映射文件的複製,即私人的“寫入時複製”(copy on write)對此區域作的任何修改都不會寫回原來的文件內容。
MAP_ANONYMOUS建立匿名映射。此時會忽略參數fd,不涉及文件,而且映射區域無法和其他進程共享。
MAP_DENYWRITE只允許對映射區域的寫入操作,其他對文件直接寫入的操作將會被拒絕。
MAP_LOCKED 將映射區域鎖定住,這表示該區域不會被置換(swap)。

參數fd:要映射到內存中的文件描述符。如果使用匿名內存映射時,即flags中設置了MAP_ANONYMOUS,fd設爲-1。有些系統不支持匿名內存映射,則可以使用fopen打開/dev/zero文件,然後對該文件進行映射,可以同樣達到匿名內存映射的效果。

參數offset:文件映射的偏移量,通常設置爲0,代表從文件最前方開始對應,offset必須是分頁大小的整數倍。

返回值:

若映射成功則返回映射區的內存起始地址,否則返回MAP_FAILED(-1),錯誤原因存於errno 中。

錯誤代碼:

EBADF 參數fd 不是有效的文件描述詞
EACCES 存取權限有誤。如果是MAP_PRIVATE 情況下文件必須可讀,使用MAP_SHARED則要有PROT_WRITE以及該文件要能寫入。
EINVAL 參數start、length 或offset有一個不合法。
EAGAIN 文件被鎖住,或是有太多內存被鎖住。
ENOMEM 內存不足。

系統調用mmap()用於共享內存的兩種方式:
(1)使用普通文件提供的內存映射:

適用於任何進程之間。此時,需要打開或創建一個文件,然後再調用mmap()

典型調用代碼如下:

fd=open(name, flag, mode); if(fd<0) ...

ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0); 

通過mmap()實現共享內存的通信方式有許多特點和要注意的地方,可以參看UNIX網絡編程第二卷。

(2)使用特殊文件提供匿名內存映射:

適用於具有親緣關係的進程之間。由於父子進程特殊的親緣關係,在父進程中先調用mmap(),然後調用 fork()。那麼在調用fork()之後,子進程繼承父進程匿名映射後的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進程就可以通過映射區 域進行通信了。注意,這裏不是一般的繼承關係。一般來說,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。 對於具有親緣關係的進程實現共享內存最好的方式應該是採用匿名內存映射的方式。此時,不必指定具體的文件,只要設置相應的標誌即可。




一、概述
內存映射,簡而言之就是將用戶空間的一段內存區域映射到內核空間,映射成功後,用戶對這段內存區域的修改可以直接反映到內核空間,同樣,內核空間對這段區域的修改也直接反映用戶空間。那麼對於內核空間<---->用戶空間兩者之間需要大量數據傳輸等操作的話效率是非常高的。
以下是一個把普遍文件映射到用戶空間的內存區域的示意圖。
圖一:



二、基本函數
mmap函數是unix/linux下的系統調用,詳細內容可參考《Unix Netword programming》卷二12.2節。
mmap系統調用並不是完全爲了用於共享內存而設計的。它本身提供了不同於一般對普通文件的訪問方式,進程可以像讀寫內存一樣對普通文件的操作。而Posix或系統V的共享內存IPC則純粹用於共享目的,當然mmap()實現共享內存也是其主要應用之一。
mmap系統調用使得進程之間通過映射同一個普通文件實現共享內存。普通文件被映射到進程地址空間後,進程可以像訪問普通內存一樣對文件進行訪問,不必再調用read(),write()等操作。mmap並不分配空間, 只是將文件映射到調用進程的地址空間裏(但是會佔掉你的 virutal memory), 然後你就可以用memcpy等操作寫文件, 而不用write()了.寫完後,內存中的內容並不會立即更新到文件中,而是有一段時間的延遲,你可以調用msync()來顯式同步一下, 這樣你所寫的內容就能立即保存到文件裏了.這點應該和驅動相關。 不過通過mmap來寫文件這種方式沒辦法增加文件的長度, 因爲要映射的長度在調用mmap()的時候就決定了.如果想取消內存映射,可以調用munmap()來取消內存映射

[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. void * mmap(void *start, size_t length, int prot , int flags, int fd, off_t offset)  


mmap用於把文件映射到內存空間中,簡單說mmap就是把一個文件的內容在內存裏面做一個映像。映射成功後,用戶對這段內存區域的修改可以直接反映到內核空間,同樣,內核空間對這段區域的修改也直接反映用戶空間。那麼對於內核空間<---->用戶空間兩者之間需要大量數據傳輸等操作的話效率是非常高的。
start:要映射到的內存區域的起始地址,通常都是用NULL(NULL即爲0)。NULL表示由內核來指定該內存地址 

length:要映射的內存區域的大小 
prot:期望的內存保護標誌,不能與文件的打開模式衝突。是以下的某個值,可以通過or運算合理地組合在一起 
PROT_EXEC //頁內容可以被執行 
PROT_READ //頁內容可以被讀取 
PROT_WRITE //頁可以被寫入 
PROT_NONE //頁不可訪問 
flags:指定映射對象的類型,映射選項和映射頁是否可以共享。它的值可以是一個或者多個以下位的組合體 
MAP_FIXED :使用指定的映射起始地址,如果由start和len參數指定的內存區重疊於現存的映射空間,重疊部分將會被丟棄。如果指定的起始地址不可用,操作將會失敗。並且起始地址必須落在頁的邊界上。 
MAP_SHARED :對映射區域的寫入數據會複製迴文件內, 而且允許其他映射該文件的進程共享。 
MAP_PRIVATE :建立一個寫入時拷貝的私有映射。內存區域的寫入不會影響到原文件。這個標誌和以上標誌是互斥的,只能使用其中一個。 
MAP_DENYWRITE :這個標誌被忽略。 
MAP_EXECUTABLE :同上 
MAP_NORESERVE :不要爲這個映射保留交換空間。當交換空間被保留,對映射區修改的可能會得到保證。當交換空間不被保留,同時內存不足,對映射區的修改會引起段違例信號。 
MAP_LOCKED :鎖定映射區的頁面,從而防止頁面被交換出內存。 
MAP_GROWSDOWN :用於堆棧,告訴內核VM系統,映射區可以向下擴展。 
MAP_ANONYMOUS :匿名映射,映射區不與任何文件關聯。 
MAP_ANON :MAP_ANONYMOUS的別稱,不再被使用。 
MAP_FILE :兼容標誌,被忽略。 
MAP_32BIT :將映射區放在進程地址空間的低2GB,MAP_FIXED指定時會被忽略。當前這個標誌只在x86-64平臺上得到支持。 
MAP_POPULATE :爲文件映射通過預讀的方式準備好頁表。隨後對映射區的訪問不會被頁違例阻塞。 
MAP_NONBLOCK :僅和MAP_POPULATE一起使用時纔有意義。不執行預讀,只爲已存在於內存中的頁面建立頁表入口。 

fd:文件描述符(由open函數返回) 

offset:表示被映射對象(即文件)從那裏開始對映,通常都是用0。 該值應該爲大小爲PAGE_SIZE的整數倍 

返回說明 
成功執行時,mmap()返回被映射區的指針,munmap()返回0。失敗時,mmap()返回MAP_FAILED[其值爲(void *)-1],munmap返回-1。errno被設爲以下的某個值 
EACCES:訪問出錯 
EAGAIN:文件已被鎖定,或者太多的內存已被鎖定 
EBADF:fd不是有效的文件描述詞 
EINVAL:一個或者多個參數無效 
ENFILE:已達到系統對打開文件的限制 
ENODEV:指定文件所在的文件系統不支持內存映射 
ENOMEM:內存不足,或者進程已超出最大內存映射數量 
EPERM:權能不足,操作不允許 
ETXTBSY:已寫的方式打開文件,同時指定MAP_DENYWRITE標誌 
SIGSEGV:試着向只讀區寫入 
SIGBUS:試着訪問不屬於進程的內存區 

[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. int munmap(void *start, size_t length)   
start:要取消映射的內存區域的起始地址 
length:要取消映射的內存區域的大小。 
返回說明 
成功執行時munmap()返回0。失敗時munmap返回-1.
int msync(const void *start, size_t length, int flags); 

對映射內存的內容的更改並不會立即更新到文件中,而是有一段時間的延遲,你可以調用msync()來顯式同步一下, 這樣你內存的更新就能立即保存到文件裏
start:要進行同步的映射的內存區域的起始地址。 
length:要同步的內存區域的大小 
flag:flags可以爲以下三個值之一: 
MS_ASYNC : 請Kernel快將資料寫入。 
MS_SYNC : 在msync結束返回前,將資料寫入。 
MS_INVALIDATE : 讓核心自行決定是否寫入,僅在特殊狀況下使用 

三、用戶空間和驅動程序的內存映射
3.1、基本過程
首先,驅動程序先分配好一段內存,接着用戶進程通過庫函數mmap()來告訴內核要將多大的內存映射到內核空間,內核經過一系列函數調用後調用對應的驅動程序的file_operation中指定的mmap函數,在該函數中調用remap_pfn_range()來建立映射關係。
3.2、映射的實現
首先在驅動程序分配一頁大小的內存,然後用戶進程通過mmap()將用戶空間中大小也爲一頁的內存映射到內核空間這頁內存上。映射完成後,驅動程序往這段內存寫10個字節數據,用戶進程將這些數據顯示出來。
驅動程序:

[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. #include <linux/miscdevice.h>   
  2. #include <linux/delay.h>   
  3. #include <linux/kernel.h>   
  4. #include <linux/module.h>   
  5. #include <linux/init.h>   
  6. #include <linux/mm.h>   
  7. #include <linux/fs.h>   
  8. #include <linux/types.h>   
  9. #include <linux/delay.h>   
  10. #include <linux/moduleparam.h>   
  11. #include <linux/slab.h>   
  12. #include <linux/errno.h>   
  13. #include <linux/ioctl.h>   
  14. #include <linux/cdev.h>   
  15. #include <linux/string.h>   
  16. #include <linux/list.h>   
  17. #include <linux/pci.h>   
  18. #include <linux/gpio.h>   
  19.   
  20.   
  21. #define DEVICE_NAME "mymap"   
  22.   
  23.   
  24. static unsigned char array[10]={0,1,2,3,4,5,6,7,8,9};   
  25. static unsigned char *buffer;   
  26.   
  27.   
  28. static int my_open(struct inode *inode, struct file *file)   
  29. {   
  30. return 0;   
  31. }   
  32.   
  33.   
  34. static int my_map(struct file *filp, struct vm_area_struct *vma)   
  35. {   
  36. unsigned long page;   
  37. unsigned char i;   
  38. unsigned long start = (unsigned long)vma->vm_start;   
  39. //unsigned long end = (unsigned long)vma->vm_end;   
  40. unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);   
  41.   
  42. //得到物理地址   
  43. page = virt_to_phys(buffer);   
  44. //將用戶空間的一個vma虛擬內存區映射到以page開始的一段連續物理頁面上   
  45. if(remap_pfn_range(vma,start,page>>PAGE_SHIFT,size,PAGE_SHARED))//第三個參數是頁幀號,由物理地址右移PAGE_SHIFT得到   
  46. return -1;   
  47.   
  48. //往該內存寫10字節數據   
  49. for(i=0;i<10;i++)   
  50. buffer[i] = array[i];   
  51.   
  52. return 0;   
  53. }   
  54.   
  55.   
  56. static struct file_operations dev_fops = {   
  57. .owner = THIS_MODULE,   
  58. .open = my_open,   
  59. .mmap = my_map,   
  60. };   
  61.   
  62. static struct miscdevice misc = {   
  63. .minor = MISC_DYNAMIC_MINOR,   
  64. .name = DEVICE_NAME,   
  65. .fops = &dev_fops,   
  66. };   
  67.   
  68.   
  69. static int __init dev_init(void)   
  70. {   
  71. int ret;   
  72.   
  73. //註冊混雜設備   
  74. ret = misc_register(&misc);   
  75. //內存分配   
  76. buffer = (unsigned char *)kmalloc(PAGE_SIZE,GFP_KERNEL);   
  77. //將該段內存設置爲保留   
  78. SetPageReserved(virt_to_page(buffer));   
  79.   
  80. return ret;   
  81. }   
  82.   
  83.   
  84. static void __exit dev_exit(void)   
  85. {   
  86. //註銷設備   
  87. misc_deregister(&misc);   
  88. //清除保留   
  89. ClearPageReserved(virt_to_page(buffer));   
  90. //釋放內存   
  91. kfree(buffer);   
  92. }   
  93.   
  94.   
  95. module_init(dev_init);   
  96. module_exit(dev_exit);   
  97. MODULE_LICENSE("GPL");   
  98. MODULE_AUTHOR("LKN@SCUT");   


應用程序:
[cpp] view plain copy
 在CODE上查看代碼片派生到我的代碼片
  1. #include <unistd.h>   
  2. #include <stdio.h>   
  3. #include <stdlib.h>   
  4. #include <string.h>   
  5. #include <fcntl.h>   
  6. #include <linux/fb.h>   
  7. #include <sys/mman.h>   
  8. #include <sys/ioctl.h>   
  9.   
  10. #define PAGE_SIZE 4096   
  11.   
  12.   
  13. int main(int argc , char *argv[])   
  14. {   
  15. int fd;   
  16. int i;   
  17. unsigned char *p_map;   
  18.   
  19. //打開設備   
  20. fd = open("/dev/mymap",O_RDWR);   
  21. if(fd < 0)   
  22. {   
  23. printf("open fail\n");   
  24. exit(1);   
  25. }   
  26.   
  27. //內存映射   
  28. p_map = (unsigned char *)mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);   
  29. if(p_map == MAP_FAILED)   
  30. {   
  31. printf("mmap fail\n");   
  32. goto here;   
  33. }   
  34.   
  35. //打印映射後的內存中的前10個字節內容   
  36. for(i=0;i<10;i++)   
  37. printf("%d\n",p_map[i]);   
  38.   
  39.   
  40. here:   
  41. munmap(p_map, PAGE_SIZE);   
  42. return 0;   
  43. }   
先加載驅動後執行應用程序,用戶空間打印如下:


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章