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

Mapping non-contiguous physical memory to userspace

提问人:jon doe 提问时间:11/8/2023 最后编辑:jon doe 更新时间:11/9/2023 访问量:88

问:

我最近一直在阅读 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)

谁能指出我做错了什么?

P.S. 我知道这本书使用了 的功能,但我想按照实验室的方式去做。nopagevm_operations_struct

C Linux 内核 linux-device-driver mmap

评论

0赞 Ulrich Eckhardt 11/8/2023
总线错误在某些 CPU 上用于发出未对齐访问的信号。x86 是一个不这样做的 arch,而是以牺牲性能和原子操作为代价来模拟未对齐的访问。您从中收到的指针值是多少?如果在输出四个字节后刷新 stdout,只是为了验证其中哪个会导致总线错误,该怎么办?mmap()
0赞 jon doe 11/8/2023
@UlrichEckhardt感谢您的评论。我更新了问题中的代码

答:

2赞 Marco Bonelli 11/9/2023 #1

TL的;DR:使用MAP_SHARED

remap_pfn_range() 中有一个检查,可确保如果映射是写入时复制 (CoW),则请求重新映射的范围必须完全从 to(即它必须在物理上是连续的)。vma->vm_startvma->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;
    }

如果映射没有设置并且已经设置,则该映射被视为 CoW(即映射未共享,可以通过 写入)。vma->vm_flagsVM_SHAREDVM_MAYWRITEmprotect

在您的情况下,VMA 被视为 CoW,并且检查失败,因为您一次映射一个页面,因此您永远不会同时匹配 和 。因此,您失败了,并且您错过了它,因为您没有检查返回值是否有错误。vma->vm_startvma->vm_endremap_pfn_range()-EINVAL

您有 3 个选择:

  1. 使用 使用户空间成为整个区域。mmapMAP_SHARED
  2. 使用 单独创建用户空间单页。mmapMAP_PRIVATE
  3. 在将页面映射到用户空间之前删除,以禁止将来使页面可写(即 with ),这反过来又会使其成为非 CoW。VM_MAYWRITEvma->vm_flagsmprotect

恕我直言,上面的第 1 条是最有意义的,并且(据我所知)是映射特殊设备时最常见的选项。


PS:请注意这是错误的,它将读取一个(一个字节)并将其提升为带有符号扩展名,因此您将获得.如果你想得到 .printf("[%x]\n", ((char*)mapped_memory)[0]);charint[ffffffdd]((unsigned*)mapped_memory)[0])[aabbccdd]

评论

0赞 jon doe 11/9/2023
这解决了它。谢谢伙计!