0%

《操作系统》Linux之内存管理

外部碎片

内存经过频繁的申请和归还后,如果不经过合并处理,将会产生大量的小碎片,而没有连续的大块内存供分配,这种碎片称为外部碎片

下面介绍伙伴算法解决外部碎片问题

伙伴算法

Linux具有11个链表,元素是一个块,块中是由连续的n个内存页组成

这11个链表中块中的内存页个数分别是 2^0、2^1、2^2、…、2^10

块与块之间在物理内存上是不连续的,如果两个块在物理上是连续的,则他们就是伙伴,两个伙伴可能在不同的链表上

申请内存时,按照申请的量,从相应的链表中分配一个块。比如需要申请8个内存页的空间,就从n=8的链表中取出一个块分配出去

回收内存时,会检查伙伴是不是空闲状态,如果是空闲状态,则他两合并成一个更大的块,然后放到相应的链表上

内部碎片

被申请的内存块没有利用充分,产生碎片。称为内部碎片

比如只需要申请1k的空间,但是内存每页4k,分配了一页,结果只用到了1k,存在3k的内部碎片

下面介绍使用slab分配器解决内部碎片问题

slab分配器

slab分配器是一个粒度更小的分配器,就是为了解决小内存分配的问题

伙伴算法是按照块分配,而slab分配器是按照对象分配

不同的对象类型存放在不同的slab中,从对应的slab中申请对象

每个slab是由多个物理页(一般都是一个页)组成的,是从伙伴系统申请到的块,而slab又被拆分成了很多一致类型的对象,这种对象的分配就由这个slab分配

slab分配器分为两大类:专用slab分配器和普通slab分配器

使用run-in-linux cat /proc/slabinfo(因为我使用的mac,所以使用了run-in-linux工具)可以查看linux中的所有slab分配器

上面是一部分截图,可以看到task_group、radix_tree_node这些都是专用slab分配器

而kmalloc-524288这种kmalloc开头的则是普通slab分配器,当需要为一些小数据分配内存时(比如一个结构体),就会从这些普通slab分配器中获取内存

每个slab分配器管理者所有一样大小的对象,所以上面可以看到各种大小的kmalloc开头的slab分配器

slab分配器只用于分配内核空间中的内存,是给内核自己或者驱动使用的,并没有通过系统调用暴露给外部使用,因为内核空间本来就不是用户进程该觊觎的

内核为驱动的开发提供了kmalloc()函数和vmalloc()函数用于分配内核空间(32位系统中,进程整个虚拟空间中前面高地址的一个G是内核空间。64位系统中,虚拟空间总共2^48,256T,前面128T属于内核空间)

为用户进程提供了brk、mmap系统调用分配用户空间(通过伙伴算法申请到若干块)

下面是32位系统下,用户空间的布局

既然用户进程不能使用内核中的slab分配器,那么需要小内存该怎么办?

用户进程需要小内存的话,需要自己实现一个类似slab分配器的内存分配器。先使用brk系统调用申请到一个空间作为堆,然后自己管理堆空间的分配,颗粒度也是自己把控

C运行库中的malloc函数其实就是这么做的,包括Go运行时中也有自己的内存分配器

本地CPU空闲对象链表

slab分配器就是一个kmem_cache结构体,其中有一个cpu_cache字段,是一个array_cache结构,是一个数组,CPU每个处理器对应着其中一个元素

每个元素中有一个空闲对象链表,叫做本地CPU空闲对象链表,代表这个处理器的分配缓存

某处理器释放某个对象后,不会立马回归slab分配器,而是放入处理器对应的本地CPU空闲对象链表中等待下次被复用(指向对象的地址不变),后续这个处理器再申请对象时,就从本地CPU空闲对象链表中取对象(先进后出)

这样做的好处是,大大提高缓存的命中率。因为刚释放的对象的地址已经存在于处理器的硬件缓存中,这个地址依然会指向从本地CPU空闲对象链表中新取出的对象,后面对这个对象的存取都将命中缓存




微信关注我,及时接收最新技术文章