用户层模拟slab算法
来源:互联网 发布:游戏编程教程视频 编辑:程序博客网 时间:2024/06/05 16:01
slab算法拖了好久,比起“伙伴算法”,感觉它有点难。中间还有个问题困扰我很久,找了好多资料才找到答案。关于slab这个分配机制,网上或者书上的资料往往讲的太多,反倒无法把精髓突出出来,导致我走了很多弯路。所以这次笔记就只罗列出最核心的思想,而且还是像“伙伴算法”一样提供应用层模拟小程序,对我们“程序猿”和“攻城狮”来说,说一千道一万不如几行代码来的清楚明了是吧。至于细节可以慢慢补充,希望能让其他同仁在学习这个内存机制的时候少花点时间。
好,开始,首先slab是个什么东西,有了前面的“伙伴管理”算法为什么还要有个这种东西?这个链接了解最合适了:
http://blog.csdn.net/vanbreaker/article/details/7664296 ;
说白了,它和前面的“伙伴算法”最主要区别就是“伙伴算法”是分配大块内存的,都是4KB的倍数,而我们平时用的数据结构一般用不了这么多内存,可能几十字节或者1~2KB就够了,这时候如果还用“伙伴算法”分配内存,就会造成内存浪费和大量内存碎片,伙伴算法就是为了克服这些缺点,针对小内存的分配,所以它在内存管理子系统中的地位就不言而喻了。
对于接口部分,这个网址罗列的很好:
http://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/
看完前两个,可以再了解一下什么是slab着色(下面网址的第5部分),因为这部分是比较有趣也是比较特别的一块:
http://www.ibm.com/developerworks/cn/linux/l-linux-slab-allocator/
ok,看完上面的东西,基本上对slab的基础就有个了解了,下面对我认为比较重要地方总结下:
1.命名规则或者说定义
对于不同的结构体,为其分配结构的slab命名规则为 ***_struct_cachep,比如:
对于task_struct就是static struct kmem_cache *task_struct_cachep; //fork.c
对于inode就是static struct kmem_cache *inode_cache_slab; //malloc.c 这个比较特殊一些。
这些结构都是kmem_cache类型的,你可以先不用去管这个结构体里边有什么成员,只需要明白它是代表一段高速缓存即可,而这块高速缓存,并不是我们常说的什么一级cache、二级cache等硬件高速缓存,它其实就是一块申请好的普普通通的物理内存,只不过它被提前分配好了,后边我们会看到。
2.两个关键的接口
分配高速缓存的接口原型是:
struct kmem_cache kmem_cache_create (const char *name, size_t size, size_t align, unsigned long flags, void (*ctor)(void ))
name代表高速缓存的名字,你可以用全局搜索在内核代码搜一下这个接口,就可以通过这个参数得知内核为哪些数据结构分配了高速缓存(slab)。size是高速缓存中每个元素的大小,align是slab内第一个对象的偏移,最后一个ctor函数指针是高速缓存的构造函数,只有新的页追加到高速缓存时候,这个函数才会被调用。linux已经不使用这个构造函数了,所以一般设置为NULL。
分配高速缓存成功后,就可以通过下列函数获取对象:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags)
好了,看到这个接口,那个困扰我的问题就出现了,从接口参数可知,只提供了一个kmem_cache的指针参数(后边的参数可以忽略),
那么内核是怎么做到仅仅通过这个参数就知道这个该去哪个slab分配器上分配内存?因为我们构造时候提供了一个名字,但是这里并没有提供名字。如果看来上面task_struct和inode的定义可能很多人已经想到了,我当时不见全局,先入为主的以为常用数据结构可以在各个内核文件中随时随地分配,才会导致一直搞不懂。其实关键就是参数cachep指针,这个指针是被锁定到具体文件的,什么意思呢?
拿进程描述符task_struct来说,内核首先用关键字static把它锁定到了fork.c,并且定义为全局变量,就是上面提到的语句:
static struct kmem_cache *task_struct_cachep; //(fork.c中)注意这里是全局变量
然后在内核初始化调用 fork_init 函数时候,就会创建高速缓存:
(fork.c中)task_struct_cachep = kmem_cache_create("task_struct", sizeof(struct task_struct),\ ARCH_MIN_TASKALIGN, SLAB_PANIC | SLAB_NOTRACK, NULL);
每当应用层调用fork函数创建一个新的进程时候,当分配新进程的task_struct结构需要的内存时候,一定会到内核中调用fork.c里边相应的接口得到所需要的内存,而不是随便在哪个文件就分配了。那么就很容易理解了,fork.c申请的高速缓存的指针是局限在本文件的,从这块高速缓存拿到的obj的空间肯定是 task_struct 类型的了。稍微看下调用
应用层:调用 fork创建新进程,新的进程需要一个 task_struct 结构体空间来描述
内核层:最终调用fork.c中的 dup_task_struct
dup_task_struct(struct task_struct *orig) //(fork.c中) struct task_struct *tsk = alloc_task_struct_node(node); //(fork.c中) kmem_cache_alloc_node(task_struct_cachep, GFP_KERNEL, node); //(fork.c中) //这个函数最终调用kmem_cache_alloc(task_struct_cachep, ...) //(fork.c中),注意这里的参数一定是task_struct_cachep
下面就是代码的模拟了,由于slab算法本身就是建立在“伙伴算法”基础之上的(高速缓存的分配是以块的形式分配,自然会利用伙伴算法分配)。所以新的代码就要建立在伙伴算法那节的代码基础上了,新添加几个文件:slab.h和slab.c是模拟slab法的,fork.c和fork.h不重要,只是为了模拟是如何运用slab算法的,你也可以自己实现其它数据结构的slab算法,比如 inode.c、inode.h等。
由于模拟“伙伴算法”时候只是重现算法思想,在分配大块内存后并没有返回分配的内存地址,而我们的slab中申请高速缓存时候需要这个地址,所以需要对原mem_alloc函数做修改,原int mem_alloc(int size)改为 int mem_alloc(int size, u32 *addr),多加了一个地址指针参数,返回分配大块内存的首地址。代码中当然也要做相应处理,很简单,只需要在分配成功地方加入这么一句:*addr = tmp->addr;其它小细节根据自己需求修改即可,这里就不再重新罗列。看一下新的代码:
首先是slab.h文件 ,它提供了模拟slab核心的结构体和一些函数预定义:
#ifndef SLAB_H#define SLAB_H#include <stdio.h>#include <stdlib.h>#include <string.h>#include "list.h"#include "mem_manage.h"#define BUFCTL_END 0xffff#define COLOR_SIZE 16struct slab { char name[20]; //名字最多为20个字符 unsigned long colouroff; //slab的颜色偏移值 void *s_mem; //slab第一个对象的地址 u32 obj_size; u32 obj_num; //对象总个数 u32 free_num; //当前空闲个数 int *obj_manager_arr; //虽然是个指针,但是它实际指向对象管理数组 struct list_head list; //这里作用我们做了修改,仅仅为了把所有slab连起来};struct slab *create_new_slab(int mem_size, int obj_size, char *slab_name);void list_all_slab();void *malloc_obj(struct slab *cur_slab);int free_obj(struct slab *cur_slab, void **obj_addr);void list_slab_occupy_condition(struct slab *cur_slab);void slab_manager_init();#endif // SLAB_H
slab.c,具体的slab算法实现:
#include "slab.h"#include <math.h>static struct list_head slab_head; //slab链表的头部static int color_off; //着色区偏移量/** * @brief slab管理的初始化 */void slab_manager_init(){ color_off = 0; INIT_LIST_HEAD(&slab_head);}/** * @brief 因为对象所占内存和管理对象的数组所占的内存是互相影响的, * 对象数目越多,管理数组就越大,数组占得内存就越多,所以必须通过 * 这个函数来预判能分配多少个对象 * @param total_size 剩余多少内存(字节为单位) * @param obj_size(单个对象所占用内存数目) * @return -1:错误 否则返回实际分配对象数目 */static int estimate_obj_num(u32 total_size, u32 obj_size){ /* * 算法思想: * 1.先用 剩余总内存/单个对象大小 得到最多分配内存数目 * 比如抛除slab描述符和着色区剩下4000字节,然后一个obj的大小为28字节 * 那么最大能分配的个数就是 4000/28 = 142.85,即142个 * 2.用总内存量减去第一步计算的obj占得总内存,看剩下的内存是否够管理数组用 * 接着上面,剩余内存量=4000-142*28=24字节,因为管理数组每个元素是u32类型的 * 所以需要总大小=4*142=568字节,上面的肯定不够了 * 3.如果剩下内存的够管理数组用,那么直接返回第一步计算的个数值,这是有可能的, * 试想还是4000字节,然后obj大小是1700字节,这种情况直接返回2就可以了 * 如果剩下的内存不够,那么就需要不断的减小申请的数目重新用1和2的算法去计算合适的数目 */ if(total_size < obj_size) //如果单个对象大小比总内存还大,那么拉倒吧,还计算个毛 { return -1; } u32 num; num = total_size / obj_size; while(total_size - num*obj_size < num*sizeof(u32)) { num--; } return num;}/** * @brief 着色偏移标识,简单模拟下 */static void color_off_increase(){ color_off++; if(color_off > 5) { color_off = 0; }}/** * @brief 产生新的slab * @param mem_size 用来存放这个slab的内存,需要从伙伴系统申请,所以单位是KB * @param obj_size slab中每个对象的大小,单位是字节 * @return NULL:错误 否则:返回指向slab描述符的 */struct slab *create_new_slab(int mem_size, int obj_size, char *slab_name){ /* * 算法理论: * 1.从伙伴系统中分配一页或者几页内存来给slab用,以页(4KB)为单位 * 2.通过对象的大小和分配的总空间,计算出能分配出多少个对象,需要达到下述要求 * a.先预留出slab结构体的空间 * b.再预留出对象管理数组的大小 * c.再预留出“着色区”的大小(16字节的倍数),我们定义最大5种颜色值,也就说着色区最大90字节 * 3.如何标示一个对象是否被使用了呢?这里用obj_manager代表的数据来标示,相当于内核的对象 * 描述符,我们不做的那么复杂,我们规定初始化这个数组为0,数组项为0代表空闲,1为使用中 * 这里的空闲和使用就模拟出了slab算法的核心,因为这些内存全部分配完毕,空闲代表可用而不是 * 未分配,如果一个程序释放一个对象,只是把这个对象对应的内存标记为可用,而不是重新为这个 * 对象分配内存 * 4.着色区我们应该怎么模拟?用全局变量color_off,然后每分配一个slab就把这个值+1,在0-5 * 之间循环 */ int ret; u32 total_size, addr, obj_num; struct slab *cur_slab; ret = mem_alloc(mem_size, &addr); if(ret == -1) { printf("mem alloc err\n"); return NULL; } total_size = mem_size*1024; //单位是KB cur_slab = (struct slab*)addr; //先把slab描述符放到这个空间 addr += sizeof(struct slab); //为描述符留出空间 cur_slab->obj_manager_arr = addr; //把管理数组指向这个位置 //除了slab描述符和着色区还剩下空间大小? total_size = total_size - sizeof(struct slab) - COLOR_SIZE * color_off; obj_num = estimate_obj_num(total_size, obj_size); if(obj_num == -1) { printf("obj_size larger than total_size\n"); return NULL; } cur_slab->obj_num = obj_num; cur_slab->free_num = obj_num; //初始化时候空闲个数就是总数 //如何来表示对象是否已经被使用了 memset(cur_slab->obj_manager_arr, 0, obj_num); addr += obj_num * sizeof(u32); //为对象管理数组留出空间 addr += COLOR_SIZE * color_off; //为着色区留出空间 cur_slab->s_mem = addr; //ok,这个地址就是第一个对象的偏移值了 cur_slab->obj_size = obj_size; list_add(&cur_slab->list, &slab_head); //把新的slab连接到维护链表中去 strncpy(cur_slab->name, slab_name, 20); //最多20字符,防止越界 color_off_increase(); printf("add slab %s to list\n", cur_slab->name); return cur_slab;}/** * @brief 把所有的slab打印出来看看 */void list_all_slab(){ struct list_head *pos; struct slab *tmp; list_for_each(pos, &slab_head) { tmp = list_entry(pos, struct slab, list); printf("%s ", tmp->name); }}/** * @brief 从管理数组中查找第一个为使用的对象下标 * @param cur_slab 被分配的slab * @return -1:无空闲的了 否则返回正确的下标 */static int find_free_obj_num(struct slab *cur_slab){ int i; for(i=0; i<cur_slab->obj_num; i++) { if(cur_slab->obj_manager_arr[i] == 0) { return i; } } return -1; //全部被用了}/** * @brief 从slab管理器中分配对象,这个分配就不是内存分配了,内存已经全部分配 * 完毕了,只要把地址返回就可以了,这样内核就大大加快了常用结构体的分配速度 * 这也就是slab子系统的核心部分 * @param cur_slab * @return NULL 分配失败 否则返回对象的地址 */void *malloc_obj(struct slab *cur_slab){ if(cur_slab == NULL) //如果slab管理器是空的,肯定出错了 { printf("serious error has occured\n"); return NULL; } printf("we will alloc obj from %s slab\n", cur_slab->name); void *obj_addr; int free_num = find_free_obj_num(cur_slab); if(free_num == -1) { printf("%s slab has no free obj\n"); //在内核中,如果slab中没有空闲对象了,应该会重新权衡是否分配新的slab管理器 //这里我们就不再去模拟这块功能了 return -1; } cur_slab->obj_manager_arr[free_num] = 1; //先把这个对象内存标记为占用 cur_slab->free_num--; //返回空闲对象的地址:slab第一个对象的地址+对象大小*查找到空闲对象的下标 obj_addr = cur_slab->s_mem + cur_slab->obj_size * free_num; return obj_addr;}/** * @brief 从slab管理器中把释放的对象标记为未占用,但不是释放内存 * @param cur_slab 当前对象的slab管理器 * @param obj_addr 要释放的对象地址 * 思想:用 对象的地址和slab中第一个对象的首地址 的绝对值(因为不知道当前编译器是按递增还是 * 递减安排对象地址)除以对象的大小即可得到对象的下标值 * 这个地方之所以用指针的指针是因为我们还要把对象的指针指向NULL,只传递指针做不到 * @return -1:错误 0:成功 */int free_obj(struct slab *cur_slab, void **obj_addr){ u32 d_value, index; d_value = abs(cur_slab->s_mem - *obj_addr); if(d_value % cur_slab->obj_size) { printf("addr err\n"); return -1; } index = d_value / cur_slab->obj_size; //剩下的只需要把占用位清零即可了 cur_slab->obj_manager_arr[index] = 0; cur_slab->free_num++; *obj_addr = NULL; //对象指针指向NULL,保证不再使用 return 0;}/** * @brief 列出当前slab管理器中对象占用情况,调试辅助 */void list_slab_occupy_condition(struct slab *cur_slab){ int i; printf("%s slab free obj num = %u\n", cur_slab->name, cur_slab->free_num); printf("%s slab occupy_condition:\n", cur_slab->name); for(i=0; i<cur_slab->obj_num; i++) { printf("%d ", cur_slab->obj_manager_arr[i]); } printf("\n");}
fork.h,进程克隆模拟的头文件,模拟进程描述符的数据结构:
#ifndef FORK_H#define FORK_H#include "slab.h"/* * 假设这个就是进程创建的文件,凡是创建进程都要从本文件获取新进程号*///假设这就是进程描述符struct task_struct { int pid; int ppid; long state; unsigned int flags; /* per process flags, defined below */ unsigned int ptrace; struct list_head list;};int fork_init();int pseudo_fork(int parent_pid);void kill_pid(int pid);void list_task_occupy_condition();#endif // FORK_H
fork.c,使用上面slab算法,模拟fork过程的内存使用:
#include "fork.h"static struct slab *task_slab; //这个指针是唯一的,而且被锁定到了这个文件static struct list_head task_head; //所有运行着的内存链表的头部/** * @brief 为fork操作初始化,创建 task_struct 的高速缓存 */int fork_init(){ printf("fork init. \n"); INIT_LIST_HEAD(&task_head); //创建高速缓存 task_slab = create_new_slab(4, sizeof(struct task_struct), "task_struct"); if(task_slab != NULL) { printf("task_struct alloc success\n"); } return 0;}/** * @brief 模拟创建一个新进程内存分配过程 * @param parent_pid 父进程 * @return -1 创建失败 否则返回子进程的进程id */int pseudo_fork(int parent_pid){ if(parent_pid) //判断下也没有这个父进程 { //假设没问题 } //子进程需要克隆父进程的内存信息,自然需要一块新的内存 //这时候就从task_struct的slab高速缓存中拿出一个obj使用即可 struct task_struct *new_task = (struct task_struct *)malloc_obj(task_slab); new_task->pid = parent_pid+rand(); //这里就随机产生个整数代表子进程id了,这不是我们的重点 new_task->ppid = parent_pid; //父进程id //.... 其它变量赋值 list_add(&new_task->list, &task_head); return new_task->pid; //返回子进程id}/** * @brief 假设用户杀死一个进程,自然要释放其内存,这时候也要调用slab的对象销毁函数 * @param pid 要杀死的进程id */void kill_pid(int pid){ struct list_head *pos; struct task_struct *tmp; list_for_each(pos, &task_head) { tmp = list_entry(pos, struct task_struct, list); if(tmp->pid == pid) { list_del(&tmp->list); free_obj(task_slab, &tmp); //调用slab核心的释放函数 } }}/** * @brief 调试用,列出task_slab的对象占用情况 */void list_task_occupy_condition(){ list_slab_occupy_condition(task_slab);}
最后是main.c:
#include "mem_manage.h"#include "slab.h"#include "fork.h"int main(){ mem_init(); //初始化内存 slab_manager_init(); //初始化slab核心管理器 fork_init(); printf("\nafter fork init \n"); list_task_occupy_condition(); //task_slab的初始状况 int init_pid = 1; //假设已经有一个进程在运行了,进程id是1 printf("\nstart fork \n"); int new_pid1 = pseudo_fork(init_pid); //创建一个新进程 int new_pid2 = pseudo_fork(new_pid1); //以新进程为父进程在创建一个新进程 printf("\nbefore kill \n"); list_task_occupy_condition(); //看下task_slab的占用情况 kill_pid(new_pid2); //杀死一个进程 printf("\nafter kill \n"); list_task_occupy_condition(); //再看下task_slab的占用情况 return 0;}
一些思想代码中注释也写的比较详细,可以参考一下,下面是运行结果:
可以看到由于fork初始化时候创建了一块4kb的高速缓存,分配的地址是0x37560352,正好是伙伴算法分配的4KB链表中最后一块的地址
fork_init以后,这4KB的高速缓存被拆分为 126 个task_struct对象空间,通过打印可以看出都没有被占用。
在执行kill之前,可以看到空闲的对象个数变为124,前两个已经被占用了,而kill一个进程之后,就增加了一个空闲对象。
通常情况下,内存被回收后再访问内存空间就会导致段错误,这块free_obj也已经做了处理,可以把kill_pid的63和64两行颠倒,
先释放slab中对象的内存块,然后在访问,就会有段错误出现,也完成了模拟。
分配和释放不同于传统意义上的分配释放操作,具体思想代码注释已经写得很清楚,这些就是slab的核心东西。
- 用户层模拟slab算法
- Slab 算法
- 内存管理 - 11.6 slab层
- Linux2.6中的Slab层
- Linux2.6中的Slab层
- 内存的slab算法
- slab算法c实现
- nginx slab算法
- linux 内存管理分析之-----SLAB层
- 模拟用户弧线滑动,算法python实现
- linux内存管理slab算法之slab初始化
- slab
- 内存分配-----伙伴算法和slab算法
- glib的slab算法实现学习
- 关于cache line 。内存分配算法 slab
- linux 用户态和内核态 slab内存分配器
- linux内存管理算法 :伙伴算法和slab
- Linux内核内存管理之SLAB内存管理算法(三) --基本数据结构及slab分配
- PHP之Traits
- JS学习30:对象简单、深度克隆(复制、Clone)
- [MacOSX]_[LaunchDaemons]_[Mac OS X 安装Tomcat开机启动服务的方法之一]
- C++中的抽象数据类型
- MongoDB安装及连接
- 用户层模拟slab算法
- 物理内存布局
- rgba()和opacity的使用
- 最小生成树之Prim(普里姆)算法
- java 环境变量配置
- PhotoKit--iOS 开发之照片框架详解
- 中兴阅读,你的移动阅读解决方案专家
- Objective-C 多参数成员函数
- Hadoop作业性能指标及参数调优实例 (三)Hadoop作业性能参数调优方法