内存访问分为两种体系结构:一致性内存访问(UMA)和非一致性内存访问(NUMA)。NUMA指CPU对不同内存单元的访问时间可能不一样,因而这些物理内存被划分为几个节点,每个节点里的内存访问时间一致,NUMA体系结构主要存在大型机器、alpha等,嵌入式的基本都是UMA。UMA也使用了节点概念,只是永远都只有1个节点。本文讲的是UMA模型的嵌入式平台,linux版本:3.10.y。
每个节点又将物理内存划分为3个管理区,在x86机器上管理区如下:
ZONE_DMA:0~16MB
ZONE_NORMAL:16MB~896MB
ZONE_HIGHMEM:896~末尾(对于64位,则不需要高端内存,虚拟地址足够直接映射)
而在hi3536嵌入式平台上实际的使用时由于系统mem没有超过896M,因此只有一个管理区ZONE_NORMAL(cat /proc/buddyinfo):
Node 0, zone Normal 70 70 36 22 11 10 2 2 3 2 37
每个管理区管理该内存区域内的所有页面(linux中每个页面的大小为4KB)。
以下linux物理内存组织结构标准的关系图:
在Documentation/arm/memory.txt文件中定义了arm平台的线性地址空间布局情况,其与物理内存关系图:
当用户空间和内核空间比例3:1时,PAGE_OFFSET=0xC0000000。上面橙色虚线地址是一一映射关系,而高于highmem则需要vmalloc申请使用。
PHYS_OFFSET=0x40000000
ZRELADDR == virt_to_phys(PAGE_OFFSET +TEXT_OFFSET) = virt_to_phys (TEXT_ADDR) = 0x40008000
INITRD_PHYS= 0x00800000
PARAMS_PHYS= 0x00000100
high_memory和VMALLOC_START之间保留了8M空间间隙
high_memory=PAGE_OFFSET+ highmem
当系统内存在0~896MB时,highmem=系统内存
当系统内存>896MB时,highmem=896MB,多余的内存需要vmalloc映射访问
VMALLOC_START和VMALLOC_END-1之间用于vmalloc() / ioremap() space映射。
cat /proc/vmallocinfo:
0xfb000000-0xfe190000 51970048iotable_init+0x0/0xb0 phys=10000000 ioremap
映射了CPU寄存器空间
1、节点
每个节点由struct pglist_data定义(include/linux/mmzone.h):
typedef struct pglist_data {
#define MAX_NR_ZONES 3
struct zonenode_zones[MAX_NR_ZONES]; //分别代表3个管理区
#define MAX_ZONELISTS 1
struct zonelistnode_zonelists[MAX_ZONELISTS]; //按照分配时的管理区顺序排列,如果在ZONE_HIGHMEM中分配失败,就有可能还原成ZONE_NORMAL或ZONE_DMA。
int nr_zones; //表示管理区的数目,值为1、2、3。
struct page *node_mem_map; //指向该节点第一个物理页面
struct bootmem_data*bdata; //指向内存引导程序
unsigned longnode_start_pfn; //该节点的起始页编号
//calculate_node_totalpages中对以下两个值进行计算
unsigned long node_present_pages;/* total number of physical pages */
unsigned longnode_spanned_pages; /* total size of physical pagerange, including holes */
int node_id; //节点号,在嵌入式上一般为0
nodemask_treclaim_nodes; /* Nodes allowed toreclaim from */
wait_queue_head_tkswapd_wait; //交换守护进程kswapd使用的等待队列
wait_queue_head_tpfmemalloc_wait;
struct task_struct*kswapd; //指向交换守护进程描述符
int kswapd_max_order;
enum zone_type classzone_idx; //管理区类型
} pg_data_t;
include/linux/mmzone.h中定义了全局节点:
extern struct pglist_data contig_page_data;
#define NODE_DATA(nid) (&contig_page_data)
mm/bootmem.c进行全局节点定义:
struct pglist_data __refdata contig_page_data = {
.bdata =&bootmem_node_data[0]
};
EXPORT_SYMBOL(contig_page_data);
2、管理区
每个管理区由structzone描述(include/linux/mmzone.h):
struct zone {
unsigned longwatermark[NR_WMARK]; //该管理区的三个水平线值,min, low, high
unsigned long percpu_drift_mark;
unsigned long lowmem_reserve[MAX_NR_ZONES]; //每个管理区必须保留的页框数
unsigned long dirty_balance_reserve;
struct per_cpu_pageset __percpu*pageset; //CPU的页面缓存
spinlock_t lock; //保护该管理区的自旋锁
int all_unreclaimable; /* All pagespinned */
struct free_area free_area[MAX_ORDER]; //通过伙伴算法管理的空闲页面
ZONE_PADDING(_pad1_)
spinlock_t lru_lock;
struct lruvec lruvec;
unsigned long pages_scanned; //管理区回收页框时使用的计数器,记录上一次回收一同扫描过的页框
unsigned long flags; /* zone flags, see below */
/* Zone statistics */
atomic_long_t vm_stat[NR_VM_ZONE_STAT_ITEMS];
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; //散列表数组的大小对2取log的结果
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; //管理区的大小不包括洞
unsigned long managed_pages;
const char *name; //管理区名字,DMA、NORMAL orHIGHMEM
}
当系统中的可用内存很少时,守护程序kswapd被唤醒释放页面。每个管理区通过数组watermark来决定唤醒还是睡眠kswapd守护进程,这个数组通过下面的枚举来分别代表page_min, page_low, page_high,他们的之间的关系图如下图:
enum zone_watermarks {
WMARK_MIN,
WMARK_LOW,
WMARK_HIGH,
NR_WMARK
};
page_low:当空闲页面数达到pages_low时,伙伴算法分配器就会唤醒kswapd释放页面。
page_min:当达到pages_min时,kswap没唤醒则唤醒,同时同步进程释放内存,如果申请内存远远超过实际内存,就会出现out_of_memory。page_min包含里管理区最小保留内存,因此这是GFP_ATOMIC还可以分配出内存。
page_high:当释放页面达到这个值,认为该管理区已经平衡,kswapd开始睡眠。
当多个进程对同一个页面进行IO操作(个人理解觉得更应该是有写操作)时,比如页面换入或换出,为了防止访问数据的不一致性,该页面会被锁住,而其他进程则通过wait_on_page()函数被添加到等待队列中。如果每个页面都有等待队列则系统会花费大量的内存存放,linux则是将等待队列存储在管理区的wait_table散列表中,这样就只有一个等待队列。其简单流程图如下:
当申请分配标志为GFP_ATOMIC时则不会睡眠,因为该标志不能睡眠的内存分配标志,用在中断处理程序、下半部、持有自旋锁以及其他不能睡眠的地方。
3、物理页面
系统中的每个物理页面都由structpage用以记录该页面的状态,结构体包含了很多union,因为现在有slab/slob/slub三种分配器使用其中一种即可,定义如下(include/linux/mm_types.h):
struct page {
/* First double word block */
unsigned long flags; //存放页的状态
struct address_space*mapping; //如果低位是clear的,则内存映射到文件或设备,它指向文件或设备节点inode的address_space,或者NULL;如果内存映射到匿名则低位被设置,指向匿名对象objects。
/* Second double word*/
struct {
union {
pgoff_tindex; //页面是文件映射的一部分,它就是页面在文件中的偏移;页面是交换高速缓存一部分,它就是在交换地址空间中address_space的偏移量
void*freelist; //指向slub/slob第一个空闲对象
boolpfmemalloc;
};
union {
unsigned counters;
struct {
union {
atomic_t_mapcount;
struct {/* SLUB */
unsigned inuse:16;
unsigned objects:15;
unsigned frozen:1;
};
intunits; /* SLOB */
};
atomic_t _count; //页的引用计数
};
};
};
/* Third double word block*/
union {
struct list_headlru; //最近最少使用(LRU)链表的链表首部
struct { /* slub per cpu partial pages */
struct page*next; /* Next partial slab */
short int pages;
short intpobjects;
};
struct list_headlist; //slob列表
struct slab *slab_page;/* slab fields */
};
/* Remainder is not doubleword aligned */
union {
unsigned long private;
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; /* Kernel virtual address (NULL if
notkmapped, ie. highmem) */
#endif /* WANT_PAGE_VIRTUAL */
}
Flags的最高ZONES_SHIFT位记录该页面所属的管理区。set_page_zone函数设置页面的管理区。
4、源码解析
linux内核的内存管理分三个阶段。
A. 启动---->bootmem初始化完成为第一阶段。此阶段只能使用memblock_reserve函数分配内存。
B. bootmem初始化完--->buddy完成前。该阶段使用引导内存分配器(boot memoryallocator)分配内存。
C. 全部内存初始化完毕,可以用cache和buddy分配内存。
(1)~(4)点完成第一阶段,该阶段主要在start_kernel—> setup_arch函数实现:
(1)获取总内存
parse_early_param—> early_mem函数根据uboot传递进来的命令行参数mem=size@start计算出起始和内存大小通过arm_add_memory添加到meminfo全局数组里。
这里需要注意parse_early_param之前setup_machine_tags里parse_tags中的parse_tag_mem32也会解析uboot通过tags方法传递进来的系统内存,但是被early_mem覆盖重新计算。
(2)计算高端内存
sanity_check_meminfo(arch/arm/mm/mmu.c)对meminfo里的内存进行判断,是否需要划分为高端内存。
(3)计算保留内存块
保留内存块包括内核(数据段,代码段等)、页目录等占用的内存块。通过arm_memblock_init(arch/arm/mm/init.c)实现:
A. 将meminof内存通过memblock_add加到memblock.memory类型内存块里(包含全部物理内存)
B.通过memblock_reserve将保留内存添加到memblock.reserved类型内存块里
memblock_reserve(__pa(_stext), _end -_stext); 内核通过查看System.map可以看到
arm_mm_memblock_reserve(); //页表
最终结果显示如下:
MEMBLOCK configuration:
memory size = 0xfa00000(250M) reservedsize = 0x6ba3cc
memory.cnt = 0x1
memory[0x0] [0x00000040000000-0x0000004f9fffff], 0xfa00000 bytes
reserved.cnt = 0x2
reserved[0x0] [0x00000040004000-0x00000040007fff], 0x4000 bytes //页表
reserved[0x1] [0x000000400081c0-0x000000406be58b], 0x6b63cc bytes //内核
在伙伴算法完成之前,内存的分配通过该方式进行。
(4)paging_init
该函数arch/arm/mm/mmu.c实现,完成页表设置、管理区zone、设置零页等。
/*
*paging_init() sets up the page tables, initialises the zone memory
*maps, and sets up the zero page, bad page and bad page tables.
*/
void __init paging_init(struct machine_desc*mdesc)
{
void *zero_page;
memblock_set_current_limit(arm_lowmem_limit);
build_mem_type_table();//建立各种类型页表的属性(内存MEMORY类型,设备DEVICE,中断向量表是HIGH_VECTORS),根据不同arm体系进行初始化,这是因为地址转换由MMU硬件单元根据页表处理,每个体系的MMU单元有差别(不是很明白)。
/*以下见第二章页表管理*/
prepare_page_table(); //清除在这之前建立的临时页表,以便下面建立正式链表
map_lowmem(); //为低端内存建立一一的映射表(0~ arm_lowmem_limit),存放在swapper_pg_dir位置,用到前面的页表类型:MT_MEMORY
dma_contiguous_remap(); //建立DMA映射表,类型为:MT_MEMORY_DMA_READY
devicemaps_init(mdesc); //完成ffff0000开始的中断向量映射表,完成CPU IO映射表(从0x10000000~0x13190000就是控制器地址空间)、刷新一下TLB
kmap_init(); //申请kmap高端内存永久映射的页表项,虚拟空间为2M(0xbfe00000 - 0xc0000000),如果未定义高端内存(CONFIG_HIGHMEM)什么也不做。
tcm_init(); //do nothing
//关于页表见第二章,此处返回中断向量表0xffff0000在pgd中的偏移量
top_pmd = pmd_off_k(0xffff0000);
/* allocate the zero page. */
zero_page = early_alloc(PAGE_SIZE);
bootmem_init(); //见下一节重点分析
//一种特殊的页,供初始化为0的数据和写时复制使用
empty_zero_page = virt_to_page(zero_page);
__flush_dcache_page(NULL, empty_zero_page);
}
(4-1)bootmem_init
在arch/arm/mm/init.c中定义,完成buddy管理内存需要的工作。
void __init bootmem_init(void)
{
unsigned longmin, max_low, max_high;
max_low = max_high = 0;
find_limits(&min,&max_low, &max_high); //最小物理页号,低端内存最大物理页号,高端内存最大物理页号
/* 申请低端内存所需位图的空间,然后赋值给节点pgdat->bdata(pgdat = NODE_DATA(0);只有一个节点,实际为一个全局变量contig_page_data)并将位图所在内存空间保留(memblock.reserved),bdata->node_bootmem_map指向位图空间(bdata 为struct bootmem_data);先将memblock.memory内存(所有物理内存)的位图清零—表示未使用,再将memblock.reserved 内存的位图置1—表示使用
node_bootmem_map:cf9f7000,该部分内存空间在建立伙伴系统的时候释放,所以放在内存的末端
*/
arm_bootmem_init(min, max_low);
arm_memory_present(); //do nothing
sparse_init();//do nothing
arm_bootmem_free(min, max_low, max_high); //见下一节介绍
max_low_pfn =max_low - PHYS_PFN_OFFSET; //低端内存的物理页数目
max_pfn = max_high- PHYS_PFN_OFFSET; //高端内存的物理页数目
}
(4-2)arm_bootmem_free
该函数(arch/arm/mm/init.c)先计算zone_size和zhole_size,然后调用free_area_init_node(mm/page_alloc.c),因此这里主要分析free_area_init_node函数:
void __paginginit free_area_init_node(intnid, unsigned long *zones_size,
unsigned long node_start_pfn, unsigned long *zholes_size)
{
pg_data_t *pgdat = NODE_DATA(nid);
pgdat->node_id = nid;
pgdat->node_start_pfn = node_start_pfn;
init_zone_allows_reclaim(nid);
//计算节点的node_spanned_pages和node_present_pages,没有高端内存两者相等
calculate_node_totalpages(pgdat, zones_size, zholes_size);
//申请所有物理页描述结构体(structpage)所需要的内存空间,mem_map= NODE_DATA(0)->node_mem_map指向该空间
alloc_node_mem_map(pgdat); //空间大小:page size:200000//2M
printk("free_area_init_node: node %d, pgdat %08lx, node_mem_map%08lx\n",
nid, (unsigned long)pgdat, (unsigned long)pgdat->node_mem_map);
free_area_init_node:node 0, pgdat c06535c0(内核数据区), node_mem_map c06bf000(内核bss后面申请的一个内存地址)
free_area_init_core(pgdat, zones_size, zholes_size); //见下节
}
(4-3)free_area_init_core
该函数(mm/page_alloc.c)实现如下:
static void __paginginitfree_area_init_core(struct pglist_data *pgdat,
unsigned long *zones_size, unsigned long *zholes_size)
{
enum zone_type j;
int nid = pgdat->node_id;
unsigned long zone_start_pfn = pgdat->node_start_pfn;
int ret;
//初始化自旋锁和相关等待队列
pgdat_resize_init(pgdat);
init_waitqueue_head(&pgdat->kswapd_wait);
init_waitqueue_head(&pgdat->pfmemalloc_wait);
pgdat_page_cgroup_init(pgdat);
//初始化各个管理区
for (j = 0; j < MAX_NR_ZONES; j++) {
struct zone *zone = pgdat->node_zones + j;
unsigned long size, realsize, freesize, memmap_pages;
/*重新计算管理区的大小(减去struct page所占用的内存空间)*/
size = zone_spanned_pages_in_node(nid, j, zones_size);
realsize = freesize = size - zone_absent_pages_in_node(nid, j,
zholes_size);
memmap_pages = calc_memmap_size(size, realsize);
if (freesize >= memmap_pages) {
freesize -= memmap_pages;
}
if (!is_highmem_idx(j))
nr_kernel_pages += freesize;
/* Charge for highmem memmap if there are enough kernel pages */
else if (nr_kernel_pages > memmap_pages * 2)
nr_kernel_pages -= memmap_pages;
nr_all_pages += freesize;
zone->spanned_pages = size;
zone->present_pages = realsize;
/*
* Set an approximate value for lowmem here, it will be adjusted
* when the bootmem allocator frees pages into the buddy system.
* And all highmem pages will be managed by the buddy system.
*/
zone->managed_pages = is_highmem_idx(j) ? realsize : freesize;
zone->name = zone_names[j];
//初始化其他自旋锁
spin_lock_init(&zone->lock);
spin_lock_init(&zone->lru_lock);
zone_seqlock_init(zone);
//指向节点
zone->zone_pgdat = pgdat;
zone_pcp_init(zone); //初始化CPU页面缓存
lruvec_init(&zone->lruvec); //初始化lru
if (!size)
continue;
set_pageblock_order();
setup_usemap(pgdat, zone, zone_start_pfn, size);
//初始化该管理区的wait_table和free_area(伙伴算法)
ret = init_currently_empty_zone(zone, zone_start_pfn,
size, MEMMAP_EARLY);
BUG_ON(ret);
//初始化该管理区的所有物理页结构体struct page
memmap_init(size, nid, j, zone_start_pfn);
zone_start_pfn += size;
}
}
(5)setup_per_cpu_areas
在init/main.c的start_kernel调用,在mm/percpu.c中定义,只有在SMP系统下才有用,在UP不做任何处理。
该函数主要是为每个处理器设置per-cpu数据区域,per_cpu数据由各个CPU独立使用,即使不锁访问,十分有效。per-cpu数据按照不同的CPU类型使用,以将性能低下引发的缓存一致性问题减小到最小。
(6)build_all_zonelists
在init/main.c的start_kernel调用,在mm/page_alloc.c中定义。初始化每个节点内的zonelists。
(7)mm_init 设置内存分配器
在init/main.c中定义,主要用于设设置伙伴算法内存分配器。源码如下:
static void __init mm_init(void)
{
/*
* page_cgroup requires contiguous pages,
* bigger than MAX_ORDER unless SPARSEMEM.
*/
page_cgroup_init_flatmem();
mem_init(); //见下面
kmem_cache_init(); //建立kmem_cache和kmem_cache_node两个高速缓存(slab分配器使用)。
percpu_init_late();
pgtable_cache_init(); //do nothing
vmalloc_init(); //vmalloc分配的内存虚拟地址连续,而物理地址无需连续,这里初始vmalloc要用的相关链表等准备工作。
}
void __init mem_init(void)
{
unsigned long reserved_pages, free_pages;
struct memblock_region *reg;
int i;
max_mapnr = pfn_to_page(max_pfn+ PHYS_PFN_OFFSET) - mem_map;
//以下两者都是释放空闲内存到伙伴系统:memblock的空闲内存,后者是bootmem的空闲内存,其实两者有交叠,伙伴系统会尝试与前后连续页框组成更大的页框块。
/* bootmem分配器核心就是node_bootmem_map这个位图,每一位代表这个node的一个页,当需要分配时就会扫描这个位图,然后获取一段物理页框进行分配,一般都会从开始处向后进行分配,并没有什么特殊的算法在其中。而伙伴系统初始化时页会根据这个位图,将位图中空闲的页释放回到伙伴系统中,而已经分配出去的页则不会在初始化阶段释放回伙伴系统,不过有可能会在系统运行过程中释放回伙伴系统中。*/
free_unused_memmap(&meminfo); //根据代码只有一个bank,所以没做啥事情
totalram_pages+= free_all_bootmem();
//释放高端内存到伙伴系统
free_highpages();
reserved_pages = free_pages = 0;
//统计空闲内存和保留内存并打印出来
for_each_bank(i, &meminfo) {
struct membank *bank = &meminfo.bank[i];
unsigned int pfn1, pfn2;
struct page *page, *end;
pfn1 = bank_pfn_start(bank);
pfn2 = bank_pfn_end(bank);
page = pfn_to_page(pfn1);
end = pfn_to_page(pfn2 - 1) + 1;
do {
if (PageReserved(page))
reserved_pages++;
else if (!page_count(page))
free_pages++;
page++;
} while (page < end);
}
}
(8)伙伴算法建立后物理内存分布
经过上面源码分析后,下图是该阶段物理内存分布情况:
4000:内核全局页表swapper_pg_dir起始地址,共16K
8000~6be58c:内核存放空间(text\data\bss等)
6bf000~6bf000+200000:存放所有页框描述数据结构structpage
f9f7000:存放早期bootmem分配内存使用的位图空间起始地址,建立伙伴算法后会释放掉
916000~fa00000:空闲页表,全部有伙伴系统管理
前面的9304K保留内存永远不释放,也不归伙伴系统管理
内核信息打印如下:
Memory: 250MB = 250MB total
Memory: 246696k/246696k available, 9304k reserved, 0
Virtual kernel memory layout:
vector : 0xffff0000 - 0xffff1000 ( 4kB)
fixmap : 0xfff00000 - 0xfffe0000 ( 896 kB)
vmalloc: 0xd0000000 - 0xff000000 ( 752 MB)
lowmem : 0xc0000000 - 0xcfa00000 ( 250 MB)
pkmap : 0xbfe00000 - 0xc0000000 ( 2MB)
modules: 0xbf000000 - 0xbfe00000 ( 14 MB)
.text: 0xc0008000 - 0xc05e85c8 (6018 kB)
.init: 0xc05e9000 - 0xc0617ec0 ( 188 kB)
.data: 0xc0618000 - 0xc06542c0 ( 241 kB)
.bss: 0xc06542c0 - 0xc06be58c ( 425 kB)