作者
彭东林
QQ 405728433
平台
Linux-4.10.17
Qemu-2.8 + vexpress-a9
DDR:1GB
参考
概述
Linux内核提供了remap_pfn_range函数来实现将内核空间的内存映射到用户空间:
1 /** 2 * remap_pfn_range - remap kernel memory to userspace 3 * @vma: user vma to map to 4 * @addr: target user address to start at 5 * @pfn: physical address of kernel memory 6 * @size: size of map area 7 * @prot: page protection flags for this mapping 8 * 9 * Note: this is only safe if the mm semaphore is held when called. 10 */ 11 int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr, 12 unsigned long pfn, unsigned long size, pgprot_t prot);
上面的注释对参数进行了说明。当用户调用mmap时,驱动中的file_operations->mmap会被调用,可以在mmap中调用remap_pfn_range,它的大部分参数的值都由VMA提供。
具体可以参考LDD3的P420.
正文
下面结合一个简单的例子学习一下。
在驱动中申请一个32个Page的缓冲区,这里的PAGE_SIZE是4KB,所以内核中的缓冲区大小是128KB。user_1和user_2将前64KB映射到自己的用户空间,其中user_1向缓冲区中写入字符串,user_2去读取。user_3和user_4将后64KB映射到自己的用户空间,其中user_3向缓冲区中写入字符串,user_4读取字符串。user_5将整个128KB映射到自己的用户空间,然后将缓冲区清零。此外,在驱动中申请缓冲区的方式有多种,可以用kmalloc、也可以用alloc_pages,当然也可用vmalloc,下面会分别针对这三个接口实现驱动。
涉及到的测试程序和驱动程序可以到下面的链接下载:
一、驱动程序
下面先以kzalloc申请缓冲区的方式为例介绍,调用kmalloc申请32个页,我们知道kzalloc返回的虚拟地址的特点是对应的物理地址也是连续的,所以在调用remap_pfn_range的时候很方便。首先在驱动init的时候申请128KB的缓冲区:
1 static int __init remap_pfn_init(void) 2 { 3 int ret = 0; 4 5 kbuff = kzalloc(BUF_SIZE, GFP_KERNEL); // 这里的BUF_SIZE是128KB 6 if (!kbuff) { 7 ret = -ENOMEM; 8 goto err; 9 } 10 11 ret = misc_register(&remap_pfn_misc); // 注册一个misc设备 12 if (unlikely(ret)) { 13 pr_err("failed to register misc device!\n"); 14 goto err; 15 } 16 17 return 0; 18 19 err: 20 return ret; 21 }
第11行注册了一个misc设备,相关信息如下:
1 static struct miscdevice remap_pfn_misc = { 2 .minor = MISC_DYNAMIC_MINOR, 3 .name = "remap_pfn", 4 .fops = &remap_pfn_fops, 5 };
这样加载驱动后会在/dev下生成一个名为remap_pfn的节点,用户程序可以通过这个节点跟驱动通信。其中remap_pfn_fops的定义如下:
1 static const struct file_operations remap_pfn_fops = { 2 .owner = THIS_MODULE, 3 .open = remap_pfn_open, 4 .mmap = remap_pfn_mmap, 5 };
第3行的open函数这里没有做什么实际的工作,只是打印一些log,比如将进程的内存布局信息输出
第4行,负责处理用户的mmap请求,这是需要关心的。
先看一下open函数具体打印了那些内容:
1 static int remap_pfn_open(struct inode *inode, struct file *file) 2 { 3 struct mm_struct *mm = current->mm; 4 5 printk("client: %s (%d)\n", current->comm, current->pid); 6 printk("code section: [0x%lx 0x%lx]\n", mm->start_code, mm->end_code); 7 printk("data section: [0x%lx 0x%lx]\n", mm->start_data, mm->end_data); 8 printk("brk section: s: 0x%lx, c: 0x%lx\n", mm->start_brk, mm->brk); 9 printk("mmap section: s: 0x%lx\n", mm->mmap_base); 10 printk("stack section: s: 0x%lx\n", mm->start_stack); 11 printk("arg section: [0x%lx 0x%lx]\n", mm->arg_start, mm->arg_end); 12 printk("env section: [0x%lx 0x%lx]\n", mm->env_start, mm->env_end); 13 14 return 0; 15 }
第5行将进程的名字以及pid打印出来
第6行打印进程的代码段的范围
第7行打印进程的data段的范围,其中存放的是已初始化全局变量。而bss段存放的是未初始化全局变量,存放位置紧跟在data段后面,堆区之前
第8行打印进程的堆区的起始地址和当前地址
第9行打印进程的mmap区的基地址,这里的mmap区是向下增长的。具体mmap区的基地址跟系统允许的当前进程的用户栈的大小有关,用户栈的最大size越大,mmap区的基地址就越小。修改用户栈的最大尺寸需要用到ulimit -s xxx命令,单位是KB,表示用户栈的最大尺寸,用户栈的尺寸可以上G,而内核栈却只有区区的2个页。
第10行打印进程的用户栈的起始地址,向下增长
第11行和第12行的暂不关心。
下面是remap_pfn_mmap的实现:
1 static int remap_pfn_mmap(struct file *file, struct vm_area_struct *vma) 2 { 3 unsigned long offset = vma->vm_pgoff << PAGE_SHIFT; 4 unsigned long pfn_start = (virt_to_phys(kbuff) >> PAGE_SHIFT) + vma->vm_pgoff; 5 unsigned long virt_start = (unsigned long)kbuff + offset; 6 unsigned long size = vma->vm_end - vma->vm_start; 7 int ret = 0; 8 9 printk("phy: 0x%lx, offset: 0x%lx, size: 0x%lx\n", pfn_start <<