用户虚拟地址
物理地址
CPU使用的是物理地址,在CPU的地址信号线上产生的就是物理地址。
总线地址
总线地址,顾名思义,是与总线相关的,就是总线的地址线或在地址周期上产生的信号。外设使用的是总线地址。基于DMA的硬件使用总线地址而非物理地址。
Linux采用虚拟内存管理技术,使得每个进程都有独立的进程地址空间,该空间大小为3G,用户看到的和接触到的都是虚拟地址,无法使用实际的物理地址。Linux将4G的虚拟地址空间划分为两个部分:内核空间和用户空间。
内核空间:
用户空间
内核逻辑地址
内核虚拟地址
内核虚拟地址是3G-4G这段地址,它与物理地址通过页表来映射,内核逻辑地址是指3G-3G+main_memory_size这段虚拟地址,它与物理地址的映射是线性的,当然也可以通过页表映射。内核逻辑地址都是内核虚拟地址,但不是所有内核虚拟地址都是内核逻辑地址。
高端内存--896M以上的部分称之为高端内存。
3G----------------------------------------------------------------------------------------------4G
直接映射区---8M-动态映射区-8K---kmap区(高端内存映射区)---固定映射区-4K
896M---------------120M---------------------------4M------------------------4M---------
地址的转换
_pa( logical-addr)//逻辑地址->物理地址
_va(physical-addr)//物理地址->逻辑地址
Linux的内核在内存管理中处理的最小单位是physical pages,内核通过struct page结构体来表示每一个页。
page的数据结构:struct page{…};
内核维护了一个或者多个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定义了虚拟内存段(Virtual Memory Area,VMA)。一个VMA段是某个进程的一段连续的虚拟空间,在这段虚拟内存空间的所有单元拥有相同的特征。
进程通常占用几个VMA段,分别用于代码段、数据段和堆栈段等。属于同一进程的VMA段通过vm_next指针链接,组成链表。
内核为设备驱动程序提供了一致的内存管理接口
分配内存的方法包括
设备驱动程序常常会反复地分配很多同一大小的内存块。为了满足这样的应用,内核实现了这种形式的内存池,通常称为后备高速缓存(lookaside cache) 。
Slab分配器
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分配虚拟地址空间的连续区域,但这段区域在物理上可能是不连续的。
#include
void *vmalloc(unsigned long size);
void vfree(void *addr);
vmalloc分配得到的地址是不能在微处理器之外使用的(只存在于软件中,没有对应的硬件意义),当驱动程序需要真正的物理地址时(像外设用以驱动系统总线的DMA地址),就不能使用vmalloc 。
使用vmalloc函数的正确场合是在分配一大块连续的、只在软件中存在的、用于缓冲的内存区域的时候。
因为vmalloc不但要获取内存,还要建立页表,它的开销比__get_free_pages大,因此,用vmalloc函数分配仅仅一页的内存空间是不值得的。
vmalloc和kmalloc区别
内核虚拟地址转化为物理地址
#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空间,也可能位于内存空间;
通常,除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端口的操作
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内存访问函数
写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()必须以PAGE_SIZE为单位进行映射。
驱动中mmap函数的原型
int (*mmap)(struct file *filp,struct vm_area_struct *vma);
驱动中的mmap()函数将在进行mmap()系统调用时最终被调用,mmap()系统调用的原型与驱动中的mmap()原型区别很大,如下:
caddr_t mmap(caddr_t addr,
size_t len,
int prot,int flags,
int fd,
off_t offest);
当用户空间调用mmap()的时候,内核会进行如下操作:
所谓Cache数据与内存数据的一致性问题指在采用Cache的系统中,同样一个数据可能圈存在于Cache中,也存在于主存中,Cache与主存中的数据一样则具有一致性,数据若不一样,则具有不一致性。
假设如果DMA的目的地址与Cache所缓存的内存地址访问有重叠.经过DMA操作,Cache缓存对应的内存的数据已经被修改, 而CPU本身并不知道,它仍然认为Cache中的数据就是内存中的数据,以后访问Cache映射的内存时,它仍然使用陈旧的Cache数据。这样就发生Cache与内存之间数据“不一致性”的错误。
解决Cache一致性方法