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 4096static 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);//初始化字元裝置cdevcdev->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/mapsbuffer = (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);//關閉檔案,最終調用驅動的closereturn 0;}
下面是makefile檔案
ifneq ($(KERNELRELEASE),)obj-m := mmap_driver.oelseKDIR := /lib/modules/3.2.0-52-generic/buildall:make -C $(KDIR) M=$(PWD) modulesclean:rm -f *.ko *.o *.mod.o *.mod.c *~ *.symvers *.orderendif
下面命令示範一下驅動程式的編譯、安裝、測試過程(註:其他使用者在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來映射,比如像串口和其他面向流的裝置,並且必須按照頁大小進行映射。
參考資料
《linux核心設計與實現》
《深入理解電腦系統》
《linux裝置驅動程式》
《深入理解linux核心》
《程式員的自我修養》
《linux裝置驅動開發詳解》
最後,各位看官辛苦了