Linux 内存管理窥探(4):zone 数据结构
本文来自,https://blog.csdn.net/gatieme/article/details/52384529,感谢作者的无私分享。 前面章节的了解,zone 结构是物理内存管理中 node 管理的物理内存区域,也就是 struct zone 结构体,这里来分析这个结构体:内存节点又被划分为内存管理区域, 一个管理区域通过 struct zone 描述, 用以表示内存的某个范围...
本文来自,https://blog.csdn.net/gatieme/article/details/52384529,感谢作者的无私分享。
前面章节的了解,zone 结构是物理内存管理中 node 管理的物理内存区域,也就是 struct zone 结构体,这里来分析这个结构体:
内存节点又被划分为内存管理区域, 一个管理区域通过 struct zone 描述, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存,是系统中预留的可用内存空间, 不能被内核直接映射。
管理内存域 | 描述 |
ZONE_DMA | 标记了适合DMA的内存域. 该区域的长度依赖于处理器类型. 这是由于古老的ISA设备强加的边界. 但是为了兼容性, 现代的计算机也可能受此影响 |
ZONE_DMA32 | 标记了使用32位地址字可寻址, 适合DMA的内存域. 显然, 只有在53位系统中ZONE_DMA32才和ZONE_DMA有区别, 在32位系统中, 本区域是空的, 即长度为0MB, 在Alpha和AMD64系统上, 该内存的长度可能是从0到4GB |
ZONE_NORMAL | 标记了可直接映射到内存段的普通内存域. 这是在所有体系结构上保证会存在的唯一内存区域, 但无法保证该地址范围对应了实际的物理地址. 例如, 如果AMD64系统只有两2G内存, 那么所有的内存都属于ZONE_DMA32范围, 而ZONE_NORMAL则为空 |
ZONE_HIGHMEM | 标记了超出内核虚拟地址空间的物理内存段, 因此这段地址不能被内核直接映射 |
ZONE_MOVABLE | 内核定义了一个伪内存域ZONE_MOVABLE, 在防止物理内存碎片的机制memory migration中需要使用该内存域. 供防止物理内存碎片的极致使用 |
ZONE_DEVICE | 为支持热插拔设备而分配的Non Volatile Memory非易失性内存 |
MAX_NR_ZONES | 充当结束标记, 在内核中想要迭代系统中所有内存域, 会用到该常亮 |
struct zone 定义在 linux/mmzone.h 中:
struct zone
{
/* Read-mostly fields */
/* zone watermarks, access with *_wmark_pages(zone) macros */
unsigned long watermark[NR_WMARK];
unsigned long nr_reserved_highatomic;
/*
* We don't know if the memory that we're going to allocate will be
* freeable or/and it will be released eventually, so to avoid totally
* wasting several GB of ram we must reserve some of the lower zone
* memory (otherwise we risk to run OOM on the lower zones despite
* there being tons of freeable ram on the higher zones). This array is
* recalculated at runtime if the sysctl_lowmem_reserve_ratio sysctl
* changes.
* 分别为各种内存域指定了若干页
* 用于一些无论如何都不能失败的关键性内存分配。
*/
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.
* 不活动页的比例,
* 接着是一些很少使用或者大部分情况下是只读的字段:
* wait_table wait_table_hash_nr_entries wait_table_bits
* 形成等待列队,可以等待某一页可供进程使用 */
unsigned int inactive_ratio;
/* 指向这个zone所在的pglist_data对象 */
struct pglist_data *zone_pgdat;
/*/这个数组用于实现每个CPU的热/冷页帧列表。内核使用这些列表来保存可用于满足实现的“新鲜”页。但冷热页帧对应的高速缓存状态不同:有些页帧很可能在高速缓存中,因此可以快速访问,故称之为热的;未缓存的页帧与此相对,称之为冷的。*/
struct per_cpu_pageset __percpu *pageset;
/*
* This is a per-zone reserve of pages that are not available
* to userspace allocations.
* 每个区域保留的不能被用户空间分配的页面数目
*/
unsigned long totalreserve_pages;
#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;
/*
* spanned_pages is the total pages spanned by the zone, including
* holes, which is calculated as:
* spanned_pages = zone_end_pfn - zone_start_pfn;
*
* present_pages is physical pages existing within the zone, which
* is calculated as:
* present_pages = spanned_pages - absent_pages(pages in holes);
*
* managed_pages is present pages managed by the buddy system, which
* is calculated as (reserved_pages includes pages allocated by the
* bootmem allocator):
* managed_pages = present_pages - reserved_pages;
*
* So present_pages may be used by memory hotplug or memory power
* management logic to figure out unmanaged pages by checking
* (present_pages - managed_pages). And managed_pages should be used
* by page allocator and vm scanner to calculate all kinds of watermarks
* and thresholds.
*
* Locking rules:
*
* zone_start_pfn and spanned_pages are protected by span_seqlock.
* It is a seqlock because it has to be read outside of zone->lock,
* and it is done in the main allocator path. But, it is written
* quite infrequently.
*
* The span_seq lock is declared along with zone->lock because it is
* frequently read in proximity to zone->lock. It's good to
* give them a chance of being in the same cacheline.
*
* Write access to present_pages at runtime should be protected by
* mem_hotplug_begin/end(). Any reader who can't tolerant drift of
* present_pages should get_online_mems() to get a stable value.
*
* Read access to managed_pages should be safe because it's unsigned
* long. Write access to zone->managed_pages and totalram_pages are
* protected by managed_page_count_lock at runtime. Idealy only
* adjust_managed_page_count() should be used instead of directly
* touching zone->managed_pages and totalram_pages.
*/
unsigned long managed_pages;
unsigned long spanned_pages; /* 总页数,包含空洞 */
unsigned long present_pages; /* 可用页数,不包哈空洞 */
/* 指向管理区的传统名字, "DMA", "NROMAL"或"HIGHMEM" */
const char *name;
#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
/*
* wait_table -- the array holding the hash table
* wait_table_hash_nr_entries -- the size of the hash table array
* wait_table_bits -- wait_table_size == (1 << wait_table_bits)
*
* The purpose of all these is to keep track of the people
* waiting for a page to become available and make them
* runnable again when possible. The trouble is that this
* consumes a lot of space, especially when so few things
* wait on pages at a given time. So instead of using
* per-page waitqueues, we use a waitqueue hash table.
*
* The bucket discipline is to sleep on the same queue when
* colliding and wake all in that wait queue when removing.
* When something wakes, it must check to be sure its page is
* truly available, a la thundering herd. The cost of a
* collision is great, but given the expected load of the
* table, they should be so rare as to be outweighed by the
* benefits from the saved space.
*
* __wait_on_page_locked() and unlock_page() in mm/filemap.c, are the
* primary users of these fields, and in mm/page_alloc.c
* free_area_init_core() performs the initialization of them.
*/
/* 进程等待队列的散列表, 这些进程正在等待管理区中的某页 */
wait_queue_head_t *wait_table;
/* 等待队列散列表中的调度实体数目 */
unsigned long wait_table_hash_nr_entries;
/* 等待队列散列表数组大小, 值为2^order */
unsigned long wait_table_bits;
ZONE_PADDING(_pad1_)
/* free areas of different sizes
页面使用状态的信息,以每个bit标识对应的page是否可以分配
是用于伙伴系统的,每个数组元素指向对应阶也表的数组开头
以下是供页帧回收扫描器(page reclaim scanner)访问的字段
scanner会跟据页帧的活动情况对内存域中使用的页进行编目
如果页帧被频繁访问,则是活动的,相反则是不活动的,
在需要换出页帧时,这样的信息是很重要的: */
struct free_area free_area[MAX_ORDER];
/* zone flags, see below 描述当前内存的状态, 参见下面的enum zone_flags结构 */
unsigned long flags;
/* Write-intensive fields used from the page allocator, 保存该描述符的自旋锁 */
spinlock_t lock;
ZONE_PADDING(_pad2_)
/* Write-intensive fields used by page reclaim */
/* Fields commonly accessed by the page reclaim scanner */
spinlock_t lru_lock; /* LRU(最近最少使用算法)活动以及非活动链表使用的自旋锁 */
struct lruvec lruvec;
/*
* 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
* 在空闲页的数目少于这个点percpu_drift_mark的时候
* 当读取和空闲页数一样的内存页时,系统会采取额外的工作,
* 防止单CPU页数漂移,从而导致水印被破坏。
*/
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
bool contiguous;
ZONE_PADDING(_pad3_)
/* Zone statistics 内存域的统计信息, 参见后面的enum zone_stat_item结构 */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
} ____cacheline_internodealigned_in_smp;
字段 | 描述 |
watermark | 每个 zone 在系统启动时会计算出 3 个水位值, 分别为 WMAKR_MIN, WMARK_LOW, WMARK_HIGH 水位, 这在页面分配器和 kswapd 页面回收中会用到 |
lowmem_reserve[MAX_NR_ZONES] | zone 中预留的内存, 为了防止一些代码必须运行在低地址区域,所以事先保留一些低地址区域的内存 |
pageset | page管理的数据结构对象,内部有一个page的列表(list)来管理。每个CPU维护一个page list,避免自旋锁的冲突。这个数组的大小和NR_CPUS(CPU的数量)有关,这个值是编译的时候确定的 |
lock | 对zone并发访问的保护的自旋锁 |
free_area[MAX_ORDER] | 页面使用状态的信息,以每个bit标识对应的page是否可以分配 |
lru_lock | LRU(最近最少使用算法)的自旋锁 |
wait_table | 待一个page释放的等待队列哈希表。它会被wait_on_page(),unlock_page()函数使用. 用哈希表,而不用一个等待队列的原因,防止进程长期等待资源 |
wait_table_hash_nr_entries | 哈希表中的等待队列的数量 |
zone_pgdat | 指向这个zone所在的pglist_data对象 |
zone_start_pfn | 和node_start_pfn的含义一样。这个成员是用于表示zone中的开始那个page在物理内存中的位置的present_pages, spanned_pages: 和node中的类似的成员含义一样 |
name | zone的名字,字符串表示: “DMA”,”Normal” 和”HighMem” |
totalreserve_pages | 每个区域保留的不能被用户空间分配的页面数目 |
ZONE_PADDING | 由于自旋锁频繁的被使用,因此为了性能上的考虑,将某些成员对齐到cache line中,有助于提高执行的性能。使用这个宏,可以确定zone->lock,zone->lru_lock,zone->pageset这些成员使用不同的cache line. |
managed_pages | zone 中被伙伴系统管理的页面数量 |
spanned_pages | zone 中包含的页面数量 |
present_pages | zone 中实际管理的页面数量. 对一些体系结构来说, 其值和 spanned_pages 相等 |
lruvec | LRU 链表集合 |
vm_stat | zone 计数 |
ZONE_PADDING将数据保存在高速缓冲行
该结构比较特殊的地方是它由ZONE_PADDING分隔的几个部分. 这是因为堆zone结构的访问非常频繁. 在多处理器系统中, 通常会有不同的CPU试图同时访问结构成员. 因此使用锁可以防止他们彼此干扰, 避免错误和不一致的问题. 由于内核堆该结构的访问非常频繁, 因此会经常性地获取该结构的两个自旋锁zone->lock和zone->lru_lock
由于 struct zone 结构经常被访问到, 因此这个数据结构要求以 L1 Cache 对齐. 另外, 这里的
ZONE_PADDING( ) 让 zone->lock 和 zone_lru_lock 这两个很热门的锁可以分布在不同的 Cahe Line 中.
一个内存 node 节点最多也就几个 zone, 因此 zone 数据结构不需要像 struct page 一样关心数据结构的
小, 因此这里的 ZONE_PADDING( ) 可以理解为用空间换取时间(性能). 在内存管理开发过程中, 内核开发者
渐发现有一些自选锁竞争会非常厉害, 很难获取. 像 zone->lock 和 zone->lru_lock 这两个锁有时需要同
获取锁. 因此保证他们使用不同的 Cache Line 是内核常用的一种优化技巧
那么数据保存在CPU高速缓存中, 那么会处理得更快速. 高速缓冲分为行, 每一行负责不同的内存区. 内核使用ZONE_PADDING宏生成”填充”字段添加到结构中, 以确保每个自旋锁处于自身的缓存行中
ZONE_PADDING宏定义在:include/linux/mmzone.h:
/*
* zone->lock and zone->lru_lock are two of the hottest locks in the kernel.
* So add a wild amount of padding here to ensure that they fall into separate
* cachelines. There are very few zone structures in the machine, so space
* consumption is not a concern here.
*/
#if defined(CONFIG_SMP)
struct zone_padding
{
char x[0];
} ____cacheline_internodealigned_in_smp;
#define ZONE_PADDING(name) struct zone_padding name;
#else
#define ZONE_PADDING(name)
#endif
内核还用了____cacheline_internodealigned_in_smp,来实现最优的高速缓存行对其方式.该宏定义在,include/linux/cache.h:
#if !defined(____cacheline_internodealigned_in_smp)
#if defined(CONFIG_SMP)
#define ____cacheline_internodealigned_in_smp \
__attribute__((__aligned__(1 << (INTERNODE_CACHE_SHIFT))))
#else
#define ____cacheline_internodealigned_in_smp
#endif
#endif
水线 watermark[NR_WMARK] 与 kswapd 内核线程
Zone的管理调度的一些参数watermarks水印, 水存量很小(MIN)进水量,水存量达到一个标准(LOW)减小进水量,当快要满(HIGH)的时候,可能就关闭了进水口.
WMARK_LOW, WMARK_LOW, WMARK_HIGH就是这个标准。
enum zone_watermarks
{
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};
#define min_wmark_pages(z) (z->watermark[WMARK_MIN])
#define low_wmark_pages(z) (z->watermark[WMARK_LOW])
#define high_wmark_pages(z) (z->watermark[WMARK_HIGH])
当系统中可用内存很少的时候,系统进程kswapd被唤醒, 开始回收释放page, 水印这些参数(WMARK_MIN, WMARK_LOW, WMARK_HIGH)影响着这个代码的行为。
每个zone有三个水平标准:watermark[WMARK_MIN], watermark[WMARK_LOW], watermark[WMARK_HIGH],帮助确定zone中内存分配使用的压力状态:
标准 | 描述 |
watermark[WMARK_MIN] | 当空闲页面的数量达到page_min所标定的数量的时候, 说明页面数非常紧张, 分配页面的动作和kswapd线程同步运行.WMARK_MIN所表示的page的数量值,是在内存初始化的过程中调用free_area_init_core中计算的。这个数值是根据zone中的page的数量除以一个>1的系数来确定的。通常是这样初始化的ZoneSizeInPages/12 |
watermark[WMARK_LOW] | 当空闲页面的数量达到WMARK_LOW所标定的数量的时候,说明页面刚开始紧张, 则kswapd线程将被唤醒,并开始释放回收页面 |
watermark[WMARK_HIGH] | 当空闲页面的数量达到page_high所标定的数量的时候, 说明内存页面数充足, 不需要回收, kswapd线程将重新休眠,通常这个数值是page_min的3倍 |
如果空闲页多于pages_high = watermark[WMARK_HIGH], 则说明内存页面充足, 内存域的状态是理想的
如果空闲页的数目低于pages_low = watermark[WMARK_LOW], 则说明内存页面开始紧张, 内核开始将页患处到硬盘.
如果空闲页的数目低于pages_min = watermark[WMARK_MIN], 则内存页面非常紧张, 页回收工作的压力就比较大
内存域标志
内存 zone 结构体中的 flag 字段描述了内存域的当前状态。
这个 flag 使用 enum zone_flags 来进行表示,他定义在
enum zone_flags
{
ZONE_RECLAIM_LOCKED, /* prevents concurrent reclaim */
ZONE_OOM_LOCKED, /* zone is in OOM killer zonelist 内存域可被回收*/
ZONE_CONGESTED, /* zone has many dirty pages backed by
* a congested BDI
*/
ZONE_DIRTY, /* reclaim scanning has recently found
* many dirty file pages at the tail
* of the LRU.
*/
ZONE_WRITEBACK, /* reclaim scanning has recently found
* many pages under writeback
*/
ZONE_FAIR_DEPLETED, /* fair zone policy batch depleted */
};
flag标识 | 描述 |
ZONE_RECLAIM_LOCKED | 防止并发回收, 在SMP上系统, 多个CPU可能试图并发的回收1个内存域. ZONE_RECLAIM_LCOKED标志可防止这种情况: 如果一个CPU在回收某个内存域, 则设置该标识. 这防止了其他CPU的尝试 |
ZONE_OOM_LOCKED | 用于某种不走运的情况: 如果进程消耗了大量的内存, 致使必要的操作都无法完成, 那么内核会尝试杀死消耗内存最多的进程, 以获取更多的空闲页, 该标志可以放置多个CPU同时进行这种操作 |
ZONE_CONGESTED | 标识当前区域中有很多脏页 |
ZONE_DIRTY | 用于标识最近的一次页面扫描中, LRU算法发现了很多脏的页面 |
ZONE_WRITEBACK | 最近的回收扫描发现有很多页在写回 |
ZONE_FAIR_DEPLETED | 公平区策略耗尽(没懂) |
内存域统计信息 vm_stat
内存域struct zone的vm_stat维护了大量有关该内存域的统计信息. 由于其中维护的大部分信息曲面没有多大意义:
vm_stat的统计信息由 enum zone_stat_item 枚举变量标识:
enum zone_stat_item
{
/* First 128 byte cacheline (assuming 64 bit words) */
NR_FREE_PAGES,
NR_ALLOC_BATCH,
NR_LRU_BASE,
NR_INACTIVE_ANON = NR_LRU_BASE, /* must match order of LRU_[IN]ACTIVE */
NR_ACTIVE_ANON, /* " " " " " */
NR_INACTIVE_FILE, /* " " " " " */
NR_ACTIVE_FILE, /* " " " " " */
NR_UNEVICTABLE, /* " " " " " */
NR_MLOCK, /* mlock()ed pages found and moved off LRU */
NR_ANON_PAGES, /* Mapped anonymous pages */
NR_FILE_MAPPED, /* pagecache pages mapped into pagetables.
only modified from process context */
NR_FILE_PAGES,
NR_FILE_DIRTY,
NR_WRITEBACK,
NR_SLAB_RECLAIMABLE,
NR_SLAB_UNRECLAIMABLE,
NR_PAGETABLE, /* used for pagetables */
NR_KERNEL_STACK,
/* Second 128 byte cacheline */
NR_UNSTABLE_NFS, /* NFS unstable pages */
NR_BOUNCE,
NR_VMSCAN_WRITE,
NR_VMSCAN_IMMEDIATE, /* Prioritise for reclaim when writeback ends */
NR_WRITEBACK_TEMP, /* Writeback using temporary buffers */
NR_ISOLATED_ANON, /* Temporary isolated pages from anon lru */
NR_ISOLATED_FILE, /* Temporary isolated pages from file lru */
NR_SHMEM, /* shmem pages (included tmpfs/GEM pages) */
NR_DIRTIED, /* page dirtyings since bootup */
NR_WRITTEN, /* page writings since bootup */
NR_PAGES_SCANNED, /* pages scanned since last reclaim */
#ifdef CONFIG_NUMA
NUMA_HIT, /* allocated in intended node */
NUMA_MISS, /* allocated in non intended node */
NUMA_FOREIGN, /* was intended here, hit elsewhere */
NUMA_INTERLEAVE_HIT, /* interleaver preferred this zone */
NUMA_LOCAL, /* allocation from local node */
NUMA_OTHER, /* allocation from other node */
#endif
WORKINGSET_REFAULT,
WORKINGSET_ACTIVATE,
WORKINGSET_NODERECLAIM,
NR_ANON_TRANSPARENT_HUGEPAGES,
NR_FREE_CMA_PAGES,
NR_VM_ZONE_STAT_ITEMS
};
内核提供了很多方式来获取当前内存域的状态信息, 这些函数大多定义在:include/linux/vmstat.h
Zone等待队列表(zone wait queue table)
struct zone中实现了一个等待队列, 可用于等待某一页的进程, 内核将进程排成一个列队, 等待某些条件. 在条件变成真时, 内核会通知进程恢复工作:
struct zone
{
---
wait_queue_head_t *wait_table;
unsigned long wait_table_hash_nr_entries;
unsigned long wait_table_bits;
---
}
字段 | 描述 |
wait_table | 待一个page释放的等待队列哈希表。它会被wait_on_page(),unlock_page()函数使用. 用哈希表,而不用一个等待队列的原因,防止进程长期等待资源 |
wait_table_hash_nr_entries | 哈希表中的等待队列的数量 |
wait_table_bits | 等待队列散列表数组大小, wait_table_size == (1 << wait_table_bits) |
冷热页与Per-CPU上的页面高速缓存
内核经常请求和释放单个页框. 为了提升性能, 每个内存管理区都定义了一个每CPU(Per-CPU)的页面高速缓存. 所有”每CPU高速缓存”包含一些预先分配的页框, 他们被定义满足本地CPU发出的单一内存请求
struct zone 的 pageset 成员用于实现冷热分配器(hot-n-cold allocator)
struct zone
{
...
struct per_cpu_pageset __percpu *pageset;
...
};
内核说页面是热的, 意味着页面已经加载到CPU的高速缓存, 与在内存中的页相比, 其数据访问速度更快. 相反, 冷页则不再高速缓存中. 在多处理器系统上每个CPU都有一个或者多个告诉缓存. 各个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
};
该结构由一个 per_cpu_pages pcp 变量组成, 该数据结构定义如下,
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 页的链表*/
struct list_head lists[MIGRATE_PCPTYPES];
};
字段 | 描述 |
count | 记录了与该列表相关的页的数目 |
high | 是一个水印. 如果count的值超过了high, 则表明列表中的页太多了 |
batch | 如果可能, CPU的高速缓存不是用单个页来填充的, 而是欧诺个多个页组成的块, batch作为每次添加/删除页时多个页组成的块大小的一个参考值 |
list | 一个双链表, 保存了当前CPU的冷页或热页, 可使用内核的标准方法处理 |
在内核中只有一个子系统会积极的尝试为任何对象维护per-cpu上的list链表, 这个子系统就是slab分配器.
struct per_cpu_pageset具有一个字段, 该字段
struct per_cpu_pages则维护了链表中目前已有的一系列页面, 高极值和低极值决定了何时填充该集合或者释放一批页面, 变量决定了一个块中应该分配多少个页面, 并最后决定在页面前的实际链表中分配多少各页面
zone_start_pfn:内存域的第一个页帧
struct zone中通过 zone_start_pfn 成员标记了内存管理区的页面地址.
然后内核也通过一些全局变量标记了物理内存所在页面的偏移, 这些变量定义在:
unsigned long max_low_pfn;
unsigned long min_low_pfn;
unsigned long max_pfn;
unsigned long long max_possible_pfn;
pfn (page frame number)是物理内存以Page为单位的偏移量
变量 | 描述 |
max_low_pfn | x86中,max_low_pfn变量是由find_max_low_pfn函数计算并且初始化的,它被初始化成ZONE_NORMAL的最后一个page的位置。这个位置是kernel直接访问的物理内存, 也是关系到kernel/userspace通过“PAGE_OFFSET宏”把线性地址内存空间分开的内存地址位置 |
min_low_pfn | 系统可用的第一个pfn是min_low_pfn变量, 开始与_end标号的后面, 也就是kernel结束的地方.在文件mm/bootmem.c中对这个变量作初始化 |
max_pfn | 系统可用的最后一个PFN是max_pfn变量, 这个变量的初始化完全依赖与硬件的体系结构. |
x86的系统中, find_max_pfn函数通过读取e820表获得最高的page frame的数值, 同样在文件mm/bootmem.c中对这个变量作初始化。e820表是由BIOS创建的
内存域之间的层级结构
当前结点与系统中其他结点的内存域之前存在一种等级次序
我们考虑一个例子, 其中内核想要分配高端内存.
它首先企图在当前结点的高端内存域找到一个大小适当的空闲段. 如果失败, 则查看该结点的普通内存域. 如果还失败, 则试图在该结点的DMA内存域执行分配.
如果在3个本地内存域都无法找到空闲内存, 则查看其他结点. 在这种情况下, 备
选结点应该尽可能靠近主结点, 以最小化由于访问非本地内存引起的性能损失.
内核定义了内存的一个层次结构, 首先试图分配”廉价的”内存. 如果失败, 则根据访问速度和容量, 逐渐尝试分配”更昂贵的”内存.
高端内存是最廉价的, 因为内核没有任何部份依赖于从该内存域分配的内存. 如果高端内存域用尽, 对内核没有任何副作用, 这也是优先分配高端内存的原因.
其次是普通内存域, 这种情况有所不同. 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域.
因此如果普通内存完全用尽, 那么内核会面临紧急情况. 所以只要高端内存域的内存没有用尽, 都不会从普通内存域分配内存.
最昂贵的是DMA内存域, 因为它用于外设和系统之间的数据传输. 因此从该内存域分配内存是最后一招.
内核还针对当前内存结点的备选结点, 定义了一个等级次序. 这有助于在当前结点所有内存域的内存都用尽时, 确定一个备选结点。内核使用 pg_data_t 中的 zone_list 数组来描述层次结构
typedef struct pglist_data {
struct zonelist node_zonelists[MAX_ZONELISTS];
/* ...... */
} pg_data_t;
node_zonelists数组对每种可能的内存域类型, 都配置了一个独立的数组项,该数组项的大小MAX_ZONELISTS用一个匿名的枚举常量定义:
enum
{
ZONELIST_FALLBACK, /* zonelist with fallback */
#ifdef CONFIG_NUMA
/*
* The NUMA zonelists are doubled because we need zonelists that
* restrict the allocations to a single node for __GFP_THISNODE.
*/
ZONELIST_NOFALLBACK, /* zonelist without fallback (__GFP_THISNODE) */
#endif
MAX_ZONELISTS
};
我们会发现在UMA结构下, 数组大小MAX_ZONELISTS = 1, 因为只有一个内存结点,zonelist中只会存储一个ZONELIST_FALLBACK 类型的结构,但是NUMA下需要多余的ZONELIST_NOFALLBACK用以表示当前结点的信息
pg_data_t->node_zonelists 数组项用 struct zonelis 结构体定义, 该结构包含了类型为 struct zoneref 的一个备用列表由于该备用列表必须包括所有结点的所有内存域,因此由 MAX_NUMNODES * MAX_NZ_ZONES 项组成,外加一个用于标记列表结束的空指针:
/* Maximum number of zones on a zonelist */
#define MAX_ZONES_PER_ZONELIST (MAX_NUMNODES * MAX_NR_ZONES)
/*
* One allocation request operates on a zonelist. A zonelist
* is a list of zones, the first one is the 'goal' of the
* allocation, the other zones are fallback zones, in decreasing
* priority.
*
* To speed the reading of the zonelist, the zonerefs contain the zone index
* of the entry being read. Helper functions to access information given
* a struct zoneref are
*
* zonelist_zone() - Return the struct zone * for an entry in _zonerefs
* zonelist_zone_idx() - Return the index of the zone for an entry
* zonelist_node_idx() - Return the index of the node for an entry
*/
struct zonelist {
struct zoneref _zonerefs[MAX_ZONES_PER_ZONELIST + 1];
};
而这个 struct zoneref 的定义为:
/*
* This struct contains information about a zone in a zonelist. It is stored
* here to avoid dereferences into large structures and lookups of tables
*/
struct zoneref {
struct zone *zone; /* Pointer to actual zone */
int zone_idx; /* zone_idx(zoneref->zone) */
};
小结
在linux中,内核也不是对所有物理内存都一视同仁,内核而是把页分为不同的区, 使用区来对具有相似特性的页进行分组。
区的实际使用与体系结构是相关的。linux把系统的内存结点划分区, 一个区包含了若干个内存页面, 形成不同的内存池,这样就可以根据用途进行分配了
需要说明的是,区的划分没有任何物理意义, 只不过是内核为了管理页而采取的一种逻辑上的分组. 尽管某些分配可能需要从特定的区中获得页, 但这并不是说, 某种用途的内存一定要从对应的区来获取,如果这种可供分配的资源不够用了,内核就会占用其他可用去的内存。
更多推荐
所有评论(0)