内存与I/O访问

来源:互联网 发布:知乎 查化学品安全 编辑:程序博客网 时间:2024/03/29 21:07
本章节带大家一起来探讨一下Linux驱动中的内存与I/O访问


CPU与内核和I/O
I/O空间:在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的。
它通过特定的指令in、out来访问

指令格式:IN 累加器,{端口号|DX}
OUT {端口号|DX},累加器


注意:目前大多数嵌入式微控制例如ARM、PowerPC等不提供I/O空间,而仅存在内存空间。
内存空间可以直接通过地址、指针来访问


为什么内存空间是必须的,I/O空间是可选的?
答:我们可以将外设只挂在到内存空间,此时CPU就可以像访问一个内存空间一样访问外设i/o端口了

如下图所示:





内存管理单元MMU
作用:辅助操作系统进行内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护
和Cache缓存控制等硬件支持。


其中的虚拟内存机制可以让用户感觉好像程序可以使用非常大的内存空间

MMU操作原理:
(1).TLB:转换旁路缓存,是MMU的核心部件,缓存少量虚拟地址和物理地址的转换关系,
是转换表的Cache,也成为快表

(2).TTW:转换表漫游,当TLB中没有对应缓冲对应的地址转换关系时,需要通过对内存中转换表(一般为多级页表)
的访问来得到虚拟地址和物理地的对应关系。TTW成功后,会将对应的转换关系写入TLB,

方便下次转换,具体如图所示:





下面给出一个典型的ARM处理器访问内存的过程:





在次叙述一下虚拟地址和物理地址的转换过程:
若TLB中没有虚拟地址的入口,则转换表遍历硬件从存放于主存储器的转换表中获取
地址转换信息和访问权限(也就是获得TTW啦),同时将信息放入TLB,它或者被放在一个没有
使用的入口或者替换一个已经存在的端口,以后当再次访问这些地址时,对
真是物理地址的访问将在Cache或者在内存中发生
具体如下图:





TLB进阶:
ARM中的TLB条目中的控制信息用于控制对对应地址的访问权限及Cache的操作
---C(高速缓存)和B(缓冲)位被用来控制对应地址的高速缓存和写缓冲,并决定是否进行
告诉缓存

---访问权限和域位作用用来控制读写访问是否被允许,如果不允许将发送一个异常




注意:Linux内核使用了三级页表PGD、PMD、PTE









Linux内存管理:
Linux系统中,进程4GB的内存空间被分成两个部分:用户空间和内核空间

用户空间地址:0~3GB
内核空间地址:3~4GB


用户进程通常只能访问用户空间的虚拟地址,不能访问内核空间的虚拟地址,用户进程
只有通过系统调用的方式才能访问到内核空间





内核空间与用户空间的区别:
每个进程的用户空间都是完全独立的用户进程各自拥有不同的页表。
内核空间是由内核负责映射的,它不会跟着进程改变,是固定的,内核地址空间有自己独立的页表




Linux内核空间(1GB)的划分:
常规内存:
物理内映射区(896MB):系统物理内存被顺序映射在内核空间的这个区域

高端内存:
虚拟内存分配区(其实地址:VMALLOC_START~VMALLOC_END,用vmalloc()函数分配)
高端页面映射区(起始地址:PKMAP_BASE)
专用页面应设置区(地址为FIXADDR_START~FIXADDR_TOP)
系统保留映射区


如下图所示:










如果物理内存超过4GB怎么办:
此时必须使用CPU的扩展分页(PAE)模式提供的64位页目录才能取到4GB以上的物理内存









内存读取:
用户空间内存动态申请
申请:malloc()
释放:free()





内核空间内存动态申请:
相关函数:
kmalloc()、__get_free_pages()和vmalloc()


详细介绍:
kmalloc()和__get_free_pages()申请的内存位于物理内存映射区,并且是连续的,它与真实的物理地址一般
只差一个固定的偏移

vmalloc()在虚拟地址空间给出一块连续的内存区,实质上这段连续的虚拟内存在物理内存中并不一定连续。也没简单的换算关系


进阶:
kmalloc(size_t size,int flags);
参数介绍:
size:分配大小
flag:分配标识
GFP_KERNEL(最常用),在内核空间进程中申请内存
GFP_USER,用来为用户空间分配内存,可能阻塞
GFP_HINSTANCE,与GFP_USER类似但是是从高端内存分配
GFP_NOIO,不允许任何IO初始化
GFP_NOFS,不允许任何文件系统调用
__GFP_DMA,要求分配在能够DMA的内存区
__GFP_HINSTANCE,指示分配的内存区可以位于高端内存
...


小知识:kmalloc()其实就是依赖于_get_free_pages()函数实现的


__get_free_pages(unsigned int flags,unsigned int order);
介绍:此宏是Linux内核本质上最底层用于获取空闲内存的方法
因为底层的伙伴算法总是以页的2的n次方为单位管理空闲内存,所以最底层的内存申请总是以页
为单位的

相关宏还包括:
__get_zeroed_pages()(申请的同时将页清空)、__get_free_page()(申请一页)

参数介绍:
order:分配的页数是2^order
flags:同kmalloc


__get_free_pages()和get_zeroed_page()的实现中调用了alloc_pages()函数,alloc_pages()既可以在内核空间分配,也
可以在用户空间分配
struct page*alloc_pages(int gfp_mask,unsigned long order);

//__get_free_pages()函数对应的释放函数
void free_page(unsigned long addr);
void free_pages(unsigned long addr,unsigned long order);




vmalloc函数
void *vmalloc(unsigned long size);
void vfree(void *addr);







slab与内存池:
引入slab的原因:
(1).完全使用页为单元申请和释放内存容易导致浪费
(2).在操作系统的运行过程中,经常涉及到对大量对象的重复生成、使用和释放问题,
此时使用slab可以大大提高效率

实际上kmalloc()就是使用slab机制实现的
使用方法:
暂且略过,详细实现,可以在用到的时候再去了解(Linux驱动程序开发详解)






                
虚拟地址和物理地址的关系:
虚拟地址到物理地址的转换
static inline unsigned long virt_to_phys(void *x);

物理地址到虚拟地址的转换
static inline void *phys_to_virt(unsigned long x);


注意:上述方法仅使用于896M以下的低端内存









设备I/O端口和I/O内存的访问

控制寄存器、数据寄存器和状态寄存器:
设备通常提供这些寄存器用来控制设备、读写寄存器和获取设备状态


I/O内存:当这些寄存器位于内存空间时
I/O端口:当这些寄存器位于I/O空间时




设备I/O端口和I/O内存的访问
Linux I/O端口和I/O内存的访问接口
1.I/O端口


(1)读写字节端口(8位宽)
unsigned inb(unsigned port);
void outb(unsigned char byte,unsigned port);
(2)读写字端口(16位宽)
unsigned inw(unsigned port);
void outw(unsigned char byte,unsigned port);
(3)读双字节端口(32位宽)
unsigned inl(unsigned port);
void outl(unsigned char byte,unsigned port);

(4)读写一串字节
void insb(unsigned port,void *addr,unsigned long count);
void outsb(unsigned port,void *addr,unsigned long count);
//insb()从端口port开始读count个字节端口,并将读取的结果写入addr指向的内存

(5)读写一串字
void insw(unsigned port,void *addr,unsigned long count);
void outsw(unsigned port,void *addr,unsigned long count);
(6)读写一串长字
void insl(unsigned port,void *addr,unsigned long count);
void outsl(unsigned port,void *addr,unsigned long count);




2.I/O内存


前面我们说过在arm、powerPC中一般都取消了I/O空间的说法,一般都直接将外设挂在到内存
而我们首先应该做的工作就是将设备所处的物理地址映射到虚拟地址

将物理地址映射到虚拟地址
void *ioremap(unsigned long offset,unsigned long size)
函数介绍:会建立页表

释放:
void ionumap(void * addr);


在将物理地址与设备地址映射完成之后,就可以通过指针访问这些地址
I/O内存读写操作:
(1).读I/O内存
unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);
//上述函数版本较早,但在Linux2.6中仍然可以使用:
新版本:
unsigned readb(void *addr);
unsigned readw(void *addr);
unsigned readl(void *addr);


(2).写I/O内存
void writeb(unsigned value,void *addr);
void readw(unsigned value,void *addr);
void readl(unsigned value,void *addr);


(3).读一串I/O内存
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);


(4).写一串I/O内存
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,const void *buf,unsigned long count);


(5).复制I/O内存
void memcpy_fromio(void *dset,void *source,unsigned int count);
void memcpy_toio(void *dest,void *source,unsigend int count);

(6).设置I/O内存
void memset_io(void *addr,u8 value,unsigned int count);

3.把I/O端口映射到内存空间(很简便)
void *ioport_map(unsigned long port,unsigned int count);
//通过此函数可以把port开始的count个连续的I/O空间重映射为一段“内存空间”
就可以在返回的地址上像访问I/O内存一样访问这些I/O端口。当不需要时撤销映射
void ioport_umap(void*addr);

小知识:对这个函数进行深层剖析后你可以发现,ioport_map函数中所谓的
映射其实是一个假象,并没有映射到内核虚拟地址,仅仅是为了让工程师使用同一的I/O内存访问
接口访问I/O端口,即沿用第二种方法





申请和释放设备I/O端口和内存
1.I/O端口申请
相关操作:
struct resource *request_region(unsigned long first,unsigned long n,const char*name);
函数介绍:向内核申请n个I/O端口,这些端口从first开始,name参数为设备的名称
返回值为NULL表示失败

将I/O端口归还给系统
void release_region(unsigned long start,unsigned long n);


2.I/O内存的申请

struct resource *request_mem_region(unsigned long start,unsigned long len,char*name);
函数简介:向内核申请n个内存地址,这些地址从first开始,name参数为设备名称
返回NULL表示分配失败

释放
void release_mem_region(unsigned long start,unsigned long len);
//这两个函数不是必须的但是建议使用




设备I/O端口和I/O内存访问流程

I/O端口的访问:
最简单的方法
request_region()+inb()/outb()+release_region()

具体如下图所示

方法二:
request_region()+ioport_map()+ioread/iowrite+ioport_unmap()+release_region()




I/O内存的访问:
将I/O端口映射到内存,对内存进行访问
request_mem_region()+ioport_remap()+ioread/iowrite+ioport_uremap()+release_region()


具体见下图:











将设备地址映射到用户空间
1.内存映射与VMA
一般情况下用户空间是不能也不应该直接访问设备
但是我们可以通过在设备驱动程序中实现mmap()函数,这个函数可以使得用户空间能直接访问物理设备

mmap()函数的实质:将用户空间的一段内存与设备内存关联,当用户访问用户空间的这段地址范围时,实际上会转化为对应的设备的访问。


注意:mmap()函数必须以页为单位进行映射


mmap()函数原型:
int (*mmap)(struct file *,struct vm_area_struct *);
驱动中的mmap()函数将在用户空间进行mmap()系统调用时被调用


用户空间的mmap()函数原型:
caddr_t mmap(caddr_t addr,size_t len,int prot,int flags,int fd,off_t offset);
参数介绍:
fd:文件描述符
len:是映射到用户空间的字节数
prot指定访问权限:PROT_READ PROT_WRITE PROT_EXEC PROT_NONE
caddr:指定文件应该被映射的起始地址,一般被指定为NULL,由内核分配



用户调用mmap()时所进行的工作
(1)在进程的虚拟地址空间查找一块VMA
(2)将这块VMA进行映射
(3)如果设备驱动程序或者文件系统的file_operations定义了mmap()操作则调用它
(4)将这个VMA插入到进程的VMA表中


小知识:驱动程序mmap()的实现机制也是建立页表


VMA结构体:
虚拟地址的描述通过VMA结构体来实现
struct vm_area_struct
{
struct mm_struct *vm_mm;/*所处的地址空间*/
unsigned long vm_start;/*开始虚拟地址*/
unsigned long vm_end;/*结束虚拟地址*/


pgprot_t vm_page_prot;/*访问权限*/
unsigned long vm_flags;/*标识,VM_READ,WM_WRITE,VM_EXEC,VM_SHARED*/
...
/*VMA的函数的指针*/
struct vm_operations_struct *vm_ops;


unsigned long vm_pgoff;/*偏移(页帧号)*/
struct file *vm_file;
void *vm_private_data;
....

};

简介:VMA结构体描述的虚拟地址位于vm_start ~vm_end之间


vm_operations_structk结构体
此结构体体描述了对VMA的相关操作
struct vm_operations_struct{
void(*open)(struct vm_area_struct *area);/*打开vma的操作*/
void (*close)(..)
struct page*(nopage)(...)/*访问的页不存在时调用*/
...
};



注意:当用户进行系统调用mmap()后,内核不会调用VMA的open函数,
通常需要在驱动的mmap()函数中显示调用vma->vm_ops->open()




/*vm_operations_struct操作范例*/
static int xxx_map(struct file*filp,struct vm_area_struct *vma)
{
   /*建立页表*/
if(remap_pfn_range(vma,vma->start,vm->vm_pgoff,vm->vm_end-vma->start,vma->page_prot))/*建立页表*/
return -EAGAIN;
vma->ops = &xxx_remap_vmops;
xxx_vma_open(vma);
return 0;
}


/*vma打开函数*/
void xxx_vm_open(struct vm_area_struct *vma)
{
...
printk(KERNEL "xxx VMA open,virt %1x,phys %1x\n",vma->vm_start,
vma->vm_pgoff<<PAGE_SHIFT);
}

/*vma关闭函数*/
void xxx_vma_close(struct vm_area_struct *vma)
{
...
printk(KERN_NOTICE "xxx VMA close.\n");
}


static struct vm_operations_struct xxx_remap_vm_ops = {
/*VMA操作结构体*/
.open =  xxx_vm_open,
.close = xxx_vma_close,
...
};


简介:remap_pfn_range(struct vm_area_struct *vma,unsigned long addr,
unsigned long pfn,unsigned long size,pgprot_t prot);
作用:创建页表,映射的虚拟地址访问为vma->cm_start~vma->cm_end
参数介绍:addr:表示内存映射开始处的虚拟地址
pfn:是虚拟地址应该映射到的物理地址的页帧号

小知识:何为页帧号
内核地址无论是虚拟的还是物理的,都是由两部分构成,往往是高N位为页号,低M位为页内偏移量。当我们将地址中的低M位
偏移量抛弃时,高N位移动到右端得到这个结果称为页帧号,宏PAGE_SHIFT告诉我们要右移多少位才能得到页帧号


prot:是新页的保护属性

/*映射kmalloc申请的内存到用户空间*/
/*内核模块加载函数*/
int __init kmalloc_map_init(void)
{
...
/*申请设备号
添加cdev结构体*/
buffer = kmalloc(BUFSIZE,GFP_KERNEL);//申请buffer

/*virt_to_page,获取对应的虚拟页*/
for(page = virt_to_page(buffer);page<vir_to_page(buffer+BUFSIZE);page++)
mem_map_reverse(page);/*设置为保留页*/
}

/*mmap()函数*/
static int kmalloc_map_mmap(struct file*filp,struct vm_area_struct *vma)
{
unsigned long page,pos;
unsigned long start = (unsigned long)vma->vm_start;
unsigned long size  = (unsigned long)(vma->vm_end-vma->vm_start);
printk(KERNEL_INFO "mmaptest_mmap called\n");

/*用户要映射的区域太大*/
if(size>BUFSIZE)
return -EINVAL;

pos = (unsigned long)buffer;
/*映射buffer中的所有页*/
while(size > 0){
/*每次映射一页*/
page = virt_to_phys((void *)pos);//先将在内核中用malloc分配的空间转换为对应的物理页地址
if(remap_page_range(start,page,PAGE_SIZE,PAGE_SHARED));/*将物理页地址映射到vma,并且每次只映射一页*/
return - EAGAIN;
start += PAGE_SIZE;
pos +=PAGE_SIZE;
size -=PAGE_SIZE;
}return 0;
}



注意:通常I/O内存被映射时需要nocahe的,这个时候需要对vma_page_prot设置nocache标识之后再进行映射
(暂略)





2.nopage()函数
简介:除了remap_pfn_range函数以外,在驱动程序中实现VMA的nopage()函数
可以为设备提供,更加灵活的映射途径,当访问的页不存在(发生缺页异常)时,
nopage()会被内核自动调用


当发生缺页异常时系统做出的响应:
(1).找到缺页的虚拟地址所在的VMA
(2).如果不要,分配中间页目录表和页表
(3).如果也表项不存在,调用VMA的nopage()方法,返回物理页面的描述符
(4).将物理页面的地址填充到页表中







I/O内存的静态映射
简介:假如我们已经做好目标电路板,而要将Linux移植到目标电路板,此时通常
会建立外I/O内存物理地址到虚拟地址的静态映射,
这里你只需要理解何为静态映射就可以了。具体用到的时候可以再回来研究,
不妨碍我们乡下学习







DMA(重点)
简介:
DMA:是一种无序CPU帮助就可以让外设与系统之间进行双向数据传输的硬件机制
简单点说就是这个样子
外设<---------->内存
而不是传统的
外设<----cpu----->内存


DMA与Cache的一致性问题:
假设DMA针对内存的目的地址与Cache缓存的对象有重叠区域,那么经过DMA操作
后,Cache缓存对应的内存的数据就会被修改,而CPU却并不知道,它仍然会认为Cache
中的数据就是内存中的数据,此时会产生Cache与内存之间的数据"不一致"错误

具体如下图所示:




解决方法:
禁止DMA目标地址范围内内存的cache功能







Linux下的DMA编程
知识储备:
内存中用于与外设交互数据的一块区域被称为DMA缓冲区,一般情况下DMA
在物理上连续的


1.DMA ZONE
对于X86系统的ISA设备而言,DMA操作只能在16MB一下的内存中使用,因此在用kmalloc()
和__get_free_pages()及类似的函数申请DMA缓冲区时应使用GFP_DMA标志,这样
获得的DMA ZONE是具备DMA能力的

Linux内核已经把此操作为我们封装好了
__get_dma_page()//它在申请时已经添加了GFP_DMA标志
#define __get_dma_pages(gfp_mask,order)\
__get_free_pages((gfp_mask)|GFP_DMA,(order))

上述函数是以2^order为大小分配的


也可以使用下面这个函数
static unsigend long dma_mem_alloc(int size);


注意:上数只是针对X86,对于大多数嵌入式设备而言,DMA操作可以在整个常规内存区域进行




2.虚拟地址、物理地址和总线地址
总线地址:是从设备的角度上看到的内存地址
物理地址:是从CPU MMU控制器外围角度上看到的内存地址


具体如下图所示:




Linux内核提供如下函数用于简单的虚拟地址/总线地址的转换
unsigned long virt_to_bus(volate void *address);
void *bus_to_virt(unsigned long address);




3.DMA地址掩码
设备不一定在所有的内存地址上执行DMA操作,此时应该通过下列函数执行DMA地址掩码
int dma_set_mask(struct device*dev,u64 mask);


例如:对于只能在24位地址上执行DMA操作的设备,就应该使用如下方法
dma_set_mask(dev,0xffffff)


4.一致性DMA缓冲区
DMA缓冲区包括两个方面的工作:
1.分配一篇DMA缓冲区(为这篇缓冲区产生设备可以访问的地址)
2.DMA映射必须考虑Cache一致性问题



内核提供如下函数用于分配就一个DMA一致性的内存区域
void *ama_alloc_coherent(struct device *dev,size_t size,dma_addr_t handle,gfp_t gfp)
返回值:为申请到的DMA缓冲区地址




/*DMA其他操作暂略,这里暂且之明白DMA的含义等到工程中具体
用到的时候再回来纤细学习*/