Linux驅動mmap記憶體映射__Linux

來源:互聯網
上載者:User
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裝置驅動開發詳解》


最後,各位看官辛苦了

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.