1. 程式人生 > >Linux ------- 記憶體對映

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;  
}