linux内存管理源码分析 - 概述

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/

  最近在学习内核模块的框架,这里做个总结,知识太多了。

分段和分页

  先看一幅图

  也就是我们实际中编码时遇到的内存地址并不是对应于实际内存上的地址,我们编码中使用的地址是一个逻辑地址,会通过分段和分页这两个机制把它转为物理地址。而由于linux使用的分段机制有限,可以认为,linux下的逻辑地址=线性地址。也就是,我们编码使用的是线性地址,之后只需要经过一个分页机制就可以把这个地址转为物理地址了。所以我们更重要的可能是去说明一下linux的分页模型。

  系统会将整个物理内存分为多个页框,每个页框大小一般是4K(硬件允许情况下也可设置为4M),也就是如果我们有1GB的物理内存,系统就会将这个物理内存分为262144个页框。当我们提供一个线性地址时,系统就会通过分页机制将这个线性地址转换为对应于某个物理页中的某个内存地址。下图是linux的分页模型

  linux采用四级分页模型,这四种页表是:页全局目录(PGD)、页上级目录(PUD)、页中间目录(PMD)、页表(PTE)。这里的所有页全局目录、页上级目录、页中间目录、页表,它们的大小都是一个页。linux下各个硬件上并不一定都是使用四级目录的,当使用于没有启动物理地址扩展的32位系统上时,只使用二级页表,linux会把页上级目录和页中间目录置空。而在启用了物理地址扩展的32位系统上时,linux使用的是三级页表,页上级目录被置空。而在64位系统上,linux根据硬件的情况会选择三级页表或者四级页表。这个整个由线性地址转换到物理地址的过程,是由CPU自动进行的。

  每个进程都有它自己的页全局目录,当进程运行时,系统会将该进程的页全局目录基地址保存到cr3寄存器中;而当进程被换出时,会将这个cr3保存的页全局目录地址保存到进程描述符中。之后我们还会介绍一个cr2寄存器,用于缺页异常处理的。

  因为说到每个进程都有它自己的页全局目录,如果有100个进程,内存中就要保存100个进程的整个页表集,看起来会耗费相当多的内存。实际上,只有进程使用到的情况下系统才会分配给进程一条路径,比如我们要求访问一个线性地址,但是这个地址可能对应的页上级目录、页中间目录、页表和页都不存在的,这时系统会产生一个缺页异常,在缺页异常处理中再给进程的这个线性地址分配页上级目录、页中间目录、页表和页所需的物理页框。

地址空间

  一个线性地址经过分页机制转为一个对应的物理地址,我们称之为映射,比如我们的一个线性地址0x00000001经过分页机制处理后,对应的物理地址是0xffffff01。

  在linux系统中分两个地址空间,一个是进程地址空间,一个是内核地址空间。对于每个进程来说,他们都有自己的大小为3G的进程地址空间,这些进程地址空间是相互隔离的,也就是进程A的0x00000001线性地址和进程B的0x00000001线性地址并不是同一个地址,进程A也不能通过自己的进程空间直接访问进程B的进程地址空间。而当线性地址大于3G时(也就是0xC0000000),这里的线性地址属于内核空间,内核地址空间的大小为1G,地址从0xC0000000到0xFFFFFFFF。在内核地址空间中,内核会把前896MB的线性地址直接与物理地址的前896MB进行映射,也就是说,内核地址空间的线性地址0xC0000001所对应的物理地址为0x00000001,它们之间相差一个0xC0000000。

  linux内核会将物理内存分为3个管理区,分别是:

  • ZONE_DMA:包含0MB~16MB之间的内存页框,可以由老式基于ISA的设备通过DMA使用,直接映射到内核的地址空间。
  • ZONE_NORMAL:包含16MB~896MB之间的内存页框,常规页框,直接映射到内核的地址空间。
  • ZONE_HIGHMEM:包含896MB以上的内存页框,不进行直接映射,可以通过永久映射和临时映射进行这部分内存页框的访问。

 整个结构如下图

  对于ZONE_DMA和ZONE_NORMAL这两个管理区,内核地址都是进行直接映射,只有ZONE_HIGHMEM管理区系统在默认情况下是不进行直接映射的,只有在需要使用的时候进行映射(临时映射或者永久映射)。

结点和管理区描述符

  为了用于NUMA架构,使用了node用来描述一个地方的内存。NUMA大致就是使众多服务器像单一系统一样地运行,这样每台服务器都有自己的内存,每台服务器的内存就是一个node。对于我们PC来说,一台PC就是一个node。node用struct pglist_data结构表示:

/* 内存结点描述符,所有的结点描述符保存在 struct pglist_data *node_data[MAX_NUMNODES] 中 */
typedef struct pglist_data {
    /* 管理区描述符的数组 */
    struct zone node_zones[MAX_NR_ZONES];
    /* 页分配器使用的zonelist数据结构的数组,将所有结点的管理区按一定的关联链接成一个链表,分配内存时会按照此链表的顺序进行分配 */
    struct zonelist node_zonelists[MAX_ZONELISTS];
    /* 结点中管理区的个数 */
    int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP    /* means !SPARSEMEM */
    /* 结点中页描述符的数组,包含了此结点中所有页框描述符,实际分配是是一个指针数组 */
    struct page *node_mem_map;
#ifdef CONFIG_MEMCG
    /* 用于资源限制机制 */
    struct page_cgroup *node_page_cgroup;
#endif
#endif
#ifndef CONFIG_NO_BOOTMEM
    /* 用在内核初始化阶段 */
    struct bootmem_data *bdata;
#endif
#ifdef CONFIG_MEMORY_HOTPLUG
    /* 自旋锁 */
    spinlock_t node_size_lock;
#endif
    /* 结点中第一个页框的下标,在numa系统中,页框会有两个序号,所有页框的一个序号,还有就是在此结点中的一个序号
     * 比如结点2中的页框1,它在结点2中的序号是1,但是在所有页框中的序号是1001,这个变量就是保存这个结点首页框的序号1000,用于方便转换
     */
    unsigned long node_start_pfn;
    /* 内存结点的大小,不包括洞(以页框为单位) */
    unsigned long node_present_pages;
    /* 结点的大小,包括洞(以页框为单位) */
    unsigned long node_spanned_pages; 

    /* 结点标识符 */
    int node_id;
    /* kswaped页换出守护进程使用的等待队列 */
    wait_queue_head_t kswapd_wait;
    wait_queue_head_t pfmemalloc_wait;
    /* 指针指向kswapd内核线程的进程描述符 */
    struct task_struct *kswapd;    /* Protected by
                       mem_hotplug_begin/end() */
    /* kswapd将要创建的空闲块大小取对数的值 */
    int kswapd_max_order;
    enum zone_type classzone_idx;
#ifdef CONFIG_NUMA_BALANCING
    /* 以下用于NUMA的负载均衡 */
    /* Lock serializing the migrate rate limiting window */
    spinlock_t numabalancing_migrate_lock;

    /* Rate limiting time interval */
    unsigned long numabalancing_migrate_next_window;

    /* Number of pages migrated during the rate limiting time interval */
    unsigned long numabalancing_migrate_nr_pages;
#endif
} pg_data_t;

  系统中所有的结点描述符都保存在node_data这个数组中。在pg_data_t这个结点描述符中,node_zones数组中保存了这个结点中所有的管理区描述符,虽然系统将物理内存分为三个区,但是在逻辑上,系统分为了四个管理区,多出的一个是ZONE_MOVABLE,这个区是一个虚拟的管理区,它并没有对应于内存的某个区域,它的主要目的就是为了避免内存碎片化,它的内存要么全部来自ZONE_HIGHMEM区,要么全部来自ZONE_NORMAL区。这些我们在后面的初始化函数中将会看到。

  每个结点都有一个内核线程kswapd,它的作用就是将进程或内核持有的,但是不常用的页交换到磁盘上,以腾出更多可用内存。

  我们再看看管理区描述符:

/* 内存管理区描述符 */
struct zone {
    /* Read-mostly fields */

    /* zone watermarks, access with *_wmark_pages(zone) macros */
    /* 包括pages_min,pages_low,pages_high
     * pages_min: 管理区中保留页的数目
     * pages_low: 回收页框使用的下界,同时也被管理区分配器作为阀值使用,一般这个数字是pages_min的5/4
     * pages_high: 回收页框使用的上界,同时也被管理区分配器作为阀值使用,一般这个数字是pages_min的3/2
     */
    unsigned long watermark[NR_WMARK];

    /* 指明在处理内存不足的临界情况下管理区必须保留的页框数目,同时也用于在中断或临界区发出的原子内存分配请求(就是禁止阻塞的内存分配请求) */
    long lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
    int node;
#endif

    /*
     * The target ratio of ACTIVE_ANON to INACTIVE_ANON pages on
     * this zone‘s LRU.  Maintained by the pageout code.
     */
    unsigned int inactive_ratio;

    /* 指向此管理区属于的结点 */
    struct pglist_data    *zone_pgdat;
    /* 实现每CPU页框高速缓存,里面包含每个CPU的单页框的链表 */
    struct per_cpu_pageset __percpu *pageset;

    /*
     * This is a per-zone reserve of pages that should not be
     * considered dirtyable memory.
     */
    unsigned long        dirty_balance_reserve;

#ifndef CONFIG_SPARSEMEM
    /*
     * Flags for a pageblock_nr_pages block. See pageblock-flags.h.
     * In SPARSEMEM, this map is stored in struct mem_section
     */
    unsigned long        *pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

#ifdef CONFIG_NUMA
    /*
     * zone reclaim becomes active if more unmapped pages exist.
     */
    unsigned long        min_unmapped_pages;
    unsigned long        min_slab_pages;
#endif /* CONFIG_NUMA */

    /* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
    /* 管理区第一个页框下标 */
    unsigned long        zone_start_pfn;

    /* 所有正常情况下可用的页,总页数(不包括洞)减去保留的页数 */
    unsigned long        managed_pages;
    /* 管理区总大小(页为单位),包括洞 */
    unsigned long        spanned_pages;
    /* 管理区总大小(页为单位),不包括洞 */
    unsigned long        present_pages;
    /* 指向管理区的传统名称,"DMA" "NORMAL" "HighMem" */
    const char        *name;

    /* 对应于伙伴系统中MIGRATE_RESEVE链的页块的数量 */
    int            nr_migrate_reserve_block;

#ifdef CONFIG_MEMORY_ISOLATION
    /*
     * Number of isolated pageblock. It is used to solve incorrect
     * freepage counting problem due to racy retrieving migratetype
     * of pageblock. Protected by zone->lock.
     */
    /* 在内存隔离中表示隔离的页框块数量 */
    unsigned long        nr_isolate_pageblock;
#endif

#ifdef CONFIG_MEMORY_HOTPLUG
    /* see spanned/present_pages for more description */
    seqlock_t        span_seqlock;
#endif

    /* 进程等待队列的hash表,这些进程在等待管理区中的某页 */
    wait_queue_head_t    *wait_table;
    /* 等待队列散列表的大小 */
    unsigned long        wait_table_hash_nr_entries;
    /* 等待队列散列表数组大小 */
    unsigned long        wait_table_bits;

    ZONE_PADDING(_pad1_)

    /* Write-intensive fields used from the page allocator */
    /* 保护该描述符的自旋锁 */
    spinlock_t        lock;

    /* free areas of different sizes */
    /* 标识出管理区中的空闲页框块,用于伙伴系统 */
    /* MAX_ORDER为11,分别代表包含大小为1,2,4,8,16,32,64,128,256,512,1024个连续页框的链表 */
    struct free_area    free_area[MAX_ORDER];

    /* zone flags, see below */
    /* 管理区标识 */
    unsigned long        flags;

    ZONE_PADDING(_pad2_)

    /* Fields commonly accessed by the page reclaim scanner */
    /* 活动及非活动链表使用的自旋锁 */
    spinlock_t        lru_lock;
    struct lruvec        lruvec;

    /* Evictions & activations on the inactive file list */
    atomic_long_t        inactive_age;

    /*
     * When free pages are below this point, additional steps are taken
     * when reading the number of free pages to avoid per-cpu counter
     * drift allowing watermarks to be breached
     */
    unsigned long percpu_drift_mark;

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
    /* pfn where compaction free scanner should start */
    unsigned long        compact_cached_free_pfn;
    /* pfn where async and sync compaction migration scanner should start */
    unsigned long        compact_cached_migrate_pfn[2];
#endif

#ifdef CONFIG_COMPACTION
    /*
     * On compaction failure, 1<<compact_defer_shift compactions
     * are skipped before trying again. The number attempted since
     * last failure is tracked with compact_considered.
     */
    unsigned int        compact_considered;
    unsigned int        compact_defer_shift;
    int            compact_order_failed;
#endif

#if defined CONFIG_COMPACTION || defined CONFIG_CMA
    /* Set to true when the PG_migrate_skip bits should be cleared */
    bool            compact_blockskip_flush;
#endif

    ZONE_PADDING(_pad3_)
    /* 管理区的一些统计数据 */
    atomic_long_t        vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;

  此管理区描述符中的实际把所有属于该管理区的页框保存在两个地方:struct free_area free_area[MAX_ORDER]和struct per_cpu_pageset __percpu * pageset。free_area是这个管理区的伙伴系统,而pageset是这个区的每CPU页框高速缓存。对管理区的理解需要结合伙伴系统和每CPU页框高速缓存

管理区页框分配器(管理所有物理内存页框)

  ZONE_NORMAL和ZONE_DMA的地址直接映射到了内核地址空间,但是也不代表内核的代码可以随心所欲的通过线性地址直接访问物理地址。内核通过一个管理区页框分配器管理着物理内存上所有的页框,在管理区分配器里的核心系统就是伙伴系统和每CPU页框高速缓存(不是硬件上的高速缓存,只是名称一样)。在linux系统中,管理区页框分配器管理着所有物理内存,无论你是内核还是进程,需要将一些内存占为己有时,都需要请求管理区页框分配器,这时才会分配给你应该获得的物理内存页框。当你所拥有的页框不再使用时,你必须释放这些页框,让这些页框回到管理区页框分配器当中。特别的,对于高端内存,即使从管理区页框分配器中获得了相应的页框,我们还需要进行映射才能够使用。

  有时候目标管理区不一定有足够的页框去满足分配,这时候系统会从另外两个管理区中获取要求的页框,但这是按照一定规则去执行的,如下:

  • 如果要求从DMA区中获取,就只能从ZONE_DMA区中获取。
  • 如果没有规定从哪个区获取,就按照顺序从 ZONE_NORMAL -> ZONE_DMA 获取。
  • 如果规定从HIGHMEM区获取,就按照顺序从 ZONE_HIGHMEM -> ZONE_NORMAL -> ZONE_DMA 获取。

  注意系统是不允许在一次分配中从不同的两个管理区获取页框的,并且当请求多个页框时,从伙伴系统中分配给目标的页框是连续的,并且请求的页数必须是2的次方个数。

  管理区分配器主要做的事情就是将页框通过伙伴系统或者每CPU页框高速缓存分配出去,这里涉及到三个结构,页描述符,伙伴系统,每CPU高速缓存。

  我们先说说页描述符,页描述符实际上并不专属于描述页框,它还用于描述一个SLAB分配器和SLUB分配器,这个之后再说,我们先说关于页的:

/* 页描述符,描述一个页框,也会用于描述一个SLAB,相当于同时是页描述符,也是SLAB描述符 */
struct page {
    /* First double word block */
    /* 用于页描述符,一组标志(如PG_locked、PG_error),也对页框所在的管理区和node进行编号 */
    unsigned long flags;        /* Atomic flags, some possibly
                     * updated asynchronously */
    union {
        /* 用于页描述符,当页被插入页高速缓存中时使用,或者当页属于匿名区时使用 */
        struct address_space *mapping;
        /* 用于SLAB描述符,用于执行第一个对象的地址 */
        void *s_mem;            /* slab first object */
    };

    /* Second double word */
    struct {
        union {
            /* 作为不同的含义被几种内核成分使用。例如,它在页磁盘映像或匿名区中标识存放在页框中的数据的位置,或者它存放一个换出页标识符 */
            pgoff_t index;        /* Our offset within mapping. */
            /* 用于SLAB描述符,指向第一个空闲对象地址 */
            void *freelist;
            /* 当管理区页框分配器压力过大时,设置这个标志就确保这个页框专门用于系统释放其他页框时使用 */
            bool pfmemalloc;
        };

        union {
#if defined(CONFIG_HAVE_CMPXCHG_DOUBLE) && defined(CONFIG_HAVE_ALIGNED_STRUCT_PAGE)
            /* SLUB使用 */
            unsigned long counters;
#else
            /* SLUB使用 */
            unsigned counters;
#endif

            struct {
                union {
                    /* 页框中的页表项计数,如果没有为-1,如果为PAGE_BUDDY_MAPCOUNT_VALUE(-128),说明此页及其后的一共2的private次方个数页框处于伙伴系统中,正在使用时应该是0 */
                    atomic_t _mapcount;

                    struct { /* SLUB使用 */
                        unsigned inuse:16;
                        unsigned objects:15;
                        unsigned frozen:1;
                    };
                    int units;    /* SLOB */
                };
                /* 页框的引用计数,如果为-1,则此页框空闲,并可分配给任一进程或内核;如果大于或等于0,则说明页框被分配给了一个或多个进程,或用于存放内核数据。page_count()返回_count加1的值,也就是该页的使用者数目 */
                atomic_t _count;        /* Usage count, see below. */
            };
            /* 用于SLAB描述符 */
            unsigned int active;    /* SLAB */
        };
    };

    /* Third double word block */
    union {
        /* 包含到页的最近最少使用(LRU)双向链表的指针,用于插入伙伴系统的空闲链表中,只有块中头页框要被插入 */
        struct list_head lru;    

        /* SLAB使用 */
        struct {        /* slub per cpu partial pages */
            struct page *next;    /* Next partial slab */
#ifdef CONFIG_64BIT
            int pages;    /* Nr of partial slabs left */
            int pobjects;    /* Approximate # of objects */
#else
            short int pages;
            short int pobjects;
#endif
        };

        struct slab *slab_page; /* slab fields */
        struct rcu_head rcu_head;

#if defined(CONFIG_TRANSPARENT_HUGEPAGE) && USE_SPLIT_PMD_PTLOCKS
        pgtable_t pmd_huge_pte; /* protected by page->ptl */
#endif
    };

    /* Remainder is not double word aligned */
    union {
        /* 可用于正在使用页的内核成分(例如: 在缓冲页的情况下它是一个缓冲器头指针,如果页是空闲的,则该字段由伙伴系统使用,在给伙伴系统使用时,表明的是块的2的次方数,只有块的第一个页框会使用) */
        unsigned long private;
#if USE_SPLIT_PTE_PTLOCKS
#if ALLOC_SPLIT_PTLOCKS
        spinlock_t *ptl;
#else
        spinlock_t ptl;
#endif
#endif
        /* SLAB描述符使用,指向SLAB的高速缓存 */
        struct kmem_cache *slab_cache;    /* SL[AU]B: Pointer to slab */
        struct page *first_page;    /* Compound tail pages */
    };

#if defined(WANT_PAGE_VIRTUAL)
    /* 此页框第一个物理地址对应的线性地址,如果是没有映射的高端内存的页框,则为空 */
    void *virtual;
#endif /* WANT_PAGE_VIRTUAL */
#ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS
    unsigned long debug_flags;    /* Use atomic bitops on this */
#endif

#ifdef CONFIG_KMEMCHECK
    void *shadow;
#endif

#ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS
    int _last_cpupid;
#endif
}

  在struct page描述一个页框时,我们比较关注的成员变量有unsigned long flags、struct list_head lru和atomic_t _count。

  • flags:包含有很多信息,包括此页框属于的node结点号,此页框属于的zone号和此页框的属性。
  • lru:用于将此页描述符放入相应的链表,比如伙伴系统或者每CPU页框高速缓存。
  • _count:代表页框的引用计数,-1代表此页框空闲,大于0代表此页框分配给了多少个进程使用(共享)。

  linux为了防止内存中产生过多的碎片,一般把页的类型分为三种:

  • 不可移动页:在内存中有固定位置,不能移动到其他地方。内核中使用的页大部分是属于这种类型。
  • 可回收页:不能直接移动,但可以删除,页中的内容可以从某些源中重新生成。例如,页内容是映射到文件数据的页就属于这种类型。对于这种类型,kswapd内核线程会根据可回收页的访问频率,周期性地释放这些页。在内存短缺(分配失败)时,也会发起页面回收,释放这些页。
  • 可移动页:可随意移动,用户空间的进程使用的页就属于这种类型,它们是通过进程页表映射的,把这些页复制到新位置时,只要更新进程页表就可以了。一般这些页是从高端内存管理区获取。

  

伙伴系统

  伙伴系统实际上是一个struct free_area的数组,数组长度是MAX_ORDER,也就是11,代表着每个数组元素中链表上保存的连续页框长度是2的order次方。free_area[0]中链表保存的是长度为1的页框,free_area[1]中链表上保存的是物理上连续的两个页框的首页框链表,free_area[2]中链表上保存的是物理上连续4个页框的首页框链表,free_area[10]中链表上保存的是物理上连续1024个页框的首页框链表,所以整个伙伴系统中将管理区中的页框分为连续的1,2,4,8,16,32,64,128,256,512,1024页框放入不同链表中保存起来。而因为伙伴系统中每个链表保存的页框都是连续的,所以只有第一个页框会加入到链表中,因为有order,也可以知道此页框之后的多少个页框是属于这一小块连续页框的。当需要在普通内存区申请4个页框大小的内存时,系统会到普通内存管理区的伙伴系统中的free_area[2]中的第一个链表结点,这个结点的页框及其之后3个页框都是空闲的,然后把首页框返回给申请者。

/* 伙伴系统的一个块,描述1,2,4,8,16,32,64,128,256,512或1024个连续页框的块 */
struct free_area {
    /* 指向这个块中所有空闲小块的第一个页描述符,这些小块会按照MIGRATE_TYPES类型存放在不同指针里 */
    struct list_head    free_list[MIGRATE_TYPES];
    /* 空闲小块的个数 */
    unsigned long        nr_free;
};

  在伙伴系统中,因为页的分类关系,在每种长度相同的连续页框中又会分出多个不同类型的链表,如下,

enum {
    MIGRATE_UNMOVABLE,         /* 不可移动页 */
    MIGRATE_RECLAIMABLE,       /* 可回收页 */
    MIGRATE_MOVABLE,           /* 可移动页 */
    MIGRATE_PCPTYPES,          /* 用来表示每CPU页框高速缓存的数据结构中的链表的可移动类型数目 */
    MIGRATE_RESERVE = MIGRATE_PCPTYPES,
#ifdef CONFIG_CMA
    MIGRATE_CMA,
#endif
#ifdef CONFIG_MEMORY_ISOLATION
    MIGRATE_ISOLATE,          /* 不能从这个链表分配页框,因为这个链表专门用于NUMA结点移动物理内存页,将物理内存页移动到使用这个页最频繁的CPU */
#endif
    MIGRATE_TYPES
};

 保存连续2个页框的free_area[2]的结构如下:

  在从伙伴系统中申请页框时,有可能会遇到一种情况,就是当前需求的连续页框链表上没有可用的空闲页框,这时后,伙伴系统会从下一级获取一个连续长度的页框块,将其拆分放入这级列表;当然在拥有者释放连续页框时伙伴系统也会适当地进行连续页框的合并,并放入下一级中。比如:我需要申请4个页框,但是长度为4个连续页框块链表没有空闲的页框块,伙伴系统会从连续8个页框块的链表获取一个,并将其拆分为两个连续4个页框块,放入连续4个页框块的链表中。释放时道理也一样,会检查释放的这几个页框的之前和之后的物理页框是否空闲,并且能否组成下一级长度的块。

每CPU页框高速缓存

  每CPU页框高速缓存也是一个分配器,配合着伙伴系统进行使用,这个分配器是专门用于分配单个页框的,它维护一个单页框的双向链表,为什么需要这个分配器,因为每个CPU都有自己的硬件高速缓存,当对一个页进行读取写入时,首先会把这个页装入硬件高速缓存,而如果进程对这个处于硬件高速缓存的页进行操作后立即释放掉,这个页有可能还保存在硬件高速缓存中,这样我另一个进程需要请求一个页并立即写入数据的话,分配器将这个处于硬件高速缓存中的页分配给它,系统效率会大大增加。

  在每CPU页框高速缓存中用一个链表来维护一个单页框的双向链表,每个CPU都有自己的链表(因为每个CPU有自己的硬件高速缓存),那些比较可能处于硬件高速缓存中的页被称为“热页”,比较不可能处于硬件高速缓存中的页称为“冷页”。其实系统判断是否为热页还是冷页很简单,越最近释放的页就比较可能是热页,所以在双向链表中,从链表头插入可能是热页的单页框,在链表尾插入可能是冷页的单页框。分配时热页就从链表头获取,冷页就从链表尾获取。

  在每CPU页框高速缓存中也可能会遇到没有空闲的页框(被分配完了),这时候每CPU页框高速缓存会从伙伴系统中拿出页框放入每CPU页框高速缓存中,相反,如果每CPU页框高速缓存中页框过多,也会将一些页框放回伙伴系统。

  在内核中使用struct per_cpu_pageset结构描述一个每CPU页框高速缓存,其中的struct per_cpu_pages是核心结构体,如下:

/* 描述一个CPU页框高速缓存 */
struct per_cpu_pageset {
    /* 高速缓存页框结构 */
    struct per_cpu_pages pcp;
#ifdef CONFIG_NUMA
    s8 expire;
#endif
#ifdef CONFIG_SMP
    s8 stat_threshold;
    s8 vm_stat_diff[NR_VM_ZONE_STAT_ITEMS];
#endif
};

struct per_cpu_pages {
    /* 当前CPU高速缓存中页框个数 */
    int count;        /* number of pages in the list */
    /* 上界,当此CPU高速缓存中页框个数大于high,则会将batch个页框放回伙伴系统 */
    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 */
    /* 页框的链表,如果需要冷高速缓存,从链表尾开始获取页框,如果需要热高速缓存,从链表头开始获取页框 */
    struct list_head lists[MIGRATE_PCPTYPES];
};

结尾

  下篇再说slab了,内容太多。到这里,记住对于物理内存来说,系统都是以页框作为最小的分配单位,而分配时必定是要通过管理区分配器进行分配的,在管理区分配器中又必定是通过伙伴系统或每CPU页框分配器进行分配的,而我们编程使用到的malloc或者内核中使用的分配小额内存的情况,是使用slab实现的,slab的作用就是将一个页框细分为多个小块内存。

时间: 2024-10-07 19:31:41

linux内存管理源码分析 - 概述的相关文章

Cocos2d-X3.0 刨根问底(四)----- 内存管理源码分析

本系列文章发表以来得到了很多朋友的关注,小鱼在这里谢谢大家对我的支持,我会继续努力的,最近更新慢了一点,因为我老婆流产了抽了很多时间来照顾她希望大家谅解,并在此预祝我老婆早日康复. 上一篇,我们完整的分析了Director这个类,并提到了Director这个继承了Ref这个类,大致看了一下Ref这个类,是一个关于引用计数的类,从而我们可以推断Cocos2d-x用了一种引用计数的方式来管理内存对象,这一章我们刨根问底Cocos2d-x是如何实现内存管理及我们如何在实际项目开发中应用Cocos2d-

linux调度器源码分析 - 运行(四)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 之前的文章已经将调度器的数据结构.初始化.加入进程都进行了分析,这篇文章将主要说明调度器是如何在程序稳定运行的情况下进行进程调度的. 系统定时器 因为我们主要讲解的是调度器,而会涉及到一些系统定时器的知识,这里我们简单讲解一下内核中定时器是如何组织,又是如何通过通过定时器实现了调度器的间隔调度.首先我们先看一下内核定时器的框架 在内核中,会使用strut clock_event_device结构描述硬件

linux调度器源码分析 - 初始化(二)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 上期文章linux调度器源码分析 - 概述(一)已经把调度器相关的数据结构介绍了一遍,本篇着重通过代码说明调度器在系统启动初始化阶段是如何初始化和工作的.通过上期文章我们知道,在多核CPU和SMP系统中,每个CPU(多核COU中的每个核)都有自己的struct rq队列,而rq队列中又有着自己的struct cfs_rq和struct rt_rq.在初始化时就是对这三个结构进行初始化. init_tas

java 1.8 动态代理源码分析

JDK8动态代理源码分析 动态代理的基本使用就不详细介绍了: 例子: class proxyed implements pro{ @Override public void text() { System.err.println("本方法"); } } interface pro { void text(); } public class JavaProxy implements InvocationHandler { private Object source; public Jav

Flink内存管理源码解读之内存管理器

回顾 上一篇文章我们谈了Flink自主内存管理的一些基础的数据结构.那篇中主要讲了数据结构的定义,这篇我们来看看那些数据结构的使用,以及内存的管理设计. 概述 这篇文章我们主要探讨Flink的内存管理类MemoryManager涉及到对内存的分配.回收,以及针对预分配内存而提供的memory segment pool.还有支持跨越多个memory segment数据访问的page view. 本文探讨的类主要位于pageckage : org.apache.flink.runtime.memor

(转)Spring对注解(Annotation)处理源码分析1——扫描和读取Bean定义

1.从Spring2.0以后的版本中,Spring也引入了基于注解(Annotation)方式的配置,注解(Annotation)是JDK1.5中引入的一个新特性,用于简化Bean的配置,某些场合可以取代XML配置文件.开发人员对注解(Annotation)的态度也是萝卜青菜各有所爱,个人认为注解可以大大简化配置,提高开发速度,同时也不能完全取代XML配置方式,XML 方式更加灵活,并且发展的相对成熟,这种配置方式为大多数 Spring 开发者熟悉:注解方式使用起来非常简洁,但是尚处于发展阶段,

linux中断源码分析 - 概述(一)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 可编程中断控制器(PIC.APIC) 为了方便说明,这里我们将PIC和APIC统称为中断控制器.中断控制器是作为中断(IRQ)和CPU核之间的一个桥梁而存在的,每个CPU内部都有一个自己的中断控制器,中断线并不是直接与CPU核相连,而是与CPU内部或外部的中断控制器相连.而为什么叫做可编程中断控制器,是因为其本身有一定的寄存器,CPU可以通过操作设置中断控制器屏蔽某个中断引脚的信号,实现硬件上的中断屏蔽.中断

Flink内存管理源码解读之基础数据结构

概述 在分布式实时计算领域,如何让框架/引擎足够高效地在内存中存取.处理海量数据是一个非常棘手的问题.在应对这一问题上Flink无疑是做得非常杰出的,Flink的自主内存管理设计也许比它自身的知名度更高一些.正好最近在研读Flink的源码,所以开两篇文章来谈谈Flink的内存管理设计. Flink的内存管理的亮点体现在作为以Java为主的(部分功能用Scala实现,也是一种遵循JVM规范并依赖JVM解释执行的函数式编程语言)的程序却自主实现内存的管理而不完全依赖于JVM的内存管理机制.它的优势在

linux调度器源码研究 - 概述(一)

本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 调度器作为操作系统的核心部件,具有非常重要的意义,其随着linux内核的更新也不断进行着更新.本系列文章通过linux-3.18.3源码进行调度器的学习和分析,一步一步将linux现有的调度器原原本本的展现出来.此篇文章作为开篇,主要介绍调度器的原理及重要数据结构. 调度器介绍 随着时代的发展,linux也从其初始版本稳步发展到今天,从2.4的非抢占内核发展到今天的可抢占内核,调度器无论从代码结构还是设