linux驱动开发(13)-内存映射mmap(一)

发布于:2025-06-22 ⋅ 阅读:(22) ⋅ 点赞:(0)

内存映射与内存分配不同,它要完成的任务是将设备的地址空间映射到用户空间或者直接使用用户空间中的地址,设备程序这样做的目的显然是从提升系统性能的角度出发。如果将这种概念更具体化,内存映射部分实际上是描述如何实现设备驱动程序中file_operations中的mmap方法。

设备缓存与设备内存

设备缓存是由驱动程序管理的位于系统主存RAM中的一段内存区域,而设备内存则是设备所固有的一段存储空间(比如某些设备的FIFO,显卡设备的Frame Buffer等)​,从设备驱动程序的角度,它应该属于特定设备的硬件范畴,与设备是紧密相关的。Linux系统下设备缓存与设备内存的典型用法是在两者之间建立DMA通道,这样当设备内存中接收到的数据达到一定的阈值时,设备将启动DMA通道将数据从设备内存传输到位于主存中的设备缓存中,发送数据则正好相反,需要发送的数据首先被放到设备缓存中,然后在设备驱动程序的介入下启动DMA传输,将缓存中的数据传输到设备内存中。

mmap

ioremap函数主要用来将内核空间的一段虚拟地址映射到外部设备的存储区(设备的I/O地址空间)中。mmap则用来将用户空间的一段虚拟地址映射到设备的I/O空间中,这样一来,用户空间进程将可以直接访问设备内存。驱动程序在这个过程中要完成的功能则是在其内部实现file_operations中的mmap方法。

struct vm_area_struct (virtual memory)

先从file_operations中定义的mmap方法的原型看起:

<include/linux/fs.h>
struct file_operations {
    …
    int (*mmap) (struct file *, struct vm_area_struct *);};

mmap函数的第一个参数是用来表示当前正在操作的一个struct file对象指针,第二个参数用来表示用户进程中一段需要被映射的虚拟地址区域。结构体struct vm_area_struct中的一些关键成员定义如下:

<include/linux/mm_types.h>
struct vm_area_struct {
    struct mm_struct*vm_mm;  /*The address space we belong to.*/
    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;
    pgprot_t vm_page_prot;     /*Access permissions of this VMA.*/
    unsigned long vm_flags;         /*Flags,see mm.h.*/
    /* Function pointers to deal with this struct. */
    const struct vm_operations_struct *vm_ops;};

struct mm_struct * vm_mm当前struct vm_area_struct对象所表示的虚拟地址段所归属的进程虚拟地址空间。unsigned long vm_start当前struct vm_area_struct对象所表示的虚拟地址段的起始地址。unsigned long vm_end当前struct vm_area_struct对象所表示的虚拟地址段的结束地址。struct vm_area_struct *vm_next, *vm_prev用来将一系列的struct vm_area_struct对象构建成链表。代表进程虚拟地址空间的struct mm_struct对象中的struct vm_area_struct * mmap成员用来指向该链表。pgprot_t vm_page_prot在将当前struct vm_area_struct对象所表示的虚拟地址段映射到设备内存时的页保护属性,主要体现在页目录(表)项的映射属性当中。unsigned long vm_flags当前struct vm_area_struct对象所表示的虚拟地址段的访问属性,比如VM_READ、VM_WRITE、VM_EXEC及VM_SHARED等。const struct vm_operations_struct *vm_ops用来定义对当前struct vm_area_struct对象所表示的虚拟地址段上的一组操作集。

内核中的每个struct vm_area_struct对象都表示用户进程地址空间的一段区域,它是访问用户进程中MMAP地址空间的最小单元,内核为管理这些struct vm_area_struct对象准备了大量的代码,一个核心的数据结构是红黑树。

用户空间虚拟地址布局(不同地址块的位置以及大小)

因为mmap用来映射用户空间的虚拟地址,所以有必要搞清楚在Linux系统下一个进程的用户虚拟地址空间的布局,此处的讨论按照经典的x86架构的3 GB/1 GB方式展开,也即用户空间虚拟地址大小是3 GB,内核空间虚拟地址大小是1 GB。此处的布局是指在3 GB的进程虚拟地址空间中规划出进程的代码段(text)​、存储全局变量和动态分配变量地址的堆,以及用于保存局部变量和实现函数调用的栈等存储块的起始地址和大小。Linux内核中采用两种布局方式。两种布局如图所示:

在这里插入图片描述

对用户进程虚拟地址空间布局的设计是操作系统要完成的任务之一。当Linux系统运行一个应用程序时,系统调用exec通过调用load_elf_binary函数来将该应用程序对应的ELF二进制文件加载到进程3 GB大小的虚拟地址空间中,布局由此产生。load_elf_binary函数建立布局相关的函数调用链是:load_elf_binary()→setup_new_exec()→arch_pick_mmap_layout()。mmap映射的地址区域出自3 GB大小的用户空间的MMAP区域)​,内核会很好地管理MMAP区域,管理该区域的最小单位是由struct vm_area_struct数据结构表示的对象,此处管理的语义是分配和释放一个vm_area_struct对象(想象一下用户进程对mmap和munmap等API的调用)​。系统中一个实际进程的MMAP区域看起来可能如下图所示:

在这里插入图片描述

从图中可以看到一个进程用户虚拟地址空间中的MMAP区域的空间状态,因为响应应用程序中mmap和munmap等系统调用的缘故,MMAP区域充斥了映射的区域和尚未被映射的空闲区域。每个区域由一个struct vm_area_struct对象表示,内核必须跟踪MMAP区域的分配情况(这跟vmalloc机制极其相似)​,并且应该能很好地处理从MMAP区域分配一个待映射的vm_area_struct对象或者释放一个被映射的vm_area_struct对象。

mmap从用户空间虚拟地址映射到设备内存的过程可以概括为:内核先在进程虚拟地址空间的MMAP区域分配一个空闲(即未映射)的struct vm_area_struct对象,然后通过修改页目录表项的方式将struct vm_area_struct对象所代表的虚拟地址空间映射到设备的存储空间中。如此,用户进程将可以直接访问设备的存储区,从而提高系统性能。页目录表项的介入也意味着每个vm_area_struct对象表示的地址空间应该是页对齐的,大小是页的整数倍。

mmap系统调用过程

在用户空间,mmap系统调用的函数原型为:

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

其中,start表示映射区的起始地址,length是映射区的长度,prot表示用户进程在映射区被映射时所期望的保护方式,常见的prot值有PROT_READ、PROT_WRITE及PROT_EXEC等,flags用于指定映射区的类型,fd是当前正在操作的文件的描述符,offset是实际数据在映射区中的偏移值。在实际使用中,start参数常常设为NULL,表示让系统在MMAP区域找一个合适的空闲区域。如果一切正常,mmap函数将返回已经被映射的MMAP区域中一段虚拟地址的起始地址,应用程序因此可以访问到对应的物理内存。当用户空间程序调用mmap函数时,Linux系统将通过系统调用sys_mmap_pgoff进入内核,由当前设备文件中实现的mmap方法来完成用户程序所要求的映射。sys_mmap_pgoff的核心代码如下:

<mm/mmap.c>
SYSCALL_DEFINE6(mmap_pgoff, unsigned long, addr, unsigned long, len,
        unsigned long, prot, unsigned long, flags,
        unsigned long, fd, unsigned long, pgoff)
{
    struct file *file = NULL;
    …
    file = fget(fd);
    flags &= ~(MAP_EXECUTABLE | MAP_DENYWRITE);
    down_write(&current->mm->mmap_sem);
    retval = do_mmap_pgoff(file, addr, len, prot, flags, pgoff);
    up_write(&current->mm->mmap_sem);}

sys_mmap_pgoff主要做两件事:一是通过fget函数由文件描述符获得对应的struct file对象指针;二是调用do_mmap_pgoff来完成后续的内存映射工作。do_mmap_pgoff的函数实现比较长,但是对比一下驱动程序中要实现的mmap方法的原型,基本上可以推测出do_mmap_pgoff函数的主体脉络应该是根据用户空间进程调用mmap API时传入的参数构造一个struct vm_area_struct对象的实例,然后调用file->f_op->mmap()。下面简单分析一下sys_mmap_pgoff函数。

<mm/mmap.c>
unsigned long do_mmap_pgoff(struct file *file, unsigned long addr,
            unsigned long len, unsigned long prot,
            unsigned long flags, unsigned long pgoff)
{
    //一些防御性代码的常规检查
    …
    len=PAGE_ALIGN(len);

此处要确保映射区的长度应该是一个PAGE大小的整数倍,因为映射发生时,最小的单位就是一个页,这是由体系结构中的MMU单元的特性决定的。

if ((pgoff + (len >> PAGE_SHIFT)) < pgoff)
    return -EOVERFLOW;

检查参数中的pgoff是否会溢出(OVERFLOW)。

addr = get_unmapped_area(file, addr, len, pgoff, flags);
if (addr & ~PAGE_MASK)
    return addr;

在进程的3 GB的虚拟地址空间中分配一块空闲区域。内核用户地址空间有两种布局方式,对任一布局而言,都会对当前进程的mm对象(memory management)中的get_unmapped_area成员进行赋值,对传统布局是mm->get_unmapped_area = arch_get_unmapped_area,对新式布局则是mm->get_unmapped _area = arch_get_unmapped_area_topdown。另外,我们知道file_operations结构体中有一个get_unmapped_area方法:

<include/linux/fs.h>
struct file_operations {
    …
    unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long,
                              unsigned long);};

mm->get_unmapped_area()和filp->get_unmapped_area()的主要作用都是在用户进程的虚拟地址空间中分配空闲的内存区域,在get_unmapped_area函数的内部,则通过mm->get_unmapped_area或者filp->get_unmapped_area来实现空闲虚拟地址的分配,这种选择基于以下代码:

<mm/mmap.c>
unsigned long
get_unmapped_area(struct file *file, unsigned long addr, unsigned long len,
        unsigned long pgoff, unsigned long flags)
{
    unsigned long (*get_area)(struct file *, unsigned long,
                  unsigned long, unsigned long, unsigned long);
    …
    get_area = current->mm->get_unmapped_area;
    if (file && file->f_op && file->f_op->get_unmapped_area)
        get_area = file->f_op->get_unmapped_area;
    addr = get_area(file, addr, len, pgoff, flags);}

如果驱动程序在其file_operations对象中没有定义get_unmapped_area方法,即file->f_op->get_unmapped_area为空,那么函数将利用当前进程mm对象中的get_unmapped_area函数来分配空闲的虚拟地址空间,否则将使用file->f_op->get_unmapped_area,现实中很少有驱动程序需要在自己的file_operations对象中实现get_unmapped_area方法,所以我们的讨论按照内核提供的标准分配函数进行。

对于传统布局而言,内核提供的分配MMAP区域中空闲虚拟地址空间的标准函数是arch_get_unmapped_area:

<mm/mmap.c>
unsigned long
arch_get_unmapped_area(struct file *filp, unsigned long addr,
        unsigned long len, unsigned long pgoff, unsigned long flags)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma;
    unsigned long start_addr;
    if (len > TASK_SIZE)
        return -ENOMEM;
    if (flags & MAP_FIXED)
        return addr;
    if (addr) {
        addr = PAGE_ALIGN(addr);
        vma = find_vma(mm, addr);
        if (TASK_SIZE - len >= addr &&
            (!vma || addr + len <= vma->vm_start))
             return addr;
    }
    if (len > mm->cached_hole_size) {
        start_addr = addr = mm->free_area_cache;
    } else {
        start_addr = addr = TASK_UNMAPPED_BASE;
        mm->cached_hole_size = 0;
    }
full_search:
    for (vma = find_vma(mm, addr); ; vma = vma->vm_next) {
        /*At this point:  (!vma||addr<vma->vm_end).*/
        if (TASK_SIZE - len < addr) {
            /*
              * Start a new search - just in case we missed
              * some holes.
              */
            if (start_addr != TASK_UNMAPPED_BASE) {
                addr = TASK_UNMAPPED_BASE;
                start_addr = addr;
                mm->cached_hole_size = 0;
                goto full_search;
            }
            return -ENOMEM;
        }
        if (!vma || addr + len <= vma->vm_start) {
            /*
              * Remember the place where we stopped the search:
              */
            mm->free_area_cache = addr + len;
            return addr;
        }
        if (addr + mm->cached_hole_size < vma->vm_start)
            mm->cached_hole_size = vma->vm_start - addr;
        addr = vma->vm_end;
    }
}

如果应用程序指定的待映射区域的长度大于TASK_SIZE,函数将把一个错误码-ENOMEM返回给用户进程,告知用户进程目前空闲的虚拟地址空间不足以满足本次映射需求。如果用户进程指定了MAP_FIXED标志,表明映射将从addr参数指定的起始地址处开始,因此这种情况函数将直接返回addr。接下来函数检查调用者有没有指定要优先映射的虚拟地址,如果有,内核将检查addr和len所确定的待映射的虚拟地址空间是否与已经被映射的虚拟地址空间重叠,如果不重叠将直接返回addr。如果调用者没有指定一个需要优先映射的地址(这种情况下应用程序在调用mmap函数时传递的参数start为NULL)​,那么内核必须遍历用户进程中所有可用区域,设法找到一个大小合适的空闲区域,通常情况下,应用程序在调用mmap时都是将start参数设定为NULL,也就是让内核自己在当前进程用户空间的MMAP区域去找一块待映射区域。无论如何,现在通过do_mmap_pgoff中的get_unmapped_area函数调用在MMAP区域获得了一个空闲的尚未被映射的vm_area_struct对象。


网站公告

今日签到

点亮在社区的每一天
去签到