在linux 系统中,几乎每一种外设都是通过读写设备中的寄存器进行通信的,这些寄存器包括控制寄存器、状态寄存器和数据寄存器三种。外设的寄存器通常都是连续的编址。由于 CPU 体系的不同,CPU 对 IO 端口的编址方式有两种:
(1) I/O 映射方式(I/O-mapped)
在x86处理器中,专门为外设实现了一个独立的地址空间,称为“I/O 地址空间”或“I/O 端口空间”,CPU 通过专门的 I/O 指令(比如 x86 的 IN 和 OUT 指令)来访问这一空间的地址单元。
(2)内存映射方式(Memory-mapped)
RISC 指令系统的 CPU (如 ARM、Powerpc 等)通常只实现【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.一个物理地址空间,外设 I/O 端口称为内存的一部分。此时,CPU 可以像访问一个内存单元那样访问外设 I/O 端口,而不需要设立专门的外设 I/O 指令。
这两者在硬件实现上的差异对于软件来说时完全透明的,驱动程序开发人员可以将内存映射方式的 I/O 端口和外设内存统一看作时“ I/O 内存资源”。
在系统启动时,BIOS 会扫描总线上的所有外设硬件信息,然后为每个外设设备统一分配物理地址资源,因此系统启动时所有硬件设备所需要的物理地址均已分配,这些都是由硬件的设计决定的。
而 CPU 访问资源使用的地址时虚拟地址,而这些外设设备目前只有物理地址而没有对应的虚拟地址。所以驱动程序并不能直接通过物理【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.地址去访问这些外设设备,因此需要把这些外设的物理地址映射到内核虚拟地址空间(3G~4G)中(通过页表),然后根据这些虚拟地址通过指令进行访问这些 I/O 资源。
在 Linux 中有一个函数 ioremmap(), 该函数的作用就是将已知的 I/O 资源的物理地址映射到内核虚拟地址空间(3G~4G)内,具体是映射到 VMALLOC_START ~ VMALLOC_END 区域内 。
从上图的内核虚拟地址空间分布可知,3G~3G+896M 为线性映射区,3G+896-4G 为 vmalloc、kmap和固定映射区。
ioremmap 函数原型
void * ioremap(unsigned longphys_a【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.ddr,unsigned long size, unsigned long
flags);
phys_addr:要映射的起始的IO地址;
size:要映射的空间的大小;
flags:要映射的IO空间的和权限有关的标志;取消映射的函数原型
extern void iounmap(volatile void __iomem *addr);
ioremap() 将 VMALLOC 区( VMALLOC_START ~ VMALLOC_END )的某段虚拟内存块映射到 io memory,其实现原理与vmalloc() 类似,都是通过在 vmalloc 区分配虚拟地址块,然后修改内核页表的方式将其映射到设备的 I/O 地址空间。【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.
与 vmalloc() 不同的是,ioremap() 并不需要通过伙伴系统去分配物理页,因为ioremap() 要映射的目标地址是 io memory,不是物理内存 (RAM)
实现
具体调用流程如下:
ioremap
|-> __ioremap
|-> get_vm_area //分配并初始化vm_struct |-> ioremap_page_range //修改页表void __iomem * __ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags) { void __iomem * addr; struct vm_struct【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业. * area; unsigned long offset, last_addr; pgprot_t prot; /* Dont allow wraparound or zero size */ // 防止地址过大发生翻转 last_addr = phys_addr + size – 1; if (!size || last_addr < phys_addr) return NULL; /* * Dont remap the low PCI/ISA area, its always mapped.. 对要映射的IO地址空间进行判断,低PCI/ISA地址不需要重新映射 640kb【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.-1Mb之间(此空洞用于连接到ISA总线上的设备) */ if (phys_addr >= ISA_START_ADDRESS && last_addr < ISA_END_ADDRESS) return (void __iomem *) phys_to_virt(phys_addr); /* * Dont allow anybody to remap normal RAM that were using.. high_memory为896Mb对应线性地址 phys_addr在小于896Mb的常规内存空间中 */ if(phys【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业._addr <= virt_to_phys(high_memory –1)) { char *t_addr, *t_end; struct page *page; //转化成线性地址 t_addr = __va(phys_addr); //若小于896MB则此页框应该被设置为保留 t_end = t_addr + (size – 1); //把内核虚拟地址转成其内存单元对应page,然后进行检查:不允许用户将IO地址空间映射到正在使用的RAM中 for(page = virt_to_page(t_addr); page <= virt_to_page(t_end); page++) if(!PageReserved(p【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.age))return NULL; } prot = __pgprot(_PAGE_PRESENT | _PAGE_RW | _PAGE_DIRTY | _PAGE_ACCESSED | flags); /* * Mappings have to be page-aligned #define PAGE_SHIFT12 #define PAGE_SIZE(1UL << PAGE_SHIFT) #define PAGE_MASK(~(PAGE_SIZE-1)) size = PAGE_ALIGN【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.(last_addr+1) – phys_addr; #define PAGE_ALIGN(addr)(((addr)+PAGE_SIZE-1)&PAGE_MASK) */ offset = phys_addr & ~PAGE_MASK; //取一页页框的偏移 phys_addr &= PAGE_MASK; //总线地址按4KB对齐 size = PAGE_ALIGN(last_addr+1) – phys_addr; //申请一个非连续映射节点描述符 area = get_vm_area(size, VM_IOREMAP | (flags << 20)); if (!area) r【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.eturn NULL; //设置vm_struct->phys_addr为ioremap的物理地址 area->phys_addr = phys_addr; //总线地址 //addr为该内存区域首部的虚拟地址 addr = (void __iomem *) area->addr; //起始线性地址 // 修改页表,实现物理地址到虚拟地址的映射 if (ioremap_page_range((unsigned long) addr, (unsigned long) addr + size, phys_addr, prot)) { vunmap((void __force *) addr); return NU【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.LL; } //offset为0, addr为线性地址,此地址被CPU用于读写EHCI I/O mem空间。在X86平台上总线地址就是物理地址 return (void __iomem *) (offset + (char __iomem *)addr); }ioremmap 的实现过程就是在 VMALLOC_START ~ VMALLOC_END 区域内寻找一段未被使用的虚拟地址(使用 vm_struct 进行记录),然后修改页表,最后返回分配的虚拟地址的起始地址。
其中 get_vm_area 在 VMALLOC_START ~ VMALLOC_END 区域内寻找一段未被使用的虚拟地址【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.,使用 vm_struct 进行记录,实现如下:
struct vm_struct *get_vm_area(unsigned long size, unsigned long flags) { return __get_vm_area(size, flags, VMALLOC_START, VMALLOC_END); }以上我们完成了将 I/O 内存资源的物理地址映射成核心虚地址,理论上讲我们就可以象读写 RAM 那样直接读写 I/O 内存资源了。为了保证驱动程序的跨平台的可移植性,我们应该使用 Linux 中特定的函数来访问 I/O 内存资源,而不应该通过指向核心虚地址的指针来访问。
读写I/O的函数【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.如下所示:
a — writel()
writel()往内存映射的 I/O 空间上写数据,wirtel() I/O 上写入 32 位数据 (4字节)。
原型: void writel (unsigned char data , unsigned shortaddr )
b — readl()
readl() 从内存映射的 I/O 空间上读数据,readl 从 I/O 读取 32 位数据 ( 4 字节 )。
原型:unsigned char readl (unsigned intaddr )
变量 addr 是 I/O 地址。
返回值 :从 I/O 空间读取的数值。比如 在 e1000 设备驱动程序中,网卡设备的 mem空间【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.通过 ioremap 完成了物理地址到虚拟地址的映射后,后续并不是直接拿着虚拟地址直接进行操作,而是通过特定的函数来访问 I/O 内存资源。
adapter->hw.hw_addr = ioremap(mmio_start, mmio_len);
E1000_WRITE_REG(hw, TDLEN, tdlen);
#define E1000_WRITE_REG(a, reg, value) ( \
writel((value), ((a)->hw_addr + …)总结
有关 ioremap 具体实现如下:
1、对地址范围相关检查;
2、分配并初始化 vm_struct 结构体,记录分配的区域信息。
3【我.爱.线.报.网.】52xbw .cn 每日持.续更新.可.实操.的副.业.、建立页表;
4、返回获取到的虚拟地址。