内存和I/O访问

来源:互联网 发布:淘宝极速退款五千额度 编辑:程序博客网 时间:2024/04/29 11:50

内存管理单元MMU

MMU具有虚拟地址和物理地址转换,内存访问权限保护等功能

(1)TLB也就是我们说的快表,他缓存了小量的虚拟地址与物理地址的转换关系;

(2)TTW,当TLB中没有缓存对应的地址转换关系时,需要通过内存中转换表(多为多级页表)来获得虚拟地址和物理地址的对应关系。

linux内存管理

对于包含MMU的处理器,使得进程能访问的内存达到4G,在linux系统中4G内存空间被分为两个部分,用户空间和内核空间,用户空间的地址一般分布为0-3GB,乘下的3-4GB为内核空间

linux中1GB的内核地址空间又被获分为物理内存映射区,虚拟内存分配区,高端内存映射区,专用页面区,保留区,一般情况下物理内存映射区最大长度为896MB,系统的物理内存被顺利映射到内核空间这个区中,未超过物理内存区通常被称为常规内存。

 

内存存取

1.用户空间内存动态申请

在用户空间动态申请内存的函数为malloc(),这个函数在各个操作系统上的使用是一致的,malloc()申请内存的释放函数是free()

malloc()的使用如下:

char *p=malloc(...);

if(p==NULL)

...;

function(p);//用完没有用完的内存

...

free(p);

p=NULL;

malloc()申请的内存一定要有free()释放,而且而且要成对出现。

 

2.内核空间内存动态申请

在linux内核空间申请内存涉及的函数主要有kmalloc()和__get_free_pages()和vmalloc()等,kmalloc()和__get_free_pages()申请的内存位于物理内存映射区,而且在物理上也是连续的,它们与真实物理地址只有一个偏移,因此转换比较简单,而vmalloc()在虚拟内存空间给出一块连续的内存区,实质上这个连续的虚拟内存在物理内存中并不一定连续

 

kmalloc()

void *kmalloc(size_t size,int flags);

第一个参数是分配块大少,的二个是分配的标志,用于控制kmalloc()的行为,常用的标志是GFP_KERNEL和GFP_ATOMIC前者申请内存时,若暂时不能满足会进入睡眠,后者直接返回。

__get_free_pages()

__get_free_pages()系列函数/宏是linux本质上最底层的用于获取内存的方法

__get_free_pages()系列函数包括get_zeroed_page(),__get_free_page()和__get_free_pages()

get_zeroed_page(unsigned int flags)

该函数返回一个指向新页的指针并将该页清零

__get_free_page(unsigned int flags)

该宏返回一个指向新页的指针不清零,他实质上是

#define __get_free_page(gfp_mask) __get_free_pages((gfp_mask),0)

就是调用(),__get_free_pages()申请了1页

__get_free_pages(unsigned int flags,unsigned int order);

该函数可以分配多个页并返回分配内存的首地址,分配2^order页,order最大值为10;

__get_free_pages()和get_zeroed_page()实质中调用了alloc_pages()函数,alloc_pages()既可以在用户空间分配也可以在内核空间分配内存

struct pags *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)

__get_free_pages()函数在使用时,其申请标志的值与kmalloc()完全一样,常用的是GFP_KERNEL和GFP_ATOMIC前者申请内存时,若暂时不能满足会进入睡眠,后者直接返回。

 

vmalloc()

vmalloc()申请的内存应使用vfree()释放

void *vmalloc(unsigned long size);

void vfree(void *addr);

vmalloc()不能用于原子上下文中,因为它使用的标志是GFP_KERNEL

使用vmalloc()函数的一个例子是create_module()系统调用(加载模块的另一种方法),它利用vmalloc()来获取被创建模块需要的空间

4.slab与内存池

一方面,完全使用页为单位申请和释放内存容易导致浪费

(1)创建slab缓存

struct kmem_cache *kmem_cache_create(const char *name,size_tsize,size_t align,unsigned long flags,void (*ctor)(void *,structkmem_cache *,unsigned long),void (*ctor)(void *,struct kmem_cache*,unsigned long));

kmem_cache_create()用于创建一个slab缓存,它是一个可以驻留任意数目全部同样大少的后被缓存,参数size是要分配每个数据结构的大少,flags是控制如何进行分配等,所谓缓存池也就是先申请到一页或几页的内存,然后把一页或几页再分割成更小的单位进行管理。

(2)分配slab缓存

void *kmem_cache_alloc(struct kmem_cache *cachep,gfp_tflags)

上述函数在kmem_cache_create()创建的slab后备缓存中分配一块并返回首地址

(3)释放slab缓存

void kmem_cache_free(struct keme_cache *cachep,void *objp);

上述函数释放由kmem_cache_alloc()分配的缓存

(4)收回slab缓存

int kmem_cache_destroy(struct kmem_cache *cachep);

下面给出slab缓存的使用例子

//创建slab缓存

struct kmem_cache *xxx_cachep;

xxx_cachep = kmem_cache_create("xxx",sizeof(structxxx),0,SLAB_HWCACHE_ALIGN|SLAB_PANIC,NULL,NULL);

 //分配slab缓存

struct xxx *ct;

ct = kmem_cache_alloc(xxx_cachep,GFP_KERNEL);

...

//释放slab缓存

kmem_cache_free(xxx_cachep,ct);

kmem_cache_destroy(xxx_cachep);

...


虚拟地址和物理地址的关系

对于内核物理内存映射区的虚拟地址,使用virt_to_phys()可以实现内核虚拟地址转化为物理地址,virt_to_phys()的实现是体系结构相关的,对于ARM处理器函数定义如下

static inline unsigned long virt_to_phys(void *x)

{

return __virt_to_phys((unsigned long)(x));

#define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)

上面的 PAGE_OFFSET通常为3GB,而PHYS_OFFSET则定于为系统DRAM内存的基地址

与之对应的phys_to_virt(),它将物理地址装化成虚拟地址(内核物理内存映射区虚拟地址)

函数定义如下:

static inline unsigned long phys_to_virt(void *x)

{

return (void *)__phys_to_virt((unsigned long)(x));

#define __virt_to_phys(x) ((x)- PHYS_OFFSET PAGE_OFFSET)

注意,上面两个函数方法仅适用于内核物理内存映射区896MB 一下的低端内存。


设备I/O端口和I/O内存

设备寄存器有控制寄存器,数据寄存器,状态寄存器,这些寄存器可能位于I/O空间也有可能位于内存空间,当位于I/O口空间时通常称为I/O端口,位于内存空间时,对应的内存空间为I/O内存

1.I/O端口

在linux内核中提供的函数来访问定位I/O空间的端口

(1)读写字节端口(32位)

unsigned inl(unsigned port);//读

void outl(unsigned longword,unsigned port);

(2)读写一串字节

void insb(unsigned port,void *addr,unsigned longcount);//从端口port开始读count字节写入addr指向内存

void outsb(unsigned port,void *addr,unsigned long count)

 

2.I/O内存

I/O内存位于内核空间的内存,在内核中访问I/O内存之前,需首先使用ioremap()函数将设备所处的物理地址映射到虚拟地址,ioremap()的原型如下:

void *ioremap(unsigned long offset,unsigned long size);

ioremap()与vmalloc()类似,也需要建立新的页表,但是它并不是进行vmalloc()中所执行的内存分配行为。ioremap()返回一个特殊的虚拟地址,该地址可以用来存取特定的物理地址范围,也就是说我们在内核中操作的是这个虚拟地址,通过ioremao()获得的虚拟地址应该被uounmap()函数释放,原型如下:

void iounmap(void *addr);

在设备的物理地址映射为虚拟地址后,我们就可以直接通过指针访问这些地址(虚拟地址),也可以用内核提供的函数如下:

(1)读I/O内存

unsigned int ioread8(void *addr);

unsigned int ioread16(void *addr);

unsigned int ioread32(void *addr);

写I/O内存

void iowrite8(u8 value,void *addr);

void iowrite16(u16 value,void *addr);

void iowrite32(u32 value,void *addr);

读一串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);

从addr指向的地址中读count字节到buf指向的内存中

写一串I/O内存

void iowrite8_rep(void *addr,const void *buf,unsigned longcount);

void iowrite16_rep(void *addr,const void *buf,unsigned longcount);

void iowrite32_rep(void *addr,const void *buf,unsigned longcount);


 3.把I/O端口映射到内存空间

函数原型如下:

void *ioport_map(unsigned long port,unsigned int count);

通过这个函数,可以把port开始的count个连续I/O端口映射为一段“内存空间”,然后就可以在其返回地址上像非、访问I/O内存一样访问这些端口,当不需要这种映射时可以用下面函数来释放

void ioport_unmap(void *addr);


申请与释放设备I/O端口和I/O内存

1.I/O端口申请

linux内核提供了一组函数用于申请I/O端口,函数原型如下

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

这个函数向内核申请n个端口,这些端口从first开始,name参数为设备名称,如果分配成功返回非NULL,返回NULL意味着失败

当用request_reqion()函数申请的I/O端口应该使用release_region()函数将它们归还给系统,函数原型如下:

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

2.I/O内存申请

函数原型如下

struct resource *request_mem_region(unsigned long start,unsignedlong len,char *name);

这个函数向内核申请n个内存地址,这些地址从first开始如果分配成功返回非NULL,返回NULL意味着失败,只是单单申请了I/O内存,没有进行映射

当用request_mem_region()函数申请的I/O内存应该用下面函数来归还该系统

void release_mem_reqion(unsigned long start,unsigned longlen)

 

上述的request_reqion()和request_mem_reqion()函数不是必须的(可以像上面ioremap()函数一样直接映射),当建议使用,有许多设备没有申请I/O端口和I/O内存之前就直接访问了,这不够安全。

 

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

(1)I/O端口的访问可以直接使用I/O函数进行操作,在设备打开或驱动模块加载时申请I/O端口,之后用inb(),outb(),inl(),outl()等函数进行端口访问

(2)I/O端口访问的另一种途径是将I/O端口映射为内存空间进行访问,通过request_reqion()函数申请到I/O端口后,用ioport_map()映射到内存,之后就可以使用I/O内存的函数进行端口的访问了ioread32(),iowrite32()等函数进行I/O内存访问;

(3)还有一种就是先申请到I/O端口后用request_mem_reqion()申请I/O内存,接着将寄存器地址通过ioremap()映射到内核空间虚拟地址

 

将设备地址映射到用户空间

1.内存映射与VMA

一般情况下用户空间是不能直接访问设备的,但是,设备驱动程序中可以实现mmao()函数,这个函数可以使得用户空间可以直接访问设备的物理地址,实质上mmap(0实现了这样一个映射过程,它将用户空间的一段内存与设备内存关连起来,当用户访问这段用户空间的这段地址范围时,实质上会转为对设备的访问

从file_operations中的操作结构体可以看出,驱动中的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,intfd,off_t offset);

参数fd为文件描述符,一般由open()返回,len是要映射到调用用户空间的字节数 ,它从映射文件开头offset个字节开始算起,offset参数一般为0,表示从文件头开始映射

prot参数是访问权限,PROT_READ(可读),PROT_WRITE(可写),PROT_EXEC(可执行),PROT_NONE(不可访问)

参数addr指定文件被映射到用户空间的起始地址,一般被指定为NULL,这样,选择起始地址的任务由内核完成。

 

当用户调用mmap()的时候,内核会进行如下处理

(1)在进程的虚拟空间查找一块VMA

 (2)将这块VMA进行映射

(3)如果设备驱动程序或者文件系统的file_operations定义了mmap()操作,侧调用它(他作用就是在在进程的虚拟空间查找的VMA上建立页表,将设备的虚拟地址映射在上面,并填充vm_operations_struct指针,这样之后我们访问这段虚拟内存就相当于访问设备内存一样了)

(4)将这个VNA插入进程的VMA 链表中

由mmap()系统调用映射的内存可由munmap()解除,函数原型如下:

int munmap(caddr_t addr,size_t len);

驱动程序中的mmap()的实现机制是建立页表,并填充VMA结构体中的vm_operations_struct指针,VMA即vm_area_struct,用于描述一个虚拟内存区域,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;//标志

...

struct vm_operations_struct *vm_ops;//操作VMA的函数集指针

 

unsigned long vm_pgoff;//偏移

struct file *vm_file;

void *vm_private_data;

...

}

VMA结构体描述符的虚拟地址介于vm_start和vm_end之间,其中vm_ops成员指向这个VMA的操作集结构体定义如下:

struct vm_operations_struct{

void(*open)(struct vm_area_struct *area);//打开VMA的函数

void(*close)(struct vm_area_struct *area);//关闭VMA的函数

struct page *(*nopage)(struct vm_area_struct *area,unsigned longaddress,int *type);//访问页不在内存是调用

...

}

在内核生成一个VMA后,它会调用VMA的open()函数,但是,当用户进行mmap()系统调用后,尽管VMA在设备驱动文件操作结构体的mmap()被调用前就产生了,内核却不会调用VMA的open()函数,下面是一个vm_operations_struct的操作例子:

 

struct int xxx_mmap(struct file *filp,struct vm_area_struct*vma)

{

if(remap_pfn_range(vma,vma->vm_start,vm->vm_pgoff,vma->vm_end-vma->vm_start,vma->vm_page_prot))//建立页表

return -EAGAIN;

vma->vm_ops =&xxx_remap_vm_ops;

xxx_vma_open(vma);

return 0;

}

void xxx_vma_open(struct vm_area_struct *vma)//VMA打开函数

{

...

printk(KERN_NOTICE "xxx VMA open,virt %1x,phys%1x\n",vma->vm_start,vma->vm_pgoff<< PAGF_SHIFT);

}

void xxx_vma_close(struct vm_area_struct *vma)//关闭VMA函数

{

...

printk(KERN_NOTICE "xxx VMA close \n");

}

struct vm_operstions_struct xxx_remap_vm_ops = {//VMA操作结构体

.open = xxx_vma_open,

.close = xxx_vma_close,

...

};

上面的remap_pfn_range()创建页表,以VMA结构体成员(VMA的数据成员是内核根据用户的请求自己填冲的)作为remap_pfn_range()的参数,映射虚拟地址范围为vma->vm_start至vma->vm_end(这个范围是设备中虚拟地址,通过这样就可以把设备对应的地址映射到用户空间)

remap_pfn_range()函数原型如下

int remap_pfn_range(struct vm_area_struct *vma,unsigned longaddr,unsigned long pfn,unsigned long size,pgprot_t prot);

其中的addr参数表示内存映射开始的虚拟地址,remap_pfn_range()函数为addr-addr+size之间的虚拟地址构造页表,pfn是虚拟地址应该映射到物理地址的页帧号,实质上是物理地址右移PAGE_SHIFT为,若页大少为4K,则右移12位。prot是新页所要求的属性。

 

I/O内存的静态映射

在将linux移植到目标电路板过程中,通常会建立外设I/O内存物理地址到虚拟地址的静态映射,也就是说在内核启动过程中就把地址映射好,这样我们在实际驱动中就不用再进行映射了,只加上一个偏移地址就可以了,,不再需要ioremap(),这个映射通过电路板对应的map_desc结构体数组中添加成员来完成,map_desc结构体定义如下:

struct map_desc{

unsigned long virtual;//虚拟地址

unsigned long pfn;//页帧号,可以说是物理地址

unsigned long length;//大小

unsigned int type;//类型

}

我们要加如I/O映射地址时在这个数组中加进即可

比如我们要在TQ2440开发板中添加新的物理地址到虚拟地址映射,只需修改arch/arm/mach-s3c2410/mach-tq2440.c中的map-desc数组即可(当然还要把这个文件加到Makefile中和Kconfig配置中编译进内核)

struct map_desc tq2440_iodesc[] = {};

 

DMA

DMA是一种无需CPU参与就可以让外设与系统内存之间进行双向数据传输的硬件机制,而cache是缓存,本来cache和DMA本身是两个毫无相关的东西,但是当DMA的目的地址(在内存中会有DMA缓存)和cache有重叠时,DMA操作会改变内存中cache数据,但是CPU不知道,它乃认为cache中的数据就是内存中数据,以后访问cache对应内存时它还是使用cache中数据,这样就发生了cache与内存之间数据“不一致的错误”。

需要注意的是cache与内存数据不一致时驱动将无法正常运行。

一致性DMA缓冲区

void *dma_alloc_coherent(struct device *dev,size_tsize,dma_addr_t *handle,gfp_t gfp);

上述函数返回值为申请到的DMA缓冲区的虚拟地址,此外,该函数还通过handle返回DMA缓冲区的总线地址,

dma_alloc_coherent()申请一片DMA缓冲区,进行地址映射并保证该缓冲区的cache一致性,与dma_alloc_coherent()函数对应的是反函数为

void *dma_free_coherent(struct device *dev,size_t size,void*cpu_addr,dma_addr_t handle);

 

最后说说虚拟地址,物理地址和总线地址

基于DMA的硬件使用的总线地址非物理地址,也就是说总线地址不一定就是物理地址这要看平台,有时候接口总线通过桥接电路被连接,桥接电路会将I/O地址映射为不同的物理地址,例如在pReP系统中,物理地址为0的设备在设备端看起来是0x80000000,而0通常又被映射为虚拟地址0xc0000000所以同一地址就有了三重身份:物理地址0,总线地址0x80000000,虚拟地址0xc0000000,总线地址是从设备角度看到的内存地址,物理地址则是从cPUMMU控制器外围角度看到的地址,虚拟地址是从cpu角度看到的地址。

 

0 0
原创粉丝点击