Linux设备驱动程序——I/O端口和I/O内存

浏览: 73 发布日期: 2016-11-27 分类: linux

在我学习Linux驱动的过程中,有个和我一块儿学习驱动的同学,他比要我更早开始学习Linux设备驱动程序,我们在学习Linux设备驱动的时候有些不同的观点,我认为学习驱动程序的时候还需要对内核中的一些重点的知识比如说Linux内存管理机制有着一定程度的了解,但是他认为不需要看这些东西,所以,我写下了前面的那些学习内存管理的一些笔记。因为我感觉在看驱动程序的时候老是因为内存这一块知识的缺乏,而看的晕晕乎乎的,所以关于内存方面的学习就比较仔细。

Linux内存管理

在LInux系统中,有着复杂的内存管理系统。进程的4G的空间被分成两个部分,用户空间和内核空间。用户空间一般分布在0~3GB,剩下的3~4GB为内核空间。在前面的学习中,我们知道每个进程的用户空间是相互独立的、互不相干的。

Linux中的1GB 的内核空间地址被划分成为:物理内存映射区、虚拟内存分配区、高端页面映射区、专用页面映射区、系统保留映射区。具体情况如下图所示:



内存存取

用户空间动态申请内存

用户空间动态申请内存的函数是malloc(),这个函数在所有的系统上基本上都是一致的,malloc()函数的释放函数为free(),否则会造成内存泄露。需要注意的一点是:有时候要实现成对出现是很难的,但是还是尽量将内存的释放放在本模块内。详细的内容在C语言基础知识部分由关于这个函数的详细介绍。在自己的笔记中详细介绍了这个函数,也可以参见百度百科上面关于这个函数的详细介绍。

内核空间中内存动态申请

是时候好好总结一下内核空间中的内存分配问题了:

Linux内核空间中动态申请内存的函数主要包括:kmalloc()、_ _get_free_pages()和vmalloc()等函数

总体上来说kmalloc()、_get_free_pages()申请的内存位于物理内存的映射区内,而且在物理上也是连续的,他们与真实的物理地址上面只有一个固定的偏移,存在着简单的转换关系。而vmalloc()在虚拟内存空间中是连续的,但是在物理内存中不一定是连续的。因此虚拟内存和物理内存之间也没有简单的换算关系。在这里提一下,《Linux驱动开发入门与实践》清华大学出版社 郑强上面说到kmalloc()函数在物理内存中为程序分配一个连续的存储空间,我认为这里是错误的。


下面先看一下kmalloc():

kmalloc 原型是:
#include <linux/slab.h>
void *kmalloc(size_t size, int flags);

这里面的第一个参数是用来表示分配的块的大小,第二个参数是分配标志是用来控制kmalloc()的行为,在这里只说明其中比较常用的三个标志

1)最常用的是GFP_KERNEL,代表运行在内核空间的进程而进行的. 换句话说, 这意味着调用函数是代表一个进程在执行一个系统调用.如果暂时不满足的话,进程会进入睡眠等待。因此不能用在中断上下文和由自旋锁的时候使用。

2)在中断处理函数、tasklet、内核定时器中当前的进程不能置为睡眠,这时候应该使用GFP_ATOMIC标志来申请

3)_ _GFP_DMA这个标志要求分配在能够 DMA 的内存区。

学习__get_free_pages()系列函数:这个函数是kmalloc()实现的基础。它包括的函数系列有:get_zerod_page(unsigned int flags)这个函数实现返回一个新页 并且将该页清零。__get_free_pages(unsigned int flags,unsigned order):这个函数可以分配多个页并且返回内存的首地址,分配的页数是2^order,但是分配的也不清零。

_ _get_free_page(unsigned int flags );这个函数本质上是由。_ _get_free_pages()来定义的,#define _ _get_free_page(unsigned int flags ) \

_ _get_free_pages(unsigned int flags,0)

_ _get_free_pages()和get_zerod_page(在实现的过程中调用了alloc_pages()函数,这个函数既可以在用户空间也可以在内核空间分配,它的原型是:

函数的原型是:

struct pages*alloc_pages(intgfp_mask,,unsignede long order),这个函数与_ _get_free_pages()函数类似,只不过它返回的是第一个页 的描述符而不是指针。

这一系列的函数在不使用的时候需要释放,相应的释放函数是:

void free_page(unsigned long addr);

void free_page(unsigned long addr,unsignedlong order);

 




每个外设都是通过读写寄存器来控制的,一般一个设备有几个寄存器,它们在内存地址空间或者I/O地址空间。这里需要学习一下i/O内存和I/O端口:

一类CPU(如M68K,Power PC,ARM,Unicore等)把这些寄存器看作内存的一部分,寄存器参与内存统一编址,访问寄存器就通过访问一般的内存指令进行,所以,这种CPU没有专门用于设备I/O的指令(可以以此判定体系为哪种)。这就是所谓的“I/O内存”方式。

另一类CPU(如X86)将外设的寄存器看成一个独立的地址空间,所以访问内存的指令不能用来访问这些寄存器,而要为对外设寄存器的读/写设置专用指令,如IN和OUT指令。这就是所谓的” I/O端口”方式。但是,用于I/O指令的“地址空间”相对来说是很小的。事实上,现在x86的I/O地址空间已经非常拥挤。

通过下面的图了解一下两者之间的区别:

这里稍微提一下:在intel中I/O空间可以通过in、out指令来访问,而内存地址可以直接由C语言中的指针来操作。其实可以直接将外设挂载到内存空间中。所以内存空间是必选的而I/O空间是可选的。


 

I/O端口分配

 使用I/O端口之前首先要确认对这个端口有唯一的权限,内核提供了一个注册接口允许驱动来声明这些需要的端口

#include <linux/ioport.h>

struct resource *request_region(unsigned longfirst, unsigned long n, const
char *name);

这个函数来告诉我们,想要使用n 个端口,从 first 开始. name 参数应当是你的设备的名字,非NULl时候表示分配成功,NULL表示分配失败,同样的在使用完一组

端口之后(在卸载模块的时候)应该讲他们返回给系统

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

还有一个函数以允许你的驱动来检查是否一个给定的 I/O 端口组可用:

int check_region(unsigned long first,unsigned long n);

端口不可用, 返回值是一个负错误码.

操作I/O端口

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

 

unsigned inb(unsigned port);
void outb(unsigned char byte, unsigned port);
读或写字节端口( 8 位宽 ). port 参数定义为 unsigned long 在某些平台以及
unsigned short 在其他的上. inb 的返回类型也是跨体系而不同的.

unsigned inw(unsigned port);
void outw(unsigned short word, unsigned port);
这些函数存取 16-位 端口( 一个字宽 ); 在为 S390 平台编译时它们不可用, 它
只支持字节 I/O.
unsigned inl(unsigned port);
void outl(unsigned longword, unsigned port);
这些函数存取 32-位 端口. longword 声明为或者 unsigned long 或者 unsigned
int, 根据平台. 如同字 I/O, "Long" I/O 在 S390 上不可用.

 

 

字串操作

字串函数的原型是:
void insb(unsigned port, void *addr, unsigned long count);
void outsb(unsigned port, void *addr, unsigned long count);
读或写从内存地址 addr 开始的 count 字节. 数据读自或者写入单个 port 端口.
void insw(unsigned port, void *addr, unsigned long count);
void outsw(unsigned port, void *addr, unsigned long count);
读或写 16-位 值到一个单个 16-位 端口.
void insl(unsigned port, void *addr, unsigned long count);
void outsl(unsigned port, void *addr, unsigned long count);
读或写 32-位 值到一个单个 32-位 端口.

使用I/O内存

 

尽管 I/O 端口在 x86 世界中流行, 用来和设备通讯的主要机制是通过内存映射的寄存器
和设备内存. 2 者都称为 I/O 内存, 因为寄存器和内存之间的区别对软件是透明的.

依赖计算机平台和所使用的总线,I/O 内存可以或者不可以通过页表来存取,当通过页表存
取, 内核必须首先安排从你的驱动可见的物理地址,并且这常常意味着你必须调用
ioremap 在做任何 I/O 之前. 如果不需要页表, I/O 内存位置看来很象 I/O 端口, 并且
你只可以使用正确的包装函数读和写它们.

 

I/O内存分配和映射

 

I/O 内存区必须在使用前分配,struct resource *request_mem_region(unsigned long start,unsigned long len,
char *name);

和I/O端口类似,在不在使用多个时候需要释放

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

 

DMA

由于代码中牵涉到DMA相关的知识,所以在这里学习一下DMAx相关的知识

DMA是直接内存存取的意思,它允许不同速度的硬件装置来沟通,而不需要依赖于CPU大量的中断负载,在此期间CPU可以处理其他的事情。

DMA 传输将数据从一个地址空间复制到另外一个地址空间。当CPU 初始化这个传输动作,传输动作本身是由 DMA 控制器来实行和完成。典型的例子就是移动一个外部内存的区块到芯片内部更快的内存区。

CPU要把总线控制权交给DMA控制器,而在结束DMA传输后,DMA控制器应立即把总线控制权再交回给CPU。

一个完整的DMA传输过程主要有下面四步:

DMA请求:CPU对DMA控制器初始化,并向I/O接口发出操作命令,I/O接口提出DMA请求。

DMA响应:DMA控制器对DMA请求的怕别别优先级以及屏蔽,向总线提出请求,此时,总线裁决逻辑输出总线应答,表示DMA已经响应,通过DMA控制器通知I/O接口开始DMA传输。

DMA传输

在DMA控制器的控制下,在存储器和外部设备之间直接进行数据传送,在传送过程中不需要中央处理器的参与。开始时需提供要传送的数据的起始位置和数据长度。

DMA结束

当完成规定的成批数据传送后,DMA控制器即释放总线控制权,并向I/O接口发出结束信号。由此可见,DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场的过程,通过硬件为RAM与I/O设备开辟一条直接传送数据的通路,使CPU的效率大为提高。

 

 

下面我们学习一下 DMA数据传输的概况

下面首先学习一下DMA是如何传输数据的,数据传输可以由两中方法触发:软件请求数据或者硬件硬件异步推数据到系统。

第一种情况下,包含二等具体步骤如下:

1、当一个进程调用read,驱动方法分配一个DMA缓冲,并且引导硬件来传送它的数据到这个缓冲区

2、硬件写数据到这个DMA缓冲区,并且在完成的时候发送一个中断

3、中观处理获得输入数据,确认中断,并且唤醒进程,现在允许进程读数据了

 

第二种情况是DMA被异步使用,这发生在数据获取设备, 它在没有人读它们的时候也持续推入数据. 在这个情况下, 驱动应当维护一个缓冲以至于后续的读调用能返回所有的累积的数据给用户空间,这类传输包含的步骤有一下几点不同:

1. 硬件引发一个中断来宣告新数据已经到达.

2. 中断处理分配一个缓冲并且告知硬件在哪里传输数据.

3. 外设写数据到缓冲并且引发另一个中断当完成时.

 

异步的方法的变体通常在网卡中进程见到,这些卡往往期望看见一个在内存和处理器共享的环形缓冲区(通常称为DMA缓冲区),每个来到的报文被放置在下一个可用的缓冲,并且发出一个中断,驱动接着传递网络文本到内核中的其他部分,并在环中放置一个新的DMA缓冲

Linux下的DMA编程

内存中用于与外界交互数据的一块区域被称作DMA缓冲区,DMA缓冲区是物理上连续的。

对于ISA设备而言,其DMA操作只能在16MB以下的内存中进行,可以使用kmalloc和 _ _get_free_pages()及其类似的额函数申请DMA缓冲区时应该使用GFP_DMA标志,

砸内核中定义了__get_free_pages()针对DMA的快捷放肆“_ _get_dma_pages(),他在申请标志中添加了GFP_DMA标志

#define _ _get_dma_pages(gfp_mask,order) __get_free_pages((gfp_mask)GFP_DMA|,(order))

 还可以使用另外一个函数:

dma_mem_alloc()

static unsigned long dma_mem_alloc(int size)

{

int order =get_order(size);

return _ _get_dma_pages(GFP _KERNEL,order);

}

基于DMA的硬件使用的是总线地址而不是物理地址,总线地址是从设备角度上看的内存地址,物理地址是从CP角度看到的未经过转换的内存地址,虽然在X86上面这两个是一样的,但是在其他的平台上面这个并不一定是相同的。

 

设备并不一定能在所有的内存地址上执行DMA操作,许多设备受限于它们能够寻址的范围。缺省的情况下,内核假定设备能够对32位地址进行DMA寻址,这种情况下,应该通过下面的额函数进行DMA地址掩码

int dma_set_mask(struct device *dev, u64mask);

mask 应当显示你的设备能够寻址的位; 如果它被限制到 24 位, 例如, 你要传递 mask
作为 0x0FFFFFF. 返回值是非零如果使用给定的 mask 可以 DMA; 如果dma_set_mask 返
回 0, 你不能对这个设备使用 DMA 操作。

 

通用DMA层

DMA操作,最后都要分配一个缓冲区并且将总线地址送到具体的设备,在编写所有体系上安全并且正确进行DMA的,不同的系统有不同的概念,于是内核提供了一个与总线—体系结构无关的DMA层来对驱动开发者隐藏这些问题,建议在驱动编写过程中使用这层的

 

DMA映射

IOMMU在设备可访问的地址范围内规划了物理内存,使得物理上分散的缓冲区对设备来说成连续的。一个DMA映射是分配一个DMA缓冲和产生一个设备可以存取的地址,同时还得考虑cache的一致性问题。IOMMU的运用需要使用到通用DMA层,而vir_to_bus函数不能完成这个任务。但是,x86平台没有对IOMMU的支持。

解决的方法就是建立回弹缓冲区,然后必要的时候将数据写入或者读出回弹缓冲区,缺点是降低系统性能。PCI分两种类型的DMA映射,这个依赖于DMA缓冲区希望杯停留多长时间:

 

一种是一致性DMA映射:连贯的 DMA 映射. 这些映射常常在驱动的生命期内存在. 一个连贯的缓冲必须是同时对
CPU 和外设可用(其他的映射类型, 如同我们之后将看到的, 在任何给定时间只对一个或
另一个可用). 结果, 一致的映射必须在缓冲一致的内存. 一致的映射建立和使用可能是
昂贵的.

另外一种是流式DMA映射,内核开发尽量使用流式DMA映射。

下面来具体学习一致DMA映射和流式DMA映射

 

 

建立一致DMA映射

一个驱动可以建立一致DMA映射,使用dma_alloc_coherent

void *dma_alloc_coherent(struct device *dev,size_t size, dma_addr_t
*dma_handle, int flag);

前 2 个参数是设备结果和需要的缓冲大小. 这个函数
返回 DMA 映射的结果在 2 个地方. 来自这个函数的返回值是缓冲的一个内核虚拟地址,
它可被驱动使用; 其间相关的总线地址在 dma_handle 中返回. 分配在这个函数中被处理
以至缓冲被放置在一个可以使用 DMA 的位置;

DMA池

这个是一个用来是分配小的, 一致DMA 映射的分配机制,从 dma_alloc_coherent 获得的映
射可能有一页的最小大小. 如果你的驱动需要比那个更小的 DMA 区域, 你应当可能使用
一个 DMA 池.

struct dma_pool *dma_pool_create(const char*name, struct device *dev,size_t size, size_t align,size_t allocation);

void dma_pool_destroy(struct dma_pool *pool);

name是DMA池的名字,dev是device结构,size是从该池中分配的缓冲区的大小,align是该池分配操作所必须遵守的硬件对齐原则(用字节表示),如果allocation不为零,表示内存边界不能超越allocation。比如说传入的allocation是4K,表示从该池分配的缓冲区不能跨越4KB的界限。

在销毁之前必须向DMA池返回所有分配的内存。

void * dma_pool_alloc(sturct dma_pool *pool,int mem_flags, dma_addr_t *handle);

void dma_pool_free(struct dma_pool *pool,void *addr, dma_addr_t addr);



建立流DMA映射

流映射比一致映射有更复杂的接口, 有几个原因. 这些映射行为使用一个由驱动已经分配
的缓冲, 因此, 必须处理它们没有选择的地址. 在一些体系上, 流映射也可以有多个不连
续的页和多部分的"发散/汇聚"缓冲. 所有这些原因, 流映射有它们自己的一套映射函数.

在某些体系结构中,流式映射也能够拥有多个不连续的页和多个“分散/聚集”缓冲区。建立流式映射时,必须告诉内核数据流动的方向。

DMA_TO_DEVICE

DEVICE_TO_DMA

如果数据被发送到设备,使用DMA_TO_DEVICE;而如果数据被发送到CPU,则使用DEVICE_TO_DMA。

DMA_BIDIRECTTONAL

如果数据可双向移动,则使用该值

DMA_NONE

该符号只是出于调试目的。

当只有一个缓冲区要被传输的时候,使用下函数映射它:

dma_addr_t dma_map_single(struct device *dev,void *buffer, size_t size, enum dma_data_direction direction);

返回值是总线地址,可以把它传递给设备;如果执行错误,返回NULL。

当传输完毕后,使用下函数删除映射:

void dma_unmap_single(struct device *dev,dma_addr_t dma_addr, size_t size, enum dma-data_direction direction);

在这里面有一些重要的规则:

缓冲必须用在只匹配它被映射时给定的方向的传输.

一是缓冲区只能用于这样的传送,即其传送方向匹配与映射时给定的方向值;

二是一旦缓冲区被映射,它将属于设备,不是处理器。直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。只用当dma_unmap_single函数被调用后,显示刷新处理器缓存中的数据,驱动程序才能安全访问其中的内容。

三是在DMA出于活动期间内,不能撤销对缓冲区的映射,否则会严重破坏系统的稳定性。          




 

 

 


返回顶部