内核代码阅读(9) - 内核缓冲区的管理slab上

简介: 内核缓冲区的管理slab上

内核缓冲区的管理

关于slab

1) 内存分配的最小单位是page。
2) slab是为了加速内核中常用结构体的分配,对象池。
3) 每种slab有 constructor 和 destrutor。所以从slab种获取的对象可以直接使用。
4) 对象分为:
   大对象:结构体的大小比一个page页还要大。
   小对象:一个page页可以装下n个结构体。
5) 一个slab可能1,2,4,8。。。最多32个page构成。
6) 每个slab的前端是 slab_t 结构体,同一类型对象的slab通过 slab_t 链在一起。
7) 每个slab都有对象区,是对象的数组。
8) 每个slab上还有一个对象连接数组,用来把对象数组链接起来。
9) 每个slab头都有一个着色区,用来调整slab,使得和 cache line 对齐。

slab_t结构体

typedef struct slab_s {
        struct list_head        list;
        unsigned long                colouroff;
        void                        *s_mem;
        unsigned int                inuse;
        kmem_bufctl_t                free;
    } slab_t;
1) slab通过list链入3个队列中的一个:
   fully-used, partital, fully-free
2) 管理的是一个slab。

cache_cache

typedef struct kmem_cache_s kmem_cache_t;
    static kmem_cache_t cache_cache = {
        slabs:                LIST_HEAD_INIT(cache_cache.slabs),
        firstnotfull:        &cache_cache.slabs,
        objsize:        sizeof(kmem_cache_t),
        flags:                SLAB_NO_REAP,
        spinlock:        SPIN_LOCK_UNLOCKED,
        colour_off:        L1_CACHE_BYTES,
        name:                "kmem_cache",
    };
1) kmem_cache_t:
   同一种对象被slab_t组织成list,而链表的头部就是 kmem_cache_t
2) 所有的 kmem_cache_t 也是在slab中管理,而这种特殊的slab也是被slab_t链起来。
3) 特殊的slab的链表头就是 cache_cache。

slab_cache

并不是所有的数据结构都从cache_cache(专用的缓冲队列)中分配,不常用的构造开销也不大的从通用缓冲区队列申请,即为 slab_cache。
slab_cache 管理的是不同大小的对象32, 64, 最大到128K。

内核内存分配

1) 在专用缓冲区(cache_cache)中分配对象,需要指定一个队列 kmem_cache_t。
   void *kmem_cache_alloc(kmem_cache_t *cachep, int flags);
   void *kmem_cache_free(kmem_cache_t *cachep, void *objp);
2) 在通用缓冲区分配的函数,需要指定大小:
   void *kmalloc(size_t size, int flags);
   void kfree(const void *objp);
3) 如果要分配的数据结构大小接近一个页面,可以直接申请一个page
   struct page * alloc_pages(int gfp_mask, unsigned long order);
4) 内核虚存的分配
   void* vmalloc(unsigned long size);
   void vfree(void *addr);
   从内核的虚存空间(3G以上)分配一块虚存和相应的物理内存,这块内存kswapd不可见。所以不能被swap出去。

专用缓冲区 cache_cache 队列的建立

数据结构负责初始化自己的队列

void __init skb_init(void)
    {
        int i;
        skbuff_head_cache = kmem_cache_create("skbuff_head_cache",
                                              sizeof(struct sk_buff),
                                              0,
                                              SLAB_HWCACHE_ALIGN,
                                              skb_headerinit, NULL);
        if (!skbuff_head_cache)
                panic("cannot create skbuff cache");
        for (i=0; i<NR_CPUS; i++)
                skb_queue_head_init(&skb_head_pool[i].list);
    }
1) 为网络驱动创建 sk_buff 数据结构的专用缓冲区队列。
2) 名字是 skbuff_head_cache。
3) SLAB_HWCACHE_ALIGN
   要求对齐 cache_line
4) 构造函数是 skb_headerinit。
5) static kmem_cache_t *skbuff_head_cache;
   kmem_cache_create 的返回值是skbuff的slab的队列头,存放放在static变量里。
   使用 kmem_cache_alloc 分配skubuff的时候要传递这个队列头。

kmem_cache_create

1) 获取一个队列头
    cachep = (kmem_cache_t *) kmem_cache_alloc(&cache_cache, SLAB_KERNEL);
2) 初始化 kmem_cache_t 中的成员
   color:头部的着色区
   size:一个对象的大小
   num:一个slab中可以装下几个obj
   flags:决定 slab_t 是放在slab上还是独立存放。

专用缓冲区 kmem_cache_alloc 缓冲区的分配

struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)
    {
        struct sk_buff *skb;
        skb = kmem_cache_alloc(skbuff_head_cache, gfp_mask);
        return skb;
    }
1) 分配的时候要把slab的队列头结构 skbuff_head_cache 传递给 kmem_cache_alloc。
static inline void * __kmem_cache_alloc (kmem_cache_t *cachep, int flags)
    {
        unsigned long save_flags;
        void* objp;
        kmem_cache_alloc_head(cachep, flags);
    try_again:
        local_irq_save(save_flags);
        objp = kmem_cache_alloc_one(cachep);
        local_irq_restore(save_flags);
        return objp;
    alloc_new_slab:
        local_irq_restore(save_flags);
        if (kmem_cache_grow(cachep, flags))
                goto try_again;
        return NULL;
    }
1) kmem_cache_alloc_head
   检查当前的cachep是否支持DMA
2) kmem_cache_alloc_one
   从cachep上分配一个object
3) 如果返回值为空,说明cachep的slab队列没有slab可供分配,则空过 kmem_cache_grow 增长cachep队列的大小。

kmem_cache_alloc_one

#define kmem_cache_alloc_one(cachep)                                \
    ({                                                                \
        slab_t        *slabp;                                        \
                                                                \
        /* Get slab alloc is to come from. */                        \
        {                                                        \
                struct list_head* p = cachep->firstnotfull;        \
                if (p == &cachep->slabs)                        \
                        goto alloc_new_slab;                        \
                slabp = list_entry(p,slab_t, list);        \
        }                                                        \
        kmem_cache_alloc_one_tail(cachep, slabp);                \
    })
1) 这是一段宏。
2) cachep->firstnotfull
   从cachep中找到第一个空闲的slab的list
3) slabp = list_entry(p,slab_t, list)
   通过list找到slabp的开头。
4) if (p == &cachep->slabs)
   如果cachep中没有空闲的slab了,则goto到 alloc_new_slab,进行分配。
5) 找到了一个slabp之后,kmem_cache_alloc_one_tail进行初始化。
static inline void * kmem_cache_alloc_one_tail (kmem_cache_t *cachep,
                                                         slab_t *slabp)
    {
        void *objp;
        STATS_INC_ALLOCED(cachep);
        STATS_INC_ACTIVE(cachep);
        STATS_SET_HIGH(cachep);
        slabp->inuse++;
        objp = slabp->s_mem + slabp->free*cachep->objsize;
        slabp->free=slab_bufctl(slabp)[slabp->free];
        if (slabp->free == BUFCTL_END)
                cachep->firstnotfull = slabp->list.next;
        return objp;
    }
1) 首先跟新cachep中的统计
2) slabp->inuse++;
   更新slabp中的obj的使用计数。
3) objp = slabp->s_mem + slabp->free*cachep->objsize;
   计算出obj的起始地址。
   s_mem: 是slab中obj的起始偏移量。
   free: 是链接数组的头部,第一个可以使用的obj的下标。
   cachep->objsize: 一个obj的大小
4) slabp->free=slab_bufctl(slabp)[slabp->free]
   free的下标指向下一个。
   slab_bufctl: 一个记录obj下标的数组。

kmem_cache_grow

static int kmem_cache_grow (kmem_cache_t * cachep, int flags)
    {
        slab_t        *slabp;
        struct page        *page;
        void                *objp;
        size_t                 offset;
        unsigned int         i, local_flags;
        unsigned long         ctor_flags;
        unsigned long         save_flags;
        offset = cachep->colour_next;
        cachep->colour_next++;
        if (cachep->colour_next >= cachep->colour)
                cachep->colour_next = 0;
        offset *= cachep->colour_off;
        cachep->dflags |= DFLGS_GROWN;
        cachep->growing++;
        objp = kmem_getpages(cachep, flags);
        slabp = kmem_cache_slabmgmt(cachep, objp, offset, local_flags);
        i = 1 << cachep->gfporder;
        page = virt_to_page(objp);
        do {
                SET_PAGE_CACHE(page, cachep);
                SET_PAGE_SLAB(page, slabp);
                PageSetSlab(page);
                page++;
        } while (--i);
        kmem_cache_init_objs(cachep, slabp, ctor_flags);
        spin_lock_irqsave(&cachep->spinlock, save_flags);
        cachep->growing--;
        list_add_tail(&slabp->list,&cachep->slabs);
        if (cachep->firstnotfull == &cachep->slabs)
                cachep->firstnotfull = &slabp->list;
        STATS_INC_GROWN(cachep);
        cachep->failures = 0;
        return 1;
    }
1) offset = cachep->colour_next;
   计算着色区的便移量。目的是使各个slab中的obj在cache line中错开位子。
   TODO: 研究一下 cache line
2) objp = kmem_getpages(cachep, flags);
   申请物理页,返回物理页的首地址。
   addr = (void*) __get_free_pages(flags, cachep->gfporder);
3) slabp = kmem_cache_slabmgmt(cachep, objp, offset, local_flags);
   初始化slabp中的slab_t结构。
4) i = 1 << cachep->gfporder;
   page = virt_to_page(objp);
   do {
           SET_PAGE_CACHE(page, cachep);
           SET_PAGE_SLAB(page, slabp);
           PageSetSlab(page);
           page++;
   } while (--i);
   这个while循环,把本次分配的page中的list的
       list.next = cachep;
       list.prev = slabp
   这个时候page已经从buddy的链表中脱离出来,list可以挪做它用了。
   记录一个page所属的cachep和slabp,为了在释放一个obj的时候,
   根据obj所在的物理页找到mem_map中的page结构体,然后根据list.prev, list.next找到cachep和slabp。
5) kmem_cache_init_objs(cachep, slabp, ctor_flags)
   初始化slab中的obj和ctl数组。
6) list_add_tail(&slabp->list,&cachep->slabs)
   把新的slabp挂在cachep后面,激活。
7) cachep->firstnotfull = &slabp->list;
   更新第一个空闲的slab
相关文章
|
2月前
|
存储 缓存 安全
深入理解内存映射:mmap映射的背后原理以及和共享内存的差异
深入理解内存映射:mmap映射的背后原理以及和共享内存的差异
166 0
|
6月前
|
存储
【OS Pintos】用户程序是如何工作的 | Pintos 运行原理 | 虚拟内存 | 页函数 | 系统调用
【OS Pintos】用户程序是如何工作的 | Pintos 运行原理 | 虚拟内存 | 页函数 | 系统调用
171 0
|
2月前
|
存储 Linux 编译器
Linux用户空间和内核空间所有15种内存分配方法
Linux用户空间和内核空间所有15种内存分配方法
81 1
|
6月前
|
存储 缓存 Linux
系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
虚拟内存的主要作用是提供更大的地址空间,使得每个进程都可以拥有大量的虚拟内存,而不受物理内存大小的限制。此外,虚拟内存还可以提供内存保护和共享的机制,保护每个进程的内存空间不被其他进程非法访问,并允许多个进程共享同一份物理内存数据,提高了系统的资源利用率。虚拟内存的实现方式有分段和分页两种,其中分页机制更为常用和灵活。分页机制将虚拟内存划分为固定大小的页,将每个进程的虚拟地址空间映射到物理内存的页框中。为了减少页表的大小和访问时间,采用了多级页表的方式,将大的页表划分为多个小的页表,只加载需要的页表项,节约了内存空间。
212 0
系统内存管理:虚拟内存、内存分段与分页、页表缓存TLB以及Linux内存管理
|
11月前
|
存储 索引 Windows
驱动开发:内核物理内存寻址读写
在某些时候我们需要读写的进程可能存在虚拟内存保护机制,在该机制下用户的`CR3`以及`MDL`读写将直接失效,从而导致无法读取到正确的数据,本章我们将继续研究如何实现物理级别的寻址读写。首先,驱动中的物理页读写是指在驱动中直接读写物理内存页(而不是虚拟内存页)。这种方式的优点是它能够更快地访问内存,因为它避免了虚拟内存管理的开销,通过直接读写物理内存,驱动程序可以绕过虚拟内存的保护机制,获得对系统中内存的更高级别的访问权限。
6978 1
|
11月前
|
存储
驱动开发:内核读写内存多级偏移
让我们继续在`《内核读写内存浮点数》`的基础之上做一个简单的延申,如何实现多级偏移读写,其实很简单,读写函数无需改变,只是在读写之前提前做好计算工作,以此来得到一个内存偏移值,并通过调用内存写入原函数实现写出数据的目的。以读取偏移内存为例,如下代码同样来源于本人的`LyMemory`读写驱动项目,其中核心函数为`WIN10_ReadDeviationIntMemory()`该函数的主要作用是通过用户传入的基地址与偏移值,动态计算出当前的动态地址。
85 0
|
12月前
驱动开发:内核解析内存四级页表
当今操作系统普遍采用64位架构,CPU最大寻址能力虽然达到了64位,但其实仅仅只是用到了48位进行寻址,其内存管理采用了`9-9-9-9-12`的分页模式,`9-9-9-9-12`分页表示物理地址拥有四级页表,微软将这四级依次命名为PXE、PPE、PDE、PTE这四项。关于内存管理和分页模式,不同的操作系统和体系结构可能会有略微不同的实现方式。9-9-9-9-12的分页模式是一种常见的分页方案,其中物理地址被分成四级页表:PXE(Page Directory Pointer Table Entry)、PPE(Page Directory Entry)、PDE(Page Table Entry)
10791 0
|
存储 API
驱动开发:内核R3与R0内存映射拷贝
在上一篇博文`《驱动开发:内核通过PEB得到进程参数》`中我们通过使用`KeStackAttachProcess`附加进程的方式得到了该进程的PEB结构信息,本篇文章同样需要使用进程附加功能,但这次我们将实现一个更加有趣的功能,在某些情况下应用层与内核层需要共享一片内存区域通过这片区域可打通内核与应用层的隔离,此类功能的实现依附于MDL内存映射机制实现。
296 0
驱动开发:内核R3与R0内存映射拷贝
驱动开发:内核MDL读写进程内存
MDL内存读写是最常用的一种读写模式,通常需要附加到指定进程空间内然后调用内存拷贝得到对端内存中的数据,在调用结束后再将其空间释放掉,通过这种方式实现内存读写操作,此种模式的读写操作也是最推荐使用的相比于CR3切换来说,此方式更稳定并不会受寄存器的影响。
617 0
驱动开发:内核MDL读写进程内存
|
存储 负载均衡 算法
【Linux进程概念——下】验证进程地址空间的基本排布 | 理解进程地址空间 | 进程地址空间如何映射至物理内存(页表的引出) | 为什么要存在进程地址空间 | Linux2.6内核进程调度队列
本文中会介绍很多结构化的知识,我们会通过很多例子里的角色和场景来对一个概念进行定位和阐述,让大家有一个宏观的认识,之后再循序渐进的填充细节,如果你一上来就玩页表怎么映射,那么你可能连页表存在的价值是什么都不知道,最后也只是竹篮打水。
197 0
【Linux进程概念——下】验证进程地址空间的基本排布 | 理解进程地址空间 | 进程地址空间如何映射至物理内存(页表的引出) | 为什么要存在进程地址空间 | Linux2.6内核进程调度队列