前面已经提到,物理内存被划分为三个区来管理,它们是ZONE_DMA、ZONE_NORMAL 和ZONE_HIGHMEM。每个区都用struct zone_struct结构来表示,定义于include/linux/mmzone.h:
typedef struct zone_struct {
/*
* Commonly accessed fields:
*/
spinlock_t lock;
unsigned long free_pages;
unsigned long pages_min, pages_low, pages_high;
int need_balance;
/*
* free areas of different sizes
*/
free_area_t free_area[MAX_ORDER];
/*
* Discontig memory support fields.
*/
struct pglist_data *zone_pgdat;
struct page *zone_mem_map;
unsigned long zone_start_paddr;
unsigned long zone_start_mapnr;
/*
* rarely used fields:
*/
char *name;
unsigned long size;
} zone_t;
#define ZONE_DMA 0
#define ZONE_NORMAL 1
#define ZONE_HIGHMEM 2
#define MAX_NR_ZONES 3
对struct zone_struct结构中每个域的描述如下:
lock :用来保证对该结构中其它域的串行访问
free_pages :在这个区中现有空闲页的个数
pages_min、pages_low及 pages_high是对这个区最少、此少及最多页面个数的描述
need_balance:与kswapd合在一起使用
free_area:在伙伴分配系统中的位图数组和页面链表
zone_pgdat:本管理区所在的存储节点
zone_mem_map:该管理区的内存映射表
zone_start_paddr:该管理区的起始物理地址
zone_start_mapnr:在mem_map中的索引(或下标)
name:该管理区的名字
size:该管理区物理内存总的大小
其中,free_area_t定义为:
#difine MAX_ORDER 10
type struct free_area_struct {
struct list_head free_list
unsigned int *map
} free_area_t
因此,zone_struct结构中的free_area[MAX_ORDER]是一组“空闲区间”链表。为什么要定义一组而不是一个空闲队列呢?这是因为常常需要成块地在物理空间分配连续的多个页面,所以要按块的大小分别加以管理。因此,在管理区数据结构中既要有一个队列来保持一些离散(连续长度为1)的物理页面,还要有一个队列来保持一些连续长度为2的页面块以及连续长度为4、8、16、…、直至2 MAX_ORDER(即
如前所述,内存中每个物理页面都有一个struct page结构,位于include/linux/mm.h,该结构包含了对物理页面进行管理的所有信息,下面给出具体描述:
typedef struct page {
struct list_head list;
struct address_space *mapping;
unsigned long index;
struct page *next_hash;
atomic_t count;
unsigned long flags;
struct list_head lru;
wait_queue_head_t wait;
struct page **pprev_hash;
struct buffer_head * buffers;
void *virtual;
struct zone_struct *zone;
} mem_map_t;
对每个域的描述如下:
list :指向链表中的下一页
mapping :用来指定我们正在映射的索引节点(inode)
index:在映射表中的偏移
next_hash:指向页高速缓存哈希表中下一个共享的页
count:引用这个页的个数
flags:页面各种不同的属性
lru:用在active_list中
wait:等待这一页的页队列
pprev_hash:与next_hash相对应
buffers:把缓冲区映射到一个磁盘块
zone:页所在的内存管理区
与内存管理区相关的三个主要函数为:
· free_area_init() 函数
· build_zonelists() 函数
· mem_init() 函数
1.free_area_init() 函数
这个函数用来初始化内存管理区并创建内存映射表,定义于mm/page_alloc.c中。
函数原型为:
void free_area_init(unsigned long *zones_size);
void free_area_init_core(int nid, pg_data_t *pgdat,
struct page **gmap,
unsigned long *zones_size,
unsigned long zone_start_paddr,
unsigned long *zholes_size,
struct page *lmem_map);
free_area_init()为封装函数,而free_area_init_core()为真正实现的函数,对该函数详细描述如下:
struct page *p;
unsigned long i, j;
unsigned long map_size;
unsigned long totalpages, offset, realtotalpages;
const unsigned long zone_required_alignment = 1UL << (MAX_ORDER-1);
if (zone_start_paddr & ~PAGE_MASK)
BUG();
检查该管理区的起始地址是否是一个页的边界。
totalpages = 0;
for (i = 0; i < MAX_NR_ZONES; i++) {
unsigned long size = zones_size[i];
totalpages += size;
}
计算本存储节点中页面的个数。
realtotalpages = totalpages;
if (zholes_size)
for (i = 0; i < MAX_NR_ZONES; i++)
realtotalpages -= zholes_size[i];
printk("On node %d totalpages: %lu\n", nid, realtotalpages);
打印除空洞以外的实际页面数。
INIT_LIST_HEAD(&active_list);
INIT_LIST_HEAD(&inactive_list);
初始化循环链表。
/*
* Some architectures (with lots of mem and discontinous memory
* maps) have to search for a good mem_map area:
* For discontigmem, the conceptual mem map array starts from
* PAGE_OFFSET, we need to align the actual array onto a mem map
* boundary, so that MAP_NR works.
*/
map_size = (totalpages + 1)*sizeof(struct page);
if (lmem_map == (struct page *)0) {
lmem_map = (struct page *)
alloc_bootmem_node(pgdat, map_size);
lmem_map = (struct page *)(PAGE_OFFSET +
MAP_ALIGN((unsigned long)lmem_map - PAGE_OFFSET));
}
给局部内存(即本节点中的内存)映射分配空间,并在sizeof(mem_map_t)边界上对齐它。
*gmap = pgdat->node_mem_map = lmem_map;
pgdat->node_size = totalpages;
pgdat->node_start_paddr = zone_start_paddr;
pgdat->node_start_mapnr = (lmem_map - mem_map);
pgdat->nr_zones = 0;
初始化本节点中的域。
/*
* Initially all pages are reserved - free ones are freed
* up by free_all_bootmem() once the early boot process is
* done.
*/
for (p = lmem_map; p < lmem_map + totalpages; p++) {
set_page_count(p, 0);
SetPageReserved(p);
init_waitqueue_head(&p->wait);
memlist_init(&p->list);
}
仔细检查所有的页,并进行如下操作:
· 把页的使用计数(count域)置为0。
· 把页标记为保留。
· 初始化该页的等待队列。
· 初始化链表指针。
offset = lmem_map - mem_map;
变量mem_map是类型为struct pages的全局稀疏矩阵。mem_map下标的起始值取决于第一个节点的第一个管理区。如果第一个管理区的起始地址为0,则下标就从0开始,并且与物理页面号相对应,也就是说,页面号就是mem_map的下标。每一个管理区都有自己的映射表,存放在zone_mem_map中,每个管理区又被映射到它所在的节点node_mem_map中,而每个节点又被映射到管理全局内存的mem_map中。
在上面的这行代码中,offset表示该节点放的内存映射表在全局mem_map中的入口点(下标)。在这里,offset为0,因为在i386上,只有一个节点。
for (j = 0; j < MAX_NR_ZONES; j++) {
这个循环对zone 的域进行初始化。
zone_t *zone = pgdat->node_zones + j;
unsigned long mask;
unsigned long size, realsize;
realsize = size = zones_size[j];
管理区的实际数据是存放在节点中的,因此,让指针指向正确的管理区,并获得该管理区的大小。
if (zholes_size)
realsize -= zholes_size[j];
printk("zone(%lu): %lu pages.\n", j, size);
计算各个区的实际大小,并进行打印。例如,在具有256MB的内存上,上面的输出为:
zone(0): 4096 pages.
zone(1): 61440 pages.
zone(2): 0 pages.
这里,管理区2为0,因为只有256MB的RAM.
zone->size = size;
zone->name = zone_names[j];
zone->lock = SPIN_LOCK_UNLOCKED;
zone->zone_pgdat = pgdat;
zone->free_pages = 0;
zone->need_balance = 0;
初始化管理区中的各个域。
if (!size)
continue;
如果一个管理区的大小为0,就没必要进一步的初始化。
pgdat->nr_zones = j+1;
mask = (realsize / zone_balance_ratio[j]);
if (mask < zone_balance_min[j])
mask = zone_balance_min[j];
else if (mask > zone_balance_max[j])
mask = zone_balance_max[j];
计算合适的平衡比率。
zone->pages_min = mask;
zone->pages_low = mask*2;
zone->pages_high = mask*3;
zone->zone_mem_map = mem_map + offset;
zone->zone_start_mapnr = offset;
zone->zone_start_paddr = zone_start_paddr;
设置该管理区中页面数量的几个界限,并把在全局变量mem_map中的入口点作为zone_mem_map的初值。用全局变量mem_map的下标初始化变量zone_start_mapnr。
if ((zone_start_paddr >> PAGE_SHIFT) &
(zone_required_alignment-1))
printk("BUG: wrong zone alignment, it will crash\n");
for (i = 0; i < size; i++) {
struct page *page = mem_map + offset + i;
page->zone = zone;
if (j != ZONE_HIGHMEM)
page->virtual = __va(zone_start_paddr);
zone_start_paddr += PAGE_SIZE;
}
对该管理区中的每一页进行处理。首先,把struct page结构中的zone域初始化为指向该管理区(zone),如果这个管理区不是ZONE_HIGHMEM,则设置这一页的虚地址(即物理地址 + PAGE_OFFSET)。也就是说,建立起每一页物理地址到虚地址的映射。
offset += size;
把offset增加size,使它指向mem_map中下一个管理区的起始位置。
for (i = 0; ; i++) {
unsigned long bitmap_size;
memlist_init(&zone->free_area[i].free_list);
if (i == MAX_ORDER-1) {
zone->free_area[i].map = NULL;
break;
}
初始化free_area[]链表,把free_area[]中最后一个序号的位图置为NULL。
/*
* Page buddy system uses "index >> (i+1)",
* where "index" is at most "size-1".
*
* The extra "+3" is to round down to byte
* size (8 bits per byte assumption). Thus
* we get "(size-1) >> (i+4)" as the last byte
* we can access.
*
* The "+1" is because we want to round the
* byte allocation up rather than down. So
* we should have had a "+7" before we shifted
* down by three. Also, we have to add one as
* we actually _use_ the last bit (it's [0,n]
* inclusive, not [0,n[).
*
* So we actually had +7+1 before we shift
* down by 3. But (n+8) >> 3 == (n >> 3) + 1
* (modulo overflows, which we do not have).
*
* Finally, we LONG_ALIGN because all bitmap
* operations are on longs.
*/
bitmap_size = (size-1) >> (i+4);
bitmap_size = LONG_ALIGN(bitmap_size+1);
zone->free_area[i].map = (unsigned long *)
alloc_bootmem_node(pgdat, bitmap_size);
}
计算位图的大小,然后调用alloc_bootmem_node给位图分配空间。
}
build_zonelists(pgdat);
在节点中为不同的管理区创建链表。
2.build_zonelists()函数
函数原型:
static inline void build_zonelists(pg_data_t *pgdat)
代码如下:
int i, j, k;
for (i = 0; i <= GFP_ZONEMASK; i++) {
zonelist_t *zonelist;
zone_t *zone;
zonelist = pgdat->node_zonelists + i;
memset(zonelist, 0, sizeof(*zonelist));
获得节点中指向管理区链表的域,并把它初始化为空。
j = 0;
k = ZONE_NORMAL;
if (i & __GFP_HIGHMEM)
k = ZONE_HIGHMEM;
if (i & __GFP_DMA)
k = ZONE_DMA;
把当前管理区掩码与三个可用管理区掩码相“与”,获得一个管理区标识,把它用在下面的switch语句中。
switch (k) {
default:
BUG();
/*
* fallthrough:
*/
case ZONE_HIGHMEM:
zone = pgdat->node_zones + ZONE_HIGHMEM;
if (zone->size) {
#ifndef CONFIG_HIGHMEM
BUG();
#endif
zonelist->zones[j++] = zone;
}
case ZONE_NORMAL:
zone = pgdat->node_zones + ZONE_NORMAL;
if (zone->size)
zonelist->zones[j++] = zone;
case ZONE_DMA:
zone = pgdat->node_zones + ZONE_DMA;
if (zone->size)
zonelist->zones[j++] = zone;
}
给定的管理区掩码指定了优先顺序,我们可以用它找到在switch语句中的入口点。如果掩码为__GFP_DMA,管理区链表zonelist将仅仅包含DMA管理区,如果为__GFP_HIGHMEM,则管理区链表中就会依次有ZONE_HIGHMEM、 ZONE_NORMAL和ZONE_DMA。
zonelist->zones[j++] = NULL;
}
用Null结束链表。
3.mem_init() 函数
这个函数由start_kernel()调用,以对管理区的分配算法进行进一步的初始化,定义于arch/i386/mm/init.c中,具体解释如下:
int codesize, reservedpages, datasize, initsize;
int tmp;
int bad_ppro;
if (!mem_map)
BUG();
#ifdef CONFIG_HIGHMEM
highmem_start_page = mem_map + highstart_pfn;
max_mapnr = num_physpages = highend_pfn;
如果 HIGHMEM 被激活,就要获得HIGHMEM 的起始地址和总的页面数。
#else
max_mapnr = num_physpages = max_low_pfn;
#endif
否则,页面数就是常规内存的页面数。
high_memory = (void *) __va(max_low_pfn * PAGE_SIZE);
获得低区内存中最后一个页面的虚地址。
/* clear the zero-page */
memset(empty_zero_page, 0, PAGE_SIZE);
/* this will put all low memory onto the freelists */
totalram_pages += free_all_bootmem();
reservedpages = 0;
free_all_bootmem()函数本质上释放所有的低区内存,从此以后,bootmem不再使用。
/*
* Only count reserved RAM pages
*/
for (tmp = 0; tmp < max_low_pfn; tmp++)
if (page_is_ram(tmp) && PageReserved(mem_map+tmp))
reservedpages++;
对mem_map 查找一遍,并统计所保留的页面数。
#ifdef CONFIG_HIGHMEM
for (tmp = highstart_pfn; tmp < highend_pfn; tmp++) {
struct page *page = mem_map + tmp;
if (!page_is_ram(tmp)) {
SetPageReserved(page);
continue;
}
if (bad_ppro && page_kills_ppro(tmp)) {
SetPageReserved(page);
continue;
}
ClearPageReserved(page);
set_bit(PG_highmem, &page->flags);
atomic_set(&page->count, 1);
__free_page(page);
totalhigh_pages++;
}
totalram_pages += totalhigh_pages;
#endif
把高区内存查找一遍,并把保留但不能使用的页面标记为PG_highmem,并调用__free_page()释放它,还要修改伙伴系统的位图。
codesize = (unsigned long) &_etext - (unsigned long) &_text;
datasize = (unsigned long) &_edata - (unsigned long) &_etext;
initsize = (unsigned long) &__init_end -
(unsigned long) &__init_begin;
printk("Memory: %luk/%luk available (%dk kernel code,
%dk reserved,
%dk data, %dk init, %ldk highmem)\n",
(unsigned long) nr_free_pages() <<
(PAGE_SHIFT-10),
max_mapnr << (PAGE_SHIFT-10),
codesize >> 10,
reservedpages << (PAGE_SHIFT-10),
datasize >> 10,
initsize >> 10,
(unsigned long) (totalhigh_pages
<< (PAGE_SHIFT-10)));
计算内核各个部分的大小,并打印统计信息。
从以上的介绍可以看出,在初始化阶段,对内存的初始化要做许多工作。但这里要说明的是,尽管在这个阶段建立起了初步的虚拟内存管理机制,但仅仅考虑了内核虚拟空间(3GB以上),还根本没有涉及用户空间的管理。因此,在这个阶段,虚拟存储空间到物理存储空间的映射非常简单,仅仅通过一种简单的线性关系就可以达到虚地址到物理地址之间的相互转换。但是,了解这个初始化阶段又非常重要,它是后面进一步进行内存管理分析的基础。