由于Linux系统提供了复杂的内存管理功能,所以内存的概念在Linux系统中的相对复杂,有常规的内存、高端的内存、虚拟地址、逻辑地址、总线地址、物理地址、I/O内存、设备内存、预留内存等概念
11.1 CPU与内存、I/O
11.1.1 内存空间与I/O空间
- I/O空间:在X86处理器中存在着I/O空间的概念,I/O空间是相对于内存空间而言的
- 它通过特定的指令in、out来访问
指令格式:IN 累加器,{端口号|DX}
OUT {端口号|DX},累加器
- 注意:目前大多数嵌入式微控制例如ARM、PowerPC等不提供I/O空间,而仅存在内存空间。 内存空间可以直接通过地址、指针来访问
11.1.2 内存管理单元MMU
- 作用:辅助操作系统进行内存管理,提供虚拟地址和物理地址的映射、内存访问权限保护和Cache缓存控制等硬件支持。
虚拟内存机制可以让用户感觉好像程序可以使用非常大的内存空间
MMU操作原理:
- TLB:转换旁路缓存,是MMU的核心部件,缓存少量虚拟地址和物理地址的转换关系,是转换表的Cache,也成为快表
- TTW:转换表漫游,当TLB中没有对应缓冲对应的地址转换关系时,需要通过对内存中转换表(一般为多级页表)的访问来得到虚拟地址和物理地的对应关系。TTW成功后,会将对应的转换关系写入TLB,方便下次转换
- 若TLB中没有虚拟地址的入口,则转换表遍历硬件从存放于主存储器的转换表中获取地址转换信息和访问权限(也就是获得TTW啦),同时将信息放入TLB,它或者被放在一个没有使用的入口或者替换一个已经存在的端口,以后当再次访问这些地址时,对真是物理地址的访问将在Cache或者在内存中发生
- ARM中的TLB条目中的控制信息用于控制对对应地址的访问权限及Cache的操作
- C(高速缓存)和B(缓冲)位被用来控制对应地址的高速缓存和写缓冲,并决定是否进行告诉缓存
- 访问权限和域位作用用来控制读写访问是否被允许,如果不允许将发送一个异常
11.2 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以上的物理内存
11.3 内存读取:
11.3.1 用户空间内存动态申请
- 申请:malloc()
- 释放:free()
11.3.2 内核空间内存动态申请
1、相关函数关系:
- kmalloc()与 - __get_free_pages()
- kmalloc()和__get_free_pages()申请的内存位于物理内存映射区,并且是连续的,它与真实的物理地址一般只差一个固定的偏移
- vmalloc()
- vmalloc()在虚拟地址空间给出一块连续的内存区,实质上这段连续的虚拟内存在物理内存中并不一定连续。也没简单的换算关系
2、具体函数详解
- 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次方为单位管理空闲内存,所以最底层的内存申请总是以页为单位的
- 参数
- order:分配的页数是2^order
- flags:同kmalloc
- 相关宏
- get_zeroed_pages()(申请的同时将页清空)
- __get_free_page()(申请一页,不清零)
- 上述两个宏在实现中调用了alloc_pages()函数
- alloc_pages()既可以在内核空间分配,也可以在用户空间分配
- 上述两个宏在实现中调用了alloc_pages()函数
- 对应的释放函数:free_page(s)()
- 介绍:此宏是Linux内核本质上最底层用于获取空闲内存的方法
__get_free_pages(unsigned int flags,unsigned int order);
get_zeroed_page(unsigned int flags);
__get_free_page(unsigned int flags);
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函数
- 使用情况
- 一般只为存在于软件中(没有对应的硬件意义)的较大的顺序缓存区分配内存,其远大于__get_free_pages()的开销
- 其不能用于原子上下文中
- 因为其内部实现使用了标志为GFP_KERNEL的kmalloc()
- vmalloc()的虚拟地址和物理地址不是一个简单的线性映射
- 因为vmolloc()在申请内存时,会进行内存的映射,改变页表项
- 使用情况
void *vmalloc(unsigned long size);
void vfree(void *addr);
- slab与内存池:
- 引入slab的原因:
- 完全使用页为单元申请和释放内存容易导致浪费
- 在操作系统的运行过程中,经常涉及到对大量对象的重复生成、使用和释放问题,此时使用slab可以大大提高效率
- 实际上kmalloc()就是使用slab机制实现的
- 使用方法:
- 创建一个slab缓存:kmem_cache_create()
- 它可以保留任意数目且全部同样大小的后备缓存
- 参数
- size:要分配的每个数据结构的大小
- flags:控制如何进行分配的位掩码
- SLAB_HWCACHE_ALIGN:每个数据对象呗对齐到一个缓存行
- SLAB_CACHE_DMA:要求数据对象在DMA区域中的分配
- 分配slab缓存:kmem_cache_alloc();
- 创建的slab后备缓存中分配一块并返回首地址指针
- 释放slab缓存:kmem_cache_free();
- 释放由kmem_cache_alloc()函数分配的缓存
- 收回slab缓存:kmem_cache_destroy();
- 创建一个slab缓存:kmem_cache_create()
- 获知当前的slab的分配和使用情况:cat /proc/slabinfo
- 注意:slab不是要替代__get_free_pages(),其在最底层仍然依赖于__get_free_pages(),slab在底层每次申请1页或多页,之后在分隔这些页为更小的单元进行管理,从而节省了内存,也提高了slab缓冲对象的访问效率
- 引入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 (*dtor)(void*,struct kmem_cache*, unsigned long));
//2.分配slab缓存