Linux 设备驱动程序(三)(上)

简介: Linux 设备驱动程序(三)

第十五章 内存映射和DMA

1、 Linux的内存管理

(1)地址类型

用户虚拟地址

这是在用户空间程序所能看到的常规地址。用户地址或者是 32 位的,或者是 64 位的,这取决于硬件的体系架构。每个进程都有自己的虚拟地址空间。


物理地址

该地址在处理器和系统内存之间使用。物理地址也是 32 位或者 64 位长的,在某些情况下甚至 32 位系统也能使用 64 位的物理内存。


总线地址

该地址在外围总线和内存之间使用。通常它们与处理器使用的物理地址相同,但这么做并不是必需的。一些计算机体系架构提供了 I/O 内存管理单元(IOMMU),它实现总线和主内存之间的重新映射。IOMMU 可以用很多种方式让事情变得简单(比如使内存中的分散缓冲区对设备来说是连续的),但是当设置 DMA 操作时,编写 IOMMU 相关的代码是一个必需的额外步骤。当然总线地址是与体系架构密切相关的。


内核逻辑地址

内核逻辑地址组成了内核的常规地址空间。该地址映射了部分(或者全部)内存,并经常被视为物理地址。在大多数体系架构中,逻辑地址和与其相关联的物理地址的不同,仅仅是在它们之间存在一个固定的偏多量。逻辑地址使用硬件内建的指针大小,因此在安装了大量内存的 32 位系统中,它无法寻址全部的物理地址。逻辑地址通常保存在 unsigned long 或者 void * 这样类型的变量中。kmalloc 返回的内存就是内核逻辑地址。


内核虚拟地址

内核虚拟地址和逻辑地址的相同之处在于,它们都将内核空间的地址映射到物理地址上。内核虚拟地址与物理地址的映射不必是线性的和一对一的,而这是逻辑地址空间的特点。所有的逻辑地址都是内核虚拟地址,但是许多内核虚拟地址不是逻辑地址。举个例子,vmalloc 分配的内存具有一个虚拟地址(但并不存在直接的物理映射)。kmap 函数(在本章后面论述)也返回一个虚拟地址。虚拟地址通常保存在指针变量中。

    如果有一个逻辑地址,宏 __pa()(在 中定义)返回其对应的物理地址;使用宏 __va() 也能将物理地址逆向映射到逻辑地址,但这只对低端内存页有效。

    不同的内核函数需要不同类型的地址。如果在 C 中已经定义好了不同的类型,那么需要的地址类型将很明确,但现实不是这样的。在本章中,将明确表述在何处使用何种类型的地址。

(2)物理地址和页

   物理地址被分成离散的单元,称之为页。系统内部许多对内存的操作都是基于单个页的。每个页的大小随体系架构的不同而不同,但是目前大多数系统都使用每页 4096 个字节。常量 PAGE_SIZE(在  中定义)给出了在任何指定体系架构下的页大小。


   仔细观察内存地址,无论是虚拟的还是物理的,它们都被分为页号和一个页内的偏移量。举个例子,如果使用每页 4096 个字节,那么最后的 12 位是偏移量,而剩余的高位则指定了页号。如果忽略了地址偏移量,并将除去偏移量的剩余位移到右端,称该结果为页帧数。移动位以在页帧数和地址间进行转换是一个常用操作;宏 PAGE_SHIFT 将告诉程序员,必须移动多少位才能完成这个转换。


(3)高端与低端内存

   在装有大量内存的 32 位系统中,逻辑和内核虚拟地址的不同将非常突出。使用 32 位只能在 4GB 的内存中寻址。由于这种建立虚拟地址空间的问题,直到最近,32 位系统的 Linux 仍被限制使用少于 4GB 的内存。


   内核(在 x86 体系架构中,这是默认的设置)将 4GB 的虚拟地址空间分割为用户空间和内核空间;在二者的上下文中使用同样的映射。一个典型的分割是将 3GB 分配给用户空间,1GB 分配给内核空间(注 1)。内核代码和数据结构必须与这样的空间相匹配,但是占用内核地址空间最大的部分是物理内存的虚拟映射。内核无法直接操作没有映射到内核地址空间的内存。换句话说,内核对任何内存的访问,都需要使用自己的虚拟地址。因此许多年来,由内核所能处理的最大物理内存数量,就是将映射至虚拟地址空间内核部分的大小,再减去内核代码自身所占用的空间。因此,基于 x86 的 Linux 系统所能使用的最大物理内存,会比 1GB 小一点。


   注 1: 许多非x86 的体系架构不需需要这里描述的内核 / 用户空间分割即可有效工作,因此,这些体系架构在 32 位系统上就能获得 4GB 的内核地址空间。但是,这一小节描述的限制对安装有多于 4GB 内存的系统仍然适用。


   为了应对商业压力,在不破坏 32 位应用程序和系统兼容性的情况下,为了能使用更多的内存,处理器制造厂家为他们的产品增添了 “地址扩展” 特性。其结果是在许多情况下,即使 32 位的处理器都可以在大于 4GB 的物理地址空间寻址。然而有多少内存可以直接映射到逻辑地址的限制依然存在。只有内存的低端部分(依赖与硬件和内核的设置,一般为 1 到 2GB)拥有逻辑地址(注 2);剩余的部分(高端内存)是没有的。在访问特定的高端内存页前,内核必须建立明确的虚拟映射,使该页可在内核地址空间中被访问。因此,许多内核数据结构必须被放置在低端内存中;而高端内存更趋向于为用户空间进程页所保留。


   注 2: 2.6 内核通过一个补丁可在 x86 硬件上持 “4G/4G” 模式,它可以让内核和用户虚拟地址空间变大,但会引入微小的性能代价。


   术语 “高端内存” 可能对一些人来说理解起来比较困难、特别是在 PC 世界中,它还有着其他的含义。因此为了弄清这个问题,这里先对它进行定义:


低端内存

存在于内核空间上的逻辑地址内存。几乎所有现在读者遇到的系统,它全部的内存都是低端内存。

高端内存

是指那些不存在逻辑地址的内存,这是因为它们处于内核虚拟地址之上。

   在 i386 系统中,虽然在内核配置的时候能够改变低端内存和高端内存的界限,但是通常将该界限设置为小于 1GB。这个界限与早期 PC 中的 640K 限制没有任何关系,并且它的设置也与硬件无关。相反它是由内核设置的,把 32 位地址空间分割成内核空间与用户空间。


   在后面的部分中,将指出使用高端内存的限制。

(4)内存映射和页结构

   由于历史的关系,内核使用逻辑地址来引用物理内存中的页。然而由于支持了高端内存,就暴露出一个明显的问题 —— 在高端内存中将无法使用逻辑地址。因此内核中处理内存的函数趋向使用指向 page 结构的指针(在  中定义)。该数据结构用来保存内核需要知道的所有物理内存信息;对系统中每个物理页,都有一个 page 结构相对应。下面介绍该结构中包含的几个成员:

// include/linux/mm_types.h
struct page {
  unsigned long flags;    /* Atomic flags, some possibly
           * updated asynchronously */
  atomic_t _count;    /* Usage count, see below. */
  union {
    atomic_t _mapcount; /* Count of ptes mapped in mms,
           * to show when page is mapped
           * & limit reverse map searches.
           */
    struct {    /* SLUB */
      u16 inuse;
      u16 objects;
    };
  };
  union {
      struct {
    unsigned long private;    /* Mapping-private opaque data:
             * usually used for buffer_heads
             * if PagePrivate set; used for
             * swp_entry_t if PageSwapCache;
             * indicates order in the buddy
             * system if PG_buddy is set.
             */
    struct address_space *mapping;  /* If low bit clear, points to
             * inode address_space, or NULL.
             * If page mapped as anonymous
             * memory, low bit is set, and
             * it points to anon_vma object:
             * see PAGE_MAPPING_ANON below.
             */
      };
#if USE_SPLIT_PTLOCKS
      spinlock_t ptl;
#endif
      struct kmem_cache *slab;  /* SLUB: Pointer to slab */
      struct page *first_page;  /* Compound tail pages */
  };
  union {
    pgoff_t index;    /* Our offset within mapping. */
    void *freelist;   /* SLUB: freelist req. slab lock */
  };
  struct list_head lru;   /* Pageout list, eg. active_list
           * protected by zone->lru_lock !
           */
  /*
   * On machines where all RAM is mapped into kernel address space,
   * we can simply calculate the virtual address. On machines with
   * highmem some memory is mapped into kernel virtual memory
   * dynamically, so we need a place to store that address.
   * Note that this field could be 16 bits on x86 ... ;)
   *
   * Architectures with slow multiplication can define
   * WANT_PAGE_VIRTUAL in asm/page.h
   */
#if defined(WANT_PAGE_VIRTUAL)
  void *virtual;      /* Kernel virtual address (NULL if
             not kmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
  unsigned long debug_flags;  /* Use atomic bitops on this */
#endif

#ifdef CONFIG_KMEMCHECK
  /*
   * kmemcheck wants to track the status of each byte in a page; this
   * is a pointer to such a status block. NULL if not tracked.
   */
  void *shadow;
#endif
};

atomic_t count;

对该页的访问计数。当计数值为 0 时,该页将返回给空闲链表。

void *virtual;

如果页面被映射,则指向页的内核虚拟地址;如果未被映射则为 NULL。低端内存页总是被映射;而高端内存页通常不被映射。并不是在所有体系架构中都有该成员;只有在页的内核虚拟地址不容易被计算时,它才被编译。如果要访问该成员,正确的方法是使用下面讲述的 page_address 宏。

unsigned long flags;

描述页状态的一系列标志。其中,PG_locked 表示内存中的页已经被锁住,而 PG_reserved 表示禁止内存管理系统访问该页。

   在 page 结构中还包含了许多信息,但这是深层次内存管理所关心的问题,而驱动程序作者不必要了解。


   内核维护了一个或者多个 page 结构数组,用来跟踪系统中的物理内存。在一些系统中,有一个单独的数组称之为 mem_map。在另外一些系统中,情况将会复杂很多。非一致性内存访问(Nonuniform Memory Access,NUMA)系统和有大量不连续物理内存的系统会有多个内存映射数组,因此从可移植性考虑,代码不要直接访问那些数组。幸运的是,通常只需要使用 page 结构的指针,而不需要了解它们是怎么来的。


有一些函数和宏用来在 page 结构指针与虚拟地址之间进行转换:

// arch/x86/include/asm/page.h
struct page *virt_to_page(void *kaddr);

#define virt_to_page(kaddr) pfn_to_page(__pa(kaddr) >> PAGE_SHIFT)
#define __pa(x)   __phys_addr((unsigned long)(x))
#define __va(x)     ((void *)((unsigned long)(x)+PAGE_OFFSET))

// arch/x86/include/asm/page_types.h
#define PAGE_SHIFT  12

    该宏在 中定义,负责将内核逻辑地址转换为相应的 page 结构指针。由于它需要一个逻辑地址,因此它不能操作 vmalloc 生成的地址以及高端内存。

// include/asm-generic/memory_model.h
struct page *pfn_to_page(int pfn);

#define __pfn_to_page(pfn)  (vmemmap + (pfn))
#define __page_to_pfn(page) (unsigned long)((page) - vmemmap)

// arch/x86/include/asm/page_64_types.h
#define vmemmap ((struct page *)VMEMMAP_START)

// arch/x86/include/asm/pgtable_64_types.h
#define VMEMMAP_START  _AC(0xffffea0000000000, UL)

// include/linux/const.h
#define __AC(X,Y) (X##Y)
#define _AC(X,Y)  __AC(X,Y)

    针对给定的页帧号,返回 page 结构指针。如果需要的话,在将页帧号传递给 pfn_to_page 前,使用 pfn_valid 检查页帧号的合法性。

void *page_address(struct page *page);

// include/linux/mm.h
#define page_address(page) lowmem_page_address(page)

static __always_inline void *lowmem_page_address(struct page *page)
{
  return __va(page_to_pfn(page) << PAGE_SHIFT);
}


  如果地址存在的话,则返回页的内核虚拟地址。对于高端内存来说,只有当内存页被映射后该地址才存在。该函数定义在 中。在大多数情况下,要使用 kmap 而不是 page_address

#include <linux/highmem.h>
void *kmap(struct page *page);
void kunmap(struct page *page);

   kmap 为系统中的页返回内核虚拟地址。对于低端内存页来说,它只返回页的逻辑地址;对于高端内存,kmap 在专用的内核地址空间创建特殊的映射。由 kmap 创建的映射需要用 kunmap 释放;对该种映射的数量是有限的,因此不要持有映射过长的时间。kmap 调用维护了一个计数器,因此如果两个或是多个函数对同一页调用 kmap,操作也是正常的。请注意当没有映射的时候,kmap 将会休眠。

#include <linux/highmem.h>
#include <asm/kmap_types.h>
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);

   kmap_atomic 是 kmap 的高性能版本。每个体系架构都为原子的 kmap 维护着一个槽(专用页表入口)的列表;kmap_atomic 的调用者必须告诉系统,type 参数使用的是哪个槽。对驱动程序有意义的槽只有 KM_USER0 和 KM_USER1(针对在用户空间中直接运行的代码),KM_IRQ0 和 KM_IRQ1(针对中断处理程序)。要注意的是原子的 kmap 必须原子地处理,也就是说,在拥有它的时候,代码不能进入睡眠状态。还要注意的是在内核中,没有任何机制能防止这两个函数使用相同的槽,以及防止它们之间的相互干涉(虽然对每个 CPU 都有一套特定的槽)。在实际情况中,对原子的 kmap 槽的争夺并不会引起什么问题。


   在研究例子代码时,以及在本章及后面的章节中,读者会看到如何使用这些面数。

(5)页表

   在任何现代的系统中,处理器必须使用某种机制、将虚拟地址转换为相应的物理地址。这种机制被称为页表;它基本上是一个多层树形结构,结构化的数组中包含了虚拟地址到物理地址的映射和相关的标志位。即使在不直接使用这种页表的体系架构中,Linux 内核也维护了一系列的页表。


   设备驱动程序执行了大量操作,用来处理页表。幸运的是,对驱动程序作者来说,在 2.6 版内核中删除了对页表直接操作的需求。因此这里不对它们做详细讲解;富有好奇心的读者可以阅读 Daniel P. Bovet 和 Marco Cesati 编写的《Understanding The Linux Kernel》(O’Reilly)一书了解详细情况。

(6)虚拟内存区

   虚拟内存区(VMA)用于管理进程地址空间中不同区域的内核数据结构。一个 VMA 表示在进程的虚拟内存中的一个同类区域: 拥有同样权限标志位和被同样对象(一个文件或者交换空间)备份的一个连续的虚拟内存地址范围。它符合更宽泛的 “段” 的概念,但是把其描述成 “拥有自身属性的内存对象” 更为贴切。进程的内存映射(至少)包含下面这些区域:


程序的可执行代码(通常称为 text)区域。

多个数据区,其中包含初始化数据(在开始执行的时候就拥有明确的值)、非初始化数据(BSS,注 3)以及程序堆栈。

与每个活动的内存映射对应的区域。

   注 3: BSS 这一名称是一个历史遗物,来自一条老的汇编操作称为 “block started by symbol(符号定义的块)”。可执行文件的 BSS 段并不会存储在磁盘上,而是由内核将零页映射到 BSS 地址范围。


   查看 /proc/(其中的 pid 要替换为具体的进程 ID)文件就能了解进程的内存区域。/proc/self 是一个特殊的文件,因为它始终指向当前进程。下面是多个内存映射的例子(注释以斜体的方式给出):

每行都是用下面的形式表示的:

start-end perm offset major:minor inode image

    在 /proc/*/maps 中的每个成员(除映像名外)都与 vm_area_struct 结构中的一个成员相对应:

start

end

该内存区域的起始处和结束处的虚拟地址。

perm

内存区域的读、写和执行权限的位掩码。该成员描述了允许什么样的进程能访问属于该区域的页。该成员的最后一个字母或者是 p 表示私有,或者是 s 表示共享。

offset

表示内存区域在映射文件中的起始位置。偏移量为 0 表示内存区域的起始位置映射到文件的开始位置。

major

minor

拥有映射文件的设备的主设备号和次设备号。对于设备映射来说,主设备号和次设备号指的是包含设备特殊文件的磁盘分区,该文件由用户而非设备自身打开。

inode

被映射的文件的索引节点号。

image

被映射文件(通常是一个可执行映像)的名称。

(a)vm_area_struct 结构

   当用户空间进程调用 mmap、将设备内存映射到它的地址空间时,系统通过创建一个表示该映射的新 VMA 作为响应。支持 mmap 的驱动程序(当然要实现 mmap 方法)需要帮助进程完成 VMA 的初始化。因此驱动程序作者为了能支持 mmap,需要对 VMA 有所了解。


   现在来学习 vm_area_struct 结构(在  中定义)中最重要的成员。在设备驱动程序对 mmap 的实现中会使用到这些成员。请注意,为优化查找方法,内核维护了 VMA 的链表和树型结构,而 vm_area_struct 中的许多成员都是用来维护这个结构的。因此驱动程序不能任意创建 VMA,或者打破这种组织结构。VMA 的主要成员如下所示(请注意这些成员和刚才看到的 /proc 文件输出之间的区别):

// 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;

  pgprot_t vm_page_prot;    /* Access permissions of this VMA. */
  unsigned long vm_flags;   /* Flags, see mm.h. */

  struct rb_node vm_rb;

  /*
   * For areas with an address space and backing store,
   * linkage into the address_space->i_mmap prio tree, or
   * linkage to the list of like vmas hanging off its node, or
   * linkage of vma in the address_space->i_mmap_nonlinear list.
   */
  union {
    struct {
      struct list_head list;
      void *parent; /* aligns with prio_tree_node parent */
      struct vm_area_struct *head;
    } vm_set;

    struct raw_prio_tree_node prio_tree_node;
  } 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, *not* PAGE_CACHE_SIZE */
  struct file * vm_file;    /* File we map to (can be NULL). */
  void * vm_private_data;   /* was vm_pte (shared mem) */
  unsigned long vm_truncate_count;/* truncate_count or restart_addr */

#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
};

unsigned long vm_start;

unsigned long vm_end;

该 VMA 所覆盖的虚拟地址范围。这是 /proc/*/maps 中最前面的两个成员。

struct file *vm_file;

指向与该区域(如果存在的话)相关联的 file 结构指针。

unsigned long vm_pgoff;

以页为单位,文件中该区域的偏移量。当映射一个文件或者设备时,它是该区域中被映射的第一页在文件中的位置。

unsigned long vm_flags;

描述该区域的一套标志。驱动程序最感兴趣的标志是 VM_IO 和 VM_RESERVED。 VM_IO 将 VMA 设置成一个内存映射 I/O 区域。VM_IO 会阻止系统将该区域包含在进程的核心转储中。VM_RESERVED 告诉内存管理系统不要将该 VMA 交换出去;大多数设备映射中都设置该标志。

struct vm_operations_struct *vm_ops;

内核能调用的一套函数,用来对该内存区进行操作。它的存在表示内存区域是一个内核 “对象” ,这点和在本书中使用的 file 结构很相似。

void *vm_private_data;

驱动程序用来保存自身信息的成员。

   与 vm_area_struct 结构类似,vm_operations_struct 结构也定义在  中,其中包含了下面列出的函数。这些操作只是用来处理进程的内存需求,并按照声明的顺序将它们列了出来。在本章后面的部分,将介绍如何实现其中的几个函数。

// include/linux/mm.h
struct vm_operations_struct {
  void (*open)(struct vm_area_struct * area);
  void (*close)(struct vm_area_struct * area);
  int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf);

  /* notification that a previously read-only page is about to become
   * writable, if an error is returned it will cause a SIGBUS */
  int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf);

  /* called by access_process_vm when get_user_pages() fails, typically
   * for use by special VMAs that can switch between memory and hardware
   */
  int (*access)(struct vm_area_struct *vma, unsigned long addr,
          void *buf, int len, int write);
#ifdef CONFIG_NUMA
  /*
   * set_policy() op must add a reference to any non-NULL @new mempolicy
   * to hold the policy upon return.  Caller should pass NULL @new to
   * remove a policy and fall back to surrounding context--i.e. do not
   * install a MPOL_DEFAULT policy, nor the task or system default
   * mempolicy.
   */
  int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new);

  /*
   * get_policy() op must add reference [mpol_get()] to any policy at
   * (vma,addr) marked as MPOL_SHARED.  The shared policy infrastructure
   * in mm/mempolicy.c will do this automatically.
   * get_policy() must NOT add a ref if the policy at (vma,addr) is not
   * marked as MPOL_SHARED. vma policies are protected by the mmap_sem.
   * If no [shared/vma] mempolicy exists at the addr, get_policy() op
   * must return NULL--i.e., do not "fallback" to task or system default
   * policy.
   */
  struct mempolicy *(*get_policy)(struct vm_area_struct *vma,
          unsigned long addr);
  int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from,
    const nodemask_t *to, unsigned long flags);
#endif
};
void (*open)(struct vm_area_struct *vma);

  内核调用 open 函数,以允许实现 VMA 的子系统初始化该区域。当对 VMA 产生一个新的引用时(比如 fork 进程时),则调用这个函数。唯一的例外发生在 mmap 第一次创建 VMA 时;在这种情况下,需要调用驱动程序的 mmap 方法。

void (*close)(struct vm_area_struct *vma);

    当销毁一个区域时,内核将调用 close 操作。请注意由于 VMA 没有使用相应的计数,所以每个使用区域的进程都只能打开和关闭它一次。

struct page *(*nopage) (struct vm_area_struct *vma, unsigned long address, int *type);

    当一个进程要访问属于合法 VMA 的页,但该页又不在内存中时,则为相关区域调用 nopage 函数(如果定义了的话)。在将物理页从辅助存储器中读入后,该函数返回指向物理页的 page 结构指针。如果在该区域没有定义 nopage 函数,则内核将为其分配一个空页。

int (*populate)(struct vm_area_struct *vm, unsigned long address, unsigned long len, 
        pgprot_t prot, unsigned long pgoff, int nonblock);


    在用户空间访问页前,该函数允许内核将这些页预先装入内存。一般来说,驱动程序不必实现 populate 方法。

(7)内存映射处理

  最后一个内存管理难题是处理内存映射结构,它负责整合所有其他的数据结构。在系统中的每个进程(除了内核空间的一些辅助线程外)都拥有一个 struct mm_struct 结构(在  中定义),其中包含了虚拟内存区域链表、页表以及其他大量内存管理信息,还包含一个信号灯(mmap_sea)和一个自旋锁(page_table_lock)。在 task 结构中能找到该结构的指针;在少数情况下当驱动程序需要访问它时,常用的办法是使用 current->mm。请注意,多个进程可以共享内存管理结构,Linux 就是用这种方法实现线程的。


   为了能对 Linux 内存管理数据结构有一个通盘的了解,现在首先看看 mmap 系统调用是如何实现的。

// include/linux/mm_types.h
struct mm_struct {
  struct vm_area_struct * mmap;   /* list of VMAs */
  struct rb_root mm_rb;
  struct vm_area_struct * mmap_cache; /* last find_vma result */
#ifdef CONFIG_MMU
  unsigned long (*get_unmapped_area) (struct file *filp,
        unsigned long addr, unsigned long len,
        unsigned long pgoff, unsigned long flags);
  void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
  unsigned long mmap_base;    /* base of mmap area */
  unsigned long task_size;    /* size of task vm space */
  unsigned long cached_hole_size;   /* if non-zero, the largest hole below free_area_cache */
  unsigned long free_area_cache;    /* first hole of size cached_hole_size or larger */
  pgd_t * pgd;
  atomic_t mm_users;      /* How many users with user space? */
  atomic_t mm_count;      /* How many references to "struct mm_struct" (users count as 1) */
  int map_count;        /* number of VMAs */
  struct rw_semaphore mmap_sem;
  spinlock_t page_table_lock;   /* Protects page tables and some counters */

  struct list_head mmlist;    /* List of maybe swapped mm's.  These are globally strung
             * together off init_mm.mmlist, and are protected
             * by mmlist_lock
             */


  unsigned long hiwater_rss;  /* High-watermark of RSS usage */
  unsigned long hiwater_vm; /* High-water virtual memory usage */

  unsigned long total_vm, locked_vm, shared_vm, exec_vm;
  unsigned long stack_vm, reserved_vm, def_flags, nr_ptes;
  unsigned long start_code, end_code, start_data, end_data;
  unsigned long start_brk, brk, start_stack;
  unsigned long arg_start, arg_end, env_start, env_end;

  unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

  /*
   * Special counters, in some configurations protected by the
   * page_table_lock, in other configurations by being atomic.
   */
  struct mm_rss_stat rss_stat;

  struct linux_binfmt *binfmt;

  cpumask_t cpu_vm_mask;

  /* Architecture-specific MM context */
  mm_context_t context;

  /* Swap token stuff */
  /*
   * Last value of global fault stamp as seen by this process.
   * In other words, this value gives an indication of how long
   * it has been since this task got the token.
   * Look at mm/thrash.c
   */
  unsigned int faultstamp;
  unsigned int token_priority;
  unsigned int last_interval;

  unsigned long flags; /* Must use atomic bitops to access the bits */

  struct core_state *core_state; /* coredumping support */
#ifdef CONFIG_AIO
  spinlock_t    ioctx_lock;
  struct hlist_head ioctx_list;
#endif
#ifdef CONFIG_MM_OWNER
  /*
   * "owner" points to a task that is regarded as the canonical
   * user/owner of this mm. All of the following must be true in
   * order for it to be changed:
   *
   * current == mm->owner
   * current->mm != mm
   * new_owner->mm == mm
   * new_owner->alloc_lock is held
   */
  struct task_struct *owner;
#endif

#ifdef CONFIG_PROC_FS
  /* store ref to file /proc/<pid>/exe symlink points to */
  struct file *exe_file;
  unsigned long num_exe_file_vmas;
#endif
#ifdef CONFIG_MMU_NOTIFIER
  struct mmu_notifier_mm *mmu_notifier_mm;
#endif
};

2、mmap 设备操作

    在现代 Unix 系统中,内存映射是最吸引人的特征。对于驱动程序来说,内存映射可以提供给用户程序直接访问设备内存的能力。

    使用 mmap 的一个例子是看一下 X Window 系统服务器的部分虚拟内存区域:

   X 服务器完整的 VMA 的清单很长,但这里对其中的大部分内容都不感兴趣。可以看到,有四个独立的 /dev/mem 的映射,它为我们揭示了 X 服务器如何使用显示卡工作的内幕。第一个映射开始位置是 a0000,这是在 640KB ISA 结构中显示 RAM 的标准位置。往下可以看到更大的一块映射区域 e8000000,其地址位于系统最大 RAM 地址之上。这是对显示适配器中显存的直接映射。


在 /proc/iomem 中也可以看到:

246e17d0970a5e1443e14640e66be48e.png


   映射一个设备意味着将用户空间的一段内存与设备内存关联起来。无论何时当程序在分配的地址范围内读写时,实际上访问的就是设备。在 X 服务器例子中,使用 mmap 就能迅速而便捷地访问显卡内存。对于那些与此类似、性能要求苛刻的应用程序,直接访问能显著提高性能。


   正如读者怀疑的那样,不是所有的设备都能进行 mmap 抽象的。比如像串口和其他面向流的设备就不能做这样的抽象。对 mmap 的另外一个限制是:必须以 PAGE_SIZE 为单位进行映射。内核只能在页表一级上对虚拟地址进行管理,因此那些被映射的区域必须是 PAGE_SIZE 的整数倍,并且在物理内存中的起始地址也要求是 PAGE_SIZE 的整数倍。如果区域的大小不是页的整数倍,则内核强制指定比区域稍大一点的尺寸作为映射的粒度。


   对于驱动程序来说这些限制并不是什么大问题,因为访问设备的程序都是与设备相关的。由于程序必须知道设备的工作过程,因此程序员不会被诸如页边界之类的需求所困扰。在某些非 x86 平台上工作的 ISA 设备面临更大的制约,因为它们的 ISA 硬件视图是不连续的。比如一些 Alpha 计算机视 ISA 内存为不可直接映射的 8 位、16 位或者 32 位的离散项的集合。在这种情况下,根本无法使用 mmap。无法将 ISA 地址直接映射到 Alpha 地址,是由于这两种系统间,存在着不兼容的数据传输规则。虽然早期的 Alpha 处理器只能解决 32 位和 64 位内存访问问题,但是对于 ISA 来说只能进行 8 位和 16 位的传输,没有办法透明地将一个协议映射到另外一个协议上。


   当灵活使用 mmap 时,它具有很大的优势。比如在 X 服务器例子中,它负责和显存间读写大量数据;与使用 lseek/write 相比,将图形显示映射到用户空间极大地提高了吞吐量。另外一个典型例子是控制 PCI 设备的程序。大多数 PCI 外围设备将它们的控制寄存器映射到内存地址中、高性能的应用程序更愿意直接访问寄存器,而不是不停的调用 ioctl 去获得需要的信息。


   mmap 方法是 file_operations 结构的一部分,并且执行 mmap 系统调用时将调用该方法。使用 mmap,内核在调用实际函数之前,就能完成大量的工作,因此该方法的原型与系统调用有着很大的不同。它也与诸如 ioctl 和 poll 不同,内核在调用那些函数前不用做什么工作。

  系统调用有着以下声明(在 mmap(2) 手册页中描述):

mmap(caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)

但是文件操作声明如下:

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

   该函数中的 filp 参数与第三章中介绍的一样,vma 包含了用于访问设备的虚拟地址的信息。因此大量的工作由内核完成;为了执行 mmap,驱动程序只需要为该地址范围建立合适的页表,并将 vma->vm_ops 替换为一系列的新操作就可以了。


   有两种建立页表的方法:使用 remap_pfn_range 函数一次全部建立,或者通过 nopage VMA 方法每次建立一个页表。这两种方法有它各自的优势和局限性。这里首先介绍一次全部建立的方法,因为它最简单。从这开始,将会为实际的实现方法逐渐增加其复杂性:

(1)使用 remap_pfn_range

    remap_pfn_rangeio_remap_page_range 负责为一段物理地址建立新的页表,它们有着如下的原型:

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

  通常函数的返回值是 0,或者是个负的错误码。现在来看看各参数的含义:


vma

虚拟内存区域,在一定范围内的页将被映射到该区域内。

virt_addr

重新映射时的起始用户虚拟地址。该函为处于 virt_addr 和 virt_addr+size 之间的虚拟地址建立页表。

pfn

与物理内存对应的页帧号,虚拟内存将要被映射到该物理内存上。页帧号只是将物理地址右移 PAGE_SHIFT 位。在多数情况下,VMA 结构中的 vm_pgoff 成员包含了用户需要的值。该函数对处于(pfn<

size

以字节为单位,被重新映射的区域大小。

prot

新 VMA 要求的 “保护(protection)” 属性。驱动程序能够(也应该)使用 vma->vm_page_prot 中的值。

   remap_pfn_range 函数的参数非常简单,当调用 mmap 函数的时候,它们中大部分的值在 VMA 中提供。也许读者会奇怪,为什么会有两个函数呢? 第一个函数(remap_pfn_range)是在 pfn 指向实际系统 RAM 的时候使用,而 io_remap_page_range 是在 phys_addr 指向 I/O 内存的时候使用。在实际应用中,除了 SPARC 外,对每个体系架构这两个函数是等价的,而在大多数情况下会使用 remap_pfn_range 函数。对于有可移植性要求的驱动程序,要使用与特定情形相符的 remap_pfn_range 变种。


   另外复杂性也表现在缓存上: 对设备内存的引用通常不能被处理器所缓存。系统的 BIOS 会正确设置缓存,但是也可以通过 protection 成员禁止缓存特定的 VMA。不幸的是,在这个层面上的禁止缓存是与处理器高度相关的。好奇的读者可以参考 drivers/char/mem.c 中的 pgprot_noncached 函数以了解其中细节。本书不对这个主题进行讨论。

(2)一个简单的实现

   如果驱动程序要将设备内存线性地映射到用户地址空间中,程序员基本上就只需要调用 remap_pfn_range 函数。下面的代码来自 drivers/char/mem.c,并且揭示了在一个被称为 simple(Simple Implementation Mapping Pages with Little Enthusiasm) 的典型模块中,该任务是如何被完成的:

static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma) {
  if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff,
            vma->vm_end - vma->vm_start,
            vma->vm_page_prot))
    return -EAGAIN;
    
  vma->vm_ops = &simple_remap_vm_ops;
  simple_vma_open(vma);
  return 0;
}

  可见,重新映射内存就是调用 remap_pfn_range 函数创建所需的页表。

(3)为 VMA 添加操作

   如上所述,vm_area_struct 结构包含了一系列针对 VMA 的操作。现在来看看如何简单实现这些函数。本节提供了针对 VMA 的 open 和 close 操作。当进程打开或者关闭 VMA 时,会调用这些操作;特别是当 fork 进程或者创建一个新的对 VMA 引用时,随时都会调用 open 函数。对 VMA 函数 open 和 close 的调用由内核处理,因此它们没有必要重复内核中的工作。它们存在的意义在于为驱动程序处理其他所需要的事情。


   除此之外,一个诸如 simple 这样的简单驱动程序不需再做什么特别的事情了。这里创建了open 和 close 函数,它们负责向系统日志中输入信息,告诉系统它们被调用了。此外没有其他特殊用途了,不过它能告诉读者: 如何提供这些函数以及何时调用它们。


因此,代码中用调用 printk 的新操作,覆盖了默认的 vma->vm_ops:

void simple_vma_open(struct vm_area_struct *vma) {
  printk(KERN_NOTICE "Simple VMA open, virt %1x, phys %1x\n",
      vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);
}
      
void simple_vma_close(struct vm_area_struct *vma) {
  printk(KERN_NOTICE "Simple VMA close.\n");
}
  
static struct vm_operations_struct simple_remap_vm_ops =
  .open = simple_vma_open,
  .close = simple_vma_close,
};

    为了使这些操作对特定的映射有效,需要在相关 VMAvm_ops 成员中保存指向 simple_remap_vm_ops 的指针。这通常在 mmap 方法中完成。如果回过头去看 simple_remap_mmap 示例,能看到如下代码:

  vma->vm_ops = &simple_remap_vm_ops;
  simple_vma_open(vma);

    请注意对 simple_vma_open 函数的显式调用。由于在原来的 mmap 中没有调用 open 函数,因此必须显式调用它才能使其正常运行。


Linux 设备驱动程序(三)(中):https://developer.aliyun.com/article/1597488


目录
相关文章
|
12天前
|
Linux 程序员 编译器
Linux内核驱动程序接口 【ChatGPT】
Linux内核驱动程序接口 【ChatGPT】
|
18天前
|
Java Linux API
Linux设备驱动开发详解2
Linux设备驱动开发详解
22 6
|
18天前
|
消息中间件 算法 Unix
Linux设备驱动开发详解1
Linux设备驱动开发详解
23 5
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(一)((下)
Linux 设备驱动程序(一)
16 3
|
18天前
|
安全 数据管理 Linux
Linux 设备驱动程序(一)(中)
Linux 设备驱动程序(一)
17 2
|
18天前
|
Linux
Linux 设备驱动程序(四)
Linux 设备驱动程序(四)
10 1
|
18天前
|
存储 数据采集 缓存
Linux 设备驱动程序(三)(中)
Linux 设备驱动程序(三)
13 1
|
18天前
|
存储 前端开发 大数据
Linux 设备驱动程序(二)(中)
Linux 设备驱动程序(二)
15 1
|
18天前
|
缓存 安全 Linux
Linux 设备驱动程序(二)(上)
Linux 设备驱动程序(二)
16 1
|
18天前
|
存储 缓存 安全
Linux 设备驱动程序(三)(下)
Linux 设备驱动程序(三)
11 0