深入浅出~Linux设备驱动之CPU与内存和I/O

那是世上最远的距离 思念让我无法去呼吸 你的一动和一举 占据我心里 陪我每个孤独无尽的夜里

用我心中盛放的画笔 描绘你微笑时的绚丽 爱让人痛彻心底 我却不怀疑 你的存在是我生命的奇迹

感受你的每一次的呼吸 多想告诉你我有多爱你 如果我说我愿意 为你而死去 可否你的梦里留下我痕迹

无数悸动变换岁月里 你会依偎在谁的怀里 那些埋藏在心里 最深的秘密 是我生命里最脆弱的美丽

感受你的每一次的呼吸 多想告诉你我有多爱你 如果我说我愿意 为你而死去 可否告诉我你心底曾是唯一

感受你的每一次的呼吸 多想告诉你我有多爱你 如果我说我愿意 为你而死去 可否告诉我你心底 我曾是唯一

这是今天的旋律,在脑海中回放,在心灵里思考,在闭上眼睛时挣扎,在睁开双眼时去看清这个不能停留太久的世界  

  由于Linux系统提供了复杂的内存管理功能,本节将讲解的是内存和I/O的访问编程。
  在X86中,I/O空间是相对于内存空间而言的,通过特定的in、out来访问,in、out指令格式如下:

IN 累加器,{端口号|DX}
OUT {端口号|DX},累加器

  下面说说MMU(内存管理单元),操作系统借助MMU可以让用户感觉到好像程序可以使用非常大的内核空间,实际上就是我们平时了解的虚拟地址一样的。为了好好了解一下MMU,先看两个概念

TLB:MMU的核心部件,缓存少量的虚拟地址与物理地址的转换关系,是转换表的Cache

TTW:当TLB中没有缓冲对应的地址转换关系时候,需要通过对内存中转换表的访问来获取虚拟地址和物理地址的对应关系,TTW成功后,结果应该写入TLB。

为了说明MMU在内存中使用的关系,下图可以说明如下关系

  对于提供了 MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux 提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。进程的 4GB 内存空间被人为的分为两个部分——用户空间与内核空间。用户空间地址分布从0 到3GB(PAGE_OFFSET,在0x86 中它等于0xC0000000),3GB 到4GB 为内核空间,

  内核空间中,从3G 到vmalloc_start 这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map 等等)kmalloc 和get_free_page 申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址:

#define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
extern inline unsigned long virt_to_phys(volatile void * address)
{
 return __pa(address);
}

  上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000)。与之对应的函数为phys_to_virt(),将内核物理地址转化为虚拟地址:

#define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
extern inline void * phys_to_virt(unsigned long address)
{
 return __va(address);
}

  virt_to_phys()和phys_to_virt()都定义在include\asm-i386\io.h中。而vmalloc申请的内存则位于vmalloc_start~vmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。我们用下面的程序来演示kmalloc、get_free_page和vmalloc的区别:

#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");
unsigned char *pagemem;
unsigned char *kmallocmem;
unsigned char *vmallocmem;

int __init mem_module_init(void)
{
 //最好每次内存申请都检查申请是否成功
 //下面这段仅仅作为演示的代码没有检查
 pagemem = (unsigned char*)get_free_page(0);
 printk("<1>pagemem addr=%x", pagemem);

 kmallocmem = (unsigned char*)kmalloc(100, 0);
 printk("<1>kmallocmem addr=%x", kmallocmem);

 vmallocmem = (unsigned char*)vmalloc(1000000);
 printk("<1>vmallocmem addr=%x", vmallocmem);

 return 0;
}

void __exit mem_module_exit(void)
{
 free_page(pagemem);
 kfree(kmallocmem);
 vfree(vmallocmem);
}

module_init(mem_module_init);
module_exit(mem_module_exit);

  设备通常会提供一组寄存器来用于控制设备、读写设备、获取设备状态,那么Linux设备驱动究竟怎样访问设备的I/O端口(寄存器)和I/O内存的访问的呢

1.操作I/O口

(1)申请I/O 端口:

在驱动还没独占设备之前,不应对端口进行操作。内核提供了一个注册接口,以允许驱动声明其需要的端口:

/* request_region告诉内核:要使用first开始的n个端口。参数name为设备名。如果分配成功返回值是非NULL;否则无法使用需要的端口(/proc/ioports包含了系统当前所有端口的分配信息,若request_region分配失败时,可以查看该文件,看谁先用了你要的端口) */
struct resource *request_region(unsigned long first, unsigned long n, const char *name);

(2)访问IO端口:

在驱动成功请求到I/O 端口后,就可以读写这些端口了。大部分硬件会将8位、16位和32位端口区分开,无法像访问内存那样混淆使用。驱动程序必须调用不同的函数来访问不同大小的端口。

Linux 内核头文件(体系依赖的头文件<asm/io.h>) 定义了下列内联函数来存取I/O端口:

/* inb/outb:读/写字节端口(8位宽)。有些体系将port参数定义为unsigned long;而有些平台则将它定义为unsigned short。inb的返回类型也是依赖体系的 */
unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);

/* inw/outw:读/写字端口(16位宽) */
unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);

/* inl/outl:读/写32位端口。longword也是依赖体系的,有的体系为unsigned long;而有的为unsigned int */
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);

(3)释放IO端口:

/* 用完I/O端口后(可能在模块卸载时),应当调用release_region将I/O端口返还给系统。参数start和n应与之前传递给request_region一致 */
void release_region(unsigned long start, unsigned long n);

2 操作IO内存

(1)申请I/O 内存: 
  I/O 内存区在使用前必须先分配。分配内存区的函数接口在<linux/ioport.h>定义中:

/* request_mem_region分配一个开始于start,len字节的I/O内存区。分配成功,返回一个非NULL指针;否则返回NULL。系统当前所有I/O内存分配信息都在/proc/iomem文件中列出,你分配失败时,可以看看该文件,看谁先占用了该内存区 */
struct resource *request_mem_region(unsigned long start, unsigned long len, char *name);

(2)映射:
  在访问I/O内存之前,分配I/O内存并不是唯一要求的步骤,你还必须保证内核可存取该I/O内存。访问I/O内存并不只是简单解引用指针,在许多体系中,I/O 内存无法以这种方式直接存取。因此,还必须通过ioremap 函数设置一个映射。

/* ioremap用于将I/O内存区映射到虚拟地址。参数phys_addr为要映射的I/O内存起始地址,参数size为要映射的I/O内存的大小,返回值为被映射到的虚拟地址 */
void *ioremap(unsigned long phys_addr, unsigned long size);

(3)访问IO内存:
  经过 ioremap之后,设备驱动就可以存取任何I/O内存地址。注意,ioremap返回的地址不可以直接解引用;相反,应当使用内核提供的访问函数。访问I/O内存的正确方式是通过一系列专门用于实现此目的的函数:

#include <asm/io.h>
/* I/O内存读函数。参数addr应当是从ioremap获得的地址(可能包含一个整型偏移); 返回值是从给定I/O内存读取到的值 */
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);

/* I/O内存写函数。参数addr同I/O内存读函数,参数value为要写的值 */
void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);

/* 以下这些函数读和写一系列值到一个给定的 I/O 内存地址,从给定的buf读或写count个值到给定的addr。参数count表示要读写的数据个数,而不是字节大小 */
void ioread8_rep(void *addr, void *buf, unsigned long count);
void ioread16_rep(void *addr, void *buf, unsigned long count);
void ioread32_rep(void *addr, void *buf, unsigned long count);
void iowrite8_rep(void *addr, const void *buf, unsigned long count);
void iowrite16_rep(void *addr, const void *buf, unsigned long count);
void iowrite32_rep(void *addr,,onst void *buf,,nsigned long count);

/* 需要操作一块I/O 地址时,使用下列函数(这些函数的行为类似于它们的C库类似函数): */
void memset_io(void *addr, u8 value, unsigned int count);
void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);

/* 旧的I/O内存读写函数,不推荐使用 */
unsigned readb(address);
unsigned readw(address);
unsigned readl(address);
void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);

(4)释放IO内存步骤:

void iounmap(void * addr); /* iounmap用于释放不再需要的映射 */
void release_mem_region(unsigned long start, unsigned long len); /* iounmap用于释放不再需要的映射 */

  几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据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等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。 但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是"I/O内存"资源。
  一般来说,在系统运行时,外设的I/O内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设I/O内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问I/O内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些I/O内存资源。Linux在io.h头文件中声明了函数ioremap(),用来将I/O内存资源的物理地址映射到核心虚地址空间(3GB-4GB)中,原型如下:

void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);

  iounmap函数用于取消ioremap()所做的映射,原型如下:

void iounmap(void * addr);

  在将I/O内存资源的物理地址映射成核心虚地址后,理论上讲我们就可以象读写RAM那样直接读写I/O内存资源了。为了保证驱动程序的跨平台的可移植性,我们应该使用Linux中特定的函数来访问I/O内存资源,而不应该通过指向核心虚地址的指针来访问。

  最后,我们要特别强调驱动程序中mmap函数的实现方法。用mmap映射一个设备,意味着使用户空间的一段地址关联到设备内存上,这使得只要程序在分配的地址范围内进行读取或者写入,实际上就是对设备的访问。

  版权所有,转载请注明转载地址:http://www.cnblogs.com/lihuidashen/p/4468223.html

时间: 2024-08-30 04:02:43

深入浅出~Linux设备驱动之CPU与内存和I/O的相关文章

深入浅出~Linux设备驱动中的阻塞和非阻塞I/O

今天意外收到一个消息,真是惊呆我了,博客轩给我发了信息,说是俺的博客文章有特色可以出本书,,这简直让我受宠若惊,俺只是个大三的技术宅,写的博客也是自己所学的一些见解和在网上看到我一些博文以及帖子里综合起来写的,,总之这又给了额外的动力,让自己继续前进,,希望和大家能够分享一些自己的经验,,在最需要奋斗的年级以及在技术的领域踽踽独行的过程中有共同的伙伴继续前进~ 今天写的是Linux设备驱动中的阻塞和非阻塞I/0,何谓阻塞与非阻塞I/O?简单来说就是对I/O操作的两种不同的方式,驱动程序可以灵活的

深入浅出~Linux设备驱动之DMA

如果不曾相逢 也许 心绪永远不会沉重 如果真的失之交臂 恐怕一生也不得轻松 一个眼神 便足以让心海 掠过飓风 在贫瘠的土地上 更深地懂得风景 一次远行 便足以憔悴了一颗 羸弱的心 每望一眼秋水微澜 便恨不得 泪水盈盈 死怎能不 从容不迫 爱又怎能 无动于衷 只要彼此爱过一次 就是无憾的人生 也许 也许,永远没有那一天 前程如朝霞般绚烂 也许,永远没有那一天 成功如灯火般辉煌 也许,只能是这样 攀援却达不到峰顶 也许,只能是这样 奔流却掀不起波浪 也许,我们能给予你的 只有一颗 饱经沧桑的心 和满

深入浅出~Linux设备驱动之字符设备驱动

一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流的设备,常见的字符设备有鼠标.键盘.串口.控制台和LED设备等. 块设备:是指可以从设备的任意位置读取一定长度数据的设备.块设备包括硬盘.磁盘.U盘和SD卡等. 每一个字符设备或块设备都在/dev目录下对应一个设备文件.linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备

深入浅出~Linux设备驱动之异步通知和异步I/O

在设备驱动中使用异步通知可以使得对设备的访问可进行时,由驱动主动通知应用程序进行访问.因此,使用无阻塞I/O的应用程序无需轮询设备是否可访问,而阻塞访问也可以被类似“中断”的异步通知所取代.异步通知类似于硬件上的“中断”概念,比较准确的称谓是“信号驱动的异步I/O". 1.异步通知的概念和作用 影响:阻塞--应用程序无需轮询设备是否可以访问 非阻塞--中断进行通知 即:由驱动发起,主动通知应用程序 2.linux异步通知编程 2.1 linux信号 作用:linux系统中,异步通知使用信号来实现

深入浅出~Linux设备驱动之按键设备驱动

在上述的驱动系列博客中,我们已经了解了关于阻塞和非阻塞.异步通知.轮询.内存和I/O口访问.并发控制等知识,按键设备驱动相对来说是比较简单的,本章内容可以加深我们对字符设备驱动架构.阻塞与非阻塞.中断定时器等相关知识的理解.在嵌入式的系统中,按键的硬件原理简单,就是通过一个上拉电阻将处理器的外部中断引脚拉高,电阻的另一端接按钮并接地就可以实现. 1.按键的确认流程如下 2 按键驱动中的有关数据结构 2.1 按键设备结构体以及定时器 #define MAX KEY BUF 16 // 键缓冲区大小

深入浅出~Linux设备驱动之中断与定时器

“我叮咛你的 你说 不会遗忘 你告诉我的 我也全部珍藏 对于我们来说 记忆是飘不落的日子 永远不会发黄 相聚的时候 总是很短 期待的时候 总是很长 岁月的溪水边 捡拾起多少闪亮的诗行 如果你要想念我 就望一望天上那 闪烁的繁星 有我寻觅你的 目光” 谢谢你,曾经来过~ 中断与定时器是我们再熟悉不过的问题了,我们在进行裸机开发学习的 时候,这几乎就是重难点,也是每个程序必要的模块信息,那么在Linux中,我们又怎么实现延时.计数,和中断呢? 一.中断 1.概述 所谓中断是指cpu在执行程序的过程中

深入浅出~Linux设备驱动中的并发控制

并发和竞争发生在两类体系中: 对称多处理器(SMP)的多个CPU 内核可抢占的单CPU系统 访问共享资源的代码区域称为临界区(critical sections),临界区需要以某种互斥机制加以保护.在驱动程序中,当多个线程同时访问相同的资源(critical sections)时(驱动程序中的全局变量是一种典型的共享资源),可能会引发"竞态",因此我们必须对共享资源进行并发控制.Linux内核中解决并发控制的方法又中断屏蔽.原子操作.自旋锁.信号量.(后面为主要方式) 中断屏蔽: 使用

深入浅出~Linux设备驱动之watchdog设备驱动

看门狗(watchdog )分硬件看门狗和软件看门狗.硬件看门狗是利用一个定时器 电路,其定时输出连接到电路的复位端,程序在一定时间范围内对定时器清零 (俗称 “喂狗”),如果程序出现故障,不在定时周期内复位看门狗,就使得看门狗定时器溢出产生复位信号 并重启系统.软件看门狗原理上一样,只是将硬件电路上的定时器用处理器的内部定 时器代替. 1 看门狗的三个寄存器 1.1 watchdog原理 S3C2410内部集成了watchdog,提供3 个寄存器对watchdog 进行操作,这3 个寄存器分别

【转】深入浅出:Linux设备驱动之字符设备驱动

深入浅出:Linux设备驱动之字符设备驱动 一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流的设备,常见的字符设备有鼠标.键盘.串口.控制台和LED设备等. 块设备:是指可以从设备的任意位置读取一定长度数据的设备.块设备包括硬盘.磁盘.U盘和SD卡等. 每一个字符设备或块设备都在/dev目录下对应一个设备文件.linux用户程序通过设备文件(或称