Linux中的内存分配和释放之kmem_cache_alloc()函数分析

来源:互联网 发布:win32 编程入门pdf 编辑:程序博客网 时间:2024/05/17 02:50

  记得上篇文章中我们提到了这个函数,在kmem_cache_create()函数调用它的语句是cachep = (kmem_cache_t *) kmem_cache_alloc(&cache_cache, SLAB_KERNEL);这里的cachep是我们要申请的高速缓存内存的描述结构体(kmem_cache_t),

但是我们也需要高速缓存内存来存放这个结构体。就是cache_cache所描述的高速缓存内存。对于这个结构体什么时候被创建和初始化,在kmem_cache_init()函数中有提到,但是只是其中的一部分。现在我来分析一下这个代码。

  void * kmem_cache_alloc (kmem_cache_t *cachep, int flags)
{
   return __cache_alloc(cachep, flags);
}

static inline void * __cache_alloc (kmem_cache_t *cachep, int flags)
{
   unsigned long save_flags;
   void* objp;
   struct array_cache *ac;

   cache_alloc_debugcheck_before(cachep, flags);//分配前要调试检测。

   local_irq_save(save_flags);//关闭中断同时把处理器的当前状态寄存器cpsr保存在save_flags中。
   ac = ac_data(cachep);//使ac指向cachep->array[smp_processor_id()]
   if (likely(ac->avail)) {//如果avail>0说明当前处理器的高速缓存内存阵列中有内存对象可以分配。
      STATS_INC_ALLOCHIT(cachep);//cachep的allochit成员加1,说明分配命中。
      ac->touched = 1;//表示该高速缓存内存阵列已被使用。
      objp = ac_entry(ac)[--ac->avail];//由于ac是记录着这次struct arrary_cache结构体存放地址,通过ac_entry()后,我们就得

                                           //到下一紧接地址,这个地址可以看做是为本高速缓存内存的内存对象指针存放首地址,这里可以看出,我

                                          //们是从最后一个对象开始分配的。
   } else {//如果现在阵列没有可用的对象用于分配。
      STATS_INC_ALLOCMISS(cachep);//将cachep->allocmiss加1,表示分配失败计数。
      objp = cache_alloc_refill(cachep, flags);//为高速缓存内存空间增加新的内存对象。
   }
   local_irq_restore(save_flags);//IRQ中断使能,同时把save_flags中的内容返回给cpsr。
   objp = cache_alloc_debugcheck_after(cachep, flags, objp, __builtin_return_address(0));//分配后调试检查。
   return objp;//返回内存对象指针。也就是该内存对象的线性地址。
 }

 我们现在来说说cache_alloc_refill()这个函数,它实现如果没有对象可以用来分配时,就使用它来新增新的对象。

 static void* cache_alloc_refill(kmem_cache_t* cachep, int flags)
{
    int batchcount;
    struct kmem_list3 *l3;
    struct array_cache *ac;

    check_irq_off();//检测IRQ中断是否关闭了。
    ac = ac_data(cachep);//使ac指向cachep->array[smp_processor_id](当前cpu对应的预留的高速缓存内存阵列。)
retry:
    batchcount = ac->batchcount;//batchcount是每次批处理变量。
    if (!ac->touched && batchcount > BATCHREFILL_LIMIT) {//如果本缓存是没有使用过的同时批处理还大于最大的限制。

        batchcount = BATCHREFILL_LIMIT;//使批处理被附上最大限制值。
    }

    l3 = list3_data(cachep);//ls指向cachep->list

    BUG_ON(ac->avail > 0);//如果cachep->array[smp_processor_id]->avail大于0说明有内存对象可用,不用再新增内存对象。
    spin_lock(&cachep->spinlock);上锁
    if (l3->shared) {//如果cachep->list.shared不为空,表示共享缓存阵列指针存在,即阵列存在
        struct array_cache *shared_array = l3->shared;//现在用share_array指向这个共享阵列
        if (shared_array->avail) {//如果这个阵列有可用的对象时
          if (batchcount > shared_array->avail)//如果批处理大于可用对象
             batchcount = shared_array->avail;
          shared_array->avail -= batchcount;//从新更新共享阵列现在可用对象数目
          ac->avail = batchcount;//同时更新一级阵列,就是先前没有可用对象的阵列。由于这里批处理,使一级阵列多了内存对象。
          memcpy(ac_entry(ac), &ac_entry(shared_array)[shared_array->avail],
          sizeof(void*)*batchcount);//把存放在共享缓存中最高可用对象开始,把对象的指针赋给一级阵列紧跟在struct array_cache

                                                //结构体后面的存储空间,对象个数为批处理个数。
          shared_array->touched = 1;//这里表示共享内存被使用过了。
          goto alloc_done;
       }

   }
   while (batchcount > 0) {
           struct list_head *entry;
           struct slab *slabp;
  
           entry = l3->slabs_partial.next;//首先先访问部分空闲SLAB链表,使entry指向第一个节点。
           if (entry == &l3->slabs_partial) {//如果部分空间SLAB链表为空
                l3->free_touched = 1;//在访问全部空闲SLAB链表前先做一个标记,表示全部空间SLAB链表被使用过了。
               entry = l3->slabs_free.next;//使entry指向第一个节点。
               if (entry == &l3->slabs_free)
                  goto must_grow;//如果发现全部空闲SLAB链表也为空时,跳转。
           }

           slabp = list_entry(entry, struct slab, list);//至少全部空闲或者是部分空闲SLAB链表有一个不为空
           check_slabp(cachep, slabp);
           check_spinlock_acquired(cachep);
           while (slabp->inuse < cachep->num && batchcount--) {//新增的内存对象不超过高速缓存内存的对象限制,同时批处理不

                                                                                       //为空,每次循环后自动减1。
           kmem_bufctl_t next;
           STATS_INC_ALLOCED(cachep);//成功分配计数器cachep->num_allocations加1.
           STATS_INC_ACTIVE(cachep);//活动对象计数器cachep->num_active加1.
           STATS_SET_HIGH(cachep);//如果cachep->num_active大于cachep->high_mark最高对象数水印值的话,我们就把

                                                    //cachep->num_active赋给cachep->high_mark。

   
          ac_entry(ac)[ac->avail++] = slabp->s_mem + slabp->free*cachep->objsize;//slab->s_sem表示该slab对象存放位

                                                  //置的起始地址,slab->free表示对象的索引号,cachep->objsize表示对象大小。

                                                //slabp->free*cachep->objsize=某个对象的偏移量,最后加上slabp->s_mem就可以得到这个

                                                 //对象的线性地址了,也就是指向这个对象的指针。赋给原来的一级阵列,我们是从已有对象的基础

                                                 //上往后赋值,就是把对象指针存放在原有的对象后面,其实一般avail为0.

          slabp->inuse++;//slab所指对象使用对象计数器加1.
          next = slab_bufctl(slabp)[slabp->free];//slab->free存放着下一个要访问对象的索引号,kmem_bufctl_t与每个对象一一对

                                                             //应并且存放在每个slab结构体的下面,我们用slab_bufctl()函数可以求得kmem_bufctl_t

                                                          //的数组首地址,我们再通过slab->free下一个索引号,找到下一个kmem_bufctl_t的指针

                                                              //这里存放着下一个对象的索引号。
#if DEBUG
         slab_bufctl(slabp)[slabp->free] = BUFCTL_FREE;
#endif
         slabp->free = next;//使free指向下个索引号的线性地址。
         }
         check_slabp(cachep, slabp);//用于调试用的。

  
         list_del(&slabp->list);//把slabp从原来的链表上删除,删除后不管是slab_free还是slab_partial链表都会指向下一个list节点。
         if (slabp->free == BUFCTL_END)
             list_add(&slabp->list, &l3->slabs_full);//如果索引号无效的话,说明没有对象可以再分配了,这样就把slabp的list就连接到

                                                           //slabs_full的表头,表示这个slabp没有空闲对象了。
        else
            list_add(&slabp->list, &l3->slabs_partial);//如果还有部分空闲对象,我们就把它的list挂在slabs_partical链表头。
 }//上面的while主要是当前没有共享的缓存阵列的话,我们就通过cache的lists内的部分空闲或者是全部空闲链表,一直循环到新增的对象

        //已经达到了batchcount的数量,即一次批处理的数量。我们就可以结束循环,但是如果一上来发现我们的两个链表全为空链表,

   //我们就直接跳转到must_grow的跳转处。

must_grow:
 l3->free_objects -= ac->avail;//不管在两次新增对象是否成功的情况下,都要跟新free_objects:这个数目只是那3个链表中的空闲对

                                //象总数(slabs_free,slabs_partical,slabs_full),就是不包括lists.share->avail对象的数目。

alloc_done:
 spin_unlock(&cachep->spinlock);//将刚才的锁解开。

 if (unlikely(!ac->avail)) {//如果新增对象成功,则不需要做if里面的语句啦。
    int x;
    x = cache_grow(cachep, flags, -1);//调用cahce_grow()函数继续新增对象,如果返回为1说明成功,如果返回为0说明失败
  
  
    ac = ac_data(cachep);//从新获得当前cpu对应的预留的高速缓存内存的指针,对于这里我老是觉得没有必要。
    if (!x && ac->avail == 0) 
        return NULL;//如果调用失败和可用对象还是为0的话,我们只好返回空指针。

    if (!ac->avail)  
       goto retry;//如果只是当前cpu对应的预留高速缓存内存没有对象的话,我们还跳转,继续从试。
 }
 ac->touched = 1;//声明这个高速缓存内存已经被使用了
 return ac_entry(ac)[--ac->avail];//返回最高位置的可用对象的指针,这时,这个指针已经被存放在array[smp_processor_id]对应

                                   //结构体struct array_cache存放位置后面紧跟的指针数组里面了。
}

  好了,到这里我们看到了如果发现当前cpu对应的预留高速缓存内存中没有对象可用时,我们有以下几个方案:

  1.我们会看看二级缓存(共享缓存cachep->lists.share)是否存在,如果有,我们在这里来分配给一级缓存(ac)。

  2.如果发现二级缓存内没有可用对象或者是根本就不存在二级缓存的话,我们就看看cachep->lists内的2个链表。

  3.如果发现这些链表都是空的话,我们只好调用cache_grow()函数。如果链表不为空,我们可以再部分或是全部空闲链表的节点找到我们需要的slab结构,随之可以访问到slab->free(下次分配对象索引号),我们有了这个索引号就可以得到对象指针既是地址,赋给一级缓存

  说到这里,我们必须要说说最后这个可以分配对象的函数cache_grow(),如果在这个函数还是新增不了对象的话,我们就返回空指针。现在,我们来看看它的具体代码吧。

static int cache_grow (kmem_cache_t * cachep, int flags, int nodeid)
{
   struct slab *slabp;
   void  *objp;
   size_t   offset;
   int   local_flags;
   unsigned long  ctor_flags;

 
   if (flags & ~(SLAB_DMA|SLAB_LEVEL_MASK|SLAB_NO_GROW))
       BUG();//如果设置了这些标志以外的,系统崩溃。SLAB_DMA=GFP_DMA=_GFP_DMA,SLAB_LEVEL_MASK=GFP_LEVEL_MA

                //SK(基本上它包含了大部分的_GFP_***),SLAB_NO_GROW=_GFP_NO_GROW
   if (flags & SLAB_NO_GROW)
       return 0;//表示不能新增slab,直接返回0。

   ctor_flags = SLAB_CTOR_CONSTRUCTOR;//设置构造函数的标志
   local_flags = (flags & SLAB_LEVEL_MASK);//取出控制标志,用local_flags作为中间变量。
   if (!(local_flags & __GFP_WAIT))
      ctor_flags |= SLAB_CTOR_ATOMIC;//如果设置等待标志,我们就设置构造函数为原子操作。

 
   check_irq_off();//检查IRQ中断关闭没有。
   spin_lock(&cachep->spinlock);//上锁
   offset = cachep->colour_next;//colour_next=下一个色块号
   cachep->colour_next++;//色块号自增
   if (cachep->colour_next >= cachep->colour)
      cachep->colour_next = 0;//如果色块号大于或者等于最大色块号,我们就归零。
   offset *= cachep->colour_off;这个求得的是按cache_line对齐的slab管理数据或者内存对象存放的起始地址。

   spin_unlock(&cachep->spinlock);//解锁

   if (local_flags & __GFP_WAIT)
       local_irq_enable();//如果设置了等待,使能IRQ中断。
   kmem_flagcheck(cachep, flags);//检查



   if (!(objp = kmem_getpages(cachep, flags, nodeid)))
         goto failed;//向伙伴系统申请内存,nodeid表示要求在哪个节点号申请内存,成功返回线性地址给objp。失败则返回0,并且跳转

 
   if (!(slabp = alloc_slabmgmt(cachep, objp, offset, local_flags)))
         goto opps1;//对于这个函数我先仔细分析,目的是加深理解色块的概念。色块,是系统不会调用的内存,它的存在就是为了使两个

     //起始地址对其,按cache_line对齐。一个是slab管理数据空间的起始地址,一个就是内存对象存放的起始地址。这个函数一上来就

     //判断这个两个空间是不是放在一起,如果不是放在一起,slab管理数据空间是无需对齐的。我们只需要另外申请一个空间给它存放就

     //可以了。但是如果是放在一块的话,我们就需要两者起始地址对齐。slabp = objp+colour_off;和slabp->s_mem =

     //objp+colour_off;都是每次调用这个函数要对这两个地址调整的核心语句,前者是slab管理数据空间,后者是对象存放空间

    //现在关键就在colour_off,这个变量更新在colour_off += cachep->slab_size;如果我们的slab_size是按cache_line对齐的话,

    //我们的两个起始地址必定对齐,很幸运,我们的slab_size是按这样对齐的。

    set_slab_attr(cachep, slabp, objp);//要求每页的struct page结构体的lur.prev指向struct slab结构,要求lur.next指向struct

                                                           //kmem_cache_t结构。这样我们就把这页交给slab管理了。

    cache_init_objs(cachep, slabp, ctor_flags);//这里对cachep和slabp设置,slabp->free设置好,使他总是存放着下一个要被分

                                                                       //配的对象索引号。

    if (local_flags & __GFP_WAIT)
         local_irq_disable();//如果设置等待,我们要禁止irq中断。
    check_irq_off();
    spin_lock(&cachep->spinlock);

 
    list_add_tail(&slabp->list, &(list3_data(cachep)->slabs_free));//把新增的slab挂在slabs_free链表头。
    STATS_INC_GROWN(cachep);//cachep->grown加1,表示slab新增了。
    list3_data(cachep)->free_objects += cachep->num;//同时跟新cachep->lists->free_objects,说明空闲对象增加了。
    spin_unlock(&cachep->spinlock);
    return 1;
opps1:
    kmem_freepages(cachep, objp);//发现没有申请到slab,则释放刚才的申请的内存。
failed:
    if (local_flags & __GFP_WAIT)
       local_irq_disable();//如果设置等待,禁止IRQ中断。
    return 0;
}
关于向伙伴系统怎么申请内存和释放内存,我想在后面的文章里讲解。

 

原创粉丝点击