Linux Slab机制

2024-01-20 20:58
文章标签 linux 机制 slab

本文主要是介绍Linux Slab机制,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

https://www.cnblogs.com/wangzahngjun/p/4977425.html

1.内部碎片和外部碎片

外部碎片
什么是外部碎片呢?我们通过一个图来解释:

假设这是一段连续的页框,阴影部分表示已经被使用的页框,现在需要申请一个连续的5个页框。这个时候,在这段内存上不能找到连续的5个空闲的页框,就会去另一段内存上去寻找5个连续的页框,这样子,久而久之就形成了页框的浪费。称为外部碎片。
内核中使用伙伴算法的迁移机制很好的解决了这种外部碎片。
内部碎片
当我们申请几十个字节的时候,内核也是给我们分配一个页,这样在每个页中就形成了很大的浪费。称之为内部碎片。
内核中引入了slab机制去尽力的减少这种内部碎片。

2.slab分配机制

slab分配器是基于对象进行管理的,所谓的对象就是内核中的数据结构(例如:task_struct,file_struct 等)。相同类型的对象归为一类,每当要申请这样一个对象时,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免内部碎片。slab分配器并不丢弃已经分配的对象,而是释放并把它们保存在内存中。slab分配对象时,会使用最近释放的对象的内存块,因此其驻留在cpu高速缓存中的概率会大大提高。

Linux 所使用的 slab 分配器的基础是 Jeff Bonwick 为 SunOS 操作系统首次引入的一种算法。Jeff 的分配器是围绕对象缓存进行的。在内核中,会为有限的对象集(例如文件描述符和其他常见结构)分配大量内存。Jeff 发现对内核中普通对象进行初始化所需的时间超过了对其进行分配和释放所需的时间。因此他的结论是不应该将内存释放回一个全局的内存池,而是将内存保持为针对特定目而初始化的状态。例如,如果内存被分配给了一个互斥锁,那么只需在为互斥锁首次分配内存时执行一次互斥锁初始化函数(mutex_init)即可。后续的内存分配不需要执行这个初始化函数,因为从上次释放和调用析构之后,它已经处于所需的状态中了。(这其实就是对象池的思想)

3.内核中slab的主要数据结构

简要分析下这个图:kmem_cache是一个cache_chain的链表,描述了一个高速缓存,每个高速缓存包含了一个slabs的列表,这通常是一段连续的内存块。存在3种slab:slabs_full(完全分配的slab),slabs_partial(部分分配的slab),slabs_empty(空slab,或者没有对象被分配)。slab是slab分配器的最小单位,在实现上一个slab有一个货多个连续的物理页组成(通常只有一页)。单个slab可以在slab链表之间移动,例如如果一个半满slab被分配了对象后变满了,就要从slabs_partial中被删除,同时插入到slabs_full中去。
举例说明:如果有一个名叫inode_cachep的struct kmem_cache节点,它存放了一些inode对象。当内核请求分配一个新的inode对象时,slab分配器就开始工作了:

  • 首先要查看inode_cachep的slabs_partial链表,如果slabs_partial非空,就从中选中一个slab,返回一个指向已分配但未使用的inode结构的指针。完事之后,如果这个slab满了,就把它从slabs_partial中删除,插入到slabs_full中去,结束;
  • 如果slabs_partial为空,也就是没有半满的slab,就会到slabs_empty中寻找。如果slabs_empty非空,就选中一个slab,返回一个指向已分配但未使用的inode结构的指针,然后将这个slab从slabs_empty中删除,插入到slabs_partial(或者slab_full)中去,结束;
  • 如果slabs_empty也为空,那么没办法,cache内存已经不足,只能新创建一个slab了。

接下来我们来分析下slab在内核中数据结构的组织,首先要从kmem_cache这个结构体说起了

struct kmem_cache {struct array_cache *array[NR_CPUS];//per_cpu数据,记录了本地高速缓存的信息,也是用于跟踪最近释放的对象,每次分配和释放都要直接访问它。unsigned int batchcount;//本地高速缓存转入和转出的大批数据数量unsigned int limit;//本地高速缓存中空闲对象的最大数目unsigned int shared;unsigned int buffer_size;/*buffer的大小,就是对象的大小*/u32 reciprocal_buffer_size;unsigned int flags;     /* constant flags */unsigned int num;       /* # of objs per slab *//*slab中有多少个对象*//* order of pgs per slab (2^n) */unsigned int gfporder;/*每个slab中有多少个页*/gfp_t gfpflags;       /*与伙伴系统交互时所提供的分配标识*/  size_t colour;          /* cache colouring range *//*slab中的着色*/unsigned int colour_off;    /* colour offset */着色的偏移量struct kmem_cache *slabp_cache;unsigned int slab_size;              //slab管理区的大小unsigned int dflags;        /* dynamic flags *//* constructor func */void (*ctor)(void *obj);    /*构造函数*//* 5) cache creation/removal */const char *name;/*slab上的名字*/struct list_head next;              //用于将高速缓存连入cache chain/* 6) statistics */ //一些用于调试用的变量
#ifdef CONFIG_DEBUG_SLABunsigned long num_active;unsigned long num_allocations;unsigned long high_mark;unsigned long grown;unsigned long reaped;unsigned long errors;unsigned long max_freeable;unsigned long node_allocs;unsigned long node_frees;unsigned long node_overflow;atomic_t allochit;atomic_t allocmiss;atomic_t freehit;atomic_t freemiss;int obj_offset;int obj_size;
#endif /* CONFIG_DEBUG_SLAB *///用于组织该高速缓存中的slabstruct kmem_list3 *nodelists[MAX_NUMNODES];/*最大的内存节点*/};/* Size description struct for general caches. */
struct cache_sizes {size_t          cs_size;struct kmem_cache   *cs_cachep;
#ifdef CONFIG_ZONE_DMAstruct kmem_cache   *cs_dmacachep;
#endif
};

由上面的总图可知,一个核心的数据结构就是kmem_list3,它描述了slab描述符的状态。

struct kmem_list3 {
/*三个链表中存的是一个高速缓存slab*/
/*在这三个链表中存放的是cache*/struct list_head slabs_partial; //包含空闲对象和已经分配对象的slab描述符struct list_head slabs_full;//只包含非空闲的slab描述符struct list_head slabs_free;//只包含空闲的slab描述符unsigned long free_objects;  /*高速缓存中空闲对象的个数*/unsigned int free_limit;   //空闲对象的上限unsigned int colour_next;   /* Per-node cache coloring *//*即将要着色的下一个*/spinlock_t list_lock;struct array_cache *shared; /* shared per node */struct array_cache **alien; /* on other nodes */unsigned long next_reap;    /* updated without locking *//**/int free_touched;       /* updated without locking */
};

接下来介绍描述单个slab的结构struct slab

struct slab {struct list_head list;   //用于将slab连入keme_list3的链表unsigned long colouroff;   //该slab的着色偏移void *s_mem;        /* 指向slab中的第一个对象*/unsigned int inuse; /* num of objs active in slab */已经分配出去的对象kmem_bufctl_t free;       //下一个空闲对象的下标unsigned short nodeid;   //节点标识符
};

在kmem_cache中还有一个重要的数据结构struct array_cache.这是一个指针数组,数组的元素是系统的cpu的个数。该结构用来描述每个cpu的高速缓存,它的主要作用是减少smp系统中对于自旋锁的竞争。

  • 实际上,每次分配内存都是直接与本地cpu高速缓存进行交互,只有当其空闲内存不足时,才会从keme_list中的slab中引入一部分对象到本地高速缓存中,而keme_list中的空闲对象也不足时,那么就要从伙伴系统中引入新的页来建立新的slab了。
struct array_cache {unsigned int avail;/*当前cpu上有多少个可用的对象*/unsigned int limit;/*per_cpu里面最大的对象的个数,当超过这个值时,将对象返回给伙伴系统*/unsigned int batchcount;/*一次转入和转出的对象数量*/unsigned int touched;/*标示本地cpu最近是否被使用*/spinlock_t lock;/*自旋锁*/void *entry[];  /** Must have this definition in here for the proper* alignment of array_cache. Also simplifies accessing* the entries.*/
};

对上面提到的各个数据结构做一个总结,用下图来描述:

4.关于slab分配器的API

下面看一下slab分配器的接口——看看slab缓存是如何创建、撤销以及如何从缓存中分配一个对象的。一个新的kmem_cache通过kmem_cache_create()函数来创建:

struct kmem_cache *
kmem_cache_create( const char *name, size_t size, size_t align,unsigned long flags, void (*ctor)(void*));

*name是一个字符串,存放kmem_cache缓存的名字;size是缓存所存放的对象的大小;align是slab内第一个对象的偏移;flag是可选的配置项,用来控制缓存的行为。最后一个参数ctor是对象的构造函数,一般是不需要的,以NULL来代替。kmem_cache_create()成功执行之后会返回一个指向所创建的缓存的指针,否则返回NULL。kmem_cache_create()可能会引起阻塞(睡眠),因此不能在中断上下文中使用。

撤销一个kmem_cache则是通过kmem_cache_destroy()函数:

int kmem_cache_destroy( struct kmem_cache *cachep);

该函数成功则返回0,失败返回非零值。调用kmem_cache_destroy()之前应该满足下面几个条件:首先,cachep所指向的缓存中所有slab都为空闲,否则的话是不可以撤销的;其次在调用kmem_cache_destroy()过程中以及调用之后,调用者需要确保不会再访问这个缓存;最后,该函数也可能会引起阻塞,因此不能在中断上下文中使用。
可以通过下面函数来从kmem_cache中分配一个对象:

void* kmem_cache_alloc(struct kmem_cache* cachep, gfp_t flags);

这个函数从cachep指定的缓存中返回一个指向对象的指针。如果缓存中所有slab都是满的,那么slab分配器会通过调用kmem_getpages()创建一个新的slab。

释放一个对象的函数如下:

void kmem_cache_free(struct kmem_cache* cachep,  void* objp);

这个函数是将被释放的对象返还给先前的slab,其实就是将cachep中的对象objp标记为空闲而已

5.使用以上的API写内核模块,生成自己的slab高速缓存。

其实到了这里,应该去分析以上函数的源码,但是几次奋起分析,都被打趴在地。所以就写个内核模块,鼓励下自己吧。

#include <linux/autoconf.h>
#include <linux/module.h>
#include <linux/slab.h>MODULE_AUTHOR("wangzhangjun");
MODULE_DESCRIPTION("slab test module");static struct kmem_cache  *test_cachep = NULL;
struct slab_test
{int val;
};
void fun_ctor(struct slab_test *object , struct kmem_cache  *cachep , unsigned long flags )
{printk(KERN_INFO "ctor fuction ...\n");object->val = 1;
}static int __init slab_init(void)
{struct slab_test *object = NULL;//slab的一个对象printk(KERN_INFO "slab_init\n");test_cachep = kmem_cache_create("test_cachep",sizeof(struct slab_test)*3,0,SLAB_HWCACHE_ALIGN,fun_ctor);if(NULL == test_cachep) return  -ENOMEM ;printk(KERN_INFO "Cache name is %s\n",kmem_cache_name(test_cachep));//获取高速缓存的名称printk(KERN_INFO "Cache object size  is %d\n",kmem_cache_size(test_cachep));//获取高速缓存的大小object = kmem_cache_alloc(test_cachep,GFP_KERNEL);//从高速缓存中分配一个对象if(object){printk(KERN_INFO "alloc one val = %d\n",object->val);kmem_cache_free( test_cachep, object );//归还对象到高速缓存//这句话的意思是虽然对象归还到了高速缓存中,但是高速缓存中的值没有做修改//只是修改了一些它的状态。printk(KERN_INFO "alloc three val = %d\n",object->val);object = NULL;}elsereturn -ENOMEM;return 0;
}static void  __exit slab_clean(void)
{printk(KERN_INFO "slab_clean\n");if(test_cachep)kmem_cache_destroy(test_cachep);//调用这个函数时test_cachep所指向的缓存中所有的slab都要为空}module_init(slab_init);
module_exit(slab_clean);
MODULE_LICENSE("GPL");

我们结合结果来分析下这个内核模块:

这是dmesg的结果,可以发现我们自己创建的高速缓存的名字test_cachep,还有每个对象的大小。

还有构造函数修改了对象里面的值,至于为什么构造函数会出现这么多次,可能是因为,这个函数被注册了之后,系统的其他地方也会调用这个函数。在这里可以分析源码,当调用keme_cache_create()的时候是没有调用对象的构造函数的,调用kmem_cache_create()并没有分配slab,而是在创建对象的时候发现没有空闲对象,在分配对象的时候,会调用构造函数初始化对象。
另外结合上面的代码可以发现,alloc three val是在kmem_cache_free之后打印的,但是它的值依然可以被打印出来,这充分说明了,slab这种机制是在将某个对象使用完之后,就其缓存起来,它还是切切实实的存在于内存中。
再结合/proc/slabinfo的信息看我们自己创建的slab高速缓存

可以发现名字为test_cachep的高速缓存,每个对象的大小(objsize)是16,和上面dmesg看到的值相同,objperslab(每个slab中的对象时202),pagesperslab(每个slab中包含的页数),可以知道objsize * objperslab < pagesperslab

6.总结

目前只是对slab机制的原理有了一个感性的认识,对于这部分相关的源码涉及到着色以及内存对齐等细节。看的不是很清楚,后面还需要仔细研究。

这篇关于Linux Slab机制的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/627319

相关文章

Linux使用fdisk进行磁盘的相关操作

《Linux使用fdisk进行磁盘的相关操作》fdisk命令是Linux中用于管理磁盘分区的强大文本实用程序,这篇文章主要为大家详细介绍了如何使用fdisk进行磁盘的相关操作,需要的可以了解下... 目录简介基本语法示例用法列出所有分区查看指定磁盘的区分管理指定的磁盘进入交互式模式创建一个新的分区删除一个存

Linux使用dd命令来复制和转换数据的操作方法

《Linux使用dd命令来复制和转换数据的操作方法》Linux中的dd命令是一个功能强大的数据复制和转换实用程序,它以较低级别运行,通常用于创建可启动的USB驱动器、克隆磁盘和生成随机数据等任务,本文... 目录简介功能和能力语法常用选项示例用法基础用法创建可启动www.chinasem.cn的 USB 驱动

一文带你理解Python中import机制与importlib的妙用

《一文带你理解Python中import机制与importlib的妙用》在Python编程的世界里,import语句是开发者最常用的工具之一,它就像一把钥匙,打开了通往各种功能和库的大门,下面就跟随小... 目录一、python import机制概述1.1 import语句的基本用法1.2 模块缓存机制1.

高效管理你的Linux系统: Debian操作系统常用命令指南

《高效管理你的Linux系统:Debian操作系统常用命令指南》在Debian操作系统中,了解和掌握常用命令对于提高工作效率和系统管理至关重要,本文将详细介绍Debian的常用命令,帮助读者更好地使... Debian是一个流行的linux发行版,它以其稳定性、强大的软件包管理和丰富的社区资源而闻名。在使用

Redis主从/哨兵机制原理分析

《Redis主从/哨兵机制原理分析》本文介绍了Redis的主从复制和哨兵机制,主从复制实现了数据的热备份和负载均衡,而哨兵机制可以监控Redis集群,实现自动故障转移,哨兵机制通过监控、下线、选举和故... 目录一、主从复制1.1 什么是主从复制1.2 主从复制的作用1.3 主从复制原理1.3.1 全量复制

Redis缓存问题与缓存更新机制详解

《Redis缓存问题与缓存更新机制详解》本文主要介绍了缓存问题及其解决方案,包括缓存穿透、缓存击穿、缓存雪崩等问题的成因以及相应的预防和解决方法,同时,还详细探讨了缓存更新机制,包括不同情况下的缓存更... 目录一、缓存问题1.1 缓存穿透1.1.1 问题来源1.1.2 解决方案1.2 缓存击穿1.2.1

Java如何通过反射机制获取数据类对象的属性及方法

《Java如何通过反射机制获取数据类对象的属性及方法》文章介绍了如何使用Java反射机制获取类对象的所有属性及其对应的get、set方法,以及如何通过反射机制实现类对象的实例化,感兴趣的朋友跟随小编一... 目录一、通过反射机制获取类对象的所有属性以及相应的get、set方法1.遍历类对象的所有属性2.获取

MySQL中的锁和MVCC机制解读

《MySQL中的锁和MVCC机制解读》MySQL事务、锁和MVCC机制是确保数据库操作原子性、一致性和隔离性的关键,事务必须遵循ACID原则,锁的类型包括表级锁、行级锁和意向锁,MVCC通过非锁定读和... 目录mysql的锁和MVCC机制事务的概念与ACID特性锁的类型及其工作机制锁的粒度与性能影响多版本

Linux Mint Xia 22.1重磅发布: 重要更新一览

《LinuxMintXia22.1重磅发布:重要更新一览》Beta版LinuxMint“Xia”22.1发布,新版本基于Ubuntu24.04,内核版本为Linux6.8,这... linux Mint 22.1「Xia」正式发布啦!这次更新带来了诸多优化和改进,进一步巩固了 Mint 在 Linux 桌面

LinuxMint怎么安装? Linux Mint22下载安装图文教程

《LinuxMint怎么安装?LinuxMint22下载安装图文教程》LinuxMint22发布以后,有很多新功能,很多朋友想要下载并安装,该怎么操作呢?下面我们就来看看详细安装指南... linux Mint 是一款基于 Ubuntu 的流行发行版,凭借其现代、精致、易于使用的特性,深受小伙伴们所喜爱。对