用户层模拟slab算法

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 16
struct 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的核心东西。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浓咖啡jy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值