Linux内存管理(一)

背景

本篇博客试图通过linux内核源码分析linux的内存管理机制,并且对比内核提供的几个分配内存的接口函数。然后聊下slab层的用法以及接口函数。

内核分配内存与用户态分配内存

内核分配内存与用户态分配内存显然是不同的,内核不可以像用户态那样奢侈的使用内存,内核使用内存一定是谨小慎微的。并且,在用户态如果出现内存溢出因为有内存保护机制,可能只是一个报错或警告,而在内核态若出现内存溢出后果就会严重的多(毕竟再没有管理者了)。

我们知道处理器处理数据的基本单位是字。而内核把页作为内存管理的基本单位。那么,页在内存中是如何描述的? 
内核用struct page结构体表示系统中的每一个物理页: 
这里写图片描述 
flags存放页的状态,如该页是不是脏页。 
_count域表示该页的使用计数,如果该页未被使用,就可以在新的分配中使用它。 
要注意的是,page结构体描述的是物理页而非逻辑页,描述的是内存页的信息而不是页中数据。 
实际上每个物理页面都由一个page结构体来描述,有的人可能会惊讶说那这得需要多少内存呢?我们可以来算一下,若一个struct page占用40字节内存,一个页有8KB,内存大小为4G的话,共有524288个页面,需要刚好20MB的大小来存放结构体。这相对于4G的内存根本九牛一毛。

有些页是有特定用途的。比如内存中有些页是专门用于DMA的。 
内核使用区的概念将具有相似特性的页进行分组。区是一种逻辑上的分组的概念,而没有物理上的意义。 
区的实际使用和分布是与体系结构相关的。在x86体系结构中主要分为3个区:ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。 
ZONE_DMA区中的页用来进行DMA时使用。ZONE_HIGHMEM是高端内存,其中的页不能永久的映射到内核地址空间,也就是说,没有虚拟地址。剩余的内存就属于ZONE_NORMAL区。 
我们可以看一下描述区的结构体struct zone(在linux/mmzone.h中定义)。 
这里写图片描述 
这个结构体比较长,我只截取了一部分出来。 
实际上不是所有的体系结构都定义了全部区,有些64位的体系结构,比如Intel的x86-64体系结构可以映射和处理64位的内存空间,所以其没有ZONE_HIGHMEM区。而有些体系结构中的所有地址都可用于DMA,所以这些体系结构就没有ZONE_DMA区。

内核中内存分配接口

我们现在已经大体了解了内核中的页与区的概念及描述。接下来我们就可以来看看内核中有哪些内存分配与释放的接口。在内核中,我们正是通过这些接口来分配与释放内存的。首先我们来看看以页为单位进行分配的接口函数。

获得页与释放页

获得页

获得页使用的接口是alloc_pages函数,我们来看下它的源码(位于linux/gfp.h中) 
这里写图片描述 
可以看到,该函数返回值是指向page结构体的指针,参数gfp_mask是一个标志,简单来讲就是获得页所使用的行为方式。order参数规定分配多少页面,该函数分配2的order次方个连续的物理页面。返回的指针指向的是第一page页面。 
获得页的方式不只一种,我们还可以使用__get_free_pages函数来获得页,该函数和alloc_pages的参数一样,然而它会返回一个虚拟地址。源码如下: 
这里写图片描述 
可以看到,这个函数其实也是调用了alloc_pages函数,只不过在获得了struct page结构体后使用page_address函数获得了虚拟地址。 
另外还有alloc_page函数与__get_free_page函数,都是获得一个页,其实就是将前面两个函数的order分别置为了0而已。这里不赘述了。

我们在使用这些接口获取页的时候可能会面对一个问题,我们获得的这些页若是给用户态用,虽然这些页中的数据都是随机产生的垃圾数据,不过,虽然概率很低,但是也有可能会包含某些敏感信息。所以,更谨慎些,我们可以将获得的页都填充为0。这会用到get_zeroed_page函数。看下它的源码: 
这里写图片描述 
这个函数也用到了__get_free_pages函数。只是加了一种叫做__GFP_ZERO的gfp_mask方式。所以,这些获得页的函数最终调用的都是alloc_pages函数。alloc_pages函数是获得页的核心函数。

释放页

当我们不再需要某些页时可以使用下面的函数释放它们: 
__free_pages(struct page *page, unsigned int order) 
__free_page 
free_pages 
free_page(unsigned long addr, unsigned int order) 
这些接口都在linux/gfp.h中。 
释放页的时候一定要小心谨慎,内核中操作不同于在用户态,若是将地址写错,或是order写错,那么都可能会导致系统的崩溃。若是在用户态进行非法操作,内核作为管理者还会阻止并发出警告,而内核是完全信赖自己的,若是在内核态中有非法操作,那么内核可能会挂掉的。

kmalloc与vmalloc

前面讲的那些接口都是以页为单位进行内存分配与释放的。而在实际中内核需要的内存不一定是整个页,可能只是以字节为单位的一片区域。这两个函数就是实现这样的目的。不同之处在于,kmalloc分配的是虚拟地址连续,物理地址也连续的一片区域,vmalloc分配的是虚拟地址连续,物理地址不一定连续的一片区域。这里依然需要特别注意的就是使用释放内存的函数kfree与vfree时一定要注意准确释放,否则会发生不可预测的严重后果。

slab层

分配和释放数据结构是内核中的基本操作。有些多次会用到的数据结构如果频繁分配内存必然导致效率低下。slab层就是用于解决频繁分配和释放数据结构的问题。为了便于数据的频繁分配和回收,编程者常常会用到一个空闲链表。该链表包含了有可供使用的、已经分配好的数据结构块。当代码需要一个新的数据结构实例时,就可以从空闲链表中抓取一个,而不需要分配内存,再把数据放进去。以后,当不再需要这个数据结构的实例时,就把它放回空闲链表,而不是释放掉它。从某种意义上说,空闲链表相当于对象高速缓存以便快速存储频繁使用的对象类型。为便于理解slab层的层次结构,请看下图 
这里写图片描述 
简单的说,物理内存中有多个高速缓存,每个高速缓存都是一个结构体类型,一个高速缓存中会有一个或多个slab,slab通常为一页,其中存放着数据结构类型的实例化对象。 
分配高速缓存的接口是struct kmem_cache kmem_cache_create (const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void ))。 
它返回的是kmem_cache结构体。第一个参数是缓存的名字,第二个参数是高速缓存中每个对象的大小,第三个参数是slab内第一个对象的偏移量。剩下的就不细说。 
总之,这个接口函数为一个结构体分配了高速缓存,那么高速缓存有了,是不是就要为缓存中分配实例化的对象呢?这个接口是 
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags) 
参数是kmem_cache结构体,也就是分配好的高速缓存,flags是标志位。 
抽象的介绍看着不直观, 我们看个具体的例子。之前我写过一个关于jbd2日志系统的博客,介绍过jbd2的模块初始化过程。其中就提到过jbd2在进行模块初始化的时候是会创建几个高速缓冲区的。如下: 
这里写图片描述 
我们看看第一个创建缓冲区的函数。 
这里写图片描述 
首先是断言缓冲区一定为空的。然后用kmem_cache_create创建了两个缓冲区。两个高速缓冲区就这么创建好了。看下图 
这里写图片描述 
这里用kmem_cache结构体,也就是jbd2_revoke_record_cache高速缓存实例化了一个对象。

总结

内存管理的linux内核源码我只分析了一小部分,主要是总结了一下内核分配与回收内存的接口函数及其用法。

猜你喜欢

转载自blog.csdn.net/weixin_38233274/article/details/80339660