from http://nieyong.github.io/wiki_cpu/mmap%E8%AF%A6%E8%A7%A3.html
內存映射,簡而言之就是將內核空間的一段內存區域映射到用戶空間。映射成功後,用戶對這段內存區域的修改可以直接反映到內核空間,相反,內核空間對這段區域的修改也直接反映用戶空間。那麼對於內核空間與用戶空間兩者之間需要大量數據傳輸等操作的話效率是非常高的。當然,也可以將內核空間的一段內存區域同時映射到多個進程,這樣還可以實現進程間的共享內存通信。
系統調用mmap()就是用來實現上面說的內存映射。最長見的操作就是文件(在Linux下設備也被看做文件)的操作,可以將某文件映射至內存(進程空間),如此可以把對文件的操作轉爲對內存的操作,以此避免更多的lseek()與read()、write()操作,這點對於大文件或者頻繁訪問的文件而言尤其受益。
概述
mmap將一個文件或者其它對象映射進內存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最後一個頁不被使用的空間將會清零。munmap執行相反的操作,刪除特定地址區域的對象映射。
當使用mmap映射文件到進程後,就可以直接操作這段虛擬地址進行文件的讀寫等操作,不必再調用read,write等系統調用。但需注意,直接對該段內存寫時不會寫入超過當前文件大小的內容。
採用共享內存通信的一個顯而易見的好處是效率高,因爲進程可以直接讀寫內存,而不需要任何數據的拷貝。對於像管道和消息隊列等通信方式,則需要在內核和用戶空間進行四次的數據拷貝,而共享內存則只拷貝兩次數據:一次從輸入文件到共享內存區,另一次從共享內存區到輸出文件。實際上,進程之間在共享內存時,並不總是讀寫少量數據後就解除映射,有新的通信時,再重新建立共享內存區域。而是保持共享區域,直到通信完畢爲止,這樣,數據內容一直保存在共享內存中,並沒有寫回文件。共享內存中的內容往往是在解除映射時才寫回文件的。因此,採用共享內存的通信方式效率是非常高的。
通常使用mmap()的三種情況: 提高I/O效率、匿名內存映射、共享內存進程通信 。
用戶空間mmap()函數void *mmap(void *start, size_t length, int prot, int flags,int fd, off_t offset)
,下面就其參數解釋如下:
- start:用戶進程中要映射的用戶空間的起始地址,通常爲NULL(由內核來指定)
- length:要映射的內存區域的大小
- prot:期望的內存保護標誌
- flags:指定映射對象的類型
- fd:文件描述符(由open函數返回)
- offset:設置在內核空間中已經分配好的的內存區域中的偏移,例如文件的偏移量,大小爲PAGE_SIZE的整數倍
- 返回值:mmap()返回被映射區的指針,該指針就是需要映射的內核空間在用戶空間的虛擬地址
內存映射的應用
- X Window服務器
- 衆多內存數據庫如MongoDB操作數據,就是把文件磁盤內容映射到內存中進行處理,爲什麼會提高效率? 很多人不解. 下面就深入分析內存文件映射.
- 通過malloc來分配大內存其實調用的是mmap,可見在malloc(10)的時候調用的是brk, malloc(10 * 1024 * 1024)調用的是mmap;
mmap()用於共享內存的兩種方式
- 使用普通文件提供的內存映射:適用於任何進程之間;此時,需要打開或創建一個文件,然後再調用mmap();典型調用代碼如下:
- fd=open(name, flag, mode);
- if(fd<0)
- ...
- ptr=mmap(NULL, len , PROT_READ|PROT_WRITE, MAP_SHARED , fd , 0);
通過mmap()實現共享內存的通信方式有許多特點和要注意的地方。
- 使用特殊文件提供匿名內存映射:適用於具有親緣關係的進程之間;由於父子進程特殊的親緣關係,在父進程中先調用mmap(),然後調用fork()。那麼在調用fork()之後,子進程繼承父進程匿名映射後的地址空間,同樣也繼承mmap()返回的地址,這樣,父子進程就可以通過映射區域進行通信了。注意,這裏不是一般的繼承關係。一般來說,子進程單獨維護從父進程繼承下來的一些變量。而mmap()返回的地址,卻由父子進程共同維護。 對於具有親緣關係的進程實現共享內存最好的方式應該是採用匿名內存映射的方式。此時,不必指定具體的文件,只要設置相應的標誌即可.
示例1-驅動+應用
首先在驅動程序分配一頁大小的內存,然後用戶進程通過mmap()將用戶空間中大小也爲一頁的內存映射到內核空間這頁內存上。映射完成後,驅動程序往這段內存寫10個字節數據,用戶進程將這些數據顯示出來。
- #include <linux/miscdevice.h>
- #include <linux/delay.h>
- #include <linux/kernel.h>
- #include <linux/module.h>
- #include <linux/init.h>
- #include <linux/mm.h>
- #include <linux/fs.h>
- #include <linux/types.h>
- #include <linux/delay.h>
- #include <linux/moduleparam.h>
- #include <linux/slab.h>
- #include <linux/errno.h>
- #include <linux/ioctl.h>
- #include <linux/cdev.h>
- #include <linux/string.h>
- #include <linux/list.h>
- #include <linux/pci.h>
- #include <linux/gpio.h>
- #define DEVICE_NAME "mymap"
- static unsigned char array[10]={0,1,2,3,4,5,6,7,8,9};
- static unsigned char *buffer;
- static int my_open(struct inode *inode, struct file *file)
- {
- return 0;
- }
- static int my_map(struct file *filp, struct vm_area_struct *vma)
- {
- unsigned long page;
- unsigned char i;
- unsigned long start = (unsigned long)vma->vm_start;
- //unsigned long end = (unsigned long)vma->vm_end;
- unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);
- //得到物理地址
- page = virt_to_phys(buffer);
- //將用戶空間的一個vma虛擬內存區映射到以page開始的一段連續物理頁面上
- if(remap_pfn_range(vma,start,page>>PAGE_SHIFT,size,PAGE_SHARED))//第三個參數是頁幀號,由物理地址右移PAGE_SHIFT得到
- return -1;
- //往該內存寫10字節數據
- for(i=0;i<10;i++)
- buffer[i] = array[i];
- return 0;
- }
- static struct file_operations dev_fops = {
- .owner = THIS_MODULE,
- .open = my_open,
- .mmap = my_map,
- };
- static struct miscdevice misc = {
- .minor = MISC_DYNAMIC_MINOR,
- .name = DEVICE_NAME,
- .fops = &dev_fops,
- };
- static int __init dev_init(void)
- {
- int ret;
- //註冊混雜設備
- ret = misc_register(&misc);
- //內存分配
- buffer = (unsigned char *)kmalloc(PAGE_SIZE,GFP_KERNEL);
- //將該段內存設置爲保留
- SetPageReserved(virt_to_page(buffer));
- return ret;
- }
- static void __exit dev_exit(void)
- {
- //註銷設備
- misc_deregister(&misc);
- //清除保留
- ClearPageReserved(virt_to_page(buffer));
- //釋放內存
- kfree(buffer);
- }
- module_init(dev_init);
- module_exit(dev_exit);
- MODULE_LICENSE("GPL");
- MODULE_AUTHOR("LKN@SCUT");
應用程序:
- #include <unistd.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <string.h>
- #include <fcntl.h>
- #include <linux/fb.h>
- #include <sys/mman.h>
- #include <sys/ioctl.h>
- #define PAGE_SIZE 4096
- int main(int argc , char *argv[])
- {
- int fd;
- int i;
- unsigned char *p_map;
- //打開設備
- fd = open("/dev/mymap",O_RDWR);
- if(fd < 0)
- {
- printf("open fail\n");
- exit(1);
- }
- //內存映射
- p_map = (unsigned char *)mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);
- if(p_map == MAP_FAILED)
- {
- printf("mmap fail\n");
- goto here;
- }
- //打印映射後的內存中的前10個字節內容
- for(i=0;i<10;i++)
- printf("%d\n",p_map[i]);
- here:
- munmap(p_map, PAGE_SIZE);
- return 0;
- }
示例2-進程間共享內存
UNIX訪問文件的傳統方法是用open打開它們, 如果有多個進程訪問同一個文件, 則每一個進程在自己的地址空間都包含有該文件的副本,這不必要地浪費了存儲空間。 下圖說明了兩個進程同時讀一個文件的同一頁的情形。 系統要將該頁從磁盤讀到高速緩衝區中, 每個進程再執行一個存儲器內的複製操作將數據從高速緩衝區讀到自己的地址空間。
現在考慮另一種處理方法共享存儲映射: 進程A和進程B都將該頁映射到自己的地址空間, 當進程A第一次訪問該頁中的數據時, 它生成一個缺頁中斷. 內核此時讀入這一頁到內存並更新頁表使之指向它.以後, 當進程B訪問同一頁面而出現缺頁中斷時, 該頁已經在內存, 內核只需要將進程B的頁表登記項指向次頁即可. 如下圖所示:
下面就是進程A和B共享內存的示例。兩個程序映射同一個文件到自己的地址空間, 進程A先運行, 每隔兩秒讀取映射區域, 看是否發生變化。進程B後運行, 它修改映射區域, 然後退出, 此時進程A能夠觀察到存儲映射區的變化。
進程A的代碼:
- #include <sys/mman.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <error.h>
- #define BUF_SIZE 100
- int main(int argc, char **argv)
- {
- int fd, nread, i;
- struct stat sb;
- char *mapped, buf[BUF_SIZE];
- for (i = 0; i < BUF_SIZE; i++) {
- buf[i] = '#';
- }
- /* 打開文件 */
- if ((fd = open(argv[1], O_RDWR)) < 0) {
- perror("open");
- }
- /* 獲取文件的屬性 */
- if ((fstat(fd, &sb)) == -1) {
- perror("fstat");
- }
- /* 將文件映射至進程的地址空間 */
- if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ |
- PROT_WRITE, MAP_SHARED, fd, 0)) == (void *)-1) {
- perror("mmap");
- }
- /* 文件已在內存, 關閉文件也可以操縱內存 */
- close(fd);
- /* 每隔兩秒查看存儲映射區是否被修改 */
- while (1) {
- printf("%s\n", mapped);
- sleep(2);
- }
- return 0;
- }
進程B的代碼:
- #include <sys/mman.h>
- #include <sys/stat.h>
- #include <fcntl.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #include <error.h>
- #define BUF_SIZE 100
- int main(int argc, char **argv)
- {
- int fd, nread, i;
- struct stat sb;
- char *mapped, buf[BUF_SIZE];
- for (i = 0; i < BUF_SIZE; i++) {
- buf[i] = '#';
- }
- /* 打開文件 */
- if ((fd = open(argv[1], O_RDWR)) < 0) {
- perror("open");
- }
- /* 獲取文件的屬性 */
- if ((fstat(fd, &sb)) == -1) {
- perror("fstat");
- }
- /* 私有文件映射將無法修改文件 */
- if ((mapped = (char *)mmap(NULL, sb.st_size, PROT_READ |
- PROT_WRITE, MAP_PRIVATE, fd, 0)) == (void *)-1) {
- perror("mmap");
- }
- /* 映射完後, 關閉文件也可以操縱內存 */
- close(fd);
- /* 修改一個字符 */
- mapped[20] = '9';
- return 0;
- }
示例3-匿名映射實現父子進程通信
- #include <sys/mman.h>
- #include <stdio.h>
- #include <stdlib.h>
- #include <unistd.h>
- #define BUF_SIZE 100
- int main(int argc, char** argv)
- {
- char *p_map;
- /* 匿名映射,創建一塊內存供父子進程通信 */
- p_map = (char *)mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE,
- MAP_SHARED | MAP_ANONYMOUS, -1, 0);
- if(fork() == 0) {
- sleep(1);
- printf("child got a message: %s\n", p_map);
- sprintf(p_map, "%s", "hi, dad, this is son");
- munmap(p_map, BUF_SIZE); //實際上,進程終止時,會自動解除映射。
- exit(0);
- }
- sprintf(p_map, "%s", "hi, this is father");
- sleep(2);
- printf("parent got a message: %s\n", p_map);
- return 0;
- }
mmap進行內存映射的原理
mmap系統調用的最終目的是將設備或文件映射到用戶進程的虛擬地址空間,實現用戶進程對文件的直接讀寫,這個任務可以分爲以下三步:
- 在用戶虛擬地址空間中尋找空閒的滿足要求的一段連續的虛擬地址空間,爲映射做準備(由內核mmap系統調用完成)
假如vm_area_struct描述的是一個文件映射的虛存空間,成員vm_file便指向被映射的文件的file結構,vm_pgoff是該虛存空間起始地址在vm_file文件裏面的文件偏移,單位爲物理頁面。mmap系統調用所完成的工作就是準備這樣一段虛存空間,並建立vm_area_struct結構體,將其傳給具體的設備驅動程序.
- 建立虛擬地址空間和文件或設備的物理地址之間的映射(設備驅動完成)
建立文件映射的第二步就是建立虛擬地址和具體的物理地址之間的映射,這是通過修改進程頁表來實現的。mmap方法是file_opeartions結構的成員:int (*mmap)(struct file *,struct vm_area_struct *)
;
linux有2個方法建立頁表:
- 使用remap_pfn_range一次建立所有頁表。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot)
。 - 使用nopage VMA方法每次建立一個頁表項。
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type)
; - 使用方面的限制:remap_pfn_range不能映射常規內存,只存取保留頁和在物理內存頂之上的物理地址。因爲保留頁和在物理內存頂之上的物理地址內存管理系統的各個子模塊管理不到。640 KB 和 1MB 是保留頁可能映射,設備I/O內存也可以映射。如果想把kmalloc()申請的內存映射到用戶空間,則可以通過mem_map_reserve()把相應的內存設置爲保留後就可以。
- 當實際訪問新映射的頁面時的操作(由缺頁中斷完成)
- page cache及swap cache中頁面的區分:一個被訪問文件的物理頁面都駐留在page cache或swap cache中,一個頁面的所有信息由struct page來描述。struct page中有一個域爲指針mapping ,它指向一個struct address_space類型結構。page cache或swap cache中的所有頁面就是根據address_space結構以及一個偏移量來區分的。
- 文件與 address_space結構的對應:一個具體的文件在打開後,內核會在內存中爲之建立一個struct inode結構,其中的i_mapping域指向一個address_space結構。這樣,一個文件就對應一個address_space結構,一個 address_space與一個偏移量能夠確定一個page cache 或swap cache中的一個頁面。因此,當要尋址某個數據時,很容易根據給定的文件及數據在文件內的偏移量而找到相應的頁面。
- 進程調用mmap()時,只是在進程空間內新增了一塊相應大小的緩衝區,並設置了相應的訪問標識,但並沒有建立進程空間到物理頁面的映射。因此,第一次訪問該空間時,會引發一個缺頁異常。
- 對於共享內存映射情況,缺頁異常處理程序首先在swap cache中尋找目標頁(符合address_space以及偏移量的物理頁),如果找到,則直接返回地址;如果沒有找到,則判斷該頁是否在交換區 (swap area),如果在,則執行一個換入操作;如果上述兩種情況都不滿足,處理程序將分配新的物理頁面,並把它插入到page cache中。進程最終將更新進程頁表。 注:對於映射普通文件情況(非共享映射),缺頁異常處理程序首先會在page cache中根據address_space以及數據偏移量尋找相應的頁面。如果沒有找到,則說明文件數據還沒有讀入內存,處理程序會從磁盤讀入相應的頁面,並返回相應地址,同時,進程頁表也會更新.
- 所有進程在映射同一個共享內存區域時,情況都一樣,在建立線性地址與物理地址之間的映射之後,不論進程各自的返回地址如何,實際訪問的必然是同一個共享內存區域對應的物理頁面。