内存映射与VMA

一般情况下,用户空间是不能也不应该直接访问设备的,但是,设备驱动程序中可以实现mmap()函数,这个函数使得用户空间能直接访问设备的物理地址。实际上,mmap()实现了这样的一个映射过程:他将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。
这种能力对于显示适配器一类的设备非常有意义,如果用户空间可以直接通过内存映射访问显存的话,屏幕帧的各像素点将不再需要一个从用户空间到内核空间的复制过程
mmap()必须以PAGE_SIZE为单位进行映射,实际上,内存只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行页对齐,强行以PAGE_SIZE的倍数大小进行映射。
从file_operations文件操作结构体中可以看出,mmap的原型如下

	int (*mmap) (struct file *, struct vm_area_struct *);  

驱动中的mmap函数将在用户进行mmap()系统调用时最终被调用,mmap()的系统调用与驱动中的系统调用区别很大,原型如下

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

参数:

  1. addr 指定文件应被映射到用户空间的起始地址,一般指定为NULL,这样,选择起始地址的任务将由内核完成,而函数的返回值就是映射到用户空间的地址。
  2. length 映射的长度
  3. port 指定访问权限,去如下几个值的“或”。PORT_READ(可读),PORT_WRITE(可写),PORT_EXEC(可执行)和PORT_NONE(不可访问)
  4. flags
  5. fd,文件描述符,也可以指定为-1,表示为匿名映射 ,此时flag应为MAP_ANON

offset 偏移 当用户调用mmap()的时候,内核会进行以下处理

  1. 在进程的虚拟空间查找一块VMA
  2. 将这块VMA进行映射
  3. 如果驱动中定义了mmap(),则调用它
  4. 将这个VMA插入进程的VMA链表中
    fileoperations中mmap()函数的第二个参数就是步骤1中找到的VMA。
    驱动程序中mmap()的实现机制是建立页表,并填充VMA结构中vm_operations_struct指针。VMA就是vm_area_struct,用于描述一个虚拟内存区域,VMA的定义如下
/*
 * This struct defines a memory VMM memory area. There is one of these
 * per VM-area/task.  A VM area is any part of the process virtual memory
 * space that has a special rule for the page-fault handlers (ie a shared
 * library, the executable area etc).
 */
struct vm_area_struct {
	/* The first cache line has the info for VMA tree walking. */

	unsigned long vm_start;		/* Our start address within vm_mm. */
	unsigned long vm_end;		/* The first byte after our end address
					   within vm_mm. */

	/* linked list of VM areas per task, sorted by address */
	struct vm_area_struct *vm_next, *vm_prev;

	struct rb_node vm_rb;

	/*
	 * Largest free memory gap in bytes to the left of this VMA.
	 * Either between this VMA and vma->vm_prev, or between one of the
	 * VMAs below us in the VMA rbtree and its ->vm_prev. This helps
	 * get_unmapped_area find a free area of the right size.
	 */
	unsigned long rb_subtree_gap;

	/* Second cache line starts here. */

	struct mm_struct *vm_mm;	/* The address space we belong to. */
	pgprot_t vm_page_prot;		/* Access permissions of this VMA. */
	unsigned long vm_flags;		/* Flags, see mm.h. */

	/*
	 * For areas with an address space and backing store,
	 * linkage into the address_space->i_mmap interval tree.
	 */
	struct {
		struct rb_node rb;
		unsigned long rb_subtree_last;
	} shared;

	/*
	 * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
	 * list, after a COW of one of the file pages.	A MAP_SHARED vma
	 * can only be in the i_mmap tree.  An anonymous MAP_PRIVATE, stack
	 * or brk vma (with NULL file) can only be in an anon_vma list.
	 */
	struct list_head anon_vma_chain; /* Serialized by mmap_sem &
					  * page_table_lock */
	struct anon_vma *anon_vma;	/* Serialized by page_table_lock */

	/* Function pointers to deal with this struct. */
	const struct vm_operations_struct *vm_ops;

	/* Information about our backing store: */
	unsigned long vm_pgoff;		/* Offset (within vm_file) in PAGE_SIZE
					   units */
	struct file * vm_file;		/* File we map to (can be NULL). */
	void * vm_private_data;		/* was vm_pte (shared mem) */

	atomic_long_t swap_readahead_info;
#ifndef CONFIG_MMU
	struct vm_region *vm_region;	/* NOMMU mapping region */
#endif
#ifdef CONFIG_NUMA
	struct mempolicy *vm_policy;	/* NUMA policy for the VMA */
#endif
	struct vm_userfaultfd_ctx vm_userfaultfd_ctx;
} __randomize_layout;  

VMA结构体描述的虚拟地址介于vm_start和vm_end之间。
前面我们说过,驱动的mmap的主要作用就是建立页表,建立页表常用到remap_pfn_range函数,该函数的原型是:

/**
 * remap_pfn_range - remap kernel memory to userspace
 * @vma: user vma to map to
 * @addr: target user address to start at
 * @pfn: physical address of kernel memory
 * @size: size of map area
 * @prot: page protection flags for this mapping
 *
 *  Note: this is only safe if the mm semaphore is held when called.
 */
int remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,
		    unsigned long pfn, unsigned long size, pgprot_t prot)  

参数:

  1. vma 内核找到的一块vma。
  2. addr 表示内存映射开始出的地址,也就是vma_start
  3. pfn 虚拟地址应该映射到的物理地址的页帧号,实际上就是物理地址右移PAGE_SHIFT位。若PAGE_SIZE为4KB,则PAGE_SHIFT为12.
  4. size 映射的大小
  5. prot 新页所要求的的保护属性

有了上面的知识之后,我们就可以来写一个简单的测试程序了
本测试程序参考https://www.cnblogs.com/pengdonglin137/p/8149859.html
在驱动中申请一个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的缓冲区:

static int __init remap_pfn_init(void)
{
    int ret = 0;

    kbuff = kzalloc(BUF_SIZE, GFP_KERNEL);  // 这里的BUF_SIZE是128KB
    if (!kbuff) {
        ret = -ENOMEM;
        goto err;
    }

    ret = misc_register(&remap_pfn_misc);   // 注册一个misc设备
    if (unlikely(ret)) {
        pr_err("failed to register misc device!\n");
        goto err;
    }

    return 0;

err:
    return ret;
}  

第11行注册了一个misc设备,相关信息如下:

static struct miscdevice remap_pfn_misc = {
    .minor = MISC_DYNAMIC_MINOR,
    .name = "remap_pfn",
    .fops = &remap_pfn_fops,
};  

这样加载驱动后会在/dev下生成一个名为remap_pfn的节点,用户程序可以通过这个节点跟驱动通信。其中remap_pfn_fops的定义如下:

static const struct file_operations remap_pfn_fops = {
    .owner = THIS_MODULE,
    .open = remap_pfn_open,
    .mmap = remap_pfn_mmap,
};  

第3行的open函数这里没有做什么实际的工作,只是打印一些log,比如将进程的内存布局信息输出

第4行,负责处理用户的mmap请求,这是需要关心的。
先看一下open函数具体打印了那些内容:

static int remap_pfn_open(struct inode *inode, struct file *file)
{
    struct mm_struct *mm = current->mm;

    printk("client: %s (%d)\n", current->comm, current->pid);
    printk("code  section: [0x%lx   0x%lx]\n", mm->start_code, mm->end_code);
    printk("data  section: [0x%lx   0x%lx]\n", mm->start_data, mm->end_data);
    printk("brk   section: s: 0x%lx, c: 0x%lx\n", mm->start_brk, mm->brk);
    printk("mmap  section: s: 0x%lx\n", mm->mmap_base);
    printk("stack section: s: 0x%lx\n", mm->start_stack);
    printk("arg   section: [0x%lx   0x%lx]\n", mm->arg_start, mm->arg_end);
    printk("env   section: [0x%lx   0x%lx]\n", mm->env_start, mm->env_end);

    return 0;
}  

第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的实现

static int remap_pfn_mmap(struct file *file, struct vm_area_struct *vma)
{
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long pfn_start = (virt_to_phys(kbuff) >> PAGE_SHIFT) + vma->vm_pgoff;
    unsigned long virt_start = (unsigned long)kbuff + offset;
    unsigned long size = vma->vm_end - vma->vm_start;
    int ret = 0;

    printk("phy: 0x%lx, offset: 0x%lx, size: 0x%lx\n", pfn_start << PAGE_SHIFT, offset, size);

    ret = remap_pfn_range(vma, vma->vm_start, pfn_start, size, vma->vm_page_prot);
    if (ret)
        printk("%s: remap_pfn_range failed at [0x%lx  0x%lx]\n",
            __func__, vma->vm_start, vma->vm_end);
    else
        printk("%s: map 0x%lx to 0x%lx, size: 0x%lx\n", __func__, virt_start,
            vma->vm_start, size);

    return ret;
}  

第3行的vma_pgoff表示的是该vma表示的区间在缓冲区中的偏移地址,单位是页。这个值是用户调用mmap时传入的最后一个参数,不过用户空间的offset的单位是字节(当然必须是页对齐),进入内核后,内核会将该值右移PAGE_SHIFT(12),也就是转换为以页为单位。因为要在第9行打印这个编译地址,所以这里将其再左移PAGE_SHIFT,然后赋值给offset。

第4行计算内核缓冲区中将被映射到用户空间的地址对应的物理页帧号。virt_to_phys接受的虚拟地址必须在低端内存范围内,用于将虚拟地址转换为物理地址,而vmaloc返回的虚拟地址不在低端内存范围内,所以需要用专门的函数。
第5行计算内核缓冲区中将被映射到用户空间的地址对应的虚拟地址
第6行计算该vma表示的内存区间的大小
第11行调用remap_pfn_range将物理页帧号pfn_start对应的物理内存映射到用户空间的vm->vm_start处,映射长度为该虚拟内存区的长度。由于这里的内核缓冲区是用kzalloc分配的,保证了物理地址的连续性,所以会将物理页帧号从pfn_start开始的(size >> PAGE_SHIFT)个连续的物理页帧依次按序映射到用户空间。

将驱动编译成模块后,insmod到内核。

用户测试程序

user_1.c:

#define PAGE_SIZE (4*1024)
#define BUF_SIZE (16*PAGE_SIZE)
#define OFFSET (0)

int main(int argc, const char *argv[])
{
    int fd;
    char *addr = NULL;

    fd = open("/dev/remap_pfn", O_RDWR);
    
    addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET);
    
    sprintf(addr, "I am %s\n", argv[0]);

    while(1)
        sleep(1);
    return 0;
}  

第10和第12行,打开设备节点,然后从内核空间映射64KB的内存到用户空间,首地址存放在addr中,由于后面既要写入也要共享,所以设置了对应的flags。这里指定的offset是0,即映射前64KB。

第14行输出字符串到addr指向的虚拟地址空间

#define PAGE_SIZE (4*1024)
#define BUF_SIZE (16*PAGE_SIZE)
#define OFFSET (0)

int main(int argc, const char *argv[])
{
    int fd;
    char *addr = NULL;

    fd = open("/dev/remap_pfn", O_RDWR);
    
    addr = mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_LOCKED, fd, OFFSET);
    
    printf("%s", addr);

    while(1)
        sleep(1);

    return 0;
}  
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值