• LDD3 第15章 内存映射和DMA


    本章内容分为三个部分:

    • 第一部分讲述了mmap系统调用的实现过程。将设备内存直接映射到用户进程的地址空间,尽管不是所有设备都需要,但是能显著的提高设备性能。
    • 如何跨越边界直接访问用户空间的内存页,一些相关的驱动程序需要这种能力。在很多情况下,内核执行了该种映射,而无需驱动程序的参与。
    • 直接内存访问(DMA)I/O操作,它使得外设具有直接访问系统内存的能力。

    一、Linux的内存管理

    关注Linux内存管理实现的主要特性,而非讲述操作系统中内存管理的理论。

    1.1 地址类型

    Linux是一个虚拟内存系统,意味着用户程序所使用的地址与硬件使用的物理地址是不同的。

    虚拟内存是一个简介层,系统中运行的程序可以分配比物理内存更多的内存。甚至单独进程都拥有比系统物理内存更多的虚拟地址空间。

    在任何情况下使用何种类型的地址,内核代码并未明确加以区分,因此程序对此要仔细处理。

    • 用户虚拟地址:这是在用户空间程序所能看到的常规地址。用户地址或者32位的,或者是64位的
    • 物理地址:该地址在处理器和系统内存之家使用。
    • 总线地址:该地址在外围总线和内存之间使用。通常他们与处理器使用的物理地址相同,但这么做并不是必须的。一些计算机提供I/O内存管理(MMU),实现总线和主内存之间的重新映射。
    • 但使用DMA时,MMU变成了一个额外的操作。
    • 内核逻辑地址:内核逻辑地址组成了内核的常规地址空间。kmalloc返回的就是内核逻辑地址
    • 内核虚拟地址:内核虚拟地址和内核逻辑地址,都将内核空间的地址映射到物理地址上。内核虚拟地址与物理地址的映射不是一一对应的。

     如果有一个逻辑地址,宏__pa()(在<asm/page.h>中定义)返回其对应的物理地址,

    使用宏__va()也能将物理地址逆向映射到逻辑地址,但这只对低端内存页有效。

    1.2 物理地址和页

    物理地址被分散成离散的单元,称之为页。系统对内存的操作都是基于单个页的。

    每个页的大小随体系架构的不同而不同,大多数系统使用4095个字节。常量PAGE_SIZE(在<asm/page.h>中定义)给出了在任何指定体系架构下的大小。

    1.3 高端与低端内存

    大量的32位系统中,系统的寻址空间不能大于4GB。内核在(x86中)将4GB的虚拟地址空间分割成用户空间和内核空间。

    典型的分配是1GB内核空间,3GB的用户空间。内核对任何内存的访问,都需要映射至虚拟地址空间内核部分的大小,再减去内核代码自身所占用的空间。

    低端内存:在于内核空间上的逻辑地址内存。

    高端内存:那些不存在逻辑地址的内存,它们处于内核虚拟地址之上。

    1.4 内存映射和页结构

     内核使用逻辑地址来引用物理内存中的页。支持高端内存后,在高端内存中无法使用逻辑地址。内核处理内存的函数趋向于使用指向page结构的指针(在<linux/mm.h>中)。

    /*
     * Each physical page in the system has a struct page associated with
     * it to keep track of whatever it is we are using the page for at the
     * moment. Note that we have no way to track which tasks are using
     * a page, though if it is a pagecache page, rmap structures can tell us
     * who is mapping it.
     */
    struct page {
        unsigned long flags;        /* Atomic flags, some possibly
                         * updated asynchronously */
        atomic_t _count;        /* Usage count, see below. */
        union {
            /*
             * Count of ptes mapped in
             * mms, to show when page is
             * mapped & limit reverse map
             * searches.
             *
             * Used also for tail pages
             * refcounting instead of
             * _count. Tail pages cannot
             * be mapped and keeping the
             * tail page _count zero at
             * all times guarantees
             * get_page_unless_zero() will
             * never succeed on tail
             * pages.
             */
            atomic_t _mapcount;
    
            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
    };
    struct page

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

    void *virtual;      如果页面被映射,指向内核虚拟地址。未被映射则是NULL

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

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

    struct page *virt_to_page(void *kaddr);
    在<asm/page.h>中定义,负责将内核逻辑地址转换为相应的page结构体指针。
    犹豫需要一个逻辑地址,因此不能操作vmalloc生成的地址以及高端内存。
    struct page *pfn_to_page(int pfn);
    针对给定的页帧号,返回page结构指针。使用pfn_valid确认页帧号的合理性
    void *page_address(struct page *page);
    如果地址存在的话,返回页的内核虚拟地址。对于高端内存来说,只有当内存页被映射后该地址才存在。
    该函数定义在
    <linux/mm.h>中。大多数情况下,使用kmap而不是page_address

    kmap相关的函数:

    #include <linux/highmem.h>
    void *kmap(struct page *page);
    void kunmap(struct page *page);
    kmap为系统中的页返回内核虚拟地址。对于低端内存页来说,它只返回页的逻辑地址;
    对于高端内存,kmap在抓弄的内核地址空间创建特殊的映射。
    kmap的映射数量是有限的,不能映射过长的时间。

    kmap_atomic相关函数:

    #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的高性能版本,以原子的处理
    type参数:对驱动程序有意义的只有KM_USER0和KM_USER1。(KM_IRQ0和KM_IRQ1中断)

    1.5 页表

    处理器必须使用某种机制,将虚拟地址转换为相应的物理地址,这种机制被称为页表。

    1.6 虚拟内存区

    用于管理进程地址空间中不同区域的内核数据结构。

    进程内存映射(至少)包含下面这些区域:

    • 程序的可执行代码(通常称为text)区域
    • 多个数据区,其中包含初始化数据、非初始化数据(BSS)以及程序堆栈。
    • 与每个活动的内存映射对应的区域

    查看/proc/<pid>/maps (其中pid要替换为具体的进程ID)文件就能了解进程的内存区域

    /proc/self 是一个特殊的文件,始终指向当前进程。

    cat /proc/<pid>/maps的结果与vm_area_struct结构中的一个成员相对应。

    start
    end        该内存区域的起始处和结束处的虚拟地址
    perm        内存区域的读、写和执行权限的位掩码。
    offset        表示内存区域在映射文件中的起始位置。
    major
    minor        拥有映射文件的设备的主设备号和次设备号
    inode        被映射文件的索引节点号
    image        被映射额文件(通常是一个可执行映象)的名称

    1.7 vm_area_struct结构

    vma的主要成员如下所示,在头文件<linux/mm.h>中定义:

    /*
     * 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 {
        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. */
    
        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) */
    
    #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
    };
    vm_area_struct

    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

    struct vm_operations_struct *vm_ops;  内核能调用的一套函数,用来对该内存区进行操作。

    void *vm_private_data;       驱动程序用来保存自身信息的成员

    /*
     * These are the virtual MM functions - opening of an area, closing and
     * unmapping it (needed to keep files on disk up-to-date etc), pointer
     * to the functions called when a no-page or a wp-page exception occurs.
     */
    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
    };
    vm_operations_struct

    1.8 内存映射处理

    每个进程都拥有一个struct mm_struct 结构(在<linux/sched.h>中定义) ,其中包括虚拟内存区域链表、页表以及其他大量内存管理信息。

    还包括信号灯(mmap_sem)和一个自旋锁(page_table_lock)。多数驱动要访问时,使用current->mm

    二、mmap设备操作

    mmap方法是file_operations结构的一部分,并且执行mmap系统调用时将调用该方法。

    这个方法和系统调用的mmap有很大的不同,原型如下:

    mmap(caddr_t addr, size_t len, int port, int flags, int fd, off_t offset);
    
    int (*mmap)(struct file *filp, struct vm_area_struct *vma);
    vma包含了用于访问设备的虚拟地址的信息

    有两种建立页表的方法:

    • 使用remap_pfn_range函数一次全部建立
    • 通过nopage VMA方法每次建立一个页表

    2.1 使用remap_pfn_range

    int remap_pfn_range(struct vm_area_struct *vma,
        unsigned long virt_addr, unsigned long pfn,
        unsigned long size, pgprot_t port);
    int io_remap_page_range(struct vm_area_struct *vma,
        unsigned long virt_addr, unsigned long phys_addr,
        unsigned long size, pgprot_t prot);
    vma:虚拟内存区域,在一定范围内的页将被映射到该区域内
    virt_addr:重新映射时的起始用户虚拟地址。
    pfn:与物理内存对应的页帧号,虚拟内存将要被映射到该物理内存上。
    size:以字节为单位,被重新映射的区域大小
    prot:新VMA要求的保护属性

    2.2 一个简单的实现

    如果驱动程序要将设备内存线性地映射到用户地址空间中,程序员基本上就只需要调用remap_pfn_range函数。例子:

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

    2.3 为VMA添加操作

    vma_area_struct 包含了一系列针对VMA的操作,当fork进程或者创建一个新的对VMA引用时,随时都会调用open函数。

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

    使用nopage映射内存

    尽管remap_page_range在许多情况下工作良好,但是并不能适应大多数的情况。

    如果VMA映射尺寸变小或变大时,使用nopage更适合,不需要做额外的操作。

    struct page *(*nopage)(struct vm_area_struct *vma,
        unsigned long address, int *type);
    当用户要访问VMA中的页,而该页又不再内存中时,将调用相关的nopage函数
    address:包含了引起错误的虚拟地址,被向下圆整到页的开始位置。
    nopage:函数必须定位并返回指向用户所需要页的page结构指针。
    
    get_page(struct page *pageptr);
    该函数调用get_page宏,用来增加返回的内存页的使用计数

    如果使用了nopage,调用mmap的时候,通常只需做一点点工作。

    static int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma)
    {
        unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
    
        if(offset >= __pa(high_memory) || (filp->f_flags & O_SYNC))
            vma->vm_flags |= VM_IO;
        vma->vm_flags |= VM_RESERVED;
    
        vma->vm_ops = &simple_nopage_vm_ops;
        simple_vma_open(vma);
        return 0;
    }

    mmap函数将默认的vm_ops指针替换成自己的操作。然后nopage函数小心的每次重新映射一页,并且返回它的page结构指针。

    重映射的步骤非常简单:需要的地址定位并返回page结构体的指针,例子如下:

    struct page *simple_vma_nopage(struct vm_area_struct *vma,
        unsigned long address, int *type)
    {
        struct page *pageptr;
        unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
        unsigned long physaddr = address - vma->vm_start + offset;
        unsigned long pageframe = physaddr >> PAGE_SHIFT;
    
        if(!pfn_valid(pageframe))
            return NOPAGE_SIGBUS;
        pageptr = pfn_to_page(pageframe);
        get_page(pageptr);
        if(type)
            *type = VM_FAULT_MINOR;
        return pageptr;
    }

    只是简单的映射了主内存,并增加了计数引用。需要的步骤是:

    计算物理地址,然后通过右移PAGE_SHIFT位,将它转换成页帧号。

    pfn_valid确保地址的合理性,超过范围返回NOPAGE_SIGBUS

    重映射特定的I/O区域

    所有例子都是对/dev/mem的再次实现,一个典型的驱动程序只映射与其外围设备相关的一小段地址,而不是映射全部地址。

    下面代码揭示了驱动程序如何对起始于物理地址simple_region_start、大小为simple_region_size字节的区域进行映射的工作过程。

    unsigned long off = vma->vm_pgoff << PAGE_SHIFT;
    unsigned long physical = simple_region_start + off;
    unsigned long vsize = vma->vm_end - vma->vm_start;
    unsigned long psize = simpel_region_size - off;
    
    if(vsize > psize)
        return -EINVAL;    /* 跨度过大 */
    remap_pfn_range(vma, vma->vm_start, physical, visze, vma->vm_page_prot);

    为防止扩展映射最简单的办法是实现一个简单的nopage方法,它会产生一个总线信号传递给故障进程。

    struct page *simple_nopage(struct vm_area_struct *vma,
        unsigned long address, int *type)
    {    return NOPAGE_SIGBUS; /* 发送SIGBUS */ }

    重新映射RAM

    remap_pfn_range函数的一个限制是:它只能访问保留页和超出物理内存的物理地址。

    在Linux中,在内存映射时,物理地址页被标记为“保留的,表示内存管理对其不起作用”。

    使用nopage方法重映射RAM

    struct page *scullp_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)
    {
        unsigned long offset;
        struct scull_dev *ptr, *dev = vma->vm_private_data;
        struct page *page = NOPAGE_SIGBUS;
        void *pageptr = NULL;       /* 默认值是没有 */
    
        down(&dev->sem);
        offset = (address - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT);
        if(offset >= dev->size) goto out;       /* 超出范围 */
    
        offset >= PAGE_SHIFT;               /* offset是页号 */
        for(ptr=dev;ptr && offset >= dev->qset;) {
            ptr = ptr->next;
            offset -= dev->qset;
        }
        if(ptr && ptr->data) pageptr = ptr->data[offset];
        if(!pageptr)    goto out;           /* 空白区或者文件末尾 */
        page = virt_to_page(pageptr);
    
        /* 获得该值,现在可以增加计数了 */
        get_page(page);
        if(type)
            *type = VM_FAULT_MINOR;
    
    out:
        up(&dev->sem);
        return page;
    }
    scullp_vma_nopage

    重新映射内核虚拟地址

    page = vmalloc_to_page(pageptr);
    
    get_page(page);
    if(type)
        *type = VM_FAULT_MINOR;
    
    out:
        up(&dev->sem);
        return page;
    View Code

    三、执行直接I/O访问

    无论如何,在字符设别中执行直接I/O是不可行的,也是有害的。只有确定执行设置缓冲I/O的开销特别巨大,才使用直接I/O。请注意块设备和网络设备根本不用担心实现直接I/O的问题。

    在这两种情况中,内核中高层代码设置和使用了直接I/O,而驱动程序级的代码升值不需要知道已经执行了直接I/O

    2.6中实现直接I/O的关键是名为get_user_pages的函数,它定义在<linux/mm.h>中,原型如下:

    int get_user_page(struct task_struct *tsk,
        struct mm_struct *mm,
        unsigned long start,
        int len,
        int write,
        int force,
        struct page **pages,
        struct vm_area_struct **vmas);
    
    tsk:指向执行I/O的任务指针,主要目的是告诉内核,当设置缓冲区时,谁负责解决页错误的问题。
    mm:知悉将描述被映射地址空间的内存你管理结构的指针。
    start
    len:start是用户空间缓冲区的地址,len是页内的缓冲区长度
    write
    force:如果write非零,对映射的页有写权限(意味着用户空间执行了读操作)
    pages
    vmas:输出参数。如果调用成功,pages中包含一个描述用户空间缓冲区page结构的指针列表,vmas包含了相应VMA的指针。
    get_user_page

    get_user_pages函数是一个底层内存管理函数,使用了比较复杂的接口。

    它还需要在调用前,将mmap为获得地址空间的读取者/写入者信号量设置为读模式。因此有:

    down_read(&cuurent->mm->mmap_sem);
    result = get_user_pages(current, current->mm, ...);
    up_read(&current->mm->mmap_sem);

    result返回一个实际被调用的页数,它可能比请求的数量少,但大于0

    1、调用成功后,就有了一个用户空间缓冲区的页数组,它将被锁在内存中。

    2、为了能直接操作缓冲区,内核空间的代码必须用kmap或者kmap_atomic函数将每个page结构指针转换成内核虚拟地址。

    3、使用直接I/O通常使用DMA操作,因此驱动程序要从page结构指针数组中创建一个分散/聚合链表。

    4、一旦直接操作I/O完成,必须是释放用户内存页。如果改变了页的内容,必须通知内核,确保内核认为它是“干净”的

    void SetPageDirty(struct page *page);

    宏在头文件<linux/page-flags.h>中。使用例子如下:

    if(!PageReserved(page))
        SetPageDirty(page);

    不管页是否被改变,他们都必须从也缓存中释放,否则他们会永远存在那里,函数:

    void page_cache_release(struct page *page);

    异步I/O

    2.6的新特性是异步I/O,异步I/O允许用户空间初始化操作,但不必等待它们完成,这样执行异步I/O时,应用程序可以进行其他的操作。

    字符设备驱动吗程序需要清楚地表示需要异步I/O的支持。如果有恰当的理由需要在同一时刻执行多余一个的I/O操作,则字符设备将会从异步I/O中受益。

    支持异步I/O的驱动程序应该包括<linux/aio.h>,有三个用于实现异步I/O的file_operations方法:

    ssize_t (*aio_read)(struct kiocb *iocb, char *buffer,
        size_t count, loff_t offset);
    ssize_t (*aio_write)(struct kiocb *iocb, const char *buffer,
        size_t count, loff_t offset);
    int (*aio_fsync)(struct kiocb *iocb, int datasync);
    aio_fsync操作只对文件系统有意义
    aio_write与常用的read和write函数非常相似,也有一些不同。
    其中一个不同是:offset参数是一个值,异步操作从不改变文件的位置,因此没有必要向它传递指针。
    异步I/O

    异步I/O的例子:

    static ssize_t scullp_aio_read(struct kiocb *iocb, char *buf, size_t count,
        loff_t pos)
    {
        return scullp_defer_op(0, iocb, buf, count, pos);
    }
    
    static ssize_t scullp_aio_write(struct kiocb *iocb, const char *buf, size_t count, loff_t pos)
    {
        return scullp_defer_op(1, iocb, (char *)buf, count, pos);
    }
    
    struct async_work{
        struct kiocb *iocb;
        int result;
        struct work_struct work;
    }
    
    static int scullp_defer_op(int write, struct kiocb *iocb, char *buf,
        size_t count, loff_t pos)
    {
        struct async_work *stuff;
        int result;
        /* 虽然可以访问缓冲区,但现在要进行拷贝操作 */
        if(write)
            result = scullp_write(iocb->ki_filp, buf, count, &pos);
        else
            result = scullp_read(iocb->ki_filp, buf, count, &pos);
    
        /* 如果这是一个同步的IOCB,则现在反悔状态值 */
        if(is_sync_kiocb(iocb))
            return result;
    
        /* 否则把完成操作向后推迟几毫秒 */
        stuff = kmalloc(sizeof(*stuff), GFP_KERNEL);
        if(stuff == NULL)
            return result;
        /* 没有可用内存了,使之完成 */
        stuff->iocb = iocb;
        stuff->result = result;
        INIT_WORK(&stuff->work, scullp_do_deferred_op, stuff);
        schedule_delayed_work(&stuff->work, HZ/100);
        return -EIOCBQUEUED;
    }
    
    static void scullp_do_defered_op(void *p)
    {
        struct async_work *stuff = (struct async_work *)p;
        aio_complete(stuff->iocb, stuff->result, 0);
        kfree(stuff);
    }
    异步I/O例子

    四、直接访问内存

    直接访问内存,或者DMA。是内存中的高级部分。

    DMA数据传输概览

    第一种情况中,所需要的步骤概括如下:

    1.当进程调用read,驱动程序函数分配一个DMA缓冲区,并让硬件将数据传输到这个缓冲区中,进程处于睡眠状态

    2.硬件将数据写入到DMA缓冲区中,当写入完毕,产生一个中断

    3.中断处理程序获得输入的数据,应答中断,并且唤醒进程,该进程现在即可读取数据。

    分配DMA缓冲区

    使用DMA缓冲区的主要问题是:当大于一页时,他们必须占据连续的物理页,这是因为设备使用ISA或者PCI系统总线传输数据,而这两种方式使用的都是物理地址。 

    DIY分配

    get_free_pages函数可以分配多达几M字节的内存,但是对较大数量的请求,甚至是远少于129KB的请求也通常会失败。

    在引导时,我们可以通过内核传递"mem=参数"的办法保留顶部的RAM。比如系统有256MB内存,参数"mem=255M"将使内核不能使用顶部的1M字节。

    dmabuf = ioremap(0xFF00000 /* 255M */, 0x100000 /* 1M */);

    总线地址

    使用DMA的设备驱动后曾虚将于连接到总线接口上的硬件通信,硬件使用的是物理地址,而程序代码使用的是虚拟地址。

    不推荐使用这些函数,在<ams/io.h>中定义的函数:

    unsigned long virt_to_bus(volatile void *address);
    void *bus_to_virt(unsigned long address);

    通用DMA层

    DMA操作最终会分配缓冲区,并将总线地址传递给设备。一个可移植的驱动程序要求对所有体系架构都能安全而正确的执行DMA操作,编写这样一个驱动程序的难度超出了一般人的想象。

    幸运的是内核提供了一个与总线-----体系架构无关的DMA层,他会隐藏大多数问题。强烈建议使用该层编写。

    涉及到的device结构指针,需要包含文件<linux/dma-mapping.h> 

    处理复杂的硬件

    在执行DMA之前,必须确定给定设备是否有能力执行该操作。

    默认情况下,内核假设设备都能在32为地址上执行DMA。如果不是这样应该调用下面的函数通知内核:

    int dma_set_mask(struct device *dev, u64 mask);

    因此一个受限的24位DMA操作应该为:

    if(dma_set_mask(dev, 0xffffff))
        card->use_dma = 1;
    else {
        card->use_dma = 0;
        printk(KERN_WARN, "mydev: DMA not supported
    ");
    }

     如何设备支持常见的23位DMA操作,则没有必要调用dma_set_mask。

    DMA映射

    一个DMA映射是 要分配的DMA缓冲区 与 为该缓冲区生成的、设备可访问地址的组合。

    DMA映射必须解决缓存一致性的问题。如果设备改变了主内存找那个的区域,则任何覆盖该区域的处理器缓存都将无效。

    否则处理器将使用不正确的主内存映射,从而产生不正确的数据。

    DMA映射建立一个新的结构类型------dma_addr_t表示总线地址。

    dma_addr_t类型的变量对驱动程序是不透明的,唯一允许的操作是将他们传递给DMA支持例程以及设备本身。

    PCI代码区分两种类型的DMA映射:

    1.一致性DMA映射

    2.流式DM映射

    建立一致性DMA映射

    驱动程序可调用pci_alloc_consistent函数建立一致性映射:

    void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
    前两个参数是device结构和所需缓冲区的大小
    函数在两处返回DMA映射的结果。返回值是缓冲区的内核虚拟地址。
    而与其相关的总线地址,返回时保存在dma_handle中
    flag参数通常是描述如何分配内存的GFP_值,GFP_KERNEL或者GFP_ATOMIC

    当不再需要缓冲区时,调用dma_free_coherent向系统返回缓冲区:

    void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);
    和通用DMA函数一样,需要提供缓冲区大小、CPU地址、总线地址等参数

    DMA池

    DMA池是一个生成小型、一致性DMA映射的机制。调用dma_alloc_coherent函数获得最小单位为页的映射。

    如果需要再小,就需要DMA池了。在头文件<linux/dmapool.h>中定义了DMA池的函数:

    struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);
    name:DMA的名字
    dev:device结构
    size:是从该池中分配的缓冲区的大小
    align:是该池分配操作所必须遵守的硬件对齐原则
    allocation:如果不为零,表示内存边界不能超越allocation

    用完DMA池后,需要调用函数释放:

    void dma_pool_destroy(struct dma_pool *pool);
    销毁之前必须分返回所有分配的内存
    void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);
    mem_flags:通常设置为GFP_分配标志
    返回的DMA地址是内核虚拟地址,并作为总线地址保存在handle中
    
    使用下面函数返回不需要的缓冲区
    void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);

    建立流式DMA映射

    只有一个缓冲区要被传输的时候,使用dma_map_single函数映射它:

    dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
    返回值是总线地址,可以把它传递给设备,如果执行错误,返回NULL
    
    当传输完毕后,使用dma_unmap_signle(strcut device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
    size和direction采纳数必须与映射缓冲区的参数像匹配

    驱动程序需要不经过撤销映射就访问流式DMA缓冲区的内容,内核提供如下调用:

    void dma_sync_signle_for_cpu(struct device *dev, dma_handle_t bus_addr, 
        size_t size, enum dma_data_direction direction);
    
    在设备访问缓冲区前,应该调用下面的函数将所有权交还给设备
    void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr,
        size_t size, enum dma_data_direction direction);
    处理器在调用该函数后,不能再访问DMA缓冲区了

    单页流式映射

    dma_addr_t dma_map_page(struct device *dev, struct page *page,
        unsigned long offset, size_t size, enum dma_data_direction direction);
    
    void dma_unmap_page(strcut device *dev, dma_addr_t dma_address,
        size_t size, enum dma_data_direction direction);

    分散/聚集映射

    映射分散表的第一步是建立并填充一个描述被传送缓冲区的scatterlist结构的数组。该结构在头文件<linux/scatterlist.h>中描述

    struct page *page;  与在scatter/gather操作中用到缓冲区响应的page结构指针

    unsigned int length;

    unsigned int offset;  在页内缓冲区的长度和偏移量

    PCI双重地址周期映射

    一个简单的PCI DMA例子

    ISA设备的DMA

    注册DMA

    与DMA控制器通信

  • 相关阅读:
    [程序员代码面试指南]栈和队列-单调栈结构(单调栈)
    快学Scala第一部分
    Add Digits
    Nim Game
    将分布式中多台节点的日志信息集中到一个节点上
    Eclipse调试的一些小技巧
    Maven的常用命令
    Eclipse插件本地扩展安装
    Spark应用程序的运行框架
    Spark运行各个时间段的解释
  • 原文地址:https://www.cnblogs.com/ch122633/p/9816066.html
Copyright © 2020-2023  润新知