1. 程式人生 > >第11章 記憶體與IO訪問之裝置IO埠和I/O記憶體的訪問

第11章 記憶體與IO訪問之裝置IO埠和I/O記憶體的訪問

11.4 裝置I/O埠和I/O記憶體的訪問

    裝置通常會提供一組暫存器來控制裝置、讀寫裝置和獲取裝置狀態,即控制暫存器、資料暫存器和狀態暫存器。這些暫存器可能位於I/O空間中,也可能位於記憶體空間中。當暫存器位於I/O空間時,被稱為I/O埠;當暫存器位於記憶體空間時,對應的記憶體空間被稱為I/O記憶體。

11.4.1 Linux I/O埠和I/O記憶體訪問介面

1.I/O埠

    在Linux裝置驅動中,使用Linux核心提供的函式來訪問定位於I/O空間的埠。

#include <asm/io.h>

1)讀寫位元組埠(8位寬)。

unsigned inb(unsigned port); //讀

void outb(unsigned char byte, unsigned port);//寫

2)讀寫字埠(16位寬)。

unsigned inw(unsigned port);//讀

void outw(unsigned short word, unsigned port);//寫

3)讀寫長字埠(32位寬)。

unsigned inl(unsigned port);//讀

void outl(unsigned longword, unsigned port);//寫

4)讀寫一串位元組。

void insb(unsigned port, void *addr, unsigned long count);//讀

insb()從埠port開始讀count個位元組埠,並將讀取結果寫入addr指向的記憶體

void outsb(unsigned port, void *addr, unsigned long count);//寫

將addr指向的記憶體中的count個位元組連續寫入以port開始的埠。

5)讀寫一串字。

void insw(unsigned port, void *addr, unsigned long count);//讀

void outsw(unsigned port, void *addr, unsigned long count);//寫

6)讀寫一串長字。

void insl(unsigned port, void *addr, unsigned long count);//讀
void outsl(unsigned port, void *addr, unsigned long count);//寫

上述各函式中I/O埠號port的型別長度依賴於具體的硬體平臺


2.I/O記憶體

    在核心中訪問I/O記憶體(通常是晶片內部的各個I2C、SPI、USB等控制器的暫存器或者外部記憶體總線上的裝置)之前,需首先使用ioremap()函式將裝置所處的實體地址對映到虛擬地址上。

ioremap()的原型:

void *ioremap(unsigned long offset, unsigned long size);

ioremap()與vmalloc()類似,也需要建立新的頁表,但是ioremap()並不進行vmalloc()中所執行的記憶體分配行為。ioremap()返回一個特殊的虛擬地址,該地址可用來存取特定的實體地址範圍,這個虛擬地址位於vmalloc對映區域。通過ioremap()獲得的虛擬地址應該被iounmap()函式釋放,其原型如下:

void iounmap(void * addr);

ioremap()有個變體是devm_ioremap(),通過devm_ioremap()進行的對映通常不需要在驅動退出和出錯處理的時候進行iounmap()。

#include <linux/io.h>

devm_ioremap()的原型為:

void __iomem *devm_ioremap(struct device *dev, resource_size_t offset, unsigned long size);

在裝置的實體地址(一般都是暫存器)被對映到虛擬地址之後,儘管可以直接通過指標訪問這些地址,但是Linux核心推薦用一組標準的API來完成裝置記憶體對映的虛擬地址的讀寫。

    讀暫存器用readb_relaxed()、readw_relaxed()、readl_relaxed()、readb()、readw()、
readl()這一組API,以分別讀8bit、16bit、32bit的暫存器,沒有_relaxed字尾的版本與有_relaxed字尾的

版本的區別是沒有_relaxed字尾的版本包含一個記憶體屏障,如:

#define readb(c)            ({ u8  __v = readb_relaxed(c); __iormb(); __v; })
#define readw(c)            ({ u16__v = readw_relaxed(c); __iormb(); __v; })

#define readl(c)            ({ u32 __v = readl_relaxed(c); __iormb(); __v; })

寫暫存器用writeb_relaxed()、writew_relaxed()、writel_relaxed()、writeb()、writew()、
writel()這一組API,以分別寫8bit、16bit、32bit的暫存器,沒有_relaxed字尾的版本與有_relaxed字尾的
版本的區別是沒有_relaxed字尾的版本包含一個記憶體屏障,如:

#define writeb(v,c)         ({ __iowmb(); writeb_relaxed(v,c); })
#define writew(v,c)         ({ __iowmb(); writew_relaxed(v,c); })
#define writel(v,c)         ({ __iowmb(); writel_relaxed(v,c); })

11.4.2 申請與釋放裝置的I/O埠和I/O記憶體

1.I/O埠申請

Linux核心提供一組函式以申請和釋放I/O埠,表明該驅動要訪問這片區域。

#include <linux/ioport.h>

struct resource *request_region(unsigned long first, unsigned long n, const char *name);// 申請IO埠

這個函式向核心申請n個埠,這些埠從first開始,name引數為裝置的名稱。如果分配成功,則返回值不是NULL,如果返回NULL,則意味著申請埠失敗。

當用request_region()申請的I/O埠使用完成後,應當使用release_region()函式將它們歸還給系統,這個函式的原型如下:

void release_region(unsigned long start, unsigned long n); //釋放IO埠

2.I/O記憶體申請

Linux核心提供一組函式用來申請和釋放I/O記憶體的範圍。此處的“申請”表明該驅動要訪問這片區域,它不會做任何記憶體對映的動作。

#include <linux/ioport.h>

struct resource *request_mem_region(unsigned long first, unsigned long n, char *name);// 申請IO記憶體

這個函式向核心申請n個記憶體地址,這些地址從first開始,name引數為裝置的名稱。如果分配成功,則返回值不是NULL,如果返回NULL,則意味著申請I/O記憶體失敗。

當用request_mem_region()申請的I/O記憶體使用完成後,應當使用release_mem_region()函式將它們歸還給系統,這個函式的原型如下:

void release_mem_region(unsigned long start, unsigned long len);// 釋放IO記憶體

request_region()和request_mem_region()也分別有變體,其為devm_request_region()

devm_request_mem_region()

11.4.3 裝置I/O埠和I/O記憶體訪問流程

1、裝置驅動訪問I/O埠的步驟

I/O埠訪問的一種途徑是直接使用I/O埠操作函式:在裝置開啟或驅動模組被載入時申請I/O埠區域,之後使用inb()、outb()等進行埠訪問,最後,在裝置關閉或驅動被解除安裝時釋放I/O埠範圍。

整個流程如圖11.10所示。


    圖11.10 I/O埠訪問流程

2、裝置驅動訪問I/O記憶體的步驟

1)呼叫request_mem_region()申請IO記憶體資源。

2)將裝置暫存器的實體地址通過ioremap()對映到核心空間的虛擬地址。

3)通過Linux裝置訪問程式設計介面訪問裝置的暫存器。

4)訪問完成後,呼叫iounmap()函式對ioremap()對映的虛擬地址解除對映,並呼叫release_mem_region()函式釋放申請的I/O記憶體資源。

整個流程如圖11.11所示。


圖11.11 I/O記憶體訪問流程

備註:有時,驅動在訪問暫存器或I/O埠前,會省去request_mem_region()、request_region()呼叫。

11.4.4 將裝置地址對映到使用者空間

1.記憶體對映與VMA

    一般情況下,使用者空間不可能也不應該直接訪問裝置,但是,裝置驅動程式中可實現mmap()函式,這個函式可使得使用者空間能直接訪問裝置的實體地址。mmap()實現對映的過程是:將使用者空間的一段記憶體與裝置記憶體關聯,當用戶訪問使用者空間的這段地址範圍時,實際上會轉化為對裝置的訪問

    這種能力對於顯示介面卡一類的裝置非常有意義,如果使用者空間可直接通過記憶體對映訪問視訊記憶體的話,螢幕幀的各點畫素將不再需要一個從使用者空間到核心空間的複製的過程。

    mmap()必須以PAGE_SIZE為單位進行對映,實際上,記憶體只能以頁為單位進行對映,若要對映非PAGE_SIZE整數倍的地址範圍,要先進行頁對齊,強行以PAGE_SIZE的倍數大小進行對映。

#include <linux/fs.h>

從結構體struct file_operations中可以看出,驅動中mmap()函式的原型:

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

驅動中的mmap()函式將在使用者空間進行mmap()系統呼叫時最終被呼叫,mmap()系統呼叫的原型:

#include <sys/mman.h>

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

引數fd為檔案描述符,一般由open()返回,fd也可以指定為-1,此時需指定flags引數中的MAP_ANON,表明進行的是匿名對映。

len是對映到呼叫使用者空間的位元組數,它從被對映檔案開頭offset個位元組開始算起,offset引數一般設為0,表示從檔案頭開始對映。

prot引數指定訪問許可權,可取如下幾個值的“或”:PROT_READ(可讀)、PROT_WRITE(可寫)、PROT_EXEC(可執行)和PROT_NONE(不可訪問)。

引數addr指定檔案應被對映到使用者空間的起始地址,一般被指定為NULL,選擇起始地址的任務將由核心完成,而函式的返回值就是對映到使用者空間的地址。

當用戶呼叫mmap()時,核心會進行如下處理。

1)在程序的虛擬空間查詢一塊VMA(虛擬記憶體區域)。

2)將這塊VMA進行對映。

3)如果裝置驅動程式或者檔案系統的file_operations定義了mmap()操作,則呼叫它。

4)將這個VMA插入程序的VMA連結串列中。

file_operations中mmap()函式的第一個引數就是步驟1)找到的VMA。

由mmap()系統呼叫對映的記憶體可由munmap()解除對映,這個函式的原型如下:

#include <sys/mman.h>

int munmap(void *addr, size_t length);

驅動程式中mmap()的實現機制是建立頁表,並填充VMA結構體中vm_operations_struct指標。

VMA用於描述一個虛擬記憶體區域,VMA結構體的定義:

#include <linux/mm_types.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 {
        /* 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, or
         * linkage of vma in the address_space->i_mmap_nonlinear list.
         *
         * For private anonymous mappings, a pointer to a null terminated string
         * in the user process containing the name given to the vma, or NULL
         * if unnamed.
         */
        union {
                struct {
                        struct rb_node rb;
                        unsigned long rb_subtree_last;
                } linear;
                struct list_head nonlinear;
                const char __user *anon_name;
        } 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
};

備註:

VMA結構體描述的虛地址介於vm_start和vm_end之間,而其vm_ops成員指向這個VMA的操作集。對VMA的操作都被包含在vm_operations_struct結構體中,vm_operations_struct結構體的定義:

#include <linux/mm.h>

/*
 * 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);
        void (*map_pages)(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);

        /* Called by the /proc/PID/maps code to ask the vma whether it
         * has a special name.  Returning non-NULL will also cause this
         * vma to be dumped unconditionally. */
        const char *(*name)(struct vm_area_struct *vma);

#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
        /* called by sys_remap_file_pages() to populate non-linear mapping */
        int (*remap_pages)(struct vm_area_struct *vma, unsigned long addr,
                           unsigned long size, pgoff_t pgoff);

};

    整個vm_operations_struct結構體的例項會在file_operations的mmap()成員函式裡被賦值給相應的vma-
>vm_ops,上述open()函式也通常在mmap()裡呼叫,close()函式會在使用者呼叫munmap()的時

候被呼叫到。

    程式碼清單11.6 vm_operations_struct操作範例

static void xxx_vma_close(struct vm_area_struct *vma)/* VMA關閉函式 */
{
          ...
          printk(KERN_NOTICE "xxx VMA close.\n");
}

static void xxx_vma_open(struct vm_area_struct *vma)/* VMA開啟函式 */
{
          ...
          printk(KERN_NOTICE "xxx VMA open, virt %lx, phys %lx\n", vma->vm_start,
          vma->vm_pgoff << PAGE_SHIFT);
}

static struct vm_operations_struct xxx_remap_vm_ops = { /* VMA操作結構體 */
          .open = xxx_vma_open,
          .close = xxx_vma_close,
          ...
};

 static int xxx_mmap(struct file *filp, struct vm_area_struct *vma)

 {

        /* 建立頁表,對映的虛擬地址範圍是vma->vm_start至vma->vm_end */

       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 = &xxx_remap_vm_ops;
       xxx_vma_open(vma);
       return 0;

 }

remap_pfn_range()函式的原型:

#include <linux/mm.h>

int remap_pfn_range(struct vm_area_struct *, unsigned long addr,

unsigned long pfn, unsigned long size, pgprot_t);

addr引數表示記憶體對映開始處的虛擬地址。remap_pfn_range()函式為addr~addr+size的虛擬地址構造頁表。

pfn是虛擬地址應該對映到的實體地址的頁幀號,就是實體地址右移PAGE_SHIFT位。若PAGE_SIZE為4KB,則PAGE_SHIFT為12,因為PAGE_SIZE等於1<<PAGE_SHIFT=4096B。

prot是新頁所要求的保護屬性。

在驅動程式中,能使用remap_pfn_range()對映記憶體中的保留頁、裝置I/O、framebuffer、camera等記憶體。在remap_pfn_range()上又可以進一步封裝出io_remap_pfn_range()、vm_iomap_memory()等API。

asm-generic/pgtable.h

#define io_remap_pfn_range remap_pfn_range

#include <linux/mm.h>

int vm_iomap_memory(struct vm_area_struct *vma, phys_addr_t start, unsigned long len);

mm/memory.c

/**
 * vm_iomap_memory - remap memory to userspace
 * @vma: user vma to map to
 * @start: start of area
 * @len: size of area
 *
 * This is a simplified io_remap_pfn_range() for common driver use. The
 * driver just needs to give us the physical memory range to be mapped,
 * we'll figure out the rest from the vma information.
 *
 * NOTE! Some drivers might want to tweak vma->vm_page_prot first to get
 * whatever write-combining details or similar.
 */
int vm_iomap_memory(struct vm_area_struct *vma, phys_addr_t start, unsigned long len)
{
        unsigned long vm_len, pfn, pages;

        /* Check that the physical memory area passed in looks valid */
        if (start + len < start)
                return -EINVAL;
        /*
         * You *really* shouldn't map things that aren't page-aligned,
         * but we've historically allowed it because IO memory might
         * just have smaller alignment.
         */
        len += start & ~PAGE_MASK;
        pfn = start >> PAGE_SHIFT;
        pages = (len + ~PAGE_MASK) >> PAGE_SHIFT;
        if (pfn + pages < pfn)
                return -EINVAL;

        /* We start the mapping 'vm_pgoff' pages into the area */
        if (vma->vm_pgoff > pages)
                return -EINVAL;
        pfn += vma->vm_pgoff;
        pages -= vma->vm_pgoff;

        /* Can we fit all of the mapping? */
        vm_len = vma->vm_end - vma->vm_start;
        if (vm_len >> PAGE_SHIFT > pages)
                return -EINVAL;


        /* Ok, let it rip */
        return io_remap_pfn_range(vma, vma->vm_start, pfn, vm_len, vma->vm_page_prot);
}

EXPORT_SYMBOL(vm_iomap_memory);

程式碼清單11.7給出LCD驅動對映framebuffer實體地址到使用者空間的典型範例

drivers/video/fbdev/core/fbmem.c

static int
fb_mmap(struct file *file, struct vm_area_struct * vma)
{
struct fb_info *info = file_fb_info(file);
struct fb_ops *fb;
unsigned long mmio_pgoff;
unsigned long start;
u32 len;

if (!info)
return -ENODEV;
fb = info->fbops;
if (!fb)
return -ENODEV;
mutex_lock(&info->mm_lock);
if (fb->fb_mmap) {
int res;
res = fb->fb_mmap(info, vma);
mutex_unlock(&info->mm_lock);
return res;
}
/*
* Ugh. This can be either the frame buffer mapping, or
* if pgoff points past it, the mmio mapping.
*/
start = info->fix.smem_start;
len = info->fix.smem_len;
mmio_pgoff = PAGE_ALIGN((start & ~PAGE_MASK) + len) >> PAGE_SHIFT;
if (vma->vm_pgoff >= mmio_pgoff) {
if (info->var.accel_flags) {
mutex_unlock(&info->mm_lock);
return -EINVAL;
}
vma->vm_pgoff -= mmio_pgoff;
start = info->fix.mmio_start;
len = info->fix.mmio_len;
}
mutex_unlock(&info->mm_lock);
vma->vm_page_prot = vm_get_page_prot(vma->vm_flags);
fb_pgprotect(file, vma, start);

return vm_iomap_memory(vma, start, len);
}

通常,I/O記憶體被對映時需要是nocache的,這時,應該對vma->vm_page_prot設定nocache標誌之後再對映,如程式碼清單11.8所示。

程式碼清單11.8 以nocache方式將核心空間對映到使用者空間

 static int xxx_nocache_mmap(struct file *filp, struct vm_area_struct *vma)
 {
       vma->vm_page_prot = pgprot_noncached(vma->vm_page_prot);/* 賦nocache標誌,依賴於CPU的體系結構 */
       vma->vm_pgoff = ((u32)map_start >> PAGE_SHIFT);
       /* 對映 */
       if (remap_pfn_range(vma, vma->vm_start, vma->vm_pgoff, vma->vm_end - vma
         ->vm_start, vma->vm_page_prot))

         return  -EAGAIN;

       return 0;

}

pgprot_noncached()是一個巨集,高度依賴於CPU的體系結構,ARM的pgprot_noncached()定義如下:

在arch/arm64/include/asm/pgtable.h檔案中:

#define pgprot_noncached(prot) \

        __pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_DEVICE_nGnRnE) | PTE_PXN | PTE_UXN)

pgprot_noncached()禁止了相關頁的Cache和寫緩衝(Write Buffer)。

另一個比pgprot_noncached()稍微少一些限制的巨集是pgprot_writecombine(),它的定義如下:

#define pgprot_writecombine(prot) \

        __pgprot_modify(prot, PTE_ATTRINDX_MASK, PTE_ATTRINDX(MT_NORMAL_NC) | PTE_PXN | PTE_UXN)

pgprot_writecombine()沒有禁止寫緩衝。

    ARM的寫緩衝器是一個非常小的FIFO(First In First Out)儲存器,位於處理器核與主存之間,其目的在於將處理器核和Cache從較慢的主存寫操作中解脫出來。寫緩衝區與Cache在儲存層次上處於同一層次,但是它只作用於寫主存

2.fault()函式

    除了remap_pfn_range()以外,在驅動程式中實現VMA的fault()函式通常可以為裝置提供更加靈活的記憶體對映途徑。當訪問的頁不在記憶體裡,發生缺頁異常時,fault()會被核心自動呼叫,而fault()的具體行為可以自定義。這是因為當發生缺頁異常時,系統會經過如下處理過程:

1)找到缺頁的虛擬地址所在的VMA。

2)如果必要,分配中間頁目錄表和頁表。

3)如果頁表項對應的物理頁面不存在,則呼叫這個VMA的fault()方法,它返回物理頁面的頁描述符。

4)將物理頁面的地址填充到頁表中。

程式碼清單11.9給出一個裝置驅動中使用fault()的典型範例。

程式碼清單11.9 fault()函式使用範例

 static int xxx_fault(struct vm_area_struct *vma, struct vm_fault *vmf)
 {
           unsigned long paddr;
           unsigned long pfn;
           pgoff_t index = vmf->pgoff;
           struct vma_data *vdata = vma->vm_private_data; /*獲取 struct vma_data型別的私有資料指標*/
 
           ...
 
          pfn = paddr >> PAGE_SHIFT;
          vm_insert_pfn(vma, (unsigned long)vmf->virtual_address, pfn);
          return VM_FAULT_NOPAGE;

備註:

    大多數裝置驅動都不需要提供裝置記憶體到使用者空間的對映能力,因為,對於串列埠等面向流的裝置,實現這種對映毫無意義。而對於顯示、視訊等裝置,建立記憶體對映可減少使用者空間和核心空間之間的記憶體複製