我最近正在阅读 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
函数,但我想跟随实验室并尝试按照自己的方式进行操作。
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 个选择:
mmap
将用户空间
MAP_SHARED
设为整个区域。
mmap
单独创建用户空间
MAP_PRIVATE
单页。
VM_MAYWRITE
中删除
vma->vm_flags
,以禁止页面在将来可写(即使用
mprotect
),这反过来又会使其成为非 CoW。
printf("[%x]\n", ((char*)mapped_memory)[0]);
是错误的,它将读取单个
char
(一个字节)并将其提升为带符号扩展的
int
,这样你就会得到
[ffffffdd]
。如果您想获得
((unsigned*)mapped_memory)[0])
,您应该做
[aabbccdd]
。