Linux ------- 記憶體對映
一、記憶體對映的原理
記憶體對映,簡而言之就是將使用者空間的一段記憶體區域對映到核心空間,對映成功後,使用者對這段記憶體區域的修改可以直接反映到核心空間,同樣,核心空間對這段區域的修改也直接反映使用者空間。那麼對於核心空間<---->使用者空間兩者之間需要大量資料傳輸等操作的話效率是非常高的。
記憶體對映分為2種:
1.檔案對映:將一個普通檔案的全部或者一部分內容對映到程序的虛擬記憶體中。對映後,程序就可以直接在對應的記憶體區域操作檔案內容!普遍檔案對映到使用者空間的記憶體區域的示意圖
2.匿名對映:匿名對映沒有對應的檔案或者對應的檔案是虛擬檔案(如:/dev/zero),對映後會把記憶體分頁全部初始化為0.
當多個程序映射了同一個記憶體區域時,他們會共享實體記憶體的相同分頁。通過fork()建立的子程序也會繼承父程序的對映副本!!!
如果多個程序都會同一個記憶體區域操作時,會根據對映的特性,會有不同的行為。對映特徵可分為私有對映和共享對映:
1.私有對映:對映的內容對其程序不可見。對於檔案對映來說,某一個程序在對映記憶體中改變檔案的內容不會反映的底層檔案中。核心會使用copy-on-write(寫時複製)技術來解決這個問題:只要有一個程序修改了分頁中的內容,核心會為該程序重新建立一個新的分頁,並將需要修改的內容複製到新分頁中。
2.共享對映:某一個程序對共享記憶體的記憶體區域操作都會對其他程序可見!!!對於檔案對映,操作的內容回反映到底層檔案中。
注意:程序指向exec()呼叫後,先前的記憶體對映會丟失,而fork()建立的子程序會繼承父程序的,對映的特徵(私有和共享)也會被繼承。
異常訊號:
1.當對映記憶體的屬性設定只讀時,如果進行寫操作會產生SIGSEGV訊號。
2.當對映記憶體的位元組數大於被對映檔案的大小,且大於該檔案當前的記憶體分頁大小時。如果訪問的區域超過了該檔案分頁大小,會產生SIGBUS訊號。
有點繞口,舉個簡單的例子:假設核心維護的記憶體分頁是4k,4096位元組),一個普通檔案a.txt的大小是10位元組。如果建立一個對映記憶體為4079位元組,並對映該檔案。此時,因為a.txt的大小用一個分頁就可以完全對映,10位元組遠小於一個分頁的4096位元組,所以核心只會給它一個分頁。記憶體地址時從0開始,0-9區間對應a.txt檔案的資料,我們也可以訪問10-4096的區間。但如果訪問4096區間時,已經超過一個分頁的大小了,此時會產生SIGBUS訊號!!!
二、函式介面
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()來取消記憶體對映
1.建立對映
#include <sys/mman.h>
void *mmap(void *addr, size_t length,int prot,int flags,int fd,off_t offset);
addr:對映後要存放的虛擬記憶體地址。如果是NULL,核心會自動幫你選擇。
length:對映記憶體的位元組數。
prot:許可權保護:PORT_NONE(無法訪問),PORT_READ(可讀),PORT_WRITE(可寫),
length:對映記憶體的位元組數。
prot:許可權保護:PROT_NONE(無法訪問),PORT_READ(可讀),PORT_WRITE(可寫),
PORT_EXEC(可執行).
flags:對映特徵:MAP_PRIVATE(私有),MAP_SHARED(共享),MAP_ANONYMOUS.還有一些其他的可查詢man手冊。
fd:要對映的檔案描述符。
offset:檔案的偏移量,如果為0,且length為檔案長度,代表對映整個檔案。
2.解除對映
#include <sys/mman.h>
int munmap(void *addr,size_t length);
addr:要解除記憶體的起始地址。如果addr不在剛剛對映區域的開始位置,解除一部分後記憶體區域可能會分成兩半!!!
length:要解除的位元組數。
3.同步對映區
#include <sys/mman.h>
int msync(void *addr, size_t length, int flags);
addr:要同步的記憶體起始地址。
length:要同步的位元組長度。
flag:MS_SYNC(執行同步檔案寫入),此操作核心會把內容直接寫入到磁碟。MS_ASYNC(執行非同步檔案寫入),此操作核心會先把內容寫到核心的快取區,某個適合的時候再寫到磁碟。
三、mmap在linux哪裡?
mmap是操作這些裝置的一種方法,所謂操作裝置,比如IO埠(點亮一個LED)、LCD控制器、磁碟控制器,實際上就是往裝置的實體地址讀寫資料。
但是,由於應用程式不能直接操作裝置硬體地址,所以作業系統提供了這樣的一種機制——記憶體對映,把裝置地址對映到程序虛擬地址,mmap就是實現記憶體對映的介面。
操作裝置還有很多方法,如ioctl、ioremap
mmap的好處是,mmap把裝置記憶體對映到虛擬記憶體,則使用者操作虛擬記憶體相當於直接操作裝置了,省去了使用者空間到核心空間的複製過程,相對IO操作來說,增加了資料的吞吐量。
四、虛擬地址空間
每個程序都有4G的虛擬地址空間,其中3G使用者空間,1G核心空間(linux),每個程序共享核心空間,獨立的使用者空間,下圖形象地表達了這點
驅動程式執行在核心空間,所以驅動程式是面向所有程序的。
使用者空間切換到核心空間有兩種方法:
(1)系統呼叫,即軟中斷
(2)硬體中斷
虛擬空間裝的大概是上面那些資料了,記憶體對映大概就是把裝置地址對映到上圖的紅色段了,暫且稱其為“記憶體對映段”,至於對映到哪個地址,是由作業系統分配的,作業系統會把程序空間劃分為三個部分:
(1)未分配的,即程序還未使用的地址
(2)快取的,快取在ram中的頁
(3)未快取的,沒有快取在ram中
作業系統會在未分配的地址空間分配一段虛擬地址,用來和裝置地址建立對映,至於怎麼建立對映,後面再揭曉。
現在大概明白了“記憶體對映”是什麼了,那麼核心是怎麼管理這些地址空間的呢?任何複雜的理論最終也是通過各種資料結構體現出來的,而這裡這個資料結構就是程序描述符。從核心看,程序是分配系統資源(CPU、記憶體)的載體,為了管理程序,核心必須對每個程序所做的事情進行清楚的描述,這就是程序描述符,核心用task_struct結構體來表示程序,並且維護一個該結構體連結串列來管理所有程序。該結構體包含一些程序狀態、排程資訊等上千個成員,我們這裡主要關注程序描述符裡面的記憶體描述符(struct mm_struct mm)
五、記憶體描述符
現在已經知道了記憶體對映是把裝置地址對映到程序空間地址(注意:並不是所有記憶體對映都是對映到程序地址空間的,ioremap是對映到核心虛擬空間的,mmap是對映到程序虛擬地址的),實質上是分配了一個vm_area_struct結構體加入到程序的地址空間,也就是說,把裝置地址對映到這個結構體,對映過程就是驅動程式要做的事了。
六、記憶體對映的實現
以字元裝置驅動為例,一般對字元裝置的操作都如下框圖
而記憶體對映的主要任務就是實現核心空間中的mmap()函式,先來了解一下字元裝置驅動程式的框架,見於部落格---
以下是mmap_driver.c的原始碼
[cpp] view plain copy
//所有的模組程式碼都包含下面兩個標頭檔案
#include <linux/module.h>
#include <linux/init.h>
#include <linux/types.h> //定義dev_t型別
#include <linux/cdev.h> //定義struct cdev結構體及相關操作
#include <linux/slab.h> //定義kmalloc介面
#include <asm/io.h>//定義virt_to_phys介面
#include <linux/mm.h>//remap_pfn_range
#include <linux/fs.h>
#define MAJOR_NUM 990
#define MM_SIZE 4096
static char driver_name[] = "mmap_driver1";//驅動模組名字
static int dev_major = MAJOR_NUM;
static int dev_minor = 0;
char *buf = NULL;
struct cdev *cdev = NULL;
static int device_open(struct inode *inode, struct file *file)
{
printk(KERN_ALERT"device open\n");
buf = (char *)kmalloc(MM_SIZE, GFP_KERNEL);//核心申請記憶體只能按頁申請,申請該記憶體以便後面把它當作虛擬裝置
return 0;
}
static int device_close(struct inode *indoe, struct file *file)
{
printk("device close\n");
if(buf)
{
kfree(buf);
}
return 0;
}
static int device_mmap(struct file *file, struct vm_area_struct *vma)
{
vma->vm_flags |= VM_IO;//表示對裝置IO空間的對映
vma->vm_flags |= VM_RESERVED;//標誌該記憶體區不能被換出,在裝置驅動中虛擬頁和物理頁的關係應該是長期的,應該保留起來,不能隨便被別的虛擬頁換出
if(remap_pfn_range(vma,//虛擬記憶體區域,即裝置地址將要對映到這裡
vma->vm_start,//虛擬空間的起始地址
virt_to_phys(buf)>>PAGE_SHIFT,//與實體記憶體對應的頁幀號,實體地址右移12位
vma->vm_end - vma->vm_start,//對映區域大小,一般是頁大小的整數倍
vma->vm_page_prot))//保護屬性,
{
return -EAGAIN;
}
return 0;
}
static struct file_operations device_fops =
{
.owner = THIS_MODULE,
.open = device_open,
.release = device_close,
.mmap = device_mmap,
};
static int __init char_device_init( void )
{
int result;
dev_t dev;//高12位表示主裝置號,低20位表示次裝置號
printk(KERN_ALERT"module init2323\n");
printk("dev=%d", dev);
dev = MKDEV(dev_major, dev_minor);
cdev = cdev_alloc();//為字元裝置cdev分配空間
printk(KERN_ALERT"module init\n");
if(dev_major)
{
result = register_chrdev_region(dev, 1, driver_name);//靜態分配裝置號
printk("result = %d\n", result);
}
else
{
result = alloc_chrdev_region(&dev, 0, 1, driver_name);//動態分配裝置號
dev_major = MAJOR(dev);
}
if(result < 0)
{
printk(KERN_WARNING"Cant't get major %d\n", dev_major);
return result;
}
cdev_init(cdev, &device_fops);//初始化字元裝置cdev
cdev->ops = &device_fops;
cdev->owner = THIS_MODULE;
result = cdev_add(cdev, dev, 1);//向核心註冊字元裝置
printk("dffd = %d\n", result);
return 0;
}
static void __exit char_device_exit( void )
{
printk(KERN_ALERT"module exit\n");
cdev_del(cdev);
unregister_chrdev_region(MKDEV(dev_major, dev_minor), 1);
}
module_init(char_device_init);//模組載入
module_exit(char_device_exit);//模組退出
MODULE_LICENSE("GPL");
MODULE_AUTHOR("ChenShengfa");
下面是測試程式碼test_mmap.c下面是makefile檔案 下面是makefile檔案
[cpp] view plain copy
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdlib.h>
#include <string.h>
int main( void )
{
int fd;
char *buffer;
char *mapBuf;
fd = open("/dev/mmap_driver", O_RDWR);//開啟裝置檔案,核心就能獲取裝置檔案的索引節點,填充inode結構
if(fd<0)
{
printf("open device is error,fd = %d\n",fd);
return -1;
}
/*測試一:檢視記憶體對映段*/
printf("before mmap\n");
sleep(15);//睡眠15秒,檢視對映前的記憶體圖cat /proc/pid/maps
buffer = (char *)malloc(1024);
memset(buffer, 0, 1024);
mapBuf = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//記憶體對映,會呼叫驅動的mmap函式
printf("after mmap\n");
sleep(15);//睡眠15秒,在命令列檢視對映後的記憶體圖,如果多出了對映段,說明對映成功
/*測試二:往對映段讀寫資料,看是否成功*/
strcpy(mapBuf, "Driver Test");//向對映段寫資料
memset(buffer, 0, 1024);
strcpy(buffer, mapBuf);//從對映段讀取資料
printf("buf = %s\n", buffer);//如果讀取出來的資料和寫入的資料一致,說明對映段的確成功了
munmap(mapBuf, 1024);//去除對映
free(buffer);
close(fd);//關閉檔案,最終呼叫驅動的close
return 0;
}