用户层模拟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的核心东西。

0 0
原创粉丝点击