memory 子系统

内核对一致和非一致内存访问使用相同的数据结构。首先,内存划分为结点:每个结点关联到系统中的一个处理器,在内核中用pg_data_t 表示。各个结点又划分为内存域,一个结点最多由3个内存域组成,用3个常量来表示:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM,此外内核还定义了一个伪内存域 ZONE_MOVABLE 用于防止物理内存碎片,ZONE_NR_ZONES 为结束标记。每个内存域都关联了一个数组,用来组织属于该内存域的物理内存页(页帧),对于每个页帧,都分配了一个struct page 实例以及所需的管理数据。各个内存结点保存在一个单链表中以供内核遍历。

1、结点管理

pg_data_t 是用于表示结点的基本元素,定义如下:

typedef struct pglist_data {
	/* 包含了结点中各内存域的数据结构 */
	struct zone node_zones[MAX_NR_ZONES];
	/* 指定备用结点及其内存域列表以便当前结点没有可用空间时在备用结点分配内存 */
	struct zonelist node_zonelists[MAX_ZONELISTS];
	/* 不同内存域的数目 */
	int nr_zones;
#ifdef CONFIG_FLAT_NODE_MEM_MAP	/* means !SPARSEMEM */
	/* 指向page实例数组的指针,用于描述结点的所有物理内存页,包含了结点中所有的内存域的页 */
	struct page *node_mem_map;
#ifdef CONFIG_CGROUP_MEM_RES_CTLR
	struct page_cgroup *node_page_cgroup;
#endif
#endif
	/* 指向自举内存分配器数据结构的实例 */
	struct bootmem_data *bdata;
#ifdef CONFIG_MEMORY_HOTPLUG
	spinlock_t node_size_lock;
#endif
	/* 该NUMA结点第一个页帧的逻辑编号 */
	unsigned long node_start_pfn;
	/* 结点中页帧的数目 */
	unsigned long node_present_pages;
	/* 该结点以页帧为单位计算的长度,包括空洞 */
	unsigned long node_spanned_pages;
	/* 全局结点ID,系统中的NUMA结点都从0开始编号 */
	int node_id;
	/* 交换守护进程的等待队列,在将页帧换出结点时会用到 */
	wait_queue_head_t kswapd_wait;
	struct task_struct *kswapd;
	/* 用于页交换子系统 */
	int kswapd_max_order;
} pg_data_t;
其中 node_start_pfn 是该NUMA(非一致内存访问)结点第一个页帧的逻辑编号,系统中所有结点的页帧是依次编号的,每个页帧的号码都是全局唯一的(不只是结点唯一)。node_start_pfn 在UMA(一致内存访问)系统中总是0,因为其中只有一个结点。如果系统中存在多个结点,内核会维护一个位图,用以提供各个结点的状态信息,状态是用位掩码指定的,可使用下列值:

enum node_states {
	N_POSSIBLE,       /* 结点在某个时候可能变为联机 */
	N_ONLINE,         /* 结点是联机的 */
	N_NORMAL_MEMORY,  /* 结点有普通内存域 */
#ifdef CONFIG_HIGHMEM
	N_HIGH_MEMORY,    /* 结点有普通内存域或高端内存域 */
#else
	N_HIGH_MEMORY = N_NORMAL_MEMORY,
#endif
	N_CPU,            /* 结点有一个或多个cpu */
	NR_NODE_STATES
};
状态 N_POSSIBLE、N_ONLINE、N_CPU 用于cpu和内存的热插拔。一般结点有普通内存域或高端内存域则使用 N_HIGH_MEMORY,仅当结点没有高端内存的时候才设置 N_NORMAL_MEMORY。
2、内存域

内核使用 struct zone 结构来描述内存域。其定义如下:

struct zone {
	/* 通常由页分配器访问的字段 */
	/* 内存域水印,通过宏 *_wmark_pages(zone) 访问 */
	unsigned long watermark[NR_WMARK];

	/* 数组分别为各种内存域指定了若干页,用于一些无论如何都不能失败的关键性内存分配 */
	unsigned long		lowmem_reserve[MAX_NR_ZONES];

#ifdef CONFIG_NUMA
	int node;
	unsigned long		min_unmapped_pages;
	unsigned long		min_slab_pages;
	struct per_cpu_pageset	*pageset[NR_CPUS];
#else
	/* 用于实现每个cpu的热/冷页帧列表 */
	struct per_cpu_pageset	pageset[NR_CPUS];
#endif
	/* 不同长度的空闲区域 */
	/* 本字段使用的自旋锁 */
	spinlock_t		lock;
#ifdef CONFIG_MEMORY_HOTPLUG
	/* see spanned/present_pages for more description */
	seqlock_t		span_seqlock;
#endif
	/* 同名数据结构的数组,用于实现伙伴系统。每个数组元素都是表示某种固定长度的一些连续内存区 */
	struct free_area	free_area[MAX_ORDER];

#ifndef CONFIG_SPARSEMEM
	unsigned long		*pageblock_flags;
#endif /* CONFIG_SPARSEMEM */

	ZONE_PADDING(_pad1_)

	/* 通常由页面回收扫描程序访问的字段 */
	/* 本字段使用的自旋锁 */
	spinlock_t		lru_lock;
	/* 不同状态页面的链表 */
	struct zone_lru {
		struct list_head list;
	} lru[NR_LRU_LISTS];
	/* 在回收内存时会用到的参数 */
	struct zone_reclaim_stat reclaim_stat;
	/* 指定了上次换出一页以来,有多少页未能成功扫描 */
	unsigned long		pages_scanned;
	/* 描述内存域的当前状态 */
	unsigned long		flags;

	/* 维护了大量有关该内存域的统计信息 */
	atomic_long_t		vm_stat[NR_VM_ZONE_STAT_ITEMS];

	/* 上次扫描该内存域的优先级,扫描操作由 try_to_free_pages 进行 */
	int prev_priority;

	unsigned int inactive_ratio;

	ZONE_PADDING(_pad2_)

	/* 很少使用或大多数情况下至上只读的字段 */
	/* 下面三个参数实现了一个等待队列,可供等待某一页变为可用的进程使用 */
	wait_queue_head_t	* wait_table;
	unsigned long		wait_table_hash_nr_entries;
	unsigned long		wait_table_bits;

	/* 不连续内存使用的字段 */
	struct pglist_data	*zone_pgdat;
	/* zone_start_pfn == zone_start_paddr >> PAGE_SHIFT */
	unsigned long		zone_start_pfn; /* 内存域第一个页帧的索引 */

	unsigned long		spanned_pages;	/* 内存域中页的总数,包含空洞 */
	unsigned long		present_pages;	/* 内存域中实际可用的页数 */
	
	const char		*name;              /* 很少使用的字段,保存该内存域的惯用名称:Normal、DMA、HighMem */
} ____cacheline_internodealigned_in_smp;
该结构比较特殊的方面是它由 ZONE_PADDING 分隔为几个部分,如果数据保存在cpu高速缓存中,那么会处理得更快。高速缓存分为行,每一行负责不同的内存区。内核使用 ZONE_PADDING 宏生成“填充”字段添加到结构中,以确保每个段的自旋锁都处于自身的缓存行中。编译器关键字  ____cacheline_internodealigned_in_smp用以实现最优的高速缓存对齐方式。

3、冷热页

struct zone 的 pageset 成员用于实现冷热分配器,内核说页是热的,意味着这页已经加载到cpu高速缓存。相反,冷页则不再高速缓存中。pageset 是一个数组,其容量与系统能够容纳的cpu数目的最大值相同。数组元素的类型为per_cpu_pageset,定义如下:

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
} ____cacheline_aligned_in_smp;
有用的数据都保存在 per_cpu_pages 里面:
struct per_cpu_pages {
	int count;		/* 记录了与该列表相关的页的数目 */
	int high;		/* high 水印 */
	int batch;		/* 伙伴系统添加、删除多页块的时候用到的数据块大小 */
	
	struct list_head lists[MIGRATE_PCPTYPES];  /* 页的链表 */
};
4、页帧

页帧代表系统内存的最小单位,对内存中的每个页都会创建 struct page 的一个实例。该结构定义如下:

struct page {
	unsigned long flags;       /* 原子标记,用于描述页的属性,有些情况下会异步更新 */
	atomic_t _count;           /* 表示内核中引用该页的次数,见下文 */
	union {
		atomic_t _mapcount;    /* 内存管理子系统中映射的页表项计数,用于表示页是否已经映射,还用于限制逆向映射搜索 */
		struct {               /* 用于SLUB分配器:对象的数目 */
			u16 inuse;
			u16 objects;
		};
	};
	union {
	    struct {
		/* 由映射私有,不透明数据:
		如果设置了PagePrivate,通常用于buffer_heads;
		如果设置了PageSwapCache,则通常用于swp_entry_t;
		如果设置了PG_buddy,则用于表示伙伴系统中的阶 */
		unsigned long private;
		/* 如果最低位为0,则指向inode address_space,或为NULL。
		如果页映射为匿名内存,最低位置位,而且该指针指向amon_vma对象:参加下文的PAGE_MAPPING_ANON */
		struct address_space *mapping;
	    };
#if USE_SPLIT_PTLOCKS
	    spinlock_t ptl;
#endif
	    struct kmem_cache *slab;	/* 用于SLUB分配器:指向slab的指针 */
	    struct page *first_page;	/* 用于复合页的尾页,指向首页 */
	};
	union {
		pgoff_t index;              /* 在映射内的偏移量 */
		void *freelist;             /* SLUB: freelist req. slab lock */
	};
	struct list_head lru;           /* lru是一个表头,用于在各种链表上维护该页 */
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;                  /* 内核虚拟地址(如果没有映射则为NULL,即高端内存) */
#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
};

内核可以将多个毗连的页合并为较大的复合页,分组中的第一个页称为首页,而所有其余的各页叫做尾页,所有尾页对应的page实例中,都将 first_page 设置为指向首页。mapping 指定了页帧所在的地址空间。index 是页帧在映射内部的偏移量。页的不同属性通过一系列的标志描述:

enum pageflags {
	PG_locked,       /* 页是否锁定,如果该bit置位,内核的其他部分不允许访问该页,防止内存管理出现竟态 */
	PG_error,        /* 如果涉及该页的I/O操作期间发生错误则置位 */
	PG_referenced,   /* 和PG_active一起控制了系统使用该页的活跃程度 */
	PG_uptodate,     /* 表示页的数据已经从块设备读取,其间没有出错 */
	PG_dirty,        /* 如果与硬盘上的数据对比页的内容已经改变,则置位,表示页为脏的 */
	PG_lru,          /* 有助于实现页面回收和切换 */
	PG_active,
	PG_slab,         /* 如果页是slab分配器中的一部分则置位 */
	PG_owner_priv_1, /* Owner use. If pagecache, fs may use*/
	PG_arch_1,
	PG_reserved,
	PG_private,      /* If pagecache, has fs-private data */
	PG_private_2,    /* If pagecache, has fs aux data */
	PG_writeback,    /* Page is under writeback */
#ifdef CONFIG_PAGEFLAGS_EXTENDED
	PG_head,         /* A head page */
	PG_tail,         /* A tail page */
#else
	PG_compound,     /* 表示该页属于一个更大的复合页 */
#endif
	PG_swapcache,    /* 如果页处于交换缓存则置位,这种情况下private包含一个类型为 swp_entry_t 的项 */
	PG_mappedtodisk, /* Has blocks allocated on-disk */
	PG_reclaim,      /* 在内核决定回收某个特定的页之后需要置位 */
	PG_buddy,        /* 如果页空闲且包含在伙伴系统中则置位 */
	PG_swapbacked,   /* Page is backed by RAM/swap */
	PG_unevictable,  /* Page is "unevictable"  */
#ifdef CONFIG_HAVE_MLOCKED_PAGE_BIT
	PG_mlocked,      /* Page is vma mlocked */
#endif
#ifdef CONFIG_ARCH_USES_PG_UNCACHED
	PG_uncached,     /* Page has been mapped as uncached */
#endif
#ifdef CONFIG_MEMORY_FAILURE
	PG_hwpoison,     /* hardware poisoned page. Don't touch */
#endif
	__NR_PAGEFLAGS,

	/* Filesystems */
	PG_checked = PG_owner_priv_1,

	PG_fscache = PG_private_2,  /* page backed by cache */

	/* XEN */
	PG_pinned = PG_owner_priv_1,
	PG_savepinned = PG_dirty,

	/* SLOB */
	PG_slob_free = PG_private,

	/* SLUB */
	PG_slub_frozen = PG_active,
	PG_slub_debug = PG_error,
};

到目前为止我们讨论的结构主要用来描述内存的结构(划分为结点和内存域),同时指定了其中包含的页帧的数量和状态(使用中或空闲)。

5、页表

页表用于向每个进程提供一致的虚拟地址空间。应用程序看到的地址空间是一个连续的内存区。该表也将虚拟内存页映射到物理内存,因而支持共享内存的实现,还可以在不额外增加物理内存的情况下,将页换出到块设备来增加有效的可用内存空间。页表管理分为两个部分:一部分依赖于体系结构;一部分是体系结构无关的。

内核源代码假定 void * 和 unsigned long 类型所需的比特数相同,因此它们之间可以进行强制转换而不损失信息。内核内存管理总是假定使用四级页表,而不管底层处理器是否如此。根据四级列表结构的需要,虚拟内存地址分为5部分(4个表项用于选择页,1个索引表示页内位置),总长度为 BITS_PER_LONG 即 unsigned long 变量的比特数:

|<---                                       BITS_PER_LONG                                         --->|

|        PGD        |        PUD        |        PMD       |        PTE        |        Offset        |

                                                                                             ->| PAGE_SHIFT |<-

                                                                        |<-            PMD_SHIFT          ->|

                                                |<-                          PUD_SHIFT                     ->|

                      |<-                                  PGDIR_SHIFT                                   ->|

PGD 指全局页目录,PUD 指上层页目录,PMD 指中间页目录,PTE 指页表数组。每个指针末端的几个比特位用于指定所选页帧内部的位置(数目由PAGE_SHIFT指定)。PMD_SHIFT 指定了页内偏移量和最后一级页表项所需的比特位总数,该值表明了一个中间层页表项管理的部分地址空间的大小。在各级页目录/页表中所能存储的指针数目,也可以通过宏定义确定。

6、bootmem 分配器

在启动期间内核采用bootmem分配器用于内存分配。该分配器使用一个位图来管理页,位图的比特位数与系统中物理内存的数目相同,比特位为1表示已用页,比特位为0表示空闲页。在需要分配内存时,分配器逐位扫描位图,直至找到一个能够提供足够连续页的位置,即所谓的最先最佳位置,该过程效率不是很高,所以只用于启动期间。内核提供了结构bootmem_data用于 bootmem 分配器管理一些数据,该结构所需的内存无法动态分配,必须在编译的时候分配给内核:

typedef struct bootmem_data {
	unsigned long node_min_pfn;  /* 当前结点第一个页的编号 */
	unsigned long node_low_pfn;  /* 可以直接管理的物理地址空间中最后一个页的编号即ZONE_NORMAL的结束页 */
	void *node_bootmem_map;      /* 指向存储分配位图的内存区的指针 */
	unsigned long last_end_off;  /* 上一次分配的页的编号 */
	unsigned long hint_idx;
	struct list_head list;       /* 连接其他的bootmem分配器 */
} bootmem_data_t;
在 UMA 系统上,只需要一个 bootmem_data_t 实例,即 contig_bootmem_data。在启动过程中会调用 init_bootmem() 执行 bootmem 分配器的第一个初始化步骤即初始化其内部成员,最初在位图 contig_bootmem_data->node_bootmem_map 中,所有的页都标记为已用,然后由特定体系结构的代码调用 free_bootmem() 扫描整个位图,将相应的比特位清零,释放所有潜在可用的内存页。由于 bootmem 分配器需要一些内存页管理分配位图,必须首先调用 reserve_bootmem 分配这些内存页。

内核提供了各种函数,用于在初始化期间分配内存,在 UMA 系统上一般用 alloc_bootmem() 以及 alloc_bootmem_pages(),这两个函数最终都会调用到 alloc_bootmem_core(),该函数原型以及操作如下:

/* a、从goal开始扫描位图,查找满足分配请求的空闲内存区
   b、如果目标页紧接着上一次分配的页,函数会判断所需的内存是否能够在上一页分配或从上一页开始分配
   c、新分配的页在位图对应比特位设置为1 */
static void * __init alloc_bootmem_core(struct bootmem_data *bdata, unsigned long size,
                              unsigned long align, unsigned long goal, unsigned long limit);
在系统初始化进行到伙伴系统分配器能够运行后,必须停用 bootmem 分配器,由  free_all_bootmem()完成。许多内核代码块和数据表只在系统初始化阶段需要,如 __init、__initdata 属性的函数、数据,在启动结束时可以完全从内存删除, free_initmem()负责释放用于初始化的内存区,并将相关的页返回给伙伴系统,一般能够释放的内存有 300k。

7、伙伴系统

系统内存中的每个物理内存页都对应于一个 struct page 实例,每个内存域都关联了一个 struct zone 的实例,其中保存了用于管理伙伴数据的主要数组:struct free_area free_area[MAX_ORDER],该结构定义如下:

struct free_area {
	struct list_head	free_list[MIGRATE_TYPES];   /* 用于连接空闲页的链表 */
	unsigned long		nr_free;  /* 指定了当前内存区空闲页块的数目,对于0阶内存区逐页计算,2阶内存区计算4页集合的数目 */
};
为了避免内存碎片,内核加入了反碎片技术,工作原理如下:内核将已分配的页划分为3种不同类型:

a、不可移动页:在内存中有固定位置,不能移动到其他地方

b、可回收页:不能直接移动,可以删除,其内容可以从某些源重新生成(kswapd守护进程会根据可回收页的访问频繁程度周期性释放内存)

c、可移动页:可以随意移动的页,用户空间应用程序的页属于该类别,它们是通过页表映射的,可以通过更新页表项来移动
内核定义了一些宏来表示不同的迁移类型:

#define MIGRATE_UNMOVABLE     0  /* 不可移动页 */
#define MIGRATE_RECLAIMABLE   1  /* 可回收页 */
#define MIGRATE_MOVABLE       2  /* 可移动页 */
#define MIGRATE_PCPTYPES      3  /* the number of types on the pcp lists */
#define MIGRATE_RESERVE       3  /* 保留页,用于紧急分配 */
#define MIGRATE_ISOLATE       4  /* can't allocate from here */
#define MIGRATE_TYPES         5  /* 页的类型数目 */
如果内核无法满足针对某一给定迁移类型的分配请求时,会根据提供的备用列表指定接下来应使用哪种迁移类型:

static int fallbacks[MIGRATE_TYPES][MIGRATE_TYPES-1] = {
	[MIGRATE_UNMOVABLE]   = { MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE,   MIGRATE_RESERVE },
	[MIGRATE_RECLAIMABLE] = { MIGRATE_UNMOVABLE,   MIGRATE_MOVABLE,   MIGRATE_RESERVE },
	[MIGRATE_MOVABLE]     = { MIGRATE_RECLAIMABLE, MIGRATE_UNMOVABLE, MIGRATE_RESERVE },
	[MIGRATE_RESERVE]     = { MIGRATE_RESERVE,     MIGRATE_RESERVE,   MIGRATE_RESERVE }, /* Never used */
};
内核提供了两个标志,分别用于表示分配的内存是可移动的(__GFP_MOVABLE)或可回收的(__GFP_RECLAIMABLE),如果这些标志都没有设置,则分配的内存假定为不可以移动的。
8、初始化内存域和结点数据结构

体系结构相关代码需要在启动期间建立以下信息:

a、系统中各个内存域的页帧边界,保存在 max_zone_pfn 数组

b、各结点页帧的分配情况,保存在全局变量 early_node_map
之后便由 free_area_init_nodes 处理体系结构代码提供的信息。

9、内核中不连续页的分配

在内核中使用了与用户空间相同的分页机制,内核分配了其虚拟地址空间的一部分,用于建立连续映射。紧随着映射的前 892M 物理内存,在插入 8M 安全间隙后,是一个用于管理不连续内存的区域。vmalloc是一个接口函数,内核代码使用它来分配在虚拟内存中连续但在物理内存中不连续的内存。在管理虚拟内存中的 vmalloc 区域时,内核必须跟踪哪些子区域被使用、哪些是空闲的,因此定义数据结构struct vm_struct 将所有使用的子区域保存在一个链表中:

struct vm_struct {
	struct vm_struct    *next;     /* 将vmalloc区域中所有子区域保存在一个单链表中 */
	void                *addr;     /* 定义了分配的子区域在虚拟地址空间中的起始地址 */
	unsigned long       size;      /* 表示该子区域的长度 */
	unsigned long       flags;     /* 存储了该子区域的标志集合 */
	struct page         **pages;   /* 指向page指针的数组,每个数组成员都表示一个映射到虚拟地址空间中的物理内存页的page实例 */
	unsigned int        nr_pages;  /* 指定了pages中数组项的数目,即涉及的内存页的数目 */
	unsigned long       phys_addr; /* 仅当用ioremap映射了由物理地址描述的物理内存区域时才需要 */
	void                *caller;   /* 回调函数 */
};
10、slab 分配器
当需要分配小于1个页面的物理内存时,会用到 slab 分配器。提供小内存块不是 slab 分配器的唯一任务,由于结构上的特点,它也用作一个缓存,主要针对经常分配并释放的对象,通过建立 slab 缓存,内核能够储备一些对象供后续使用。slab 分配器将释放的内存块保存在一个内部列表中,并不马上返回给伙伴系统,在请求为该类对象分配一个新实例时,会使用最近释放的内存块。经常使用的内核对象保存在 cpu 的高速缓存中,从 slab 分配器的角度进行衡量,伙伴系统的高速缓存和 TLB 占用较大,这是一个负面效应,因为这会导致不重要的数据驻留在 cpu 高速缓存中,而重要的数据被置换到内存。通过 slab 着色(slab coloring),slab 分配器能够均匀地分布对象,以实现均匀的缓存利用。尽管 slab 分配器对许多可能的工作负荷都工作良好,但也有一些情形它无法提供最优性能:如嵌入式系统,slab 分配器的代码量和复杂性太高;配备有大量物理内存的大规模并行系统中,其数据结构会占用大量内存。因此在2.6的内核中增加了 slab 分配器的两个替代品: slob 分配器用于嵌入式系统与 slub 分配器用于大型计算机。内核的其余部分无需关注底层选择使用了哪个分配器,所有分配器的前端接口都是相同的,每个分配器都必须实现一组特定的函数,用于内存分配和缓存:

a、kmalloc、__kmalloc kmalloc_node 是一般的内存分配函数

b、kmem_cache_alloc、kmem_cache_alloc_node 提供特定类型的内核缓存

一般情况下内核代码申请内存的流程如下:

内核代码 -> 标准接口(slab、slob、slub 分配器) -> 伙伴系统 -> 物理页帧

从程序员的角度来看,建立和使用缓存的过程如下:首先用 kmem_cache_create 建立一个适当的缓存,接下来即可使用 kmem_cache_alloc 和 kmem_cache_free 分配和释放其中包含的对象,slab 分配器负责完成于伙伴系统的交互,来分配所需的页。在内核中出来常用的一些缓存对象外还有一些对象名如 kmalloc-size,长度为2的幂次。这些对象是 kmalloc 函数的基础,每次调用 kmalloc 时,内核找到最合适的缓存,并从中分配一个对象满足请求(如果没有大小刚好合适的对象则分配稍大的对象)。每个缓存由 kmem_cache 结构的一个实例表示,该结构内容如下:

struct kmem_cache {
	/* 1) per-cpu 数据,在每次分配/释放期间都会访问 */
	struct array_cache *array[NR_CPUS];
	/* 2) 可调整的缓存参数,由 cache_chain_mutex 保护 */
	unsigned int batchcount;
	unsigned int limit;
	unsigned int shared;

	unsigned int buffer_size;
	u32 reciprocal_buffer_size;
	/* 3) 后端每次分配和释放内存时都会访问 */

	unsigned int flags;		/* constant flags */
	unsigned int num;		/* # of objs per slab */

	/* 4) 缓存的增长/缩减 */
	/* 每个slab中的页数,取以2为底数的对数 */
	unsigned int gfporder;

	/* 强制的 GFP 标志,e.g. GFP_DMA */
	gfp_t gfpflags;

	size_t colour;			/* 缓存着色范围 */
	unsigned int colour_off;	/* 着色偏移 */
	struct kmem_cache *slabp_cache;
	unsigned int slab_size;
	unsigned int dflags;		/* 动态标志 */

	/* 构造函数 */
	void (*ctor)(void *obj);

	/* 5) 缓存创建/删除 */
	const char *name;
	struct list_head next;

	struct kmem_list3 *nodelists[MAX_NUMNODES];
};

array 是一个指向数组的指针,每个数组项都对应于系统中的一个 cpu,每个数组项都包含了另一个指针,指向 array_cache 结构的实例;batchcount 指定了在 per-CPU 列表为空的情况下,从缓存的 slab 中获取对象的数目,它还表示在缓存增长时分配的对象数目;limit 指定了 per-CPU 列表中保存的对象的最大数目,如果超过该值,内核会将 batchcount 个对象返回到 slab;buffer_size 指定了缓存中管理的对象的长度;第3、第4部分包含了管理 slab 所需的全部变量,在填充或清空 per-CPU 缓存时需要访问这两部分;flags 定义了缓存的全局性质;gfporder 指定了 slab 包含的页数目以2为底的对数;colour指定了颜色的最大数目;colour_off 是基本偏移量乘以颜色值获得的绝对偏移量;ctor 是一个指针;name 是该缓存的名称,在 cat /proc/slabinfo中或用到;next 用于将 kmem_cache 的所有实例保存在全局链表 cache_chain 中;nodelists是一个数组,每个数组项对于于系统中一个可能的内存结点并包含struct kmem_list3 的一个实例,该结构中有3个 slab 列表(完全用尽、部分空闲、完全空闲);内核对每个 cpu 都提供了一个struct array_cache 实例,该结构定义如下:

struct array_cache {
	unsigned int avail;  /* 保存了当前可用对象的数目 */
	unsigned int limit;
	unsigned int batchcount;
	unsigned int touched;
	spinlock_t lock;
	void *entry[];       /* 伪数组,只是为了便于访问内存中array_cache实例之后缓存中的各个对象 */
};
其中 limit 和 batchcount 的作用和 kmem_cache 相同。当从缓存移除一个对象时,将 touched 设置为1,而缓存收缩时将 touched 设置为0。用于管理 slab 链表的表头保存在一个独立的数据结构中,定义如下:

struct kmem_list3 {
	struct list_head slabs_partial;	/* 首先是部分空闲链表,以便生成性能更好的汇编代码 */
	struct list_head slabs_full;
	struct list_head slabs_free;
	unsigned long free_objects;
	unsigned int free_limit;
	unsigned int colour_next;       /* 各结点缓存着色 */
	spinlock_t list_lock;
	struct array_cache *shared;     /* 结点内共享 */
	struct array_cache **alien;     /* 在其他结点上 */
	unsigned long next_reap;        /* 无需锁定即可更新 */
	int free_touched;               /* 无需锁定即可更新 */
};

free_objects 表示部分空闲和全部空闲列表中所有空闲对象的总数;free_touched 表示缓存是否是活动的;next_reap 定义了内核在两次尝试收缩缓存之间,必须经过的时间间隔;free_limit 指定了所有 slab 上容许未使用对象的最大数目。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值