1.mmap簡介
mmap
可以将檔案或者其他對象映射到記憶體中,即将一個檔案或者其它對象的位址空間映射到程序的位址空間,實作了檔案磁盤位址和程序一段虛拟位址的對映關系。實作這樣的映射關系後,程序就可以采用指針的方式讀寫這一段記憶體,系統會自動将映射檔案讀取到映射的記憶體空間當中,同時将髒頁回寫到對應的檔案磁盤上,這樣就完成了檔案的讀寫操作,而不必再調用read,write等系統調用函數。相反,核心空間對這段區域的修改也可直接反映到使用者空間,進而可以實作不同程序間的檔案共享。函數原型如下:
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
addr:映射虛拟位址空間的起始位址,如為NULL,則由核心自動配置設定。
length:映射虛拟位址空間的大小,系統自動取為頁的整數倍,不足一頁,按一頁映射。
port:描述了映射記憶體的權限,可取PROT_EXEC、PROT_READ、PROT_WRITE、PROT_NONE。
flags:描述了映射空間更新後對映射了相同區域的程序是否可見及是否将檔案寫回。常用一下選項。
MAP_SHARED:映射空間更新後對映射了相同區域的程序可見,可将髒頁寫回映射的檔案
MAP_PRIVATE:采用了私有copy-on-write的映射方式,寫入時會拷貝一份副本,在副本上更新,
這些資料不會寫回檔案,映射空間更新後對映射相同區域的程序不可見。
fd:映射對象的檔案句柄,在映射之前,必須先打開對象,得到檔案描述符。
offset:映射對象位址空間的偏移值。
傳回值:映射成功,傳回映射虛拟位址空間的首位址,失敗傳回(void *)-1。
關于長度
length
和偏移值
offset
的說明:
length
:傳入的長度隻要大于0即可,可以不是頁的整數倍。但核心按最大頁的整數倍進行映射,不足一頁,按一頁映射。測試發現,映射長度為100位元組,讀取的位元組隻要不超過4096位元組,都可正常讀取,映射長度為4096位元組,讀取4000-4500之間的資料,4000-4096之間的資料可正常讀取,超過4096後顯示為亂碼且讀取位元組數不正确,說明核心按整頁進行映射。
offset
:經過測試發現,offset必須為頁的整數倍,如不是頁的整數倍,映射會失敗,錯誤碼為22,顯示無效的參數。
2.mmap在核心中的執行流程
mmap為POSIX标準定義的系統調用,核心中調用的是
CALL(OBSOLETE(sys_old_mmap))
,屬于比較老的系統調用。Linux核心從2.3.31版本開始,提供了進行記憶體映射的私有系統調用
mmap2
。
兩者之間的差别很小,差別在于
mmap
進入核心後會将使用者空間的參數拷貝到核心空間,然後檢查偏移值是否是頁的整數倍,如不是,則傳回錯誤,表示無效的參數,這和之前的測試結果保持一緻,後續執行的函數就和
mmap2
一緻了。
mmap
和
mmap2
函數在核心中的執行流程如圖1所示。主要流程如下:
- 根據傳入的fd,找到打開檔案對應的file結構體。
- 從程序的虛拟位址空間配置設定虛拟位址範圍
- 檢查配置設定的虛拟位址範圍
- 如不能和已有的虛拟記憶體區域合并,則配置設定記憶體區域
- 調用具體裝置中實作的
函數,這裡就執行mmap
裝置注冊的/dev/mem
函數mmap
- 将虛拟記憶體區域添加到管理虛拟記憶體的連結清單和紅黑樹中
- 設定記憶體頁保護标志 每一個具體的(虛拟)裝置,都要向系統注冊一個操作函數的結構體,常見的
、open
、write
函數都在裡面,read
函數就包含在裡面,若裝置要支援記憶體映射,就必須實作mmap
函數。mmap
裝置注冊的結構體如下圖所示。 對于/dev/mem
裝置來說,應用層調用/dev/mem
系統調用,最終要調用到mmap
裝置在核心中注冊的/dev/mem
函數,其執行流程如下圖所示,主要流程如下:mmap
- 檢查映射的實體位址是否超過支援的實體位址範圍
- 是否支援私有映射,CPU有MMU則預設支援
- 檢查實體位址是否被允許映射,此選項由
控制,如定義則會對映射的位址進行嚴格的檢查,如沒有定義,則不檢查(CONFIG_STRICT_DEVMEM
的配置路徑:CONFIG_STRICT_DEVMEM
)。Kernel hacking ---> [ ] Filter access to /dev/mem
- 如開啟了檢查選項,将會檢查映射的實體位址是否是裝置獨占保留的IO映射區域、是否是系統記憶體、實體記憶體的通路權限是否符合要求。
- 通過後,則設定頁的通路權限
- 将核心中的實體位址映射到使用者空間。
3.mmap在核心中的執行流程
mem
裝置節點位于
dev
目錄下,
/dev/mem
是實體位址空間的全映像,可以用來通路CPU的實體位址空間,一般用法是
open("/dev/mem",O_RDWR)
,得到
mem
裝置的檔案描述符,接着用
mmap
來映射實體記憶體或外設的IO資源,利用
mmap
傳回的虛拟位址,可以操作映射的實體記憶體或外設的IO資源,這就是實作使用者空間硬體驅動的方法。
mem
的驅動檔案名為
mem.c
,位于核心源碼
drivers/char
目錄下。
4.mmap驅動源碼
如果不使用
/dev/mem
裝置,可自己實作mmap驅動,映射想要映射的位址空間。
#include <linux/miscdevice.h>
#include <linux/delay.h>
#include <linux/kthread.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>
#include <linux/interrupt.h>
#include <linux/time.h>
#define DEVICE_NAME1 "mem_mmap1"
#define DEVICE_NAME2 "mem_mmap2"
static const char drv_name1[] = "mem_mmap-program1";
static const char drv_name2[] = "mem_mmap-program2";
#define MEM_MMAP_ADDR1 (0x90000000) /* 映射記憶體的實體基位址 */
#define MEM_MMAP_ADDR2 (MEM_MMAP_ADDR1 + MEM_MMAP_LEN)
#define MEM_MMAP_LEN (0x4000000) /* 映射記憶體的最大長度 */
//#define DBG_INFO
static int mem_mmap_open1(struct inode *inode, struct file *file)
{
#ifdef INFO
pr_info("mem_mmap_open1: memory mmap module open ok\n");
#endif
return 0;
}
static int mem_mmap1(struct file *filp, struct vm_area_struct *vma)
{
unsigned long page;
// 偏移位址必須按PAGE_SIZE對齊,vm_pgoff為偏移的PAGE數量
// 右移PAGE_SHIFT,表示偏移的位元組數
unsigned long offset = (unsigned long)vma->vm_pgoff << PAGE_SHIFT;
unsigned long start = (unsigned long)vma->vm_start;
unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);
#ifdef INFO
pr_info("mem_mmap1: vm_pgoff %lx, offset %lx, start %lx, "
"end is %lx, size is %lx\n",
(unsigned long)vma->vm_pgoff, offset, start, vma->vm_end ,size);
#endif
if(size > MEM_MMAP_LEN) {
#ifdef INFO
pr_info("mem_mmap1: mmap size is too big, size %lx, max size %lx\n",
size, (unsigned long)MEM_MMAP_LEN);
#endif
return -ENXIO;
}
if(offset + size > MEM_MMAP_LEN) {
#ifdef INFO
pr_info("mem_mmap1: offset is too big, size %lx, offset %lx, max size %lx\n",
size, offset, (unsigned long)MEM_MMAP_LEN);
#endif
return -ENXIO;
}
/* 映射的起始實體位址 */
page = MEM_MMAP_ADDR1 + offset;
vma->vm_flags |= VM_IO | VM_SHARED;
// 關閉cache
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* mmap the request addr */
if(remap_pfn_range(vma, start, page >> PAGE_SHIFT, size, vma->vm_page_prot)) {
pr_err("mem_mmap1: mem_mmap:mmap faild\n");
return -EAGAIN;
}
#ifdef INFO
pr_info("mem_mmap1: mmap ok\n");
#endif
return 0;
}
static struct file_operations dev_fops_mem_mmap1 = {
.owner = THIS_MODULE,
.open = mem_mmap_open1,
.mmap = mem_mmap1,
};
static struct miscdevice misc_mem_mmap1 = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME1,
.fops = &dev_fops_mem_mmap1,
};
static int mem_mmap_open2(struct inode *inode, struct file *file)
{
#ifdef INFO
pr_info("mem_mmap_open2: memory mmap module open ok\n");
#endif
return 0;
}
static int mem_mmap2(struct file *filp, struct vm_area_struct *vma)
{
unsigned long page;
// 偏移位址必須按PAGE_SIZE對齊,vm_pgoff為偏移的PAGE數量
// 右移PAGE_SHIFT,表示偏移的位元組數
unsigned long offset = (unsigned long)vma->vm_pgoff << PAGE_SHIFT;
unsigned long start = (unsigned long)vma->vm_start;
unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);
#ifdef INFO
pr_info("mem_mmap2: vm_pgoff %lx, offset %lx, start %lx, "
"end is %lx, size is %lx\n",
(unsigned long)vma->vm_pgoff, offset, start, vma->vm_end ,size);
#endif
if(size > MEM_MMAP_LEN) {
#ifdef INFO
pr_info("mem_mmap2: mmap size is too big, size %lx, max size %lx\n",
size, (unsigned long)MEM_MMAP_LEN);
#endif
return -ENXIO;
}
if(offset + size > MEM_MMAP_LEN) {
#ifdef INFO
pr_info("mem_mmap2: offset is too big, size %lx, offset %lx, max size %lx\n",
size, offset, (unsigned long)MEM_MMAP_LEN);
#endif
return -ENXIO;
}
/* 映射的起始實體位址 */
page = MEM_MMAP_ADDR2 + offset;
vma->vm_flags |= VM_IO | VM_SHARED;
// 關閉cache
vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);
/* mmap the request addr */
if(remap_pfn_range(vma, start, page >> PAGE_SHIFT, size, vma->vm_page_prot)) {
pr_err("mem_mmap2: mem_mmap:mmap faild\n");
return -EAGAIN;
}
#ifdef INFO
pr_info("mem_mmap2: mmap ok\n");
#endif
return 0;
}
static struct file_operations dev_fops_mem_mmap2 = {
.owner = THIS_MODULE,
.open = mem_mmap_open2,
.mmap = mem_mmap2,
};
static struct miscdevice misc_mem_mmap2 = {
.minor = MISC_DYNAMIC_MINOR,
.name = DEVICE_NAME2,
.fops = &dev_fops_mem_mmap2,
};
static int __init mem_mmap_init(void)
{
int ret = 0;
/*register misc device for sharemem*/
ret = misc_register(&misc_mem_mmap1);
if (ret != 0) {
pr_err("mem_mmap_init: misc_mem_mmap1 register error...\n");
ret = -ENOENT;
return ret;
}
ret = misc_register(&misc_mem_mmap2);
if (ret != 0) {
pr_err("mem_mmap_init: misc_mem_mmap2 register error...\n");
ret = -ENOENT;
misc_deregister(&misc_mem_mmap1);
return ret;
}
#ifdef INFO
pr_info("mem_mmap_init: mem_mmap_init ok\n" );
#endif
return ret;
}
static void __exit mem_mmap_exit(void)
{
misc_deregister(&misc_mem_mmap1);
misc_deregister(&misc_mem_mmap2);
}
module_init(mem_mmap_init);
module_exit(mem_mmap_exit);
MODULE_DESCRIPTION("memory mmap module");
MODULE_AUTHOR("[email protected]");
MODULE_LICENSE("GPL");