将非连续物理内存映射到用户空间

问题描述 投票:0回答:1

我最近正在阅读 Linux 设备驱动程序第 3 版,并已阅读第 15 章:内存映射和 DMA。

我还遇到过 linux-kernel-labs,特别是他们在 内存映射实验室中的练习。

我尝试做第二个练习,即实现一个将非连续物理内存(例如通过

vmalloc()
获得)映射到用户空间的设备驱动程序。

书上读到

vmalloc()
并没有获得物理上连续的内存,所以每个页面都需要单独映射。

这是我的尝试 -

/*
 * PSO - Memory Mapping Lab(#11)
 *
 * Exercise #2: memory mapping using vmalloc'd kernel areas
 */

#include <linux/version.h>
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
#include <linux/sched.h>
#include <linux/sched/mm.h>
#include <linux/mm.h>
#include <asm/io.h>
#include <linux/uaccess.h>
#include <linux/proc_fs.h>
#include <linux/seq_file.h>


MODULE_DESCRIPTION("simple mmap driver");
MODULE_AUTHOR("PSO");
MODULE_LICENSE("Dual BSD/GPL");

#define MY_MAJOR    42

/* how many pages do we actually vmalloc */
#define NPAGES      16

/* character device basic structure */
static struct cdev mmap_cdev;

/* pointer to the vmalloc'd area, rounded up to a page boundary */
static char *vmalloc_area;

static int my_open(struct inode *inode, struct file *filp)
{
    return 0;
}

static int my_release(struct inode *inode, struct file *filp)
{
    return 0;
}

static int my_mmap(struct file *filp, struct vm_area_struct *vma)
{
    int i;
    long length = vma->vm_end - vma->vm_start;
    unsigned long start = vma->vm_start;
    char *vmalloc_area_ptr = vmalloc_area;
    unsigned long pfn;

    if (length > NPAGES * PAGE_SIZE)
        return -EIO;

    /* TODO 1: map pages individually */

    for (i = 0; i < length; i += PAGE_SIZE) {
        pfn = vmalloc_to_pfn(vmalloc_area_ptr + i); 
        remap_pfn_range(vma, vma->vm_start + i, pfn, PAGE_SIZE, vma->vm_page_prot);
    }

    return 0;
}

static const struct file_operations mmap_fops = {
    .owner = THIS_MODULE,
    .open = my_open,
    .release = my_release,
    .mmap = my_mmap,
};

static int __init my_init(void)
{
    int ret = 0;
    int i;

    ret = register_chrdev_region(MKDEV(MY_MAJOR, 0), 1, "maps");
    if (ret < 0) {
        pr_err("could not register region\n");
        goto out;
    }

    /* TODO 1: allocate NPAGES using vmalloc */

    vmalloc_area = (char *) vmalloc(NPAGES * PAGE_SIZE);
    if (!vmalloc_area) {
        pr_err("Failed to allocate vmalloc area\n");
        ret = -ENOMEM;
        goto out_unreg;
    }

    /* TODO 1: mark pages as reserved */
    
    for (i = 0; i < NPAGES * PAGE_SIZE; i += PAGE_SIZE) {
        SetPageReserved(vmalloc_to_page((void*) vmalloc_area + i)); 
    }

    /* TODO 1: write data in each page */

    for (i = 0; i < NPAGES * PAGE_SIZE; i += PAGE_SIZE) {
        vmalloc_area[i + 0] = 0xdd; 
        vmalloc_area[i + 1] = 0xcc; 
        vmalloc_area[i + 2] = 0xbb; 
        vmalloc_area[i + 3] = 0xaa; 
    }

    cdev_init(&mmap_cdev, &mmap_fops);
    mmap_cdev.owner = THIS_MODULE;
    ret = cdev_add(&mmap_cdev, MKDEV(MY_MAJOR, 0), 1);
    if (ret < 0) {
        pr_err("could not add device\n");
        goto out_vfree;
    }

    return 0;

out_vfree:
    vfree(vmalloc_area);
out_unreg:
    unregister_chrdev_region(MKDEV(MY_MAJOR, 0), 1);
out:
    return ret;
}

static void __exit my_exit(void)
{
    int i;

    cdev_del(&mmap_cdev);

    /* TODO 1: clear reservation on pages and free mem.*/

    if (vmalloc_area) {
        for (i = 0; i < NPAGES * PAGE_SIZE; i += PAGE_SIZE) {
            ClearPageReserved(vmalloc_to_page((void*)vmalloc_area + i)); 
        }
        vfree(vmalloc_area);
    }

    unregister_chrdev_region(MKDEV(MY_MAJOR, 0), 1);
}

module_init(my_init);
module_exit(my_exit);

写入每个页面的前 4 个字节的目的是这样我可以在映射内存后测试用户空间中的这些值。

这是我为测试该驱动程序而编写的程序 -

#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/mman.h>

int main(void) {
    int fd, i, page_size = getpagesize();
    void* mapped_memory = NULL;

    fd = open("/dev/maps0", O_RDONLY);
    if (fd < 0) {
        printf("Failed to open /dev/maps\n");
        return -1;
    }
    mapped_memory = mmap(NULL, page_size*16, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    if (mapped_memory == MAP_FAILED) {
        printf("Mapping failed\n");
        return -1;
    }
    printf("Mapped memory is at %p\n", mapped_memory);
    printf("[%x]\n", ((char*)mapped_memory)[0]);
   
    return 0;
}

问题是,当我加载驱动程序并尝试使用程序测试它时,它崩溃了并且我得到以下输出 -

Mapped memory is at 0x7f502b436000
Bus error (core dumped)

任何人都可以指出我做错了什么吗?

附注我知道这本书使用了

nopage
vm_operations_struct
函数,但我想跟随实验室并尝试按照自己的方式进行操作。

c linux linux-kernel linux-device-driver mmap
1个回答
0
投票

remap_pfn_range()

 中有一个检查,确保如果映射是写时复制 (CoW),则重新映射的请求范围必须恰好从 vma->vm_start
vma->vm_end
(即,它必须是物理上的)连续)。

/* [...] * * There's a horrible special case to handle copy-on-write * behaviour that some programs depend on. We mark the "original" * un-COW'ed pages by matching them up with "vma->vm_pgoff". * See vm_normal_page() for details. */ if (is_cow_mapping(vma->vm_flags)) { if (addr != vma->vm_start || end != vma->vm_end) return -EINVAL; vma->vm_pgoff = pfn; }
如果映射的 

vma->vm_flags

 没有设置 
VM_SHARED
 但设置了 
VM_MAYWRITE
(即映射可能已经可写或将来通过 
mprotect
 变得可写),则该映射被视为 CoW。

在您的情况下,VMA 被视为 CoW,并且检查失败,因为您一次映射一页,因此您永远不会同时匹配

vma->vm_start

vma->vm_end
。因此,您的 
remap_pfn_range()
-EINVAL
 而失败,并且您错过了它,因为您没有检查返回值是否有错误。

您有 3 个选择:

    使用
  1. mmap
     将用户空间 
    MAP_SHARED
     设为整个区域。
  2. 使用
  3. mmap
     单独创建用户空间 
    MAP_PRIVATE
     单页。
  4. 在将页面映射到用户空间之前,从
  5. VM_MAYWRITE
     中删除 
    vma->vm_flags
    ,以禁止页面在将来可写(即使用 
    mprotect
    ),这反过来又会使其成为非 CoW。
上面的数字 1 是恕我直言最有意义的,并且(从我所看到的)映射特殊设备时最常见的选项。


P.S.:请注意,

printf("[%x]\n", ((char*)mapped_memory)[0]);

是错误的,它将读取单个
char
(一个字节)并将其提升为带符号扩展的
int
,这样你就会得到
[ffffffdd]
。如果您想获得 
((unsigned*)mapped_memory)[0])
,您应该做 
[aabbccdd]

© www.soinside.com 2019 - 2024. All rights reserved.