一、概述
記憶體映射,簡而言之就是將使用者空間的一段記憶體區域對應到核心空間,映射成功後,使用者對這段記憶體地區的修改可以直接反映到核心空間,同樣,核心空間對這段地區的修改也直接反映使用者空間。那麼對於核心空間<---->使用者空間兩者之間需要大量資料轉送等操作的話效率是非常高的。
以下是一個把普遍檔案對應到使用者空間的記憶體地區的示意圖。
圖一:
二、基本函數
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()來取消記憶體映射
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:試著訪問不屬於進程的記憶體區
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個位元組資料,使用者進程將這些資料顯示出來。
驅動程式:
#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; }
先載入驅動後執行應用程式,使用者空間列印如下: