在介绍mmap之前,我们首先要了解,如果没有mmap,在我们对一个设备进行操作时,内核中其实有映射一份该设备的物理内存地址,这是一份虚拟地址,但是这是属于内核的,我们的应用程序是无法直接来对其进行操作的,如果操作起来,势必会消耗很大的资源。因此,为了方便应用程序对设备的操作,于是就有了mmap。
mmap其实是一个字符设备提供的一个接口,通过这个接口,我们可以将原本设备映射到内核空间中的虚拟地址,再映射一份到用户空间,这样就可以通过直接操作用户空间映射的虚拟空间来实现对设备物理内存的访问,提高了操作效率。
有关mmap的具体使用方法,接下来通过一个小例子来详细说明下。
我通过在内核中申请一片空间,然后通过mmap映射到用户空间中,在用户空间中对映射而来的虚拟地址空间进行一个写入值的操作,然后在内核中使用printk打印信息查看是否有变化,以此来验证mmap的功能。
下面是全部的驱动代码
#include <linux/module.h>
#include <linux/init.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/uaccess.h>
#include <linux/io.h>
#include <linux/mm.h>
#include <linux/memory.h>
#include <linux/slab.h>
#include <linux/device.h>
MODULE_LICENSE("GPL");
#define KBUF_MAX 8
char kbuf[KBUF_MAX] = "abcdefg";
dev_t devnumber; //字符设备的设备号,先申请一个空间
unsigned baseminor = 0; //次设备号,自己设定为0号
unsigned devicecount = 1; //设备的数量
char *kptr = NULL; //此处定义一个指针,后面用它去在内核中申请一片空间
struct cdev mydevice; //代表着一个设备的驱动程序
typedef struct {
unsigned int conaddr;
unsigned int dataddr;
unsigned int *pcon;
unsigned int *pdat;
unsigned int ledpin;
}LEDRES;
LEDRES myledres[1] = {//led3------- GPX1_0
{
.conaddr = 0x11000c20,
.dataddr = 0x11000c24,
.pcon = NULL,
.pdat = NULL,
.ledpin = 0,
},
};
ssize_t dev_read(struct file *f,char __user *buf,size_t length ,loff_t *pos)
{
long n;
printk("length = %lu,*pos = %lld\n ",length,*pos);
n = copy_to_user(buf,kbuf,length);
printk("after copy_to_user , n = %ld\n",n);
printk("dev read...\n");
return length-n;
}
ssize_t dev_write(struct file *f,const char __user *buf,size_t length ,loff_t *pos)
{
long n;
if(length > KBUF_MAX)
{
length = KBUF_MAX;
}
n = copy_from_user(kbuf,buf,length);
printk("after copy_from_user , n = %ld\n ,kbuf = %s\n",n,kbuf);
printk("dev write...\n");
return length - n;
}
int dev_open(struct inode *nod ,struct file *f)
{
printk("dev open ...\n");
return 0;
}
int dev_close(struct inode *nod ,struct file *f)
{
printk(" in kernel, kptr = %s\n",kptr);
printk("dev close ...\n");
return 0;
}
int dev_mmap (struct file *f, struct vm_area_struct *vvv)
{
remap_pfn_range(vvv,vvv->vm_start,virt_to_phys(kptr)>>12,vvv->vm_end-vvv->vm_start,vvv->vm_page_prot);
return 0;
}
struct file_operations dev_ops = {
.release = dev_close,
.read = dev_read,
.write = dev_write,
.open = dev_open,
.mmap = dev_mmap,
};
//int init_module()
static int __init test_init(void)
{
int ret = -1;
printk("dev, init .....\n");
ret = alloc_chrdev_region(&devnumber,baseminor,devicecount,"test"); //动态分配设备号
//在这里,第一个参数是传入为了将系统给分配的设备号给带出来
//此处的,第二个、第三个、都是传入的给定的值,自己限定的
//第四个也是自己给定的名字
if(ret < 0)
goto ALLOC_ERROR;
printk("devnumber = %x , majorno = %x , minorno = %x\n",devnumber,MAJOR(devnumber),MINOR(devnumber));
cdev_init(&mydevice,&dev_ops);
ret = cdev_add(&mydevice, devnumber,devicecount);
//添加到系统字符设备列表中
if(ret < 0 )
goto CDEV_ADD_ERROR;
kptr = kmalloc(4096,GFP_KERNEL);
memset(kptr,0,4096);
printk("dev, init ..... kptr = %s\n",kptr);
return 0;
CDEV_ADD_ERROR:
cdev_del(&mydevice);
ALLOC_ERROR:
unregister_chrdev_region(devnumber,devicecount);
return -1;
}
static void __exit test_exit(void)
{
kfree(kptr);//释放内核里kmalloc 申请的内存
cdev_del(&mydevice);
unregister_chrdev_region(devnumber,devicecount);
printk("dev,exit...\n");
}
module_init(test_init);
module_exit(test_exit);
应用程序代码
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
#define MAX_LENGTH 8
char rbuf[MAX_LENGTH] = {0};
char wbuf[MAX_LENGTH] = "1234567";
int main()
{
int fd;
int count;
char *uptr;
fd = open("/dev/test",O_RDWR);
perror("open:");
count = read(fd,rbuf,MAX_LENGTH);
perror("read:");
printf(" count = %d,rbuf = %s\n",count,rbuf);
count = write(fd, wbuf, strlen(wbuf));
printf("write, count = %d\n",count);
perror("write:");
//对由内核空间映射地址的操作
uptr = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
strcpy(uptr,"hijklmn");
close(fd);
perror("close:");
return 0;
}
在驱动的入口函数处,用指针kptr去指向了一片有kmalloc创建的空间,清空后并打印输出了其值,以便后续做比较
kptr = kmalloc(4096,GFP_KERNEL);
memset(kptr,0,4096);
printk("dev, init ..... kptr = %s\n",kptr);
驱动中具体实现映射,主要是调用了remap_pfn_range函数,将其封装在dev_mmap函数之中,并在结构体struct file_operations dev_ops之中初始化。
int dev_mmap (struct file *f, struct vm_area_struct *vvv)
{
remap_pfn_range(vvv,vvv->vm_start,virt_to_phys(kptr)>>12,vvv->vm_end-vvv->vm_start,vvv->vm_page_prot);
return 0;
}
struct file_operations dev_ops = {
.release = dev_close,
.read = dev_read,
.write = dev_write,
.open = dev_open,
.mmap = dev_mmap,
};
有关remap_pfn_range函数,其原型如下
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);
参数1:vma是用来描述一片映射区域的结构体指针
参数2:addr是用户指定的映射后的虚拟地址起始地址,若用户未指定,则由内核指定
参数3:物理内存所对应的页框号(将物理地址除以页大小的所得值)
参数4:想要映射的空间大小
参数5:内存区域的访问权限
在用户空间中调用mmap函数,或得映射地址指针,并对所得区域进行赋值
uptr = mmap(NULL,4096,PROT_READ|PROT_WRITE,MAP_SHARED, fd, 0);
strcpy(uptr,"hijklmn");
执行下面这三步
下面来看看效果如何,使用dmesg | tail 查看内核缓冲区信息
起初
后来
效果理想!