Linux驅動mmap內存映射

mmap在linux哪裏?

什麼是mmap?

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

可是,因爲應用程序不能直接操做設備硬件地址,因此操做系統提供了這樣的一種機制——內存映射,把設備地址映射到進程虛擬地址,mmap就是實現內存映射的接口。linux

操做設備還有不少方法,如ioctl、ioremap數組

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


什麼是內存映射?

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

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


什麼是虛擬地址空間?

每一個進程都有4G的虛擬地址空間,其中3G用戶空間,1G內核空間(linux),每一個進程共享內核空間,獨立的用戶空間,下圖形象地表達了這點框架

驅動程序運行在內核空間,因此驅動程序是面向全部進程的。函數

用戶空間切換到內核空間有兩種方法:測試

(1)系統調用,即軟中斷ui

(2)硬件中斷


虛擬地址空間裏面是什麼?

瞭解了什麼是虛擬地址空間,那麼虛擬地址空間裏面裝的是什麼?看下圖

虛擬空間裝的大概是上面那些數據了,內存映射大概就是把設備地址映射到上圖的紅色段了,暫且稱其爲「內存映射段」,至於映射到哪一個地址,是由操做系統分配的,操做系統會把進程空間劃分爲三個部分:

(1)未分配的,即進程還未使用的地址

(2)緩存的,緩存在ram中的頁

(3)未緩存的,沒有緩存在ram中

操做系統會在未分配的地址空間分配一段虛擬地址,用來和設備地址創建映射,至於怎麼創建映射,後面再揭曉。

如今大概明白了「內存映射」是什麼了,那麼內核是怎麼管理這些地址空間的呢?任何複雜的理論最終也是經過各類數據結構體現出來的,而這裏這個數據結構就是進程描述符。從內核看,進程是分配系統資源(CPU、內存)的載體,爲了管理進程,內核必須對每一個進程所作的事情進行清楚的描述,這就是進程描述符,內核用task_struct結構體來表示進程,而且維護一個該結構體鏈表來管理全部進程。該結構體包含一些進程狀態、調度信息等上千個成員,咱們這裏主要關注進程描述符裏面的內存描述符(struct mm_struct mm)


內存描述符

具體的結構,請參考下圖

如今已經知道了內存映射是把設備地址映射到進程空間地址(注意:並非全部內存映射都是映射到進程地址空間的,ioremap是映射到內核虛擬空間的,mmap是映射到進程虛擬地址的),實質上是分配了一個vm_area_struct結構體加入到進程的地址空間,也就是說,把設備地址映射到這個結構體,映射過程就是驅動程序要作的事了。


內存映射的實現

以字符設備驅動爲例,通常對字符設備的操做都以下框圖

而內存映射的主要任務就是實現內核空間中的mmap()函數,先來了解一下字符設備驅動程序的框架

如下是mmap_driver.c的源代碼

  1. //全部的模塊代碼都包含下面兩個頭文件  
  2. #include <linux/module.h>  
  3. #include <linux/init.h>  
  4.   
  5. #include <linux/types.h> //定義dev_t類型  
  6. #include <linux/cdev.h> //定義struct cdev結構體及相關操做  
  7. #include <linux/slab.h> //定義kmalloc接口  
  8. #include <asm/io.h>//定義virt_to_phys接口  
  9. #include <linux/mm.h>//remap_pfn_range  
  10. #include <linux/fs.h>  
  11.   
  12. #define MAJOR_NUM 990  
  13. #define MM_SIZE 4096  
  14.   
  15. static char driver_name[] = "mmap_driver1";//驅動模塊名字  
  16. static int dev_major = MAJOR_NUM;  
  17. static int dev_minor = 0;  
  18. char *buf = NULL;  
  19. struct cdev *cdev = NULL;  
  20.   
  21. static int device_open(struct inode *inode, struct file *file)  
  22. {  
  23.     printk(KERN_ALERT"device open\n");  
  24.     buf = (char *)kmalloc(MM_SIZE, GFP_KERNEL);//內核申請內存只能按頁申請,申請該內存以便後面把它看成虛擬設備  
  25.     return 0;  
  26. }  
  27.   
  28. static int device_close(struct inode *indoe, struct file *file)  
  29. {  
  30.     printk("device close\n");  
  31.     if(buf)  
  32.     {  
  33.         kfree(buf);  
  34.     }  
  35.     return 0;  
  36. }  
  37.   
  38. static int device_mmap(struct file *file, struct vm_area_struct *vma)  
  39. {  
  40.     vma->vm_flags |= VM_IO;//表示對設備IO空間的映射  
  41.     vma->vm_flags |= VM_RESERVED;//標誌該內存區不能被換出,在設備驅動中虛擬頁和物理頁的關係應該是長期的,應該保留起來,不能隨便被別的虛擬頁換出  
  42.     if(remap_pfn_range(vma,//虛擬內存區域,即設備地址將要映射到這裏  
  43.                        vma->vm_start,//虛擬空間的起始地址  
  44.                        virt_to_phys(buf)>>PAGE_SHIFT,//與物理內存對應的頁幀號,物理地址右移12位  
  45.                        vma->vm_end - vma->vm_start,//映射區域大小,通常是頁大小的整數倍  
  46.                        vma->vm_page_prot))//保護屬性,  
  47.     {  
  48.         return -EAGAIN;  
  49.     }  
  50.     return 0;  
  51. }  
  52.   
  53. static struct file_operations device_fops =  
  54. {  
  55.     .owner = THIS_MODULE,  
  56.     .open  = device_open,  
  57.     .release = device_close,  
  58.     .mmap = device_mmap,  
  59. };  
  60.   
  61. static int __init char_device_init( void )  
  62. {  
  63.     int result;  
  64.     dev_t dev;//高12位表示主設備號,低20位表示次設備號  
  65.     printk(KERN_ALERT"module init2323\n");  
  66.     printk("dev=%d", dev);  
  67.     dev = MKDEV(dev_major, dev_minor);  
  68.     cdev = cdev_alloc();//爲字符設備cdev分配空間  
  69.     printk(KERN_ALERT"module init\n");  
  70.     if(dev_major)  
  71.     {  
  72.         result = register_chrdev_region(dev, 1, driver_name);//靜態分配設備號  
  73.         printk("result = %d\n", result);  
  74.     }  
  75.     else  
  76.     {  
  77.         result = alloc_chrdev_region(&dev, 0, 1, driver_name);//動態分配設備號  
  78.         dev_major = MAJOR(dev);  
  79.     }  
  80.       
  81.     if(result < 0)  
  82.     {  
  83.         printk(KERN_WARNING"Cant't get major %d\n", dev_major);  
  84.         return result;  
  85.     }  
  86.       
  87.       
  88.     cdev_init(cdev, &device_fops);//初始化字符設備cdev  
  89.     cdev->ops = &device_fops;  
  90.     cdev->owner = THIS_MODULE;  
  91.       
  92.     result = cdev_add(cdev, dev, 1);//向內核註冊字符設備  
  93.     printk("dffd = %d\n", result);  
  94.     return 0;  
  95. }  
  96.   
  97. static void __exit char_device_exit( void )  
  98. {  
  99.     printk(KERN_ALERT"module exit\n");  
  100.     cdev_del(cdev);  
  101.     unregister_chrdev_region(MKDEV(dev_major, dev_minor), 1);  
  102. }  
  103.   
  104. module_init(char_device_init);//模塊加載  
  105. module_exit(char_device_exit);//模塊退出  
  106.   
  107. MODULE_LICENSE("GPL");  
  108. MODULE_AUTHOR("ChenShengfa");  


下面是測試代碼test_mmap.c

  1. #include <stdio.h>  
  2. #include <fcntl.h>  
  3. #include <sys/mman.h>  
  4. #include <stdlib.h>  
  5. #include <string.h>  
  6.   
  7. int main( void )  
  8. {  
  9.     int fd;  
  10.     char *buffer;  
  11.     char *mapBuf;  
  12.     fd = open("/dev/mmap_driver", O_RDWR);//打開設備文件,內核就能獲取設備文件的索引節點,填充inode結構  
  13.     if(fd<0)  
  14.     {  
  15.         printf("open device is error,fd = %d\n",fd);  
  16.         return -1;  
  17.     }  
  18.     /*測試一:查看內存映射段*/  
  19.     printf("before mmap\n");  
  20.     sleep(15);//睡眠15秒,查看映射前的內存圖cat /proc/pid/maps  
  21.     buffer = (char *)malloc(1024);  
  22.     memset(buffer, 0, 1024);  
  23.     mapBuf = mmap(NULL, 1024, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);//內存映射,會調用驅動的mmap函數  
  24.     printf("after mmap\n");  
  25.     sleep(15);//睡眠15秒,在命令行查看映射後的內存圖,若是多出了映射段,說明映射成功  
  26.       
  27.     /*測試二:往映射段讀寫數據,看是否成功*/  
  28.     strcpy(mapBuf, "Driver Test");//向映射段寫數據  
  29.     memset(buffer, 0, 1024);  
  30.     strcpy(buffer, mapBuf);//從映射段讀取數據  
  31.     printf("buf = %s\n", buffer);//若是讀取出來的數據和寫入的數據一致,說明映射段的確成功了  
  32.       
  33.       
  34.     munmap(mapBuf, 1024);//去除映射  
  35.     free(buffer);  
  36.     close(fd);//關閉文件,最終調用驅動的close  
  37.     return 0;  
  38. }  

 

下面是makefile文件

[plain] view plain copy
  1. ifneq ($(KERNELRELEASE),)  
  2.   
  3. obj-m := mmap_driver.o  
  4.   
  5. else  
  6. KDIR := /lib/modules/3.2.0-52-generic/build  
  7.   
  8. all:  
  9.     make -C $(KDIR) M=$(PWD) modules  
  10. clean:  
  11.     rm -f *.ko *.o *.mod.o *.mod.c *~ *.symvers *.order  
  12.   
  13. 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轉換成物理地址,是模擬了一個硬件設備,即把虛擬設備映射到虛擬地址,在實際中能夠直接使用物理地址。


總結

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

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

③並非全部設備驅動均可以使用mmap來映射,好比像串口和其餘面向流的設備,而且必須按照頁大小進行映射。

相關文章
相關標籤/搜索