简介
平时我们写Linux驱动和用户空间交互时,都是使用read
或者write
函数接口,并且在函数内使用copy_to_user
或者copy_from_user
。因为用户空间是不能直接访问内核空间数据的,只能先将数据拷贝过来,然后再操作。如果用户空间需要传几MB的数据给内核,那么原来的拷贝方式显然效率比较低,这个时候就需要使用mmap
操作,简单来说就是让一块物理内存拥有两份映射,即拥有两个虚拟地址,一个在内核空间,一个在用户空间,这样用户空间和内核空间就可以访问同一块内存了。关系如下
mmap
系统调用并不是完全为了用于共享内存而设计的,它本身提供了不同于一般对普通文件的访问方式,普通文件被映射到进程地址空间后,进程可以像访问普通内存一样对文件进行访问,不必再调用read
或write
操作。
函数接口
// include<sys/mman.h>
//成功执行时,mmap()返回被映射区的指针,munmap()返回0。
//失败时,mmap()返回MAP_FAILED[其值为(void *)-1],munmap返回-1。
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void *addr, size_t length);
start:要映射到的内存区域的起始地址,通常都是用NULL(NULL即为0)。NULL表示由内核来指定该内存地址。
length:要映射的内存区域的大小
prot:期望的内存保护标志,不能与文件的打开模式冲突。是以下的某个值,可以通过or运算合理地组合在一起。
PROT_EXEC
//页内容可以被执行PROT_READ
//页内容可以被读取PROT_WRITE
//页可以被写入PROT_NONE
//页不可访问flags:指定映射对象的类型,映射选项和映射页是否可以共享。它的值可以是一个或者多个以下位的组合体。
MAP_FIXED
:使用指定的映射起始地址,如果由start和len参数指定的内存区重叠于现存的映射空间,重叠部分将会被丢弃。如果指定的起始地址不可用,操作将会失败。并且起始地址必须落在页的边界上。MAP_SHARED
:对映射区域的写入数据会复制回文件内, 而且允许其他映射该文件的进程共享。MAP_PRIVATE
:建立一个写入时拷贝的私有映射。内存区域的写入不会影响到原文件。这个标志和以上标志是互斥的,只能使用其中一个。MAP_DENYWRITE
:这个标志被忽略。MAP_EXECUTABLE
:同上 。MAP_NORESERVE
:不要为这个映射保留交换空间。当交换空间被保留,对映射区修改的可能会得到保证。当交换空间不被保留,同时内存不足,对映射区的修改会引起段违例信号。MAP_LOCKED
:锁定映射区的页面,从而防止页面被交换出内存。MAP_GROWSDOWN
:用于堆栈,告诉内核VM系统,映射区可以向下扩展。MAP_ANONYMOUS
:匿名映射,映射区不与任何文件关联。MAP_ANON
:MAP_ANONYMOUS
的别称,不再被使用。MAP_FILE
:兼容标志,被忽略。MAP_32BIT
:将映射区放在进程地址空间的低2GB,MAP_FIXED指定时会被忽略。当前这个标志只在x86-64平台上得到支持。MAP_POPULATE
:为文件映射通过预读的方式准备好页表。随后对映射区的访问不会被页违例阻塞。MAP_NONBLOCK
:仅和MAP_POPULATE
一起使用时才有意义。不执行预读,只为已存在于内存中的页面建立页表入口。fd:文件描述符(由open函数返回),匿名映射时设为-1。
offset:表示被映射对象(即文件)从那里开始对映,通常都是用0。 该值应该为大小为PAGE_SIZE的整数倍。
start:要进行同步的映射的内存区域的起始地址。
length:要同步的内存区域的大小
flag:flags可以为以下三个值之一:
MS_ASYNC : 请Kernel快将资料写入。
MS_SYNC : 在msync结束返回前,将资料写入。
MS_INVALIDATE : 让核心自行决定是否写入,仅在特殊状况下使用
int msync(const void *start, size_t length, int flags);
对映射内存的内容的更改并不会立即更新到文件中,而是有一段时间的延迟,你可以调用msync()来显式同步一下, 这样你内存的更新就能立即保存到文件里。
vma:用户虚拟内存区域,由内核自动填充。
addr:用户空间中内存映射的起始地址。
pfn:要映射的内核内存的起始页帧号,是物理地址右移PAGE_SHIFT位得到的。
size:映射的内存大小。
prot:页表项的保护属性
int remap_pfn_range(struct vm_area_struct *vma,unsigned long addr,unsigned long pfn,unsigned long size,pgprot_t prot);
示例
驱动层代码示例
#include "haptic_misc.h"
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/sysfs.h>
#include <linux/mm.h>
#include <linux/gfp.h>
#include <linux/slab.h>
#include <linux/miscdevice.h>
#define HAPTICS_MISC_DEV_NAME "haptics"
#define BUFF_SIZE (10)
static unsigned char * buffer=NULL;
//打开设备
static int haptics_open(struct inode* inode,struct file * filp)
{
pr_info("%s\n",__FUNCTION__);
return 0;
}
//关闭设备
static int haptics_release(struct inode* inode ,struct file* filp)
{
pr_info("%s\n",__FUNCTION__);
return 0;
}
//读设备
static ssize_t haptics_read(struct file* filp, char __user *buf,size_t count,loff_t* ppos)
{
int ret;
int tmp = count ;
if (tmp > BUFF_SIZE)
tmp = BUFF_SIZE;
ret = copy_to_user(buf, buffer, tmp);
return tmp;
}
//写设备
static ssize_t haptics_write(struct file* filp,const char __user *buf,size_t count,loff_t* ppos)
{
int ret;
int tmp = count ;
if (tmp > BUFF_SIZE )
tmp = BUFF_SIZE ;
memset(buffer,0,BUFF_SIZE);
ret = copy_from_user(buffer, buf, tmp);
return tmp;
}
static int haptics_mmap(struct file *file, struct vm_area_struct *vma)
{
unsigned long page;
unsigned long start = (unsigned long)vma->vm_start;
unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);
//得到物理地址
page = virt_to_phys(buffer);
//将用户空间的一个vma虚拟内存区映射到以page开始的一段连续物理页面上
if(remap_pfn_range(vma,start,page>>PAGE_SHIFT,size,PAGE_SHARED))//第三个参数是页帧号,由物理地址右移PAGE_SHIFT得到
return -1;
return 0;
}
static struct file_operations haptics_fops=
{
.owner = THIS_MODULE,
.open = haptics_open,
.release = haptics_release,
.read = haptics_read,
.write = haptics_write,
.mmap = haptics_mmap,
};
static struct miscdevice mdev =
{
.minor = MISC_DYNAMIC_MINOR,
.name = HAPTICS_MISC_DEV_NAME,
.fops = &haptics_fops,
};//定义一个杂项设备结构体
int register_misc_dev(void)
{
int ret = 0;
ret = misc_register(&mdev);
if(0 == ret)
{
pr_info("misc_register ok minor=%d\n",mdev.minor);
}
buffer = kzalloc(PAGE_SIZE,GFP_KERNEL);
memcpy(buffer,"i am .....",BUFF_SIZE);
return ret;
}
int deregister_misc_dev(void)
{
misc_deregister(&mdev);
kfree(buffer);
pr_info("%s\n",__FUNCTION__);
return 0;
}
应用层代码示例
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include "haptics_ioctl.h"
/*
argc:应用程序参数个数,包括应用程序本身
argv[]:具体的参数内容,字符串形式
./main <filename> <r:w> r表示读 w表示写
*/
#define PAGE_SIZE 4096
static char msg[10]="abcdefghjk";
int main(int argc,char * argv[])
{
char* filename;
int fd=0;
if(argc!=3)
{
printf("error usage\n");
return -1;
}
filename=argv[1];
fd = open(filename,O_RDWR);
if(fd<0)
{
printf("can not open file %s\n",filename);
return -2;
}
char* addr = mmap(0, PAGE_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,fd, 0);
if(0 == strcmp(argv[2],"r"))//设备文件测试读接口
{
char buf[10]={0};
read(fd,buf,10);
printf("read %s\n", buf);
}else if(0 == strcmp(argv[2],"w"))//设备文件测试写接口
{
char buf[10]={0};
memcpy(buf,msg,10);
write(fd,buf,10);
printf("read %s\n", addr);
}else//普通文件测试接口
{
printf("read %s\n", addr);
char msg[128] = "hello i am new kernel driver......hahaha";
memcpy(addr,msg,128);
}
close(fd);
}
mmap
设备文件示例
root@RK3588:/bin# ./main /dev/haptics r
[ 101.150745] haptics_open
read i am .....!
[ 101.151100] haptics_release
root@RK3588:/bin# ./main /dev/haptics w
[ 104.609699] haptics_open
read abcdefghjk
[ 104.610043] haptics_release
mmap
普通文件示例
root@RK3588:/bin# vi test
root@RK3588:/bin# ./main test 1
read i am kernel driver.....hahaha
root@RK3588:/bin# cat test
hello i am new kernel driver..root@RK3588:/bin#
总结
- 映射内存大小的基本单位的是页,也就是
PAGE_SIZE
的整数倍。 - 我们一般会先申请一块连续的、大小是
PAGE_SIZE
的整数倍的虚拟内存,但是有的文档上写着需要SetPageReserved
,这个地方还需要继续深入。。。。。。 - 对于普通文件映射时,修改内存内容后,是否不会立即更新到文件中,而是有一段延时,需要调用
msync
函数来显示同步。这个地方也还没有验证。。。。。。 - 对于设备文件映射时,如果应用层修改了映射内存的内容,驱动层好像是没有通知之类的东西去知道的。这里一般的做法是,在映射内存中定义一个flag,驱动层可以去轮询检查flag的值,就知道映射内存是否有更新了。