内核内存映射示例

Comments(0)


Posted on 2016-04-16 06:19:41 os


内核内存映射示例

前一篇博客中,我们介绍了Linux内核中内存映射的实现,以及用户态调用mmap系统调用的原理。接下来,我们会通过Linux内核模块的方式,映射两种物理内存:物理地址空间和内核模块申请的内存,演示内核模块中如何与用户态进程之间共享内存。

/dev/mem

在开始之前,介绍一下/dev/mem这个字符设备,打开/dev/mem之后,通过mmap系统调用,就可以映射物理地址空间。需要注意的是,这里的物理地址空间,是站在CPU角度看到的地址空间,而不仅仅是RAM空间,还有外设的IO空间,例如BIOS ROM、Video ROM、PCI BUS等。

映射系统物理内存

下面的内核模块代码工作的行为类似于/dev/mem,打开对应的设备文件后,同样可以映射CPU地址空间。完整的代码在文章最后。

/*
 * 第三个参数直接使用vma->vm_pgoff,
 * 是因为这部分代码是直接从drivers/char/mem.c中拷贝而来,所以它模拟的设备工作方式也类似/dev/mem,
 * 而/dev/mem的pgoff刚好等于pgn。
 *
 * 1.在进程的虚拟空间查找一块VMA.
 * 2.将这块VMA进行映射.
 * 3.如果设备驱动程序中定义了mmap函数,则调用它.
 * 4.将这个VMA插入到进程的VMA链表中.
 * 内存映射工作大部分由内核完成,驱动程序中的mmap函数只需要为该地址范围建立合适的页表,
 * 并将vma->vm_ops替换为一系列的新操作就可以了。
 *
 * vma->start代表要建立页表的用户虚拟地址的起始地址,remap_pfn_range函数为处于vma->start和vma->start+size之间的虚拟地址建立页表。
 * pfn是与物理内存起始地址对应的页帧号,虚拟内存将要被映射到该物理内存上。
 * 页帧号只是将物理地址右移PAGE_SHIFT位。在大多数情况下,VMA结构中的vm_pgoff赋值给pfn即可。
 * remap_pfn_range函数建立页表,对应的物理地址是pfn<<PAGE_SHIFT到pfn<<(PAGE_SHIFT)+size。
 * size代表虚拟内存区域大小。
 */

static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
    size_t size;
    size = vma->vm_end - vma->vm_start;
    printk (KERN_NOTICE "########### CALL remap_mmap");

    if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size))
        return -EINVAL;

    if (!private_mapping_ok(vma))
        return -ENOSYS;

    if (!range_is_allowed(vma->vm_pgoff, size))
        return -EPERM;

    if (!phys_mem_access_prot_allowed(filp, vma->vm_pgoff, size,
                        &vma->vm_page_prot))
        return -EINVAL;

    vma->vm_page_prot = phys_mem_access_prot(filp, vma->vm_pgoff,
                         size,
                         vma->vm_page_prot);

    /* Remap-pfn-range will mark the range VM_IO */
    if (remap_pfn_range(vma,
                vma->vm_start,
                vma->vm_pgoff,
                size,
                vma->vm_page_prot)) {
        return -EAGAIN;
    }
    vma->vm_ops = &simple_remap_vm_ops;
    simple_vma_open(vma);

    return 0;
}



/*
 * The fault version.
 */
/*
 * 每次只映射一个PAGE
 * 1.找到缺页的虚拟地址所在的VMA。
 * 2.如果必要,分配中间页目录表和页表。
 * 3.如果页表项对应的物理页面不存在,则调用fault函数,它返回物理页面的页描述符。
 * 4.将物理页面的地址填充到页表中。
 * 在上面第3步中,分配好的页目录和页表直接对应的是物理PAGE,
 * 如果在不修改页表映射的情况下是这样,如果修改了就是别的PAGE物理地址。
 * 下面的代码拿到的物理地址没有经过修改,所以是直接映射到物理内存的。
 * 和上面的remap的行为一直,都是直接映射虚拟地址对应的物理地址。
 * 和/dev/mem一致。
 */

int simple_vma_fault(struct vm_area_struct *vma,
                struct vm_fault *vmf)
{
    printk (KERN_NOTICE "########### CALL fault_mmap");

    struct page *pageptr;
    // 得到起始物理地址保存在offset中
    unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    // 得到vmf->virtual_address对应的物理地址,保存在physaddr中
    unsigned long physaddr = (unsigned long)(vmf->virtual_address - vma->vm_start) + offset;
    // 得到物理地址对应的页帧号,保存在pageframe中。
    unsigned long pageframe = physaddr >> PAGE_SHIFT;

    // Eventually remove these printks
    printk (KERN_NOTICE "---- fault, off %lx phys %lx\n", offset, physaddr);
    printk (KERN_NOTICE "VA is %p\n", __va (physaddr));
    printk (KERN_NOTICE "Page at %p\n", virt_to_page (__va (physaddr)));
    if (!pfn_valid(pageframe))
        return VM_FAULT_SIGBUS;
    // 由页帧号返回对应的page结构指针保存在pageptr中
    pageptr = pfn_to_page(pageframe);
    printk (KERN_NOTICE "page->index = %ld mapping %p\n", pageptr->index, pageptr->mapping);
    printk (KERN_NOTICE "Page frame %ld\n", pageframe);
    // 调用get_page增加pageptr指向页面的引用计数
    get_page(pageptr);
        vmf->page = pageptr;
    return 0;
}

注意这里实现内存映射的方式有两种,simple_remap_mmap中是通过remap_pfn_page函数映射整个内存区域,而simple_vma_fault中只对单一的内存页面做映射。

remap_pfn_page的函数原型含义如下:

remap_pfn_range(struct vma_area_struct *vma,
                unsigned long addr,
                unsigned long pfn,
                unsigned long size,
                pgprot_t prot);
  • vma: 指向了进程中vm_area_struct,它定义了一个进程中的内存映射区域
  • addr: vm_area_struct中定义的起始虚拟地址
  • pfn: 物理地址的页帧号,给定一个物理地址,它的页帧号可以通过address >> PAGE_SHIFT计算,PAGE_SHIFT是体系结构相关的宏定义,在32位下是12。
  • size: 映射区域的大小
  • prot: 虚拟映射区域的访问权限

上面的参数中比较关键的是pfn参数,它指的是物理页帧号。对应于mmap系统调用的最后一个参数。因为内核模块(包括/dev/mem)实现中,没有做页地址对齐,所以在用户态进程中调用mmap时,要根据当前机器的PAGE大小做好地址对齐:

pa_offset = offset & ~(sysconf(_SC_PAGE_SIZE) - 1);

刚才介绍的是通过simple_remap_mmap映射整个虚拟内存区域,一次性映射了整个vm_area_struct指定的空间。如果我们每次只想映射一页呢?可以通过vma->vm_ops指定fault回调函数,

fault回调函数中,比较关键的是下面几行:

struct page *pageptr;
    
// 得到起始物理地址保存在offset中
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    
// 得到vmf->virtual_address对应的物理地址,保存在physaddr中
unsigned long physaddr = (unsigned long)(vmf->virtual_address - vma->vm_start) + offset;
    
// 得到物理地址对应的页帧号,保存在pageframe中。
unsigned long pageframe = physaddr >> PAGE_SHIFT;

// 由页帧号返回对应的page结构指针保存在pageptr中
pageptr = pfn_to_page(pageframe);

// 调用get_page增加pageptr指向页面的引用计数
get_page(pageptr);

// 将指向page的指针赋值给vmf->page
vmf->page = pageptr;

对应的用户态代码:

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <error.h>
#include <errno.h>

// BIOS ROM
#define SIZE 0x10000
#define OFFSET 0xf0000

// Video ROM
//#define SIZE 32767
//#define OFFSET 0x000c0000

// ACPI Tables
//#define SIZE 65535
//#define OFFSET 0x2fff0000

int main(int argc, char *argv[])
{
    int fdin, fdout;
    void *src, *dst;
    struct stat statbuf;
    unsigned char sz[SIZE]={0};
    /*
    if ((fdin = open("/dev/simple_fault", O_RDWR|O_SYNC)) < 0)
        perror("can't open /dev/simple_fault for reading");
    */
    if ((fdin = open("/dev/simple_remap", O_RDWR|O_SYNC)) < 0)
        perror("can't open /dev/simple_remap for reading");

    if ((src = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
                    fdin, OFFSET)) == MAP_FAILED)
        perror("mmap error for simplen");

    memcpy(sz, src, SIZE);

    int i;
    for (i=0; i<SIZE; i++) {
        printf("%c", sz[i]);
    }

    munmap(src, SIZE);
    close(fdin);
    exit(0);
}

上面我们定义了此次映射内存的范围和大小:

#define SIZE 0x10000
#define OFFSET 0xf0000

0xf0000开始的大小为0x10000(64K)内存区域,此处物理内存保存的是BIOS ROM。

映射设备内存

上面一节的代码,映射的是物理内存,也就是从CPU角度看到的物理内存地址空间。 下面的代码中,映射了设备驱动中的内存。在用户态使用mmpa映射的方式和上面一样,区别在于设备驱动中对于这些虚拟内存地址不同的处理方式。

设备驱动中申请的内存是线性地址,需要将虚拟地址转换为物理地址,再得到物理地址的页帧号。


// 申明设备的内存地址指针
static unsigned char *myaddr_fault = NULL;
static unsigned char *myaddr_remap = NULL;

...

// 在内核高端内存空间分配一个页框,并返回指向页的线性地址
myaddr_fault = __get_free_pages(GFP_KERNEL, 1);
myaddr_remap = __get_free_pages(GFP_KERNEL, 1);

// 拷贝一些内容
strcpy(myaddr_fault, "00000000, fault version");
strcpy(myaddr_remap, "11111111, remap version");

...

/*
The remap version
*/
static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
    size_t size;
    size = vma->vm_end - vma->vm_start;
    printk (KERN_NOTICE "########### CALL remap_mmap");

    // 将线性地址转换为物理地址,并得到物理地址页框号
    vma->vm_pgoff = __pa(myaddr_remap)>>PAGE_SHIFT;

    /* Remap-pfn-range will mark the range VM_IO */
    if (remap_pfn_range(vma,
                vma->vm_start,
                vma->vm_pgoff,
                size,
                vma->vm_page_prot)) {
        return -EAGAIN;
    }
    vma->vm_ops = &simple_remap_vm_ops;
    simple_vma_open(vma);

    return 0;
}

...

/*
 * The fault version.
 */
int simple_vma_fault(struct vm_area_struct *vma,
                struct vm_fault *vmf)
{
    struct page *pageptr;
    printk (KERN_NOTICE "########### CALL fault_mmap");

    unsigned long offset = vmf->virtual_address - vma->vm_start;
    pageptr = virt_to_page(myaddr_fault);

    printk (KERN_NOTICE "page->index = %ld mapping %p\n", pageptr->index, pageptr->mapping);
    get_page(pageptr);
    vmf->page = pageptr;
    return 0;
}

上面的代码中,首先为设备申请了一页内存。申请内存使用使用了__get_free_pages函数。

__get_free_pages在内核高端内存空间分配连续的页,并第一页的线性地址。在内核高端内存中,由于内核只能使用线性地址的最后一个GB,所以在高端内存中分配了一个页框之后,在内线线性地址空间最后128MB中的一部分映射高端内存页框,这种映射是暂时的。

因为内核只有1GB的线性地址空间,所以调用__pa宏将内核线性地址转换为物理地址,宏的定义是:

// 将线性地址减去PAGE_OFFSET
// 32位下PAGE_OFFSET是3GB
#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)

接着执行有两种不同的映射方式:simple_remap_mmap和simple_vma_fault。

simple_remap_mmap的流程如下:

  • 将设备申请虚拟地址转换为物理地址,并得到物理地址的页帧号:
__pa(myaddr_remap)>>PAGE_SHIFT
  • 调用remap_pfn_range为进程建立物理地址的页表

simple_vma_fault的流程如下:

  • 调用virt_to_page从一个内核线性地址,得到它的也面描述符结构地址。原理是先将线性地址变为物理地址并得到物理地址的页帧号。得到页帧号之后,在全局的mem_map数据中就很容得到该页的页描述符地址。

  • get_page(pageptr)增加页的引用计数

  • 将页描述符地址赋值给vmf->page

用户空间的代码如下:

#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <error.h>
#include <errno.h>

#define SIZE 4096
#define OFFSET 0x00000000
//#define VERSION "remap"
#define VERSION "fault"

int main(int argc, char *argv[])
{
    int fdin, fdout;
    void *src, *dst;
    struct stat statbuf;
    unsigned char sz[SIZE]={0};

    if (VERSION == "fault") {
        if ((fdin = open("/dev/simple_fault", O_RDWR|O_SYNC)) < 0)
            perror("can't open /dev/simple_fault for reading");
    } else if (VERSION == "remap") {
        if ((fdin = open("/dev/simple_remap", O_RDWR|O_SYNC)) < 0)
            perror("can't open /dev/simple_remap for reading");
    } else {
        printf("bad version\n");
        exit(1);
    }

    if ((src = mmap(NULL, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED,
                    fdin, OFFSET)) == MAP_FAILED)
        perror("mmap error for simplen");

    memcpy(sz, src, SIZE);
    printf("%s\n", sz);

    munmap(src, SIZE);
    close(fdin);
    exit(0);
}

完整代码

前一篇: mmap实例及原理分析 后一篇: Python内存分配和垃圾回收

Captcha:
验证码

Email:

Content: (Support Markdown Syntax)