ARM9嵌入式Linux开发-内存与IO操作

Linux内存管理

ARM9嵌入式Linux开发-内存与IO操作_第1张图片

地址类型

用户虚拟地址

  • 用户空间程序所能看到的常规地址
  • 每个进程都有自己的虚拟地址空间

物理地址

CPU使用的是物理地址,在CPU的地址信号线上产生的就是物理地址。

总线地址

总线地址,顾名思义,是与总线相关的,就是总线的地址线或在地址周期上产生的信号。外设使用的是总线地址。基于DMA的硬件使用总线地址而非物理地址。

  • 该地址在外围总线和内存之间使用
  • 它实现总线和主内存之间的重新映射
  • 通常它们与处理器使用的物理地址相同

Linux采用虚拟内存管理技术,使得每个进程都有独立的进程地址空间,该空间大小为3G,用户看到的和接触到的都是虚拟地址,无法使用实际的物理地址。Linux将4G的虚拟地址空间划分为两个部分:内核空间和用户空间。

内核空间:

  • 运行在处理器最高级别的超级用户模式下的代码或数据
  • 从0xC0000000到0xFFFFFFFF的1GB线性地址空间
  • 只有运行在内核态的进程才能访问
  • 用户进程可以通过系统调用切换到内核态访问内核空间

用户空间

  • 从0x00000000到0xBFFFFFFF共3GB的线性地址空间
  • 每个进程都有一个独立的3GB用户空间,所有用户空间由每个进程独有
  • 内核线程没有用户空间,因为它不产生用户空间地址。运行在用户态和内核态的进程都可以访问用户空间

内核逻辑地址

  • 内核逻辑地址组成了内核的常规地址空间
  • 该地址映射了部分(或者全部)内存,并经常被视为物理地址
  • 与物理地址是线性映射的
  • 例如,kmalloc返回的是逻辑地址

内核虚拟地址

  • 内核虚拟地址和逻辑地址的相同之处在于,它们都将内核空间的地址映射到物理地址上
  • 与物理地址不必是线性映射关系
  • 例如,vmalloc与kmap都是返回内核虚拟地址

内核虚拟地址是3G-4G这段地址,它与物理地址通过页表来映射,内核逻辑地址是指3G-3G+main_memory_size这段虚拟地址,它与物理地址的映射是线性的,当然也可以通过页表映射。内核逻辑地址都是内核虚拟地址,但不是所有内核虚拟地址都是内核逻辑地址。

高端内存--896M以上的部分称之为高端内存。

3G----------------------------------------------------------------------------------------------4G

直接映射区---8M-动态映射区-8K---kmap区(高端内存映射区)---固定映射区-4K      

896M---------------120M---------------------------4M------------------------4M---------

ARM9嵌入式Linux开发-内存与IO操作_第2张图片

地址的转换

_pa( logical-addr)//逻辑地址->物理地址

_va(physical-addr)//物理地址->逻辑地址

 

物理地址和页

  • 物理地址被分成离散的大小相等单元,称之为页;
  • Linux系统内部许多对内存的操作都是基于页的;
  • 每个页的大小通常为4096个字节(4KB),具体的大小在中用PAGE_SIZE定义;

内存映射和页结构

Linux的内核在内存管理中处理的最小单位是physical pages,内核通过struct page结构体来表示每一个页。

page的数据结构:struct page{…};

  • atomic_t count; 对该页的访问计数。当计数值为0时,该页将返回给空闲链表。
  • void *virtual; 如果页面被映射,则指向页的内核虚拟地址;如果未被映射则为NULL。
  • unsigned long flags; 描述页状态的一系列标志. PG_locked表示内存中的页已经被锁住 PG_reserved表示禁止内存管理系统访问该页。

内核维护了一个或者多个page结构数组,用来跟踪系统中的物理内存。在一些系统中,有一个单独的数组称之为mem_map,用来描述所有的物理内存。考虑可移植性,代码不要直接访问那些数组;

page结构指针与虚拟地址之间进行转换

struct page *virt_to_page(void *kaddr);    // 从一个内核虚地址得到该页的描述结构
struct page *pfn_to_page(int pfn);         // 根据给出的页帧号求出对应的页地址

void *kmap(struct page *page);             // 主要用在高端存储器页框的内核映射中
void kunmap(struct page *page);

kmap为系统中的任何页返回一个内核虚拟地址。对于低内存页,它只返回页的逻辑地址;对于高内存,kmap 在内核地址空间的一个专用部分中创建一个特殊的映射。使用 kmap 创建的映射应当一直使用 kunmap 来释放。

kmap()的一般用法:使用alloc_pages()在高端存储器区得到struct page结构,然后调用kmap(struct *page)在内核地址空间PAGE_OFFSET+896M之后的地址空间中建立永久映射(如果page结构对应的是低端物理内存的页,该函数仅仅返回该页对应的虚拟地址)。kmap()也可能引起睡眠,所以不能用在中断和持有锁的代码中,kmap只能对一个物理页进行分配。对于高端物理内存(896M之后),并没有和内核地址空间建立一一对应的关系(即虚拟地址=物理地址+PAGE_OFFSET这 样的关系),所以不能使用get_free_pages()这样的页分配器进行内存的分配,而必须使用alloc_pages()这样的伙伴系统算法的接口得到struct *page结构,然后将其映射到内核地址空间,注意这个时候映射后的地址并非和物理地址相差PAGE_OFFSET。

页表

在任何操作系统上,处理器必须有一个机制来转换虚拟地址到它的对应物理地址。这个机制被称为一个页表;它本质上是一个多级树型结构数组,包含了虚拟-到-物理的映射和几个关联的标志。Linux内核维护一套页表即便在没有直接使用这样页表的体系上。

内存管理结构

每个进程(除了内核空间的一些辅助线程外)都拥有一个struct mm_struct结构(在中定义)。

  • 多个进程可以共享内存管理结构,Linux就是用这种方法实现线程的。
  • 驱动程序可直接通过current->mm访问mm_struct结构。
  • mm_struct结构包含了
    • 虚拟内存区域链表、页表以及其他大量内存管理信息;
    • 一个信号灯(mmap_sem)和一个自旋锁(page_table_lock);
    • mmap表示进程的vma链表的头。

为了能以自然的方式管理进程虚拟内存空间,Linux定义了虚拟内存段(Virtual Memory Area,VMA)。一个VMA段是某个进程的一段连续的虚拟空间,在这段虚拟内存空间的所有单元拥有相同的特征。

进程通常占用几个VMA段,分别用于代码段、数据段和堆栈段等。属于同一进程的VMA段通过vm_next指针链接,组成链表。

Linux内存分配与回收

内核为设备驱动程序提供了一致的内存管理接口

分配内存的方法包括

  • kmalloc函数
  • 后备高速缓存
  • get_free_page和相关函数
  • vmalloc及其辅助函数

kmalloc函数

ARM9嵌入式Linux开发-内存与IO操作_第3张图片

后备高速缓存

设备驱动程序常常会反复地分配很多同一大小的内存块。为了满足这样的应用,内核实现了这种形式的内存池,通常称为后备高速缓存(lookaside cache) 。

Slab分配器

  • Kmalloc的机制是基于slab分配器的
  • 后备高速缓存的操作
    • Create,创建slab缓存
    • Destroy,收回slab缓存
    • Allocate,分配slab缓存
    • Free,释放slab缓存

get_free_page和相关函数

kmalloc要求分配的内存大小应该小于128KB,大于128KB的怎么办?

分配页面函数或宏

unsigned long get_zeroed_page(unsigned int flags);
//返回一个指向新页的指针并且清零
unsigned long __get_free_page(unsigned int flags);
//返回一个指向新页的指针但是该页不清零,它实际上是:
#define __ get_free_page(gfp_mask) \
	 __get_free_pages(gfp_mask,0)
unsigned long __get_free_pages(unsigned int flags,unsigned int order);
//分配2order页,并返回分配内存的首地址,分配的页不清零

释放页面函数

void free_page(unsigned long addr);
void free_pages(unsigned long addr,unsigned long order);

__get_free_page系类函数/宏是Linux内核本质上最底层的用于获取空闲内存的方法,底层的伙伴算法以Page的2的n次幂来管理空闲内存,所以最底层的内存申请总是以页为单位的。

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

vmalloc及其辅助函数

vmalloc分配虚拟地址空间的连续区域,但这段区域在物理上可能是不连续的。

#include
void *vmalloc(unsigned long size);
void vfree(void *addr);

vmalloc分配得到的地址是不能在微处理器之外使用的(只存在于软件中,没有对应的硬件意义),当驱动程序需要真正的物理地址时(像外设用以驱动系统总线的DMA地址),就不能使用vmalloc 。

使用vmalloc函数的正确场合是在分配一大块连续的、只在软件中存在的、用于缓冲的内存区域的时候。

因为vmalloc不但要获取内存,还要建立页表,它的开销比__get_free_pages大,因此,用vmalloc函数分配仅仅一页的内存空间是不值得的。

vmalloc和kmalloc区别

  • vmalloc分配得到的地址是不能在微处理器之外使用的,当驱动程序需要真正的物理地址时(像外设用以驱动系统总线的DMA地址),就不能使用vmalloc;
  • 通常,kmalloc分配小于128KB的内存,vmalloc可以分配更大的内存;
  • vmalloc不能在原子上下文中使用,因为它的内部实际调用了kmalloc(GFP_KERNEL) 。

虚拟地址与物理地址关系

内核虚拟地址转化为物理地址

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

物理地址转化为内核虚拟地址

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

IO端口和IO内存访问

设备通常会提供一组寄存器,如控制寄存器、数据寄存器和状态寄存器等。这些寄存器可能位于IO空间,也可能位于内存空间;

  • 当这些寄存器位于IO空间时,通常被称为IO端口
  • 当这些寄存器位于内存空间时,对应的内存空间被称为IO内存

通常,除x86外,嵌入式处理器(比如ARM,PowerPC等)一般只存在内存空间。

几乎每一种外设都是通过读写设备上的寄存器来进行的,通常包括控制寄存器、状态寄存器和数据寄存器三大类,外设的寄存器通常被连续地编址。根据CPU体系结构的不同,CPU对IO端口的编址方式有两种:

1)IO映射方式(IO-mapped)    

典型地,如x86处理器为外设专门实现了一个单独的地址空间,称为“IO地址空间”或者“IO端口空间”,CPU通过专门的IO指令(如x86的IN和OUT指令)来访问这一空间中的地址单元。

2)内存映射方式(Memory-mapped)    

RISC指令系统的CPU(如ARM、PowerPC等)通常只实现一个物理地址空间,外设I/O端口成为内存的一部分。此时,CPU可以象访问一个内存单元那样访问外设I/O端口,而不需要设立专门的外设I/O指令。      

但是,这两者在硬件实现上的差异对于软件来说是完全透明的,驱动程序开发人员可以将内存映射方式的I/O端口和外设内存统一看作是“I/O内存”资源。

IO端口和IO内存访问接口

IO端口的操作

unsigned inb(unsigned port);//读写字节端口(8位宽)
void outb(unsigned char byte, unsigned port);

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

unsigned inl(unsigned port);//读写长字端口(32位宽)
void outl(unsigned longword, unsigned port);

void insb(unsigned port, void *addr, unsigned long count);//读写一串字节
void outsb(unsigned port, void *addr, unsigned long count);

void insw(unsigned port, void *addr, unsigned long count);//读写一串字
void outsw(unsigned port, void *addr, unsigned long count);

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

把IO端口映射到内存空间

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

通过这个函数,可以把port开始的count个连续的IO端口映射重新映射到一段“内存空间”。然后就可以在其返回的地址上像访问IO内存一样访问这些IO端口。当不需要这种映射时,调用下面的函数来撤销。

解除映射

void *ioport_umap(void *addr);

IO内存

  • 一般来说,在系统运行时,外设的IO内存资源的物理地址是已知的,由硬件的设计决定。但是CPU通常并没有为这些已知的外设IO内存资源的物理地址预定义虚拟地址范围,驱动程序并不能直接通过物理地址访问IO内存资源,而必须将它们映射到核心虚地址空间内(通过页表),然后才能根据映射所得到的核心虚地址范围,通过访内指令访问这些IO内存资源。
  • 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);

IO内存访问函数

写IO内存

void iowrite8(u8 value, void *addr);
void iowrite16(u16 value, void *addr);
void iowrite32(u32 value, void *addr);

较早版本的函数

void writeb(unsigned value, address);
void writew(unsigned value, address);
void writel(unsigned value, address);

读IO内存

unsigned int ioread8(void *addr);
unsigned int ioread16(void *addr);
unsigned int ioread32(void *addr);

较早版本的函数

unsigned readb(address);
unsigned readw(address);
unsigned readl(address);

读一串IO内存

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);

写一串IO内存

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);

复制IO内存

void memcpy_fromio(void *dest, void *source, unsigned int count);
void memcpy_toio(void *dest, void *source, unsigned int count);

设置IO内存

void memset_io(void *addr, u8 value, unsigned int count);

IO端口申请与释放

#include 
struct resource *request_region(unsigned long first, unsigned long n,const char *name);
void release_region(unsigned long start, unsigned long n);

IO内存的申请与释放

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

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

用户空间不能也不应该直接访问设备。

设备驱动程序中可以实现mmap()函数,使得用户空间能直接访问设备的物理地址。这种能力对于显示适配器一类设备非常有意义,如果用户可以直接通过内存映射访问显存的话,屏幕上的各点的像素不再需要从一个用户空间到内核空间的复制过程。

  • mmap()将用户空间的一段内存与设备内存关联
  • 当用户访问用户空间的这段地址范围时,会转化为对设备的访问

mmap()必须以PAGE_SIZE为单位进行映射。

  • 内存只能以页为单位进行映射
  • 并且进行页对齐

驱动中mmap函数的原型

int (*mmap)(struct file *filp,struct vm_area_struct *vma);

ARM9嵌入式Linux开发-内存与IO操作_第4张图片

驱动中的mmap()函数将在进行mmap()系统调用时最终被调用,mmap()系统调用的原型与驱动中的mmap()原型区别很大,如下:

caddr_t mmap(caddr_t addr,
		size_t len,
		int prot,int flags,
		int fd,
		off_t offest);
  • fd:文件描述符,一般由open()返回,fd也可以指定为-1,此时需指定flags参数中的MAP_ANON,表明进行匿名映射。
  • len:映射到调用用户的用户空间的字节数,从被映射文件开头offest个字节开始算起,
  • offest参数一般设为0,表示从文件开头开始映射。
  • prot:访问权限,可以取如下几个值的“或”:PROT_READ、PROT_WRITE、PROT_EXEC和PROT_NONE(不可访问)。
  • addr:指定文件应该被映射用户空间的起始地址,一般设定为NULL,这样,选择起始地址的任务将由内核完成,而函数的返回值就是映射到用户空间的地址。

当用户空间调用mmap()的时候,内核会进行如下操作:

  1. 在进程的虚拟空间查找一块VMA。
  2. 将这块VMA进行映射。
  3. 如果设备驱动程序或者文件系统的file_operation定义了mmap()操作,则调用它。
  4. 将这个VMA插入进程的VMA链表。     
  5. 驱动程序中的mmap()函数的实现机制是建立页表,并填充VMA结构体中vm_opetations_struct指针。     
  6. 解除映射,可调用下面的这个函数:
  7. int munmap(caddr_t addr,size_t len);

DMA

  • DMA(直接内存访问)是一种无须CPU的参与就可以让外设与系统内存之间进行双向数据传输的硬件机制。
  • 使用DMA可以使系统CPU从实际的I/O数据传输过程中摆脱出来,从而提高系统的吞吐率。
  • DMA方式的数据传输由DMA控制器(DMAC)控制
  1. 在传输期间,CPU可以并发地执行其他任务,
  2. 当DMA结束后,DMAC通过中断通知CPU数据传输已经结束,
  3. 然后由CPU执行相应的中断服务程序进行后处理。

DMA和Cache一致性问题

所谓Cache数据与内存数据的一致性问题指在采用Cache的系统中,同样一个数据可能圈存在于Cache中,也存在于主存中,Cache与主存中的数据一样则具有一致性,数据若不一样,则具有不一致性。

假设如果DMA的目的地址与Cache所缓存的内存地址访问有重叠.经过DMA操作,Cache缓存对应的内存的数据已经被修改, 而CPU本身并不知道,它仍然认为Cache中的数据就是内存中的数据,以后访问Cache映射的内存时,它仍然使用陈旧的Cache数据。这样就发生Cache与内存之间数据“不一致性”的错误。

解决Cache一致性方法 

  • 最简单方法是直接禁DMA目标地址范围内内存的Cache功能。当然,这将牺牲性能,但是却更可靠;
  • 如果DMA传输数据从内存到外设,在传输前先Flush对应的Cache line到内存;
  • 如果DMA传输数据从外设到内存,在传输结束时 Invalidate(使无效,即丢掉)对应的Cache line里的原来的数据,如果CPU要使用DMA接收的数据,则需要重新到内存中取数据到Cache。

ARM9嵌入式Linux开发-内存与IO操作_第5张图片

你可能感兴趣的:(ARM9嵌入式Linux开发)