目前, 大多数嵌入式微控制器(如 ARM、 PowerPC 等) 中并不提供 I/O 空间, 而仅存在内存空间。 内存空间可以直接通过地址、 指针来访问, 程序及在程序运行中使用的变量和其他数据都存在于内存空间中。内存地址可以直接由 C 语言指针操作, 例如, 操作一个寄存器, 可以定义一个指针来操作寄存器, 如下所示:
unsighted int *p = 0x12345678;
*p=0x87654321;
以上程序的意义是在绝对地址 0x12345678(ARM、 PowerPC) 中写入 0x87654321。高性能处理器一般会提供一个内存管理单元(MMU) , 该单元辅助操作系统进行内存管理, 提供虚拟地址和物理地址的映射、 内存访问权限保护和 Cache 缓存控制等硬件支持。 操作系统内核借助 MMU 可以让用户感觉到程序好像可以使用非常大的内存空间, 从而使得编程人员在写程序时不用考虑计算机中物理内存的实际容量。
MMU 具有虚拟地址和物理地址转换、 内存访问权限保护等功能, 这将使得 Linux 操作系统能单独为系统的每个用户进程分配独立的内存空间并保证用户空间不能访问内核空间的地址, 为操作系统的虚拟内存管理模块提供硬件基础。 上层应用看到的内存都是虚拟内存, 应用就不能直接访问硬件, 所以这样就保证了系统安全。
对于包含 MMU 的处理器而言, Linux 系统提供了复杂的存储管理系统, 使得进程所能访问的内存达到4GB。 在 Linux 系统中, 进程的 4GB 内存空间被分为两个部分——用户空间与内核空间。 用户空间的地址一般分布为 0~3GB(即 PAGE_OFFSET, 在 0x86 中它等于 0xC0000000) , 这样, 剩下的 3~4GB 为内核空间, 如下图所示。 用户进程通常只能访问用户空间的虚拟地址, 不能访问内核空间的虚拟地址。 用户进程只有通过系统调用(代表用户进程在内核态执行) 等方式才可以访问到内核空间。
每个进程的用户空间都是完全独立、 互不相干的, 用户进程各自有不同的页表。 而内核空间是由内核负责映射, 它并不会跟着进程改变, 是固定的。 内核空间的虚拟地址到物理地址映射是被所有进程共享的,内核的虚拟空间独立于其他程序。
MMU 非常复杂, 那么我们如何完成物理地址到虚拟地址的转换呢? 内核给我们提供了相关的函数, 我们先来了解下这些函数。 函数定义在内核源码目录 include/asm-generic/io.h
ioremap: 把物理地址转换成虚拟地址
iounmap: 释放掉 ioremap 映射的地址
函数 | static inline void __iomem *ioremap(phys_addr_t offset, size_t size) |
参数 phys_addr_t offset | 映射物理地址的起始地址 |
参数 size_t size | 要映射多大的内存空间 |
返回值 | 成功返回虚拟地址的首地址失败返回 NULL。 |
功能 | 把物理地址转换成虚拟地址 |
函数 | static inline void iounmap(void __iomem *addr) |
参数*addr | 要取消映射的虚拟地址的首地址 |
参数 size_t size | 要映射多大的内存空间 |
功能 | 释放掉 ioremap 映射的地址 |
注意: 物理地址只能被映射一次, 多次映射会失败。 如何查看哪些物理地址被映射过了呢? 可以使用以下命令来查看。
cat /proc/iomem