mmap常规的应用是将普通文件映射到用户进程空间,提高文件读写效率。linux的思想是“一切皆文件”,对于设备文件而言,同样可以实现mmap映射。一个设备,一般涉及到帧缓存会考虑实现映射接口,常见的LCD framebuffer设备的显存空间,经过映射后,用户进程可以直接操作进程内存空间将LCD显示数据写入,提高刷新效率,节省CPU拷贝内存开销。
1. 驱动mmap方法
进程调用mmap()
函数建立mmap映射,而mmap()
是在内核空间(驱动)实现的,如果驱动层未实现该函数,进程调用时,会返回-ENODEV
错误。mmap
是标准虚拟文件系统(VFS)struct file_operations
提供的接口之一,驱动实现时只需实现该函数指针实体,然后注册到驱动fops
中。
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
int (*mmap) (struct file *, struct vm_area_struct *);
.......
}
进程mmap
到驱动mamp
大体过程
1.1 mmap描述
linux内核采用struct vm_area_struct
数据结构描述内核mmap,原型位于linux/mm_types.h
中。
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */
/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next, *vm_prev;
struct rb_node vm_rb;
/*
* Largest free memory gap in bytes to the left of this VMA.
* Either between this VMA and vma->vm_prev, or between one of the
* VMAs below us in the VMA rbtree and its ->vm_prev. This helps
* get_unmapped_area find a free area of the right size.
*/
unsigned long rb_subtree_gap;
/* Second cache line starts here. */
struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, see mm.h. */
/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap interval tree.
*
* For private anonymous mappings, a pointer to a null terminated string
* in the user process containing the name given to the vma, or NULL
* if unnamed.
*/
union {
struct {
struct rb_node rb;
unsigned long rb_subtree_last;
} shared;
const char __user *anon_name;
};
/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_chain; /* Serialized by mmap_sem &
* page_table_lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */
/* Function pointers to deal with this struct. */
const struct vm_operations_struct *vm_ops;
/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
#ifndef CONFIG_MMU
struct vm_region *vm_region; /* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif
struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
};
mmap描述参数比较多,驱动编程下,我们关注几个参数
- vm_start, 映射进程空间起始地址
- vm_end,映射进程空间结束地址
- vm_page_prot,映射保护属性
- vm_flags,映射访问标识,常用标识如下
VM_IO
,与IO相关映射或者类似的映射
VM_LOCKED
,锁定物理空间,进制swap
VM_DONTEXPAND
,不可通过mremap函数扩展
VM_DONTDUMP
,不包括核心转存储空间
其他访问标识,查看"include/linux/mm.h"中定义。
1.2 构建页表函数
用户进程调用mmap()
函数建立映射后,此时进程空间是没有实际内容的,只有触发缺页中断,最终是在驱动程序中建立页表,通过MMU映射到实际物理内存中才完成整个映射过程。
内存的最小颗粒度是页(page),一页大小一般为4K字节,每一页的编号称为页帧号(page frame number),简称pfn。
建立页表一般有两种构建方法。
【1】使用remap_pfn_range
函数一次建立所有页表
remap_pfn_range
原型位于linux/mm.h
中。
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
unsigned long pfn, unsigned long size, pgprot_t prot);
- vma,映射进程空间描述符
- addr,映射进程空间起始地址
- pfn,映射内核物理地址
- size,映射进程空间大小
- prot,映射保护属性
- 返回,成功返回0
除了内核映射物理空间在驱动层外,其他参数是根据用户调用mmap()
函数传入。
【2】使用nopage VMA方法每次建立一个页表
实现nopage
函数实体,进程触发内核缺页中断时,由内核申请内存中的物理页,由driver在nopage
函数中将page与vma挂钩。
nopage
函数首先计算缺页虚拟内存地址的实际物理地址与映射文件偏移量offset
;检查偏移量有效性(是否超出文件大小);如有效则将该缺页地址的虚拟地址变换成页帧号并申请该页,实际文件内容映射到该内存页上。
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type)
-
vma,映射进程空间描述符
-
address,映射进程空间起始地址
-
type,保存返回错误类似
-
返回,成功则返回一个有效映射页地址,失败返回NULL,错误类型存入type中
两者区别:
remap_pfn_range
不能映射常规内存,只存取保留页和在物理内存顶之上的物理地址。因为保留页和在物理内存顶之上的物理地址内存管理系统的各个子模块管理不到。640 KB 和1MB 是保留页可能映射,设备I/O内存也是可以映射。如果想把kmalloc()
申请的内存映射到用户空间,可以通过mem_map_reserve()
把相应的内存设置为保留后即可。
1.3 虚拟地址与物理地址转换
在linux内核下申请的内存也就是虚拟内存,只不过是属于内核态(32位系统为3G—4G范围),因此构建页表时,需要将内核虚拟内存的转换为物理内存。
常用的内核内存申请方式有三种:
-
kmalloc(),申请的内存物理空间地址是连续的,虚拟空间地址也是连续的;CPU DMA映射内存要求物理空间地址要连续
-
__get_free_pages ,与kmalloc类似
-
vmalloc ,申请的内存虚拟空间地址是连续,实际物理空间地址不一定连续;kmalloc一般用来分配小于128K内存,大内存块则使用vmallco分配
对于连续的物理内存,即是通过kamlloc
和__get_free_pages
申请的内存,可以使用virt_to_phys()
和phys_to_virt()
来实现物理地址和内核虚拟地址之间的相互转换。因为物理地址是连续的,所以上述两者函数仅仅做了简单的3G(32位系统来说)的地址偏移计算;通过vmalloc
分配的大内存块或者高端内存块,则不能通过该函数转换,因为由于物理地址的不连续导致涉及到分离物理页和内存跨页等相关处理,而不是简单的偏移计算。
物理地址转虚拟地址函数phys_to_virt
,对于32位CPU,位于"arch/arm/include/asm/memory.h"中,对于64位CPU,位于"arch/arm64/include/asm/memory.h"中。
#define __virt_to_phys(x) ({ \
phys_addr_t __x = (phys_addr_t)(x); \
__x & BIT(VA_BITS - 1) ? (__x & ~PAGE_OFFSET) + PHYS_OFFSET : \
(__x - kimage_voffset); })
/*
* Note: Drivers should NOT use these. They are the wrong
* translation for translating DMA addresses. Use the driver
* DMA support - see dma-mapping.h.
*/
#define virt_to_phys virt_to_phys
static inline phys_addr_t virt_to_phys(const volatile void *x)
{
return __virt_to_phys((unsigned long)(x));
}
#define phys_to_virt phys_to_virt
static inline void *phys_to_virt(phys_addr_t x)
{
return (void *)(__phys_to_virt(x));
}
2. 实例
基于“Linux 字符驱动之platform框架”文章中的字符设备驱动源码,增加mmap方法实现。简单回顾下该字符驱动的作用:
- 实现一个“软驱动”,通过内核一片物理内存交换多进程数据,实现进程通信(IPC)
- 支持
read/write/ioctl
函数访问
通过mmap方法,进程间通过该字符驱动通信,此时类似于“共享内存”机制,省去“用户与内核”之间拷贝,在进程间交互数据时,效率高。
2.1 增加驱动mmap
回调函数实体
static int memory_drv_mmap(struct file *pfile, struct vm_area_struct *vma)
{
struct memory_device *p;
int ret = 0;
p = pfile->private_data;
vma->vm_flags |= (VM_IO | VM_LOCKED | VM_DONTEXPAND | VM_DONTDUMP);
ret = remap_pfn_range(vma, /* 映射虚拟内存空间 */
vma->vm_start,/* 映射虚拟内存空间起始地址 */
virt_to_phys(p->mem_buf)>>PAGE_SHIFT,/* 与物理内存对应的页帧号,物理地址右移12位 */
(vma->vm_end - vma->vm_start),/* 映射虚拟内存空间大小,页大小的整数倍 */
vma->vm_page_prot);/* 保护属性 */
return ret;
}
设备文件描述信息注册
static const struct file_operations memory_fops =
{
.owner = THIS_MODULE,
.open = memory_drv_open,
.read = memory_drv_read,
.write = memory_drv_write,
.release = memory_drv_close,
.unlocked_ioctl = memory_drv_ioctl,
.llseek = memory_drv_llseek,
.mmap = memory_drv_mmap, /* mmap内存映射 */
};
2.3 修改Makefile
原字符驱动采用的是platform框架,源码包括了platform device
和platform driver
,Makefile把源码文件编译成device.ko和driver.ko文件。现修改源码和Makefile,使得两个源码文件编译成一个devmem.ko模块文件,方便后续使用。
源码修改
- 屏蔽"dev_mem.c"模块初始化函数和析构函数;然后增加"dev_mem.h"头文件,将两个函数声明为全局
module_init(memory_dev_init);
module_exit(memory_dev_exit);
MODULE_LICENSE("GPL");
/* dev_mem.h */
extern int __init memory_dev_init(void);
extern void __exit memory_dev_exit(void);
- 在"drv_mem.c"模块初始化和析构函数分别添加加上述函数
static int __init memory_drv_init(void)
{
memory_dev_init();
return platform_driver_register(&memory_drv);
}
static void __exit memory_drv_exit(void)
{
memory_dev_exit();
platform_driver_unregister(&memory_drv);
}
修改Makefile
ifeq ($(KERNELRELEASE),)
KERNELDIR = /usr/src/linux-headers-4.15.0-91-generic
PWD := $(shell pwd)
modules:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
clean:
rm -rf *.o *.ko .mod.o *.mod.c *.symvers *.order
else
obj-m += devmem.o
devmem-objs = dev_mem.o drv_mem.o
endif
注:
如果编译到本地Ubuntu执行,可以先执行"uname -a"查看系统内核版本,然后修改Makefile内核路径"KERNELDIR"。因为系统如果不关闭自动更新功能,在联网情况下可能已经自动更新内核了,编译后加载驱动会因为内核版本不一致导致失败。比如,之前内核版本是"linux-headers-4.15.0-88",现在可能已经更新为"linux-headers-4.15.0-91"。
2.4 app程序
fork一个进程,open设备文件"/dev/dev_mem",将设备文件内核空间映射到用户进程空间。父进程向映射空间写数据,子进程读取数据;父子进程读写数据通过信号量同步,父进程写完数据后通知子进程读数据。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/ioctl.h>
#include <string.h>
#include <semaphore.h>
#include <linux/ioctl.h>
#include <sys/mman.h>
#define DEV_MEM_MEMSET _IO('M', 0)
#define DEV_MEM_GET_SIZE _IOR('M', 1, int)
#define DEV_MEM_SET_SIZE _IOW('M', 2, int)
sem_t *r_sem;
int main(int argc, char** argv)
{
int pid;
char buf[16] = {0};
int fd;
int mem_size = 0;
char *mmap_mem = NULL;
fd = open("/dev/dev_mem", O_RDWR);
if (-1 == fd)
{
perror("open error");
return -2;
}
printf("open \"dev_mem\" success\n");
r_sem = sem_open("notempty", O_CREAT|O_RDWR, 0777, 0);
if(r_sem == SEM_FAILED)
{
perror("sem_open");
close(fd);
return -1;
}
ioctl(fd, DEV_MEM_GET_SIZE, &mem_size); /* 获取dev_mem 内存空间大小 */
/* 将设备物理空间映射到进程虚拟空间 */
mmap_mem = (char *) mmap (NULL, mem_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
memset(mmap_mem, 0, mem_size); /* 清空内存 */
pid = fork();
if (pid > 0)
{
printf("Write [%s] to dev_mem mmap\n", "Parent write message");
sprintf(mmap_mem, "%s", "Parent write message");
sem_post(r_sem);
wait(NULL);
}
else if (pid == 0)
{
sem_wait(r_sem);
printf("Read dev_mem mmap:%s\n", mmap_mem);
close(fd);
}
else
{
perror("fork");
close(fd);
}
/* 释放映射区 */
munmap(mmap_mem, mem_size);
return 0;
}
2.5 编译执行
- 进入源码目录,执行"make -j4"编译生成"devmem.ko"驱动模块
- 执行"sudo insmod devmem.ko"加载驱动,加载成功后在"/dev"目录生成"dev_mem"设备
- 执行"gcc app.c -o app -lpthread"编程测试应用程序,编译成生成"app"执行文件
- 执行"sudo ./app"执行测程序
- 执行结果如下图
2.5 代码仓库
【1】https://github.com/Prry/linux-drivers
3. 参考
【1】 Linux驱动mmap内存映射