從零開始之驅動發開、linux驅動(三十、mmap使用舉例)
上節學習了mmap的對映原理,我們知道mmap對映分為四步:
1.在程序的虛擬地址空間的,建立虛擬對映區域(vm_area_struct)
2.檔案實體地址和程序虛擬地址的一一對映關係(remap_pfn_range 將核心記憶體重新對映到使用者空間)
3.程序發起對這片對映空間的訪問,引發缺頁異常,實現檔案內容到實體記憶體(主存)的拷貝,
4.系統延遲同步或強制同步(munmap或msync)。
通俗點來說就是,先根據使用者層的mmap指定大小來找到滿足該程序的一塊虛擬空間,大小,位置都儲存在一個vm_area_struct 結構中,
呼叫驅動中的mmap來實現vm_area_struct 中的虛擬地址和檔案地址的繫結。
,
舉例1.利用寫記憶體方式寫檔案
原理:把一個檔案對映到該程序的虛擬地址空間,利用操縱虛擬地址來讀寫檔案。
#include <stdio.h> #include <sys/stat.h> #include <sys/mman.h> #include <fcntl.h> #include <stdlib.h> int main(int argc, char *argv[]) { int fd = -1; int i; char *mmaped = NULL; char *mmaped = NULL; fd = open(argv[1], O_RDWR); if (fd < 0) { fprintf(stderr, "open %s fail\n", argv[1]); exit(-1); } fd = open(argv[1], O_RDWR); if (fd < 0) { fprintf(stderr, "open %s fail\n", argv[1]); exit(-1); } /* 將檔案對映至程序的地址空間 */ mmaped = (char *)mmap(NULL, 500, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (mmaped == (char *)-1) { fprintf(stderr, "mmap fail\n"); goto err; } /* 對映完後, 關閉檔案也可以操縱記憶體 */ close(fd); /* 打印出來檔案字元 */ printf("%s", mmaped); /* 修改10~20個字元為$符號 */ for(i = 10;i < 20; i++) mmaped[i] = '$'; /* 同步mmap對映的檔案從記憶體寫到硬碟檔案中 */ if (msync(mmaped, 500, MS_SYNC) < 0) { fprintf(stderr, "msync fail\n"); goto err; } return 0; err: if (fd > 0) close(fd); if (mmaped != (char *)-1) munmap(mmaped, 500); return -1; }
測試結果如下:
可以看到關掉檔案後仍然可以寫,並且是以寫記憶體的方式進行的寫操作。
舉例2.直接在應用層操縱硬體暫存器
led字元驅動
#include <linux/fs.h> /* 包含file_operation結構體 */ #include <linux/init.h> /* 包含module_init module_exit */ #include <linux/module.h> /* 包含LICENSE的巨集 */ #include <asm/uaccess.h> #include <linux/io.h> #include <linux/device.h> #include <linux/gpio.h> #include <mach/gpio.h> #include <asm/gpio.h> #include <linux/gfp.h> #include <linux/mm.h> //remap_pfn_range #include <linux/cdev.h> static dev_t dev_no; /* 裝置號 */ static struct cdev led_cdev_t; /* 字元裝置 */ static struct class *leds_class; /* 類 */ static struct device *led_dev; /* 裝置模型 */ /* open函式 */ static int leds_drv_open(struct inode *inode, struct file *file) { printk(KERN_INFO"leds_drv_open sucess! \n"); return 0; } /* 硬體暫存器地址對映 */ int leds_drv_mmap(struct file *file, struct vm_area_struct *vma) { //表示對裝置IO空間的對映 vma->vm_flags |= VM_IO; /* 區域不能被換出 */ vma->vm_flags |= (VM_DONTEXPAND | VM_DONTDUMP); //替代來VM_RESERVED標誌,3.8以後核心刪除了這個 printk(KERN_INFO"leds_drv_mmap \n"); if(remap_pfn_range(vma, //虛擬記憶體區域,即裝置地址將要對映到這裡 vma->vm_start, //虛擬空間的起始地址 vma->vm_pgoff, //與實體記憶體對應的頁幀號,實體地址右移12位 vma->vm_end - vma->vm_start, //對映區域大小,一般是頁大小的整數倍 vma->vm_page_prot)) //保護屬性, { return -EAGAIN; } return 0; } static const struct file_operations leds_drv_file_operation = { .owner = THIS_MODULE, .open = leds_drv_open, .mmap = leds_drv_mmap, }; static int __init leds_drv_init(void) { int ret; /* 獲取一個自動的主裝置號 */ ret = alloc_chrdev_region(&dev_no, 0, 1, "leds_dev"); if(ret) { printk(KERN_INFO"alloc_chrdev_region \n"); goto err_alloc_chrdev_region; } /* 初始化led_cdev */ cdev_init(&led_cdev_t, &leds_drv_file_operation); /* 把字元裝置加入到字元裝置陣列中 */ ret = cdev_add(&led_cdev_t, dev_no, 1); if(ret) { printk(KERN_INFO"cdev_add \n"); goto err_cdev_add; } /* 建立一個類 */ leds_class = class_create(THIS_MODULE, "leds_class"); if(!leds_class) { printk("class_create leds fail\n"); goto err_class_create; } /* 建立從屬這個類的裝置 */ led_dev = device_create(leds_class,NULL, dev_no , NULL, "leds"); if(!led_dev[0]) { goto err_device_create_led; } return 0; /* 倒影式錯誤處理機制 */ err_device_create_led: class_destroy(leds_class); err_class_create: cdev_del(&led_cdev_t); err_cdev_add: unregister_chrdev_region(dev_no, 1); err_alloc_chrdev_region: return -EIO; } static void __exit leds_drv_exit(void) { device_unregister(led_dev); /* 登出類 */ class_destroy(leds_class); /* 登出字元裝置 */ unregister_chrdev_region(dev_no, 1); cdev_del(&led_cdev_t); } module_init(leds_drv_init); module_exit(leds_drv_exit); MODULE_LICENSE("GPL");
led測試應用
#include <stdio.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
int fd = -1;
int i;
char *mmaped = NULL;
unsigned int *reg = NULL;
fd = open(argv[1], O_RDWR);
if (fd < 0) {
fprintf(stderr, "open %s fail\n", argv[1]);
exit(-1);
}
/* 將裝置對映至程序的地址空間,0xe0200000是gpio的物理暫存器基址 */
mmaped = (char *)mmap(NULL, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0xe0200000);
if (mmaped == (char*)-1) {
fprintf(stderr, "mmap fail\n");
close(fd);
return -1;
}
close(fd); /* 這裡關掉,即字元裝置已經解除安裝 */
reg = (unsigned int *)&mmaped[0x240]; /* gpj0暫存器的基址 */
/* 設定gpj0con暫存器為輸出模式 */
(*reg) &= ~(0xf <<12);
(*reg) |= (1 << 12);
reg ++; /* gpj0dat */
/* 設定led燈的亮滅 */
if(*argv[2] == '0')
*reg |= (1<<3);
else
(*reg) &= ~(1<<3);
/* 同步mmap對映的檔案從記憶體寫到硬碟檔案中 */
msync(mmaped, 0x1000, MS_SYNC);
return 0;
}
可以看到即使我們解除安裝裝置驅動,但仍然可以操縱被對映成虛擬地址的物理暫存器。
最後我們大概說一下對映函式的原理。
/**
* remap_pfn_range - remap kernel memory to userspace
* @vma: user vma to map to
* @addr: target user address to start at
* @pfn: physical address of kernel memory
* @size: size of map area
* @prot: page protection flags for this mapping
*
* Note: this is only safe if the mm semaphore is held when called.
*/
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot);
程序需要一塊空間來對映某個檔案(假設以頁的整數倍類對映),程序會在0~3G的使用者虛擬空間找到我們要對映大小的空間,並申請一個vm_area_struct 用來管理這塊空間。
如果是我們例子1中的普通檔案,訪問檔案時因為系統會把檔案調入記憶體,通過轉換得到這塊記憶體的實體地址,然後再把這塊實體地址,通過建立頁表對映給程序到使用者空間地址,之後使用者空間的寫地址操作,寫的就是檔案本身調入核心時的那塊實體地址上的。
普通檔案寫不同的是,普通檔案寫時,首先把字元寫到使用者空間的一個數組中,先要通過write系統呼叫,通過copy_form_user把使用者空間的拷貝到檔案調入核心時的記憶體上。
接下來看我們例子2中的裝置檔案,我們要說的是最後一個引數,這個引數必須是以頁對齊的引數。如果對映的是普通檔案,則mmap返回值是從檔案開頭到這個偏移位置的地址,即在調入記憶體的檔案開頭+偏移的地址。但我們是裝置檔案,所以我傳入的是暫存器的實體地址,它將來會被又移12位,得到物理頁號。最終設定到vma->vm_pgoff上。因為驅動是我們自己寫,所以我門直接對映這塊物理頁地址到程序的一塊未使用的虛擬地址上就可以了(也可以不使用vma->vm_pgoff,在驅動中指定暫存器實體地址所在頁)
下一節我們分析framebuffer驅動的mmap函式。