对于内存管理,我们首先简单的了解以下几个概念:逻辑地址,线性地址和物理地址。
学过微机原理的应该知道(本人控制出身),通常我们说的逻辑地址来于段式内存管理方式,即一个逻辑地址,是由一个段标识符加上一个指定段内相对地址的偏移量,表示为 [段标识符:段内偏移量]。物理地址是物理内存实际的地址,线性地址是逻辑地址到物理地址变换之间的中间层,线性地址就是逻辑地址中段的偏移地址,加上相应段的基地址(基地址需要左移再相加(Intel))。启用了分页机制,线性地址会使用页目录和页表中的项变换成物理地址。
机器语言指令中出现的内存地址,都是逻辑地址,需要转换为线性地址,再经过MMU(CPU中的内存管理单元)转换成物理地址才能够被访问到。
在Linux中,其逻辑地址等于线性地址。因为Linux 所有的段(用户代码段、用户数据段、内核代码段、内核数据段)的段基线性地址都是从 0x00000000 开始,长度4G,这样线性地址 = 0 + 偏移地址,也就是说逻辑地址等于线性地址了,更具体点,就是逻辑地址的偏移量字段的值与线性地址的值总是相同的。
图中的段起始线性地址就是段的基地址,这样相当于Linux 绕过了分段,所以Linux 主要以分页的方式实现内存管理。
先再了解下虚拟内存和物理内存
物理内存就是实实际际存在的内存,程序最终运行的地方。现在的内存管理方法在程序和物理内存之间引入了虚拟内存这个概念,虚拟内存介于程序和物理内存之间,程序只能看见虚拟内存,不能直接访问物理内存,如我们的 malloc、new 等函数开辟的都是虚拟内存空间。每个进程都有自己独立的进程地址空间(虚拟地址),这样就做到了进程隔离。最终都需要将虚拟地址映射到物理地址。内核为每个进程维护不同的页表,不同进程可以虚拟地址一样,但映射后的物理地址不一样。
前面简单的介绍了分段机制,下面简单了解分页机制,不讨论中间较为复杂的二级模式。
硬件中的分页
分页单元把线性地址转换成物理地址,为了效率起见,线性地址被分成以固定长度为单位的组,称为页。页内部连续的线性地址被映射到连续的物理地址中。分页单元把所有的RAM(物理内存)分成固定长度的页框(也叫物理页),每一个页框包含一个页,也就是说一个页框的长度与一个页的长度一致,页框是主存的一部分,因此也是一个存储区域。
分页机制就是把内存地址空间分为若干个很小的固定大小的页,Linux 中一般页的大小是 4KB,我们把进程的地址空间按页分割,把常用的数据和代码页装载在内存中,不常用的代码和数据则保存在磁盘中。内核只是创建虚拟内存(初始化进程控制表中内存相关的链表),实际上并不立即就把虚拟内存对应位置的程序数据和代码拷贝到物理内存中,只是建立好虚拟内存和磁盘文件间的映射,等到运行到对应的程序时,才会通过缺页异常,调用缺页异常处理程序,从磁盘拷贝数据到对应物理内存。
下面理解Linux内核是如何管理内存的:
内核把物理页作为内存管理的基本单位。内存管理单元(MMU)以页大小为单位来管理系统中的页表,从虚拟内存角度来看,页就是最小单位。体系结构不同,支持的页大小也不尽相同。
内核用 struct page 结构表示系统中的每个物理页
struct page { unsigned long flags; /*存放页的状态*/ atomic_t _count; /* 页的引用计数*/ union { atomic_t _mapcount; /* Count of ptes mapped in mms, * to show when page is mapped * & limit reverse map searches. */ struct { /* SLUB */ u16 inuse; u16 objects; }; }; union { struct { unsigned long private; /* Mapping-private opaque data: * usually used for buffer_heads * if PagePrivate set; used for * swp_entry_t if PageSwapCache; * indicates order in the buddy * system if PG_buddy is set. */ struct address_space *mapping; /* If low bit clear, points to * inode address_space, or NULL. * If page mapped as anonymous * memory, low bit is set, and * it points to anon_vma object: * see PAGE_MAPPING_ANON below. */ }; void *virtual; /* 页的虚拟地址 Kernel virtual address (NULL if not kmapped, ie. highmem) */ …… };内核用这一结构来管理系统中所有的页,因为内核需要知道一个页是否空闲,也就是页有没有被分配。如果页已经被分配,内核还需要知道谁拥有这个页,拥有者可能是用户空间进程、动态分配的内核数据、静态内核代码或页高速缓存等。
系统中的每个物理页都要分配一个这样的结构体,内核仅仅用这个数据结构来描述当前时刻在相关的物理页中存放的东西,这种数据结构的目的在于描述物理内存本身,而不是描述包含在其中的数据。
获得页
内核提供了一种请求内存的底层机制,并提供了对它进行访问的几个接口,所有这些接口都是以页为单位分配内存(函数定义于 linux/gfp.h)中
上面的函数接口中 alloc* 返回的是内存的物理地址,get* 返回的是分配的物理页所在的逻辑地址。请求分配的页都是连续的。
内核还提供了一个函数把给定的页转换为它的逻辑地址
void* page_address(struct page *page) //返回指向给定物理页当前所在的逻辑地址
释放页
页资源也是有限的,当不再需要页时可以用下面的函数释放它们:
void __free_pages(struct page *page, unsigned int order) /* *该函数先检查page指向的页描述符,如果该页框未被保留,就把描述符的count字段减1。 *如果count值变为0,就假定从与page对应的页框开始的1<<order个连续页框(物理页)不再使用 */ void free_pages(unsigned long addr, unsigned int order) /* *类似于__free_pages,但是它接收的参数为要释放的第一页框的线性地址addr */ void free_page(unsigned long addr) /* *free_pages(addr, 0) */
当需要以页为单位的一族连续物理页时,尤其是在你只需要一两页时,这些低级页函数很有用,对于常用的以字节为单位的分配来说,内核提供的函数是 kmalloc()
kmalloc() 在<linux/slab.h> 中声明,用它可以获得以字节为单位的一块内核内存:
void* kmalloc(size_t size, gfp_t flags) //返回一个指向内存块的指针,其内存块至少要有size大小,所分配的内存区在物理上是连续的,出错时,返回NULL void kfree(const *ptr) //释放由kmalloc()分配出来的内存块。PS:kfree(NULL)是安全的
kmalloc/kfree 是工作在slab分配的基础上的,当通过 kmalloc 申请内存时,内核会根据所请求的大小来从通用缓冲池中选择最合适的缓冲池进行内存分配,所谓的最合适就是大于等于申请大小的所有缓冲池中 slab 对象大小最小的那个。也就是说内核只能分配一些预定义、固定大小的字节数组。kmalloc 能够处理的最小内存块是 32 或 64字节(体系结构依赖)。
内核还提供了 vmalloc() 函数,其工作方式类似于 kmalloc(),只不过vmalloc 分配的内存虚拟地址是连续的,而物理地址则无须连续。这也是用户控件分配函数的工作方式:由 malloc() 返回的页在进程的虚拟地址空间内是连续的,但是这并不保证它们在物理 RAM 中也是连续的。而kmalloc 函数确保页在物理地址上是连续的(虚拟地址自然也是连续的)。vmalloc() 函数值确保页在虚拟地址空间内是连续的,它通过分配非连续的物理内存块,再“修正”页表,把内存映射到逻辑地址空间的连续区域中。
参考资料:《Linux 内核设计与实现》