Linux驅動mmap內存映射

mmap在linux哪裏?

什麼是mmap?

上圖說了,mmap是操作這些設備的一種方法,所謂操作設備,比如IO端口(點亮一個LED)、LCD控制器、磁盤控制器,實際上就是往設備的物理地址讀寫數據。

但是,由於應用程序不能直接操作設備硬件地址,所以操作系統提供了這樣的一種機制——內存映射,把設備地址映射到進程虛擬地址,mmap就是實現內存映射的接口。

操作設備還有很多方法,如ioctl、ioremap

mmap的好處是,mmap把設備內存映射到虛擬內存,則用戶操作虛擬內存相當於直接操作設備了,省去了用戶空間到內核空間的複製過程,相對IO操作來說,增加了數據的吞吐量。

什麼是內存映射?

既然mmap是實現內存映射的接口,那麼內存映射是什麼呢?看下圖

每個進程都有獨立的進程地址空間,通過頁表和MMU,可將虛擬地址轉換爲物理地址,每個進程都有獨立的頁表數據,這可解釋爲什麼兩個不同進程相同的虛擬地址,卻對應不同的物理地址。

什麼是虛擬地址空間?

每個進程都有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的源代碼


    //所有的模塊代碼都包含下面兩個頭文件  
    #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


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

下面是makefile文件


    ifneq ($(KERNELRELEASE),)  
      
    obj-m := mmap_driver.o  
      
    else  
    KDIR := /lib/modules/3.2.0-52-generic/build  
      
    all:  
        make -C $(KDIR) M=$(PWD) modules  
    clean:  
        rm -f *.ko *.o *.mod.o *.mod.c *~ *.symvers *.order  
      
    endif  

 

下面命令演示一下驅動程序的編譯、安裝、測試過程(注:其他用戶在mknod之後還需要chmod改變權限)

# make    //編譯驅動

# insmod mmap_driver.ko    //安裝驅動

# mknod /dev/mmap_driver c 999 0    //創建設備文件

# gcc test_mmap.c -o test.o    //編譯應用程序

# ./test.o    //運行應用程序來測試驅動程序


拓展:

關於這個過程,涉及一些術語

(1)設備文件:linux中對硬件虛擬成設備文件,對普通文件的各種操作均適用於設備文件

(2)索引節點:linux使用索引節點來記錄文件信息(如文件長度、創建修改時間),它存儲在磁盤中,讀入內存後就是一個inode結構體,文件系統維護了一個索引節點的數組,每個元素都和文件或者目錄一一對應。

(3)主設備號:如上面的999,表示設備的類型,比如該設備是lcd還是usb等

(4)次設備號:如上面的0,表示該類設備上的不同設備

(5)文件(普通文件或設備文件)的三個結構

        ①文件操作:struct file_operations

        ②文件對象:struct file

        ③文件索引節點:struct inode

 

關於驅動程序中內存映射的實現,先了解一下open和close的流程

(1)設備驅動open流程

①應用程序調用open("/dev/mmap_driver", O_RDWR);

②Open就會通過VFS找到該設備的索引節點(inode),mknod的時候會根據設備號把驅動程序的file_operations結構填充到索引節點中(關於mknod /dev/mmap_driver c 999 0,這條指令創建了設備文件,在安裝驅動(insmod)的時候,會運行驅動程序的初始化程序(module_init),在初始化程序中,會註冊它的主設備號到系統中(cdev_add),如果mknod時的主設備號999在系統中不存在,即和註冊的主設備號不同,則上面的指令會執行失敗,就創建不了設備文件)

③然後根據設備文件的索引節點中的file_operations中的open指針,就調用驅動的open方法了。

④生成一個文件對象files_struct結構,系統維護一個files_struct的鏈表,表示系統中所有打開的文件

⑤返回文件描述符fd,把fd加入到進程的文件描述符表中

 

(2)設備驅動close流程

應用程序調用close(fd),最終可調用驅動的close,爲什麼根據一個簡單的int型fd就可以找到驅動的close函數?這就和上面說的三個結構(struct file_operations、struct file、struct inode)息息相關了,假如fd = 3

(3)設備驅動mmap流程

由open和close得知,同理,應用程序調用mmap最終也會調用到驅動程序中mmap方法

①應用程序test.mmap.c中mmap函數

void* mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);

addr:映射後虛擬地址的起始地址,通常爲NULL,內核自動分配

length:映射區的大小

prot:頁面訪問權限(PROT_READ、PROT_WRITE、PROT_EXEC、PROT_NONE)

flags:參考網絡資料

fd:文件描述符

offset:文件映射開始偏移量

 

②驅動程序的mmap_driver.c中mmap函數

上面說了,mmap的主要工作是把設備地址映射到進程虛擬地址,也即是一個vm_area_struct的結構體,這裏說的映射,是一個很懸的東西,那它在程序中的表現是什麼呢?——頁表,沒錯,就是頁表,映射就是要建立頁表。進程地址空間就可以通過頁表(軟件)和MMU(硬件)映射到設備地址上了

virt_to_phys(buf),buf是在open時申請的地址,這裏使用virt_to_phys把buf轉換成物理地址,是模擬了一個硬件設備,即把虛擬設備映射到虛擬地址,在實際中可以直接使用物理地址。

 

總結

①從以上看到,內核各個模塊錯綜複雜、相互交叉

②單純一個小小驅動模塊,就涉及了進程管理(進程地址空間)、內存管理(頁表與頁幀映射)、虛擬文件系統(structfile、structinode)

③並不是所有設備驅動都可以使用mmap來映射,比如像串口和其他面向流的設備,並且必須按照頁大小進行映射。

原文地址:https://www.cnblogs.com/wanghuaijun/p/7624564.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章