来自 服务器&运维 2020-03-02 00:39 的文章
当前位置: 澳门威尼斯人平台 > 服务器&运维 > 正文

Linux中的冷热页机制概述

什么是冷热页?

在Linux Kernel的物理内存管理的Buddy System中,引入了冷热页的概念。冷页表示该空闲页已经不再高速缓存中了(一般是指L2 Cache),热页表示该空闲页仍然在高速缓存中。冷热页是针对于每CPU的,每个zone中,都会针对于所有的CPU初始化一个冷热页的per-cpu-pageset.

Linux信号机制概述

通过前面所有代码的分析和总结,已经把各个部分熟悉了一遍,在此对Linux内核中slab机制做最后的总结。

为什么要有冷热页?

作用有3点:

  • Buddy Allocator在分配order为0的空闲页的时候,如果分配一个热页,那么由于该页已经存在于L2 Cache中了。CPU写访问的时候,不需要先把内存中的内容读到Cache中,然后再写。如果分配一个冷页,说明该页不在L2 Cache中。一般情况下,尽可能用热页,是容易理解的。什么时候用冷页呢?While allocating a physical page frame, there is a bit specifying whether we would like a hot or a cold page (that is, a page likely to be in the CPU cache, or a page not likely to be there). If the page will be used by the CPU, a hot page will be faster. If the page will be used for device DMA the CPU cache would be invalidated anyway, and a cold page does not waste precious cache contents.
    简单翻译一下:当内核分配一个物理页框时,有一些规范来约束我们是分配热页还是冷页。当页框是CPU使用的,则分配热页。当页框是DMA设备使用的,则分配冷页。因为DMA设备不会用到CPU高速缓存,所以没必要使用热页。
  • Buddy System在给某个进程分配某个zone中空闲页的时候,首先需要用自旋锁锁住该zone,然后分配页。这样,如果多个CPU上的进程同时进行分配页,便会竞争。引入了per-cpu-set后,当多个CPU上的进程同时分配页的时候,竞争便不会发生,提高了效率。另外当释放单个页面时,空闲页面首先放回到per-cpu-pageset中,以减少zone中自旋锁的使用。当页面缓存中的页面数量超过阀值时,再将页面放回到伙伴系统中。
  • 使用每CPU冷热页还有一个好处是,能保证某个页一直黏在1个CPU上,这有助于提高Cache的命中率。

 

伙伴系统算法采用页作为基本内存区,这适合于大块内存的请求。对于小内存区的申请,比如说几十或几百个字节,我们用slab机制。

冷热页的数据结构

struct per_cpu_pages {
        int count;              // number of pages in the list
        int high;               // high watermark, emptying needed
        int batch;              // chunk size for buddy add/remove
         // Lists of pages, one per migrate type stored on the pcp-lists
         每个CPU在每个zone上都有MIGRATE_PCPTYPES个冷热页链表(根据迁移类型划分)
         struct list_head lists[MIGRATE_PCPTYPES];
 };

在Linux中,对于UMA的架构,冷热页是在一条链表上进行管理。热页在前,冷页在后。CPU每释放一个order为0的页,如果per-cpu-pageset中的页数少于其指定的阈值,便会将释放的页插入到冷热页链表的开始处。这样,之前插入的热页便会随着其后热页源源不断的插入向后移动,其页由热变冷的几率便大大增加。

还是先看看Linux中用户空间怎么运用的,用户空间编程实例如下:

Slab分配器把对象分组放进高速缓存。每个高速缓存都是同类型对象的一种“储备”。包含高速缓存的主内存区被划分为多个slab,每个slab由一个活多个连续的页组成,这些页中既包含已分配的对象,也包含空闲的对象。

怎样分配冷热页

在分配order为0页的时候(冷热页机制只处理单页分配的情况),先找到合适的zone,然后根据需要的migratetype类型定位冷热页链表(每个zone,对于每个cpu,有3条冷热页链表,对应于:MIGRATE_UNMOVABLE、MIGRATE_RECLAIMABLE、MIGRATE_MOVABLE)。若需要热页,则从链表头取下一页(此页最“热”);若需要冷页,则从链表尾取下一页(此页最“冷”)。

分配函数(关键部分已添加注释):

/*
 * Really, prep_compound_page() should be called from __rmqueue_bulk().  But
 * we cheat by calling it from here, in the order > 0 path.  Saves a branch
 * or two.
 */
static inline
struct page *buffered_rmqueue(struct zone *preferred_zone,
            struct zone *zone, int order, gfp_t gfp_flags,
            int migratetype)
{
    unsigned long flags;
    struct page *page;
    //分配标志是__GFP_COLD才分配冷页
    int cold = !!(gfp_flags & __GFP_COLD);
again:
    if (likely(order == 0)) {
        struct per_cpu_pages *pcp;
        struct list_head *list;
        local_irq_save(flags);
        pcp = &this_cpu_ptr(zone->pageset)->pcp;
        list = &pcp->lists[migratetype];
        if (list_empty(list)) {
          //如果缺少页,则从Buddy System中分配。
            pcp->count += rmqueue_bulk(zone, 0,
                    pcp->batch, list,
                    migratetype, cold);
            if (unlikely(list_empty(list)))
                goto failed;
        }
        if (cold)
        //分配冷页时,从链表尾部分配,list为链表头,list->prev表示链表尾
            page = list_entry(list->prev, struct page, lru);
        else
        //分配热页时,从链表头分配
            page = list_entry(list->next, struct page, lru);
       //分配完一个页框后从冷热页链表中删去该页
        list_del(&page->lru);
        pcp->count--;
    } else {//如果order!=0(页框数>1),则不从冷热页链表中分配
        if (unlikely(gfp_flags & __GFP_NOFAIL)) {
            /*
             * __GFP_NOFAIL is not to be used in new code.
             *
             * All __GFP_NOFAIL callers should be fixed so that they
             * properly detect and handle allocation failures.
             *
             * We most definitely don't want callers attempting to
             * allocate greater than order-1 page units with
             * __GFP_NOFAIL.
             */
            WARN_ON_ONCE(order > 1);
        }
        spin_lock_irqsave(&zone->lock, flags);
        page = __rmqueue(zone, order, migratetype);
        spin_unlock(&zone->lock);
        if (!page)
            goto failed;
        __mod_zone_page_state(zone, NR_FREE_PAGES, -(1 << order));
    }
    __count_zone_vm_events(PGALLOC, zone, 1 << order);
    zone_statistics(preferred_zone, zone, gfp_flags);
    local_irq_restore(flags);
    VM_BUG_ON(bad_range(zone, page));
    if (prep_new_page(page, order, gfp_flags))
        goto again;
    return page;
failed:
    local_irq_restore(flags);
    return NULL;
}

参考:

  • 认识Linux/ARM 中的冷热页
  • Linux源码

#include<signal.h>

相关阅读:http://www.linuxidc.com/Linux/2012-01/51348.htm 与 http://www.linuxidc.com/Linux/2012-01/51349.htm

#include<stdio.h>

1,cache对象管理器

#include<unistd.h>

Cache对象管理器为kmem_cache结构,如下:

/*下面为两个新的信号操作函数*/

[cpp]

void handler(int sig)

  1. /* 
  2.  * struct kmem_cache 
  3.  * 
  4.  * manages a cache. 
  5.  */  
  6.   
  7. struct kmem_cache {  
  8. /* 1) per-cpu data, touched during every alloc/free */  
  9.     struct array_cache *array[NR_CPUS];/*local cache*/  
  10. /* 2) Cache tunables. Protected by cache_chain_mutex */  
  11.     unsigned int batchcount;  
  12.     unsigned int limit;  
  13.     unsigned int shared;  
  14.   
  15.     unsigned int buffer_size;/*slab中对象大小*/  
  16.     u32 reciprocal_buffer_size;/*slab中对象大小的倒数*/  
  17. /* 3) touched by every alloc & free from the backend */  
  18.   
  19.     unsigned int flags;     /* constant flags */  
  20.     unsigned int num;       /* # of objs per slab */  
  21.   
  22. /* 4) cache_grow/shrink */  
  23.     /* order of pgs per slab (2^n) */  
  24.     unsigned int gfporder;  
  25.   
  26.     /* force GFP flags, e.g. GFP_DMA */  
  27.     gfp_t gfpflags;  
  28.   
  29.     size_t colour;/*着色块个数*/ /* cache colouring range */  
  30.     unsigned int colour_off;/* cache的着色块的单位大小 */    /* colour offset */  
  31.     struct kmem_cache *slabp_cache;  
  32.     unsigned int slab_size;/*slab管理区大小,包含slab对象和kmem_bufctl_t数组*/  
  33.     unsigned int dflags;        /* dynamic flags */  
  34.   
  35.     /* constructor func */  
  36.     void (*ctor)(void *obj);  
  37.   
  38. /* 5) cache creation/removal */  
  39.     const char *name;  
  40.     struct list_head next;  
  41.   
  42. /* 6) statistics */  
  43. #ifdef CONFIG_DEBUG_SLAB   
  44.     unsigned long num_active;  
  45.     unsigned long num_allocations;  
  46.     unsigned long high_mark;  
  47.     unsigned long grown;  
  48.     unsigned long reaped;  
  49.     unsigned long errors;  
  50.     unsigned long max_freeable;  
  51.     unsigned long node_allocs;  
  52.     unsigned long node_frees;  
  53.     unsigned long node_overflow;  
  54.     atomic_t allochit;/*cache命中计数,在分配中更新*/  
  55.     atomic_t allocmiss;/*cache未命中计数,在分配中更新*/  
  56.     atomic_t freehit;  
  57.     atomic_t freemiss;  
  58.   
  59.     /* 
  60.      * If debugging is enabled, then the allocator can add additional 
  61.      * fields and/or padding to every object. buffer_size contains the total 
  62.      * object size including these internal fields, the following two 
  63.      * variables contain the offset to the user object and its size. 
  64.      */  
  65.     int obj_offset;  
  66.     int obj_size;  
  67. #endif /* CONFIG_DEBUG_SLAB */   
  68.   
  69.     /* 
  70.      * We put nodelists[] at the end of kmem_cache, because we want to size 
  71.      * this array to nr_node_ids slots instead of MAX_NUMNODES 
  72.      * (see kmem_cache_init()) 
  73.      * We still use [MAX_NUMNODES] and not [1] or [0] because cache_cache 
  74.      * is statically defined, so we reserve the max number of nodes. 
  75.      */  
  76.     struct kmem_list3 *nodelists[MAX_NUMNODES];  
  77.     /* 
  78.      * Do not add fields after nodelists[] 
  79.      */  
  80. };  

{

在初始化的时候我们看到,为cache对象、三链结构、本地cache对象预留了三个cache共分配。其他为通用数据cache,整体结构如下图

         printf("Receive signal :%un",sig);

图片 1

}

其中,kmalloc使用的对象按照大小分属不同的cache,32、64、128、……,每种大小对应两个cache节点,一个用于DMA,一个用于普通分配。通过kmalloc分配的对象叫作通用数据对象。

void sigroutine(int num)

可见通用数据cache是按照大小进行划分的,结构不同的对象,只要大小在同一个级别内,它们就会在同一个general cache中。专用cache指系统为特定结构创建的对象,比如struct file,此类cache中的对象来源于同一个结构。

{

2,slab对象管理器

         switch(num)

Slab结构如下

         {

[cpp]

         case 1:

  1. /* 
  2.  * struct slab 
  3.  * 
  4.  * Manages the objs in a slab. Placed either at the beginning of mem allocated 
  5.  * for a slab, or allocated from an general cache. 
  6.  * Slabs are chained into three list: fully used, partial, fully free slabs. 
  7.  */  
  8. struct slab {  
  9.     struct list_head list;  
  10.     /* 第一个对象的页内偏移,对于内置式slab,colouroff成员不仅包括着色区 
  11.     ,还包括管理对象占用的空间 
  12.     ,外置式slab,colouroff成员只包括着色区。*/  
  13.     unsigned long colouroff;  
  14.     void *s_mem;/* 第一个对象的虚拟地址 *//* including colour offset */  
  15.     unsigned int inuse;/*已分配的对象个数*/ /* num of objs active in slab */  
  16.     kmem_bufctl_t free;/* 第一个空闲对象索引*/  
  17.     unsigned short nodeid;  
  18. };  

                   printf("SIGUP signaln");

关于slab管理对象的整体框架以及slab管理对象与对象、页面之间的联系在前面的slab创建一文中已经总结的很清楚了。

                   break;

3,slab着色

         case 2:

CPU访问内存时使用哪个cache line是通过低地址的若干位确定的,比如cache line大小为32,那么是从bit5开始的若干位。因此相距很远的内存地址,如果这些位的地址相同,还是会被映射到同一个cache line。Slab cache中存放的是相同大小的对象,如果没有着色区,那么同一个cache内,不同slab中具有相同slab内部偏移的对象,其低地址的若干位是相同的,映射到同一个cache line。如图所示。

                   printf("SIGINT signaln");

图片 2

                   break;

 如此一来,访问cache line冲突的对象时,就会出现cache miss,不停的在cache line和内存之间来回切换,与此同时,其他的cache line可能无所事事,严重影响了cache的效率。解决这一问题的方法是通过着色区使对象的slab内偏移各不相同,从而避免cache line冲突。

         case 3:

着色貌似很好的解决了问题,实质不然,当slab数目不多时,着色工作的很好,当slab数目很多时,着色发生了循环,仍然存在cache line冲突的问题。

                   printf("SIGQUIT signaln");

图片 3

                   break;

         default:

                   break;

         }  www.2cto.com  

         return;

}

int main(void)

{

         struct sigaction sa;

         int count;

         sa.sa_handler=handler;

         sigemptyset(&sa.sa_mask);

         sa.sa_flags=0;

         printf("task id is:%dn",getpid());

/*下面四条语句为相应的信号设置新的处理方法*/

         sigaction(SIGTERM,&sa,NULL);

         signal(SIGHUP,sigroutine);

         signal(SIGINT,sigroutine);

         signal(SIGQUIT,sigroutine);

 

         while(1)

         {

                   sigsuspend(&sa.sa_mask);/*阻塞,一直等待信号到达*/

                   printf("loopn");

         }  www.2cto.com  

         return 0;

}

       可见,用户空间调用了很多系统调用来实现信号的编程,为了弄清楚他的内在原理,决定将内核中的实现做一个大致的梳理。为了理清思路,我们由内核中实现信号操作涉及的关键数据结构关系画出下图,我们看到,内核中的数据结构实现较简单,主要分两部分,一部分用于信号操作(即handler),由进程的sighand字段开始;另一部分用于信号的挂起,由进程的signal和pending字段索引。

图片 4  

由关系图,我们大致观其实现原理如下:

1,   进程的所有信号(现为32个)由一个数组task->sighand->action[]保存,数组的下标即为信号的ID,比如SIGQUIT等,每个操作由一个数据结构sigaction实现,该字段的sa_handler即为实现的操作;

2,   进程对挂起的信号有两种队列,一种为所有进程共享的。该队列的每一项为一个sigqueue结构,通过该结构info字段的si_signo等属性可以定位到对应的信号ID。其中sigset_t结构为一个32位整型,用于定位到ID,即类似位图的表示。

我们看几个最基本的操作于内核中的实现。

1,   设置新的action;

系统调用signal用于实现这个功能,当然也可以用sigaction系统调用,

SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)

{

         struct k_sigaction new_sa, old_sa;

         int ret;

 

         new_sa.sa.sa_handler = handler;

         new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;

         sigemptyset(&new_sa.sa.sa_mask);

 

         ret = do_sigaction(sig, &new_sa, &old_sa);

 

         return ret ? ret : (unsignedlong)old_sa.sa.sa_handler;

}

该系统调用分配一个新的action后,调用do_sigaction完成实际工作,最终返回旧的action的handler。  www.2cto.com  

int do_sigaction(int sig,struct k_sigaction *act, struct k_sigaction *oact)

{

         struct task_struct *t = current;

         struct k_sigaction *k;

         sigset_t mask;

……

         k = &t->sighand->action[sig-1];

         spin_lock_irq(¤t->sighand->siglock);

         if (oact)

                   *oact = *k;/*保存旧的action*/

         if (act) {

                   sigdelsetmask(&act->sa.sa_mask,

                                  sigmask(SIGKILL) | sigmask(SIGSTOP));

                   *k = *act;/*设置新的action*/

 

                  /*对两种handler的特殊处理*/

                   if (sig_handler_ignored(sig_handler(t, sig), sig)) {

                            ……

                   }

         }

         spin_unlock_irq(¤t->sighand->siglock);

         return 0;

}

实现很简单,先保存旧的action,用于系统调用返回,然后设置新的action。

2,   发送信号

发送信号的系统调用有很多,最终都会调用__send_signal()函数。

staticint __send_signal(int sig,struct siginfo *info, struct task_struct *t,

                            int group, int from_ancestor_ns)

{

         struct sigpending *pending;

         struct sigqueue *q;

         int override_rlimit;

         ……

         /*找到需要挂起的队列*/

         pending = group ? &t->signal->shared_pending : &t->pending;

         ……

         /*分配队列项结构*/

q = __sigqueue_alloc(t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,

本文由澳门威尼斯人平台发布于服务器&运维,转载请注明出处:Linux中的冷热页机制概述

关键词: