设备I/O端口及内存

设备I/O端口及内存访问

I/O端口

在linux中,内核提供了如下函数用于访问定位于I/O空间的端口。

  • 读写字节端口(8bit)
1
2
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
  • 读写字端口(16bit)
1
2
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
  • 读写长字端口(32bit)
1
2
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
  • 读写一串子节
1
2
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
  • 读写一串字
1
2
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
  • 读写一串长字
1
2
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);

因为关于上述一系列函数与硬件相关,因此每个平台具体实现不同,具体可参考arch/xxx/include/io.h

I/O内存

在内核访问I/O内存之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址上。

1
2


ioremap与vmalloc类似,也需要建立新的页表,但它并不进行内存分配。ioremap返回一个特殊的虚拟地址,该地址可用于存取特定物理地址范围这个虚拟地址位于vmalloc映射区域。

当ioremap将设备物理地址映射到虚拟地址后,就可以通过内存数据交换来实现与设备的通信。尽管可以直接通过指针访问这些虚拟地址来访问对应的设备,但Linux提供了一组标准的接口来完成设备内存映射的虚拟地址的读写。

1
2
3
4
5
6
7
readb(c)
readw(c)
readl(c)

writeb(v,c)
writew(v,c)
writel(v,c)

申请和释放I/O端口和内存

linux内核提供了一组函数用于申请和释放I/O端口,表明该驱动要访问这片区域

1
2
3
struct resource *request_region(unsigned long first, unsigned long n, const char *name);

void release_region(unsigned long start, unsigned long n);

linux同样提供了一组函数用于申请和释放I/O内存的范围。申请表明该驱动要访问这片区域,不会做任何内存映射的动作。

1
2
3
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);

void release_mem_region(unsigned long start, unsigned long len);

I/O端口与内存的访问流程

I/O端口的访问流程:

1
2
3
4
5
6
7
> request_region
> |
> \ /
> inb(),outb
> |
> \ /
> release_region

I/O内存的访问流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
> request_mem_region
> |
> \ /
> ioremap
> |
> \ /
> readb(),writeb()
> |
> \ /
> iounmap
> |
> \ /
> release_mem_region

设备空间地址映射到用户空间

对于大多数设备来说,将设备空间地址映射到用户空间毫无意义,但对于如视频相关设备来说,将设备空间地址映射到用户空间可以减少内核到用户空间的内存复制操作,大大地提高效率。

内存映射与VMA

如何实现将设备空间地址映射到用户空间呢?对于设备驱动来讲,首先需要实现file_operations中的mmap接口,这个接口是实现映射的关键。而对于用户空间来讲,同样也需要调用mmap系统调用发起地址映射。其步骤如下:

  1. 用户调用mmap()在进程的虚拟地址空间查找一块VMA
  2. 将这块VMA进行映射
  3. 如果设备驱动程序或文件系统的file_operations实现了对应的mmap()接口,则调用它
  4. 将这歌VMA插入进程的VMA链表中

首先来看看mmap系统调用的原型:

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

从上面的步骤我们可以知道mmap系统调用所做的工作,但是具体的映射实现工作却是在file_operations的mmap()接口中实现的。

在驱动程序中mmap()的实现机制是建立页表,并填充VMA结构体中vm_operations_struct指针。VMA就是vm_area_struct,用于描述一个虚拟内存区域,其描述的虚拟地址介于vm_start和vm_end之间,而vm_operations_struct类型成员vm_ops指向这个VMA的操作集。可通过vm_operations_struct的定义知道vm_ops操作集的概念指什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
struct vm_operations_struct {
void (*open)(struct vm_area_struct * area);
void (*close)(struct vm_area_struct * area);
int (*split)(struct vm_area_struct * area, unsigned long addr);
int (*mremap)(struct vm_area_struct * area);
vm_fault_t (*fault)(struct vm_fault *vmf);
vm_fault_t (*huge_fault)(struct vm_fault *vmf,
enum page_entry_size pe_size);
void (*map_pages)(struct vm_fault *vmf,
pgoff_t start_pgoff, pgoff_t end_pgoff);
unsigned long (*pagesize)(struct vm_area_struct * area);

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

/* same as page_mkwrite when using VM_PFNMAP|VM_MIXEDMAP */
vm_fault_t (*pfn_mkwrite)(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);
...
};

如下展示了drivers/char/mem.c中mmap接口的实现可供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
static int mmap_mem(struct file *file, struct vm_area_struct *vma)
{
size_t size = vma->vm_end - vma->vm_start;
phys_addr_t offset = (phys_addr_t)vma->vm_pgoff << PAGE_SHIFT;

/* Does it even fit in phys_addr_t? */
if (offset >> PAGE_SHIFT != vma->vm_pgoff)
return -EINVAL;

/* It's illegal to wrap around the end of the physical address space. */
if (offset + (phys_addr_t)size - 1 < offset)
return -EINVAL;

if (!valid_mmap_phys_addr_range(vma->vm_pgoff, size))
return -EINVAL;

if (!private_mapping_ok(vma))
return -ENOSYS;

if (!range_is_allowed(vma->vm_pgoff, size))
return -EPERM;

if (!phys_mem_access_prot_allowed(file, vma->vm_pgoff, size,
&vma->vm_page_prot))
return -EINVAL;

vma->vm_page_prot = phys_mem_access_prot(file, vma->vm_pgoff,
size,
vma->vm_page_prot);

vma->vm_ops = &mmap_mem_ops;

/* Remap-pfn-range will mark the range VM_IO */
if (remap_pfn_range(vma,
vma->vm_start,
vma->vm_pgoff,
size,
vma->vm_page_prot)) {
return -EAGAIN;
}
return 0;
}

fault()函数

除了如上方法中使用remap_pfn_range()外,在驱动程序中实现VMA的fault()函数通常可以为设备提供更加灵活的内存映射途径。当访问的页不在内存中,即发生缺页异常是,fault()会被内核自动调用,而fault()的具体行为可以自定义。这是由于系统对与缺页异常的处理步骤如下:

  1. 找到缺页的虚拟地址所在的VMA
  2. 如果必要,分配中间页目录表和页表
  3. 如果页表项对应的物理地址不存在,则调用这个VMA的fault()方法,它返回物理页面的页描述符
  4. 将物理页面的地址填充到页表中

I/O内存静态映射

在将Linux移植到目标板的过程中,可能会建立外设I/O内存物理地址到虚拟地址的静态映射,这个映射通过在与电路板对应的map_desc结构体数组中添加新的成员来完成。

map_desc结构定义如下:

1
2
3
4
5
6
struct map_desc {
unsigned long virtual;
unsigned long pfn;
unsigned long length;
unsigned int type;
};

如下为linux中用map_desc进行静态映射的范例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
static struct map_desc ixp4xx_of_io_desc[] __initdata = {
/*
* This is needed for runtime system configuration checks,
* such as reading if hardware so-and-so is present. This
* could eventually be converted into a syscon once all boards
* are converted to device tree.
*/
{
.virtual = IXP4XX_EXP_CFG_BASE_VIRT,
.pfn = __phys_to_pfn(IXP4XX_EXP_CFG_BASE_PHYS),
.length = SZ_4K,
.type = MT_DEVICE,
},
#ifdef CONFIG_DEBUG_UART_8250
/* This is needed for LL-debug/earlyprintk/debug-macro.S */
{
.virtual = CONFIG_DEBUG_UART_VIRT,
.pfn = __phys_to_pfn(CONFIG_DEBUG_UART_PHYS),
.length = SZ_4K,
.type = MT_DEVICE,
},
#endif
};

static void __init ixp4xx_of_map_io(void)
{
iotable_init(ixp4xx_of_io_desc, ARRAY_SIZE(ixp4xx_of_io_desc));
}

/*
* We handle 4 differen SoC families. These compatible strings are enough
* to provide the core so that different boards can add their more detailed
* specifics.
*/
static const char *ixp4xx_of_board_compat[] = {
"intel,ixp42x",
"intel,ixp43x",
"intel,ixp45x",
"intel,ixp46x",
NULL,
};

DT_MACHINE_START(IXP4XX_DT, "IXP4xx (Device Tree)")
.map_io = ixp4xx_of_map_io,
.dt_compat = ixp4xx_of_board_compat,
MACHINE_END

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 yxhlfx@163.com

文章标题:设备I/O端口及内存

本文作者:红尘追风

发布时间:2016-09-11, 22:59:09

原始链接:http://www.micernel.com/2016/09/11/%E8%AE%BE%E5%A4%87IO%E7%AB%AF%E5%8F%A3%E5%8F%8A%E5%86%85%E5%AD%98/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录