内存重映射

内核内存有时需要重新映射,无论是从内核到用户空间还是从内核空间到内核。常见的情况是将内核内存重新映射到用户空间,但还有其他一些情况,例如需要访问高内存的情况。

1 kmap

kmap()用于将指定的页面映射到内核地址空间

Linux内核将其896MB地址空间永久映射到物理内存较低的896MB(低端内存)。在4GB系统上,内核仅剩下128MB用来映射剩余3.2GB物理内存(高端内存)。由于低端内存采用永久一对一映射,因此内核可以直接寻址。而对于高端内存(高于896MB的内存),内核必须将所请求的高端内存区域映射到其地址空间,前面提到的128MB就是专门为此保留。用于执行此操作的函数是kmap()。kmap()用于将指定的页面映射到内核地址空间。

void *kmap(struct page *page);

当分配到高端内存页时,它不能直接寻址,必须调用调用kmap()函数将高端内存映射到内存地址空间。该映射将持续到kunmap()位置:

void *kunmap(struct page *page);

所谓暂时,指的是映射应该在不需要的时候立即撤销。请记住,128MB不足以映射3.2GB。最好的编程习惯是在不需要时取消高端内存映射。这就是必须在每次访问高端内存页面时输入kmap()-kunmap序列的原因了,

该函数适用于高端内存和低端内存,也就是说,如果页面结构驻留在低端内存中,那么返回的是页面的虚拟地址(因为低端内存页面已经有永久映射)。如果页面属于高端内存,则在内核页表中创建永久映射,并返回地址。

2 映射内核内存到用户空间

映射物理地址是其中一个有用的功能,特别是在嵌入式系统中。有时可能想要与用户空间共享部分内核内存。如前所述,CPU在用户空间时以非特权模式运行要让进程访问内核内存区域,需要将该区域映射到进程地址空间

使用remap_pfn_range

remap_pfn_range()将物理内存(通过内核逻辑地址)映射到用户空间进程。它对于实现mmap()特别有用。
在文件上调用mmap()系统调用后,CPU切换到特权模式,运行相应的file_operations.mmap()内核函数,它反过来调用remap_pfn_range()。这将产生映射区域的PTE,将其赋给进程,当然还有不同的保护标志,进程的VMA列表更新为新的VMA项,这将使用PTE访问相同的内存。

这样,内核不是通过复制来浪费内存,而只是复制PTE,但是内核和用户空间PTE具有不同的属性。remap_pfn_range()原型如下:

int remap_pfn_range (	struct vm_area_struct * vma,
 	unsigned long virt_addr,
 	unsigned long pfn,
 	unsigned long size,
 	pgprot_t prot);

  • vma: 这个我们不用担心,因为在调用file_operations.mmap函数时,mmap调用do_mmap()会创建一个新的VMA并初始化,此vma就是创建的新的VMA,加入进程的虚拟地址空间里,这个已经确定了。
  • virt_addr:VMA开始位置的用户虚拟地址(vma->vm_start),这将导致映射的虚拟地址范围位于virt_addr~virt_addr+size
  • pfn:所映射内核内存区域的页面帧码,它对应于通过PAGE_SHIFT位右移得到的物理地址。产生pfn时应应该考虑vma偏移量。由于vma结构的vm_pgoff字段在页码中包含偏移值,因此需要以字节形式精确提取偏移量:offset= vma->vm_pgoff<<PAGE_SHIFT.最后,pnf=virt_to_phys(buffer+offset)>>PAGE_SHIFT.
  • size:需要建立映射的VMA的大小,以字节为单位
  • prot:代表新VMA所要求的保护。驱动程序可以修改默认值,但应该使用OR运算符将vma->vm_page_port中的值作基础,因为它的某些位已经由用户空间设置,其中一些标志如下:
  1. VM_IO:指定设备内存映射I/O
  2. VM_DONTCOPY:告诉内核不要在分叉上复制该vma
  3. VM_DONTEXPAND:防止vma通过mremap扩展
  4. VM_DONTDUMP:禁止在核心转储内包含vma

使用io_remap_pfn_range

前面讨论的remap_pfn_range()不适用于将I/O内存映射到用户空间。在这种情况下,相应的函数是io_remap_pfn_range(),它们的参数相同,唯一改变的是PFN的来源,其原型如下:

  int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr,
                            unsigned long phys_addr, unsigned long size, 
                            pgprot_t prot);

当试图将I/O内存映射到用户空间时,不需要使用ioremap(),ioremap()用于内核映射(将I/O内存映射到内核地址空间)。

只需要将真实的物理I/O地址(通过PAGE_SHIFT向下移位生成PFN)直接传递给io_remap_pfn_range()。即使有一些体系将io_remap_pfn_range()定义为remap_pfn_range(),但在其他体系结构中并非如此,考虑到移植能力,只有在PFN参数指向RAM的情况下,才使用remap_pfn_range(),在pys_addr指向I/O聂村的情况下,才使用io_remap_pfn_range()。

mmap文件操作

内核mmap函数是struct file_operations结构的一部分,当用户执行系统调用mmap(2),把物理内存映射到用户虚拟地址时才执行它。出于安全考虑,用户空间进程不能直接访问设备内存,因此,用于空间进程使用mmap()系统调用将该设备映射到调用进程的虚拟地址空间。在映射之后,用户空间进程可以通过返回的地址直接写入设备内存。

       #include <sys/mman.h>

       void *mmap(void *addr, size_t length, int prot, int flags,
                  int fd, off_t offset);
       int munmap(void *addr, size_t length);

用户空间的mmap()会通过系统调用调用内核的do_mmap()函数。
do_mmap()函数会:

  1. 首先创建一个新的VMA并初始化,然后加入进程的虚拟地址空间里
  2. 调用底层的mmap函数建立VMA和实际物理地址的映射(建立页表)

什么是底层的mmap函数呢?在不同的设备是不一样的。比如说我们映射的是一个普通文件,底层文件系统已经帮我们实现了mmap,我们可以直接使用,但是如果我们新写了一个驱动,我们想为驱动提供mmap的接口,那么就需要我们实现mmap的接口,设备驱动的mmap实现主要是将这个物理设备的可操作区域映射到一个进程的虚拟地址空间,这样用户空间就可以直接采用指针的方式访问设备的可操作区域。在驱动中的mmap实现主要完成一件事,就是建立设备的可操作区域到进程虚拟空间地址的映射过程。

  • addr:映射开始的用户空间虚拟地址,如果指定NULL,则自动确定正确的地址
  • length:指定映射长度
  • prot:指定VMA的权限
  • flags:决定映射类型(私有还是共享)
  • fd:设备文件描述符
  • offset:指定映射区的偏移量(在物理内存里面)

建立VMA和实际物理地址的映射

在 linux 驱动中建立映射关系的方法主要有如下两种:

  1. 一次性映射 —— 在 mmap 回调函数中,一次性建立好整块内存的映射关系,通常以 remap_pfn_range() 为代表 。
  2. Page Fault —— mmap 先不建立映射关系,等上层触发缺页异常时,在 fault 中断处理函数中建立映射关系,缺哪块补哪块,通常以 vm_insert_page() 为代表。

而内存分配的时机也会影响驱动程序的设计,大致分为如下三种:

  • 在 mmap 系统调用之前分配
  • 在 mmap 系统调用过程中分配
  • 在 fault 中断处理函数中分配

因此不同的分配时机 + 不同的映射机制,就会得到不同的 mmap 的实现策略。

下面就以示例代码的形式为大家展示几种典型的 mmap 驱动实现方式。

mmap 之前分配 + 一次性映射

请添加图片描述

描述:

  1. 驱动初始化时先分配好 3 个 PAGE。
  2. 上层执行 mmap 系统调用时,在底层 mmap 回调函数中通过 remap_pfn_range() 一次性建立好所有的映射关系,并将映射后的起始虚拟地址返回给应用程序。
  3. 应用程序使用返回的虚拟地址进行内存读写操作。

驱动代码:

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>

static void *kaddr;

static int my_mmap(struct file *file, struct vm_area_struct *vma)
{
	return remap_pfn_range(vma, vma->vm_start,
				(virt_to_phys(kaddr) >> PAGE_SHIFT) + vma->vm_pgoff,
				vma->vm_end - vma->vm_start, vma->vm_page_prot);
}

static struct file_operations my_fops = {
	.owner	= THIS_MODULE,
	.mmap	= my_mmap,
};
 
static struct miscdevice mdev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "my_dev",
	.fops = &my_fops,
};
 
static int __init my_init(void)
{
	kaddr = kzalloc(PAGE_SIZE * 3, GFP_KERNEL);
	return misc_register(&mdev);
}
module_init(my_init);


mmap 之前分配 + Page Fault

请添加图片描述
描述:

  1. 驱动初始化时预先分配好 3 个 PAGE。
  2. 上层执行 mmap 系统调用,底层驱动在 mmap 回调函数中不建立映射关系,而是将本地实现的 vm_ops 挂接到进程的 vma->vm_ops 指针上,然后函数返回。
  3. 上层获取到一个未经映射的进程地址空间,并对其进行内存读写操作,导致触发缺页异常。缺页异常最终会调用前面挂接的 vm_ops->fault() 回调接口,在该接回调中通过 vm_insert_page() 建立物理内存与用户地址空间的映射关系。
  4. 异常返回后,应用程序就可以继续之前被中断的读写操作了。

注意:这种情况每次 Page Fault 中断只能映射一个 Page

驱动代码:

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>

static void *kaddr;

static int my_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	int offset, ret;

	offset = vmf->pgoff * PAGE_SIZE;
	ret = vm_insert_page(vma, vmf->address, virt_to_page(kaddr + offset));
	if (ret)
		return VM_FAULT_SIGBUS;

	return VM_FAULT_NOPAGE;
}

static const struct vm_operations_struct vm_ops = {
	.fault = my_fault,
};

static int my_mmap(struct file *file, struct vm_area_struct *vma)
{
	vma->vm_flags |= VM_MIXEDMAP;
	vma->vm_ops = &vm_ops;
	return 0;
}

static struct file_operations my_fops = {
	.owner	= THIS_MODULE,
	.mmap	= my_mmap,
};
 
static struct miscdevice mdev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "my_dev",
	.fops = &my_fops,
};
 
static int __init my_init(void)
{
	kaddr = kzalloc(PAGE_SIZE * 3, GFP_KERNEL);
	return misc_register(&mdev);
}
module_init(my_init);


Page Fault 中分配 + 映射

请添加图片描述
映射的过程和示例二完全一样,只是内存分配的时机是在 page fault 中断处理函数中进行的。

这里为了简化代码,总共只分配一个 page,多个 page 可通过 vmf->pgoff 来进行区分。

#include <linux/module.h>
#include <linux/miscdevice.h>
#include <linux/mm.h>
#include <linux/uaccess.h>
#include <linux/fs.h>
#include <linux/slab.h>

static struct page *page;

static int my_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	int ret;

	if (!page)
		page = alloc_page(GFP_KERNEL);

	ret = vm_insert_page(vma, vmf->address, page);
	if (ret)
		return VM_FAULT_SIGBUS;

	return VM_FAULT_NOPAGE;
}

static const struct vm_operations_struct vm_ops = {
	.fault = my_fault,
};

static int my_mmap(struct file *file, struct vm_area_struct *vma)
{
	vma->vm_flags |= VM_MIXEDMAP;
	vma->vm_ops = &vm_ops;
	return 0;
}

static struct file_operations my_fops = {
	.owner	= THIS_MODULE,
	.mmap	= my_mmap,
};
 
static struct miscdevice mdev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = "my_dev",
	.fops = &my_fops,
};
 
static int __init my_init(void)
{
	return misc_register(&mdev);
}
module_init(my_init);


  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值