The mmap Device Operation - awokezhou/LinuxPage GitHub Wiki
Linux内存分为内核空间和用户空间,用户空间不能够直接访问内核空间的资源,例如内核中的内存空间、驱动中设备的数据等。Linux提供了多种内核空间与用户空间数据互通的方法,如netlink、system call、file system、ioctl、proc system等。这些方法能够在用户空间与内核空间传递一些稍微简单的数据结构,但是如果要让用户空间与内核空间共享一片较大的内存,以上方法就变得捉襟见肘,需要用到mmap。
mmap可以在用户空间与内核空间创建一块共享内存,用户空间可以直接操作、访问内核空间开辟的一块区域,或者访问设备内存,不存在经过kernel时的多次拷贝。当这片共享内存创建成功,用户空间的程序对其进行read、write,实际上就是在操作内核空间的内存区域或者设备内存。
- 并不是所有的设备都会或者需要将其本身映射,例如串口或是其他流式设备。
- 映射是页式的。kernel是通过页表来管理虚拟地址,所以mmap的内存块必须是页对齐的。
mmap()是file_operations
结构体的一部分,当对文件进行调用mmap()系统调用时,内核或驱动中的mmap()函数执行,类似read()、write()、open()和close()。
在内核中,对文件系统mmap的定义为
int (*mmap) (struct file *filp, struct vm_area_struct *vma);
在用户空间,对mmap系统调用的声明为
mmap(caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset);
filp和文件系统file_operations结构中其他函数如open()、close()并无区别。vma里包含了用户空间用于映射到设备或者内核地址的空间范围信息。
要使用mmap,在驱动或内核中需要做的就是为地址映射构建页表,如果有必要的话,为vma->vm_ops指定操作函数。
构建页表的方法有两种
- 通过调用
remap_pfn_range()
函数一次性构建所有页表 - 通过nopage VMA方法一次构建一个页表
以上每种方法都有其优势和限制,从remap_pfn_range()
开始
一次性构建多个页表的工作由以下两个函数完成
int remap_pfn_range(struct vm_area_struct *vma,
unsigned long virt_addr,
unsigned long prot);
int io_remap_page_range(struct vm_area_struct *vma,
unsigned long virt_addr,
unsigned long phys_addr,
unsigned long size,
pgprot_t prot);
这两个函数的返回值通常都是0,或者错误码。函数中的参数解释如下:
- vma
虚拟内存结构体指针,含有映射范围等信息
- virt_addr
映射起始的用户空间地址,函数就是为virt_addr和virt_addr+size范围内的虚拟地址构建页表
- pfn
要映射的虚拟地址的物理地址对应的页帧号码。页帧号仅仅是物理地址以PAGE_SHIFT右移的bit位。通常,在VMA结构体中的vm_pgoff成员就是该参数所需的值。该值从(pfn<<PAGE_SHIFT)到(pfn<<PAGE_SHIFT)+size的范围内生效
- size
映射尺寸大小,以字节为单位
prot
对创建出来的VMA的保护机制,驱动可以在vma->vm_page_prot获取到该值
创建一个misc设备,并创建一块空间,应用程序读取misc设备,映射这块空间,并读取数据结构中的值
驱动代码
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/miscdevice.h>
typedef struct {
int num;
char buf[256];
} mmmptest_ctx;
#define DEV_NAME "mmptest"
static mmmptest_ctx *sh_mem = NULL;
#define SHARE_MEM_SIZE (PAGE_SIZE * 2)
static struct vm_operations_struct mmptest_vm_ops;
static int mmptest_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
struct page *page = NULL;
unsigned long size = (unsigned long)(vma->vm_end - vma->vm_start);
if (size > SHARE_MEM_SIZE)
{
ret = -EINVAL;
goto err;
}
page = virt_to_page((unsigned long)sh_mem + (vma->vm_pgoff << PAGE_SHIFT));
ret = remap_pfn_range(vma, vma->vm_start, page_to_pfn(page), size, vma->vm_page_prot);
if (ret)
{
goto err;
}
vma->vm_ops = &mmptest_vm_ops;
return 0;
err:
return ret;
}
static struct file_operations mmptest_fops =
{
.owner = THIS_MODULE,
.mmap = mmptest_mmap,
};
static struct miscdevice mmptest_dev =
{
.minor = MISC_DYNAMIC_MINOR,
.name = DEV_NAME,
.fops = &mmptest_fops,
};
static int mmptest_init(void)
{
int ret;
ret = misc_register(&mmptest_dev);
if (ret) {
printk("register misc device error\n");
return ret;
}
printk("register misc ok\n");
sh_mem = kmalloc(SHARE_MEM_SIZE, GFP_KERNEL);
if (!sh_mem) {
printk("kmalloc error\n");
goto err;
}
sh_mem->num = 56;
sprintf(sh_mem->buf, "SDASDACAC");
printk("kmalloc ok\n");
return 0;
err:
misc_deregister(&mmptest_dev);
return ret;
}
static void mmptest_exit(void)
{
kfree(sh_mem);
misc_deregister(&mmptest_dev);
}
module_init(mmptest_init);
module_exit(mmptest_exit);
MODULE_LICENSE("Dual BSD/GPL");
用户空间代码
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
typedef struct {
int num;
char buf[256];
} mmmptest_ctx;
#define DEV_NAME "/dev/mmptest"
static mmmptest_ctx *ctx = NULL;
int main(int argc, char *argv[])
{
int kfd;
kfd = open(DEV_NAME, O_RDWR|O_NDELAY);
if (kfd < 0) {
printf("open file %s error\n", DEV_NAME);
return -1;
}
ctx = (mmmptest_ctx *)mmap(0, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, kfd, 0);
printf("num %d, buf %s\n", ctx->num, ctx->buf);
munmap(ctx, 4096);
return 0;
}
如需在应用层实时监控内核的mmap空间,则需要定时做映射,而不是只做一次映射