ioremap函数用法(ioremap 原理剖析)

在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 short

addr )

b — readl()

readl() 从内存映射的 I/O 空间上读数据,readl 从 I/O 读取 32 位数据 ( 4 字节 )。

  原型:unsigned char readl (unsigned int

addr )

  变量 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、返回获取到的虚拟地址。

推荐阅读

给力项目线报网会员可免费下载 加入会员
友情提醒: 请尽量登录购买,防止付款了不发货!
QQ交流群:226333560 站长微信:qgzmt2
温馨提示:本站提供的一切软件、教程和内容信息都来自网络收集整理,仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负,版权争议与本站无关。用户必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。如果您喜欢该程序和内容,请支持正版,购买注册,得到更好的正版服务。我们非常重视版权问题,如有侵权请邮件与我们联系处理。敬请谅解!

给TA打赏
共{{data.count}}人
人已打赏
行业资讯

btctools官网下载地址(btrace 开源!基于 Systrace 高性能 Trace 工具)

2024-3-27 8:58:44

行业资讯

雍正王朝中的邬思道是什么人(雍正上位的首功之臣,邬思道的最终结局如何?)

2024-3-27 9:20:25

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索