内存范围ios9和ios8的内存范围

内存和I/O访问
内存管理单元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);//用完没有用完的内存
malloc()申请的内存一定要有free()释放,而且而且要成对出现。
2.内核空间内存动态申请
在linux内核空间申请内存涉及的函数主要有kmalloc()和__get_free_pages()和vmalloc()等,kmalloc()和__get_free_pages()申请的内存位于物理内存映射区,而且在物理上也是连续的,它们与真实物理地址只有一个偏移,因此转换比较简单,而vmalloc()在虚拟内存空间给出一块连续的内存区,实质上这个连续的虚拟内存在物理内存中并不一定连续
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()申请的内存应使用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_t
size,size_t align,unsigned long flags,void (*ctor)(void *,struct
kmem_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_t
上述函数在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_
xxx_cachep = kmem_cache_create("xxx",sizeof(struct
xxx),0,SLAB_HWCACHE_ALIGN|SLAB_PANIC,NULL,NULL);
&//分配slab缓存
struct xxx *
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)
-&PHYS_OFFSET&+&PAGE_OFFSET)
注意,上面两个函数方法仅适用于内核物理内存映射区896MB 一下的低端内存。
设备I/O端口和I/O内存
设备寄存器有控制寄存器,数据寄存器,状态寄存器,这些寄存器可能位于I/O空间也有可能位于内存空间,当位于I/O口空间时通常称为I/O端口,位于内存空间时,对应的内存空间为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 long
count);//从端口port开始读count字节写入addr指向内存
void outsb(unsigned port,void *addr,unsigned long count)
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);
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 long
void iowrite16_rep(void *addr,const void *buf,unsigned long
void iowrite32_rep(void *addr,const void *buf,unsigned long
&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,unsigned
long 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,unsigned
long len,char *name);
这个函数向内核申请n个内存地址,这些地址从first开始如果分配成功返回非NULL,返回NULL意味着失败,只是单单申请了I/O内存,没有进行映射
当用request_mem_region()函数申请的I/O内存应该用下面函数来归还该系统
void release_mem_reqion(unsigned long start,unsigned long
上述的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,int
fd,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_ //所处的地址空间
unsigned long vm_ //开始虚拟地址
unsigned long vm_ //结束虚拟地址
pgprot_t vm_page_ //访问权限
unsigned long vm_//标志
struct vm_operations_struct *vm_//操作VMA的函数集指针
unsigned long vm_//偏移
struct file *vm_
void *vm_private_
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 long
address,int *type);//访问页不在内存是调用
在内核生成一个VMA后,它会调用VMA的open()函数,但是,当用户进行mmap()系统调用后,尽管VMA在设备驱动文件操作结构体的mmap()被调用前就产生了,内核却不会调用VMA的open()函数,下面是一个vm_operations_struct的操作例子:
struct int xxx_mmap(struct file *filp,struct vm_area_struct
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_
xxx_vma_open(vma);
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 long
addr,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{
u//虚拟地址
//页帧号,可以说是物理地址
我们要加如I/O映射地址时在这个数组中加进即可
比如我们要在TQ2440开发板中添加新的物理地址到虚拟地址映射,只需修改arch/arm/mach-s3c2410/mach-tq2440.c中的map-desc数组即可(当然还要把这个文件加到Makefile中和Kconfig配置中编译进内核)
struct map_desc tq2440_iodesc[] = {};
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_t
size,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的设备在设备端看起来是0x,而0通常又被映射为虚拟地址0xc0000000所以同一地址就有了三重身份:物理地址0,总线地址0x,虚拟地址0xc0000000,总线地址是从设备角度看到的内存地址,物理地址则是从cPU
MMU控制器外围角度看到的地址,虚拟地址是从cpu角度看到的地址。
已投稿到:
以上网友发言只代表其个人观点,不代表新浪网的观点或立场。

我要回帖

更多关于 ios9和ios8内存 的文章

 

随机推荐