上节学习了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函数。