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
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
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
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
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
从结构体struct file_operations中可以看出,驱动中mmap()函数的原型:
int (*mmap) (struct file *filep, struct vm_area_struct *vma);
驱动中的mmap()函数将在用户空间进行mmap()系统调用时最终被调用,mmap()系统调用的原型:
#include
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
int munmap(void *addr, size_t length);
驱动程序中mmap()的实现机制是建立页表,并填充VMA结构体中vm_operations_struct指针。
VMA用于描述一个虚拟内存区域,VMA结构体的定义:
#include
/*
* 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
/*
* 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 */
}
remap_pfn_range()函数的原型:
#include
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<
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
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;
}
备注:
大多数设备驱动都不需要提供设备内存到用户空间的映射能力,因为,对于串口等面向流的设备,实现这种映射毫无意义。而对于显示、视频等设备,建立内存映射可减少用户空间和内核空间之间的内存复制。