The mmap Device Operation - awokezhou/LinuxPage GitHub Wiki

概述

Linux内存分为内核空间和用户空间,用户空间不能够直接访问内核空间的资源,例如内核中的内存空间、驱动中设备的数据等。Linux提供了多种内核空间与用户空间数据互通的方法,如netlink、system call、file system、ioctl、proc system等。这些方法能够在用户空间与内核空间传递一些稍微简单的数据结构,但是如果要让用户空间与内核空间共享一片较大的内存,以上方法就变得捉襟见肘,需要用到mmap。

mmap可以在用户空间与内核空间创建一块共享内存,用户空间可以直接操作、访问内核空间开辟的一块区域,或者访问设备内存,不存在经过kernel时的多次拷贝。当这片共享内存创建成功,用户空间的程序对其进行read、write,实际上就是在操作内核空间的内存区域或者设备内存。

局限性

  1. 并不是所有的设备都会或者需要将其本身映射,例如串口或是其他流式设备。
  2. 映射是页式的。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()开始

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空间,则需要定时做映射,而不是只做一次映射

⚠️ **GitHub.com Fallback** ⚠️