技术的乐趣在于分享,欢迎多多交流,多多沟通。
全部博文(877)
分类: LINUX
2014-06-19 11:16:21
6.2.1 启用分页机制
当Linux启动时,首先运行在实模式下,随后就要转到保护模式下运行。因为在第二章段机制中,我们已经介绍了Linux对段的设置,在此我们主要讨论与分页机制相关的问题。Linux内核代码的入口点就是/arch/i386/kernel/head.S中的startup_32。
1.页表的初步初始化:
/* * The page tables are initialized to only 8MB here - the final page * tables are set up later depending on memory size. */ .org 0x2000 ENTRY(pg0) rg 0x3000 ENTRY(pg1) /* * empty_zero_page must immediately follow the page tables ! (The * initialization loop counts until empty_zero_page) */ .org0x4000 ENTRY(empty_zero_page) /* * Initialize page tables */ movl $pg0-__PAGE_OFFSET,%edi /* initialize page tables */ movl $007,%eax /* "007" doesn't mean with right to kill, but PRESENT+RW+USER */ 2: stsl add $0x1000,%eax cmp $empty_zero_page-__PAGE_OFFSET,%edi jne 2b |
内核的这段代码执行时,因为页机制还没有启用,还没有进入保护模式,因此指令寄存器EIP中的地址还是物理地址,但因为pg0中存放的是虚拟地址(想想gcc编译内核以后形成的符号地址都是虚拟地址),因此,“$pg0-__PAGE_OFFSET ”获得pg0的物理地址,可见pg0存放在相对于内核代码起点为0x2000的地方,即物理地址为0x00102000,而pg1的物理地址则为0x00103000。Pg0和pg1这个两个页表中的表项则依次被设置为0x007、0x1007、0x2007等。其中最低的三位均为1,表示这两个页为用户页,可写,且页的内容在内存中(参见图2.24)。所映射的物理页的基地址则为0x0、0x1000、0x2000等,也就是物理内存中的页面0、1、2、3等等,共映射2K个页面,即8MB的存储空间。由此可以看出,Linux内核对物理内存的最低要求为8MB。紧接着存放的是empty_zero_page页(即零页),零页存放的是系统启动参数和命令行参数,具体内容参见第十三章。
2.启用分页机制:
/* * This is initialized to create an identity-mapping at 0-8M (for bootup * purposes) and another mapping of the 0-8M area at virtual address * PAGE_OFFSET. */ .org 0x1000 ENTRY(swapper_pg_dir) .long 0x00102007 .long 0x00103007 .fill BOOT_USER_PGD_PTRS-2,4,0 /* default: 766 entries */ .long 0x00102007 .long 0x00103007 /* default: 254 entries */ .fill BOOT_KERNEL_PGD_PTRS-2,4,0 /* * Enable paging */ 3: movl $swapper_pg_dir-__PAGE_OFFSET,%eax movl %eax,%cr3 /* set the page table pointer.. */ movl %cr0,%eax orl $0x80000000,%eax movl %eax,%cr0 /* ..and set paging (PG) bit */ jmp 1f /* flush the prefetch-queue */ 1: movl $1f,%eax jmp *%eax /* make sure eip is relocated */ 1: |
我们先来看这段代码的功能。这段代码就是把页目录swapper_pg_dir的物理地址装入控制寄存器cr3,并把cr0中的最高位置成1,这就开启了分页机制。
但是,启用了分页机制,并不说明Linux内核真正进入了保护模式,因为此时,指令寄存器EIP中的地址还是物理地址,而不是虚地址。“jmp 1f”指令从逻辑上说不起什么作用,但是,从功能上说它起到丢弃指令流水线中内容的作用(这是Intel在i386技术资料中所建议的),因为这是一个短跳转,EIP中还是物理地址。紧接着的mov和jmp指令把第二个标号为1的地址装入EAX寄存器并跳转到那儿。在这两条指令执行的过程中, EIP还是指向物理地址“1MB+某处”。因为编译程序使所有的符号地址都在虚拟内存空间中,因此,第二个标号1的地址就在虚拟内存空间的某处((PAGE_OFFSET+某处),于是,jmp指令执行以后,EIP就指向虚拟内核空间的某个地址,这就使CPU转入了内核空间,从而完成了从实模式到保护模式的平稳过渡。
然后再看页目录swapper_pg_dir中的内容。从前面的讨论我们知道pg0和pg1这两个页表的起始物理地址分别为0x00102000和0x00103000。从图2?.22可知,页目录项的最低12位用来描述页表的属性。因此,在swapper_pg_dir中的第0和第1个目录项0x00102007、0x00103007,就表示pg0和pg1这两个页表是用户页表、可写且页表的内容在内存。
接着,把swapper_pg_dir中的第2~767共766个目录项全部置为0。因为一个页表的大小为4KB,每个表项占4个字节,即每个页表含有1024个表项,每个页的大小也为4KB,因此这768个目录项所映射的虚拟空间为768?1024?4K=3G,也就是swapper_pg_dir表中的前768个目录项映射的是用户空间。
最后,在第768和769个目录项中又存放pg0和pg1这两个页表的地址和属性,而把第770~1023共254个目录项置0。这256个目录项所映射的虚拟地址空间为256?1024?4K=1G,也就是swapper_pg_dir表中的后256个目录项映射的是内核空间。
图6.6 初始页目录swapper_pg_dir的映射图
由此可以看出,在初始的页目录swapper_pg_dir中,用户空间和内核空间都只映射了开头的两个目录项,即8MB的空间,而且有着相同的映射,如图6.6所示。
读者会问,内核开始运行后运行在内核空间,那么,为什么把用户空间的低区(8M)也进行映射,而且与内核空间低区的映射相同?简而言之,是为了从实模式到保护模式的平稳过渡。具体地说,当CPU进入内核代码的起点startup_32后,是以物理地址来取指令的。在这种情况下,如果页目录只映射内核空间,而不映射用户空间的低区,则一旦开启页映射机制以后就不能继续执行了,这是因为,此时CPU中的指令寄存器EIP仍指向低区,仍会以物理地址取指令,直到以某个符号地址为目标作绝对转移或调用子程序为止。所以,Linux内核就采取了上述的解决办法。
但是,在CPU转入内核空间以后,应该把用户空间低区的映射清除掉。后面读者将会看到,页目录swapper_pg_dir经扩充后就成为所有内核线程的页目录。在内核线程的正常运行中,处于内核态的CPU是不应该通过用户空间的虚拟地址访问内存的。清除了低区的映射以后,如果发生CPU在内核中通过用户空间的虚拟地址访问内存,就可以因为产生页面异常而捕获这个错误。
3.物理内存的初始分布
经过这个阶段的初始化,初始化阶段页目录及几个页表在物理空间中的位置如图6.7所示:
图6.7 初始化阶段页目录及几个页表在物理空间中的位置
其中empty_zero_page中存放的是在操作系统的引导过程中所收集的一些数据,叫做引导参数。因为这个页面开始的内容全为0,所以叫做“零页”,代码中常常通过宏定义ZERO_PAGE来引用这个页面。不过,这个页面要到初始化完成,系统转入正常运行时才会用到。为了后面内容介绍的方便,我们看一下复制到这个页面中的命令行参数和引导参数。这里假定这些参数已被复制到“零页”,在setup.c中定义了引用这些参数的宏:
/* * This is set up by the setup-routine at boot-time */ #define PARAM ((unsigned char *)empty_zero_page) #define SCREEN_INFO (*(struct screen_info *) (PARAM+0)) #define EXT_MEM_K (*(unsigned short *) (PARAM+2)) #define ALT_MEM_K (*(unsigned long *) (PARAM+0x1e0)) #define E820_MAP_NR (*(char*) (PARAM+E820NR)) #define E820_MAP ((struct e820entry *) (PARAM+E820MAP)) #define APM_BIOS_INFO (*(struct apm_bios_info *) (PARAM+0x40)) #define DRIVE_INFO (*(struct drive_info_struct *) (PARAM+0x80)) #define SYS_DESC_TABLE (*(struct sys_desc_table_struct*)(PARAM+0xa0)) #define MOUNT_ROOT_RDONLY (*(unsigned short *) (PARAM+0x1F2)) #define RAMDISK_FLAGS (*(unsigned short *) (PARAM+0x1F8)) #define ORIG_ROOT_DEV (*(unsigned short *) (PARAM+0x1FC)) #define AUX_DEVICE_INFO (*(unsigned char *) (PARAM+0x1FF)) #define LOADER_TYPE (*(unsigned char *) (PARAM+0x210)) #define KERNEL_START (*(unsigned long *) (PARAM+0x214)) #define INITRD_START (*(unsigned long *) (PARAM+0x218)) #define INITRD_SIZE (*(unsigned long *) (PARAM+0x21c)) #define COMMAND_LINE ((char *) (PARAM+2048)) #define COMMAND_LINE_SIZE 256 |
其中宏PARAM就是empty_zero_page的起始位置,随着代码的阅读,读者会逐渐理解这些参数的用途。这里要特别对宏E820_MAP进行说明。E820_MAP是个struct e820entry数据结构的指针,存放在参数块中位移为0x2d0的地方。这个数据结构定义在include/i386/e820.h中:
struct e820map { int nr_map; struct e820entry { unsigned long long addr; /* start of memory segment */ unsigned long long size; /* size of memory segment */ unsigned long type; /* type of memory segment */ } map[E820MAX]; }; extern struct e820map e820; |
其中,E820MAX被定义为32。从这个数据结构的定义可以看出,每个e820entry都是对一个物理区间的描述,并且一个物理区间必须是同一类型。如果有一片地址连续的物理内存空间,其一部分是RAM,而另一部分是ROM,那就要分成两个区间。即使同属RAM,如果其中一部分要保留用于特殊目的,那也属于不同的一个分区。在e820.h文件中定义了4种不同的类型:
#define E820_RAM 1
#define E820_RESERVED 2
#define E820_ACPI 3 /* usable as RAM once ACPI tables have been read */
#define E820_NVS 4
#define HIGH_MEMORY (1024*1024)
其中E820_NVS表示“Non-Volatile Storage”,即“不挥发”存储器,包括ROM、EPROM、Flash存储器等。
在PC中,对于最初1MB存储空间的使用是特殊的。开头640KB(0x0~0x9FFFF为RAM,从0xA0000开始的空间则用于CGA、EGA、VGA等图形卡。现在已经很少使用这些图形卡,但是不管是什么图形卡,开机时总是工作于EGA或VGA模式。从0xF0000开始到0xFFFFF,即最高的4KB,就是在EPROM或Flash存储器中的BIOS。所以,只要有BIOS存在,就至少有两个区间,如果nr_map小于2,那就一定出错了。由于BIOS的存在,本来连续的RAM空间就不连续了。当然,现在已经不存在这样的存储结构了。1MB的边界早已被突破,但因为历史的原因,把1MB以上的空间定义为“HIGH??_MEMORY”,这个称呼一直沿用到现在,于是代码中的常数HIGH??_MEMORY就定义为“1024?1024”。现在,配备了128MB的内存已经是很普遍了。但是,为了保持兼容,就得留出最初1MB的空间。
这个阶段初始化后,物理内存中内核映像的分布如图6.8所示:
图6.8 内核映象在物理内存中的分布
符号_text对应物理地址0x00100000,表示内核代码的第一个字节的地址。内核代码的结束位置用另一个类似的符号_etext表示。内核数据被分为两组:初始化过的数据和未初始化过的数据。初始化过的数据在_etext后开始,在_edata处结束,紧接着是未初始化过的数据,其结束符号为_end,这也是整个内核映像的结束符号。
图中出现的符号是由编译程序在编译内核时产生的。你可以在System.map文件中找到这些符号的线性地址(或叫虚拟地址),System.map是编译内核以后所创建的。
6.2.2 物理内存的探测
我们知道,BIOS不仅能引导操作系统,还担负着加电自检和对资源的扫描探测,其中就包括了对物理内存的自检和扫描(你刚开机时所看到的信息就是此阶段BIOS显示的信息)。对于这个阶段中获得的内存信息可以通过BIOS调用“int 0x15”加以检查。由于Linux内核不能作BIOS调用,因此内核本身就得代为检查,并根据获得的信息生成一幅物理内存构成图,这就是上面所介绍的e820图,然后通过上面提到的参数块传给内核。使得内核能知道系统中内存资源的配置。之所以称为e820图,是因为在通过”int 0x15”查询内存的构成时要把调用参数之一设置成0xe820。
分页机制启用以后,与内存管理相关的操作就是调用init/main.c中的start_kernel()函数,start_kernel()函数要调用一个叫setup_arch()的函数,setup_arch()位于arch/i386/kernel/setup.c文件中,我们所关注的与物理内存探测相关的内容就在这个函数中。
1.setup_arch()函数
这个函数比较繁琐和冗长,下面我们只对setup_arch()中与内存相关的内容给予描述。
· 首先调用setup_memory_region()函数,这个函数处理内存构成图(map),并把内存的分布信息存放在全局变量e820中,后面会对此函数进行具体描述。
· 调用parse_mem_cmdline(cmdline_p)函数。在特殊的情况下,有的系统可能有特殊的RAM空间结构,此时可以通过引导命令行中的选择项来改变存储空间的逻辑结构,使其正确反映内存的物理结构。此函数的作用就是分析命令行中的选择项,并据此对数据结构e820中的内容作出修正,其代码也在setup.c中。
· 宏定义:
#define PFN_UP(x) (((x) + PAGE_SIZE-1) >> PAGE_SHIFT)
#define PFN_DOWN(x) ((x) >> PAGE_SHIFT)
#define PFN_PHYS(x) ((x) << PAGE_SHIFT)
PFN_UP() 和PFN_DOWN()都是将地址x转换为页面号(PFN即Page Frame Number的缩写),二者之间的区别为:PFN_UP()返回大于x的第一个页面号,而PFN_DOWN()返回小于x的第一个页面号。宏PFN_PHYS()返回页面号x的物理地址。
· 宏定义
/*
* 128MB for vmalloc and initrd
*/
#define VMALLOC_RESERVE (unsigned long)(128 << 20)
#define MAXMEM (unsigned long)(-PAGE_OFFSET-VMALLOC_RESERVE)
#define MAXMEM_PFN PFN_DOWN(MAXMEM)
#define MAX_NONPAE_PFN (1 << 20)
对这几个宏描述如下:
VMALLOC_RESERVE :为vmalloc()函数访问内核空间所保留的内存区,大小为128MB。
MAXMEM :内核能够直接映射的最大RAM容量,为1GB-128MB=896MB(-PAGE_OFFSET就等于1GB)
MAXMEM_PFN :返回由内核能直接映射的最大物理页面数。
MAX_NONPAE_PFN :给出在4GB之上第一个页面的页面号。当页面扩充(PAE)功能启用时,才能访问4GB以上的内存。
· 获得内核映像之后的起始页面号
/*
* partially used pages are not usable - thus
* we are rounding upwards:
*/
start_pfn = PFN_UP(__pa(&_end));
在上一节已说明,宏__pa()返回给定虚拟地址的物理地址。其中标识符_end表示内核映像在内核空间的结束位置。因此,存放在变量start_pfn中的值就是紧接着内核映像之后的页面号。
· 找出可用的最高页面号
/* * Find the highest page frame number we have available */ max_pfn = 0; for (i = 0; i < e820.nr_map; i++) { unsigned long start, end; /* RAM? */ if (e820.map[i].type != E820_RAM) continue; start = PFN_UP(e820.map[i].addr); end = PFN_DOWN(e820.map[i].addr + e820.map[i].size); if (start >= end) continue; if (en > max_pfn) max_pfn = end; |
上面这段代码循环查找类型为E820_RAM(可用RAM)的内存区,并把最后一个页面的页面号存放在max_pfn中。
· 确定最高和最低内存范围
/* * Determine low and high memory ranges: */ max_low_pfn = max_pfn; if (max_low_pfn > MAXMEM_PFN) { max_low_pfn = MAXMEM_PFN; #ifndef CONFIG_HIGHMEM /* Maximum memory usable is what is directly addressable */ printk(KERN_WARNING "Warning only %ldMB will be used.\n", MAXMEM>>20); if (max_pfn > MAX_NONPAE_PFN) printk(KERN_WARNING "Use a PAE enabled kernel.\n"); else printk(KERN_WARNING "Use a HIGHMEM enabled kernel.\n"); #else /* !CONFIG_HIGHMEM */ #ifndef CONFIG_X86_PAE if (mx_pfn > MAX_NONPAE_PFN) { max_pfn = MAX_NONPAE_PFN; printk(KERN_WARNING "Warning only 4GB will be used.\n"); printk(KERN_WARNING "Use a PAE enabled kernel.\n"); } #endif /* !CONFIG_X86_PAE */ #endif /* !CONFIG_HIGHMEM */ } |
有两种情况:
(1) 如果物理内存RAM大于896MB,而小于4GB,则选用CONFIG_HIGHMEM选项来进行访问。
(2) 如果物理内存RAM大于4GB,则选用CONFIG_X86_PAE(启用PAE模式)来进行访问。
上面这段代码检查了这两种情况,并显示适当的警告信息。
#ifdef CONFIG_HIGHMEM highstart_pfn = highend_pfn = max_pfn; if (max_pfn > MAXMEM_PFN) { highstart_pfn = MAXMEM_PFN; printk(KERN_NOTICE "%ldMB HIGHMEM available.\n", pages_to_mb(highend_pfn - highstart_pfn)); } #endif |
如果使用了CONFIG_HIGHMEM 选项,上面这段代码仅仅打印出大于896MB的可用物理内存数量。
· 初始化引导时的分配器
* Initialize the boot-time allocator (with low memory only):
*/
bootmap_size = init_bootmem(start_pfn, max_low_pfn);
通过调用init_bootmem()函数,为物理内存页面管理机制的建立做初步准备,为整个物理内存建立起一个页面位图。这个位图建立在从start_pfn开始的地方,也就是说,把内核映像终点_end上方的若干页面用作物理页面位图。在前面的代码中已经搞清楚了物理内存顶点所在的页面号为max_low_pfn,所以物理内存的页面号一定在0~max_low_pfn之间。可是,在这个范围内可能有空洞(hole),另一方面,并不是所有的物理内存页面都可以动态分配。建立这个位图的目的就是要搞清楚哪一些物理内存页面可以动态分配的。后面会具体描述bootmem分配器。
· 用bootmem 分配器,登记全部低区(0~896MB)的可用RAM页面
/* * Register fully available low RAM pages with the * bootmem allocator. */ for (i = 0; i < e820.nr_map; i++) { unsigned long curr_pfn, last_pfn, size; /* * Reserve usable low memory */ if (e820.map[i].type != E820_RAM) continue; /* * We are rounding up the start address of usable memory: */ curr_pfn = PFN_UP(e820.map[i].addr); if (curr_pfn >= max_low_pfn) continue; /* * ... and at the end of the usable range downwards: */ last_pfn = PFN_DOWN(e820.map[i].addr + e820.map[i].size); if (last_pfn > max_low_pfn) last_pfn = max_low_pfn; /* * .. finally, did all the rounding and playing * around just make the area go away? */ if (last_pfn <= curr_pfn) continue; size = last_pfn - curr_pfn; free_bootmem(PFN_PHYS(curr_pfn), PFN_PHYS(size)); } |
这个循环仔细检查所有可以使用的RAM,并调用free_bootmem()函数把这些可用RAM标记为可用。这个函数调用以后,只有类型为1(可用RAM)的内存被标记为可用的,参看后面对这个函数的具体描述。
· 保留内存
/*
* Reserve the bootmem bitmap itself as well. We do this in two
* steps (first step was init_bootmem()) because this catches
* the (very unlikely) case of us accidentally initializing the
* bootmem allocator with an invalid RAM area.
*/
reserve_bootmem(HIGH_MEMORY, (PFN_PHYS(start_pfn) +
bootmap_size + PAGE_SIZE-1) - (HIGH_MEMORY));
这个函数把内核和bootmem位图所占的内存标记为“保留”。 HIGH_MEMORY为1MB,即内核开始的地方,后面还要对这个函数进行具体描述
· 分页机制的初始化
paging_init();
这个函数初始化分页内存管理所需要的数据结构,参见后面的详细描述。
2. setup_memory_region() 函数
这个函数用来处理BIOS的内存构成图和并把这个构成图拷贝到全局变量e820中。如果操作失败,就创建一个伪内存构成图。这个函数的主要操作为:
· 调用sanitize_e820_map()函数,以删除内存构成图中任何重叠的部分,因为BIOS所报告的内存构成图可能有重叠。
· 调用copy_e820_map()进行实际的拷贝。
· 如果操作失败,创建一个伪内存构成图,这个伪构成图有两部分:0到640K及1M到最大物理内存。
· 打印最终的内存构成图
3.copy_e820_map() 函数
函数原型为:
static int __init sanitize_e820_map(struct e820entry * biosmap, char * pnr_map)
其主要操作为:
· 如果物理内存区间小于2,那肯定出错。因为BIOS至少和RAM属于不同的物理区间。
if (nr_map < 2)
return -1;
· 从BIOS构成图中读出一项
do {
unsigned long long start = biosmap->addr;
unsigned long long size = biosmap->size;
unsigned long long end = start + size;
unsigned long type = biosmap->type;
· 进行检查
/* Overflow in 64 bits? Ignore the memory map. */
if (start > end)
return -1;
· 一些BIOS把640K~1MB之间的区间作为RAM来用,这是不符合常规的。因为从0xA0000开始的空间用于图形卡,因此,在内存构成图中要进行修正。如果一个区的起点在0xA0000以下,而终点在1MB之上,就要将这个区间拆开成两个区间,中间跳过从0xA0000到1MB边界之间的那一部分。
/* * Some BIOSes claim RAM in the 640k - 1M region. * Not right. Fix it up. */ if (type == E820_RAM) { if (start < 0x100000ULL && end > 0xA0000ULL) { if (start < 0xA0000ULL) add_memory_region(start, 0xA0000ULL-start, type) if (end <= 0x100000ULL) continue; start = 0x100000ULL; size = end - start; } } add_memory_region(start, size, type); } while (biosmap++,--nr_map); return 0; |
4. add_memory_region() 函数
这个函数的功能就是在e820中增加一项,其主要操作为:
· 获得已追加在e820中的内存区数
int x = e820.nr_map;
· 如果数目已达到最大(32),则显示一个警告信息并返回
if (x == E820MAX) {
printk(KERN_ERR "Oops! Too many entries in
the memory map!\n");
return;
}
· 在e820中增加一项,并给nr_map加1
e820.map[x].addr = start;
e820.map[x].size = size;
e820.map[x].type = type;
e820.nr_map++;
5. print_memory_map() 函数
这个函数把内存构成图在控制台上输出,函数本身比较简单,在此给出一个运行实例。例如函数的输出为(BIOS所提供的物理RAM区间):
BIOS-e820: 0000000000000000 - 00000000000a0000 (usable)
BIOS-e820: 0000000000f0000 - 0000000000100000 (reserved)
BIOS-e820: 0000000000100000 - 000000000c000000 (usable)
BIOS-e820: 00000000ffff0000 - 0000000100000000 (reserved)
6.2.3 物理内存的描述
为了对内存的初始化内容进行进一步的讨论,我们首先要了解Linux对物理内存的描述机制。
1.一致存储结构(UMA)和非一致存储结构(NUMA)
在传统的计算机结构中,整个物理内存都是均匀一致的,CPU访问这个空间中的任何一个地址所需要的时间都相同,所以把这种内存称为“一致存储结构(Uniform Memory Architecture)”,简称UMA。可是,在一些新的系统结构中,特别是多CPU结构的系统中,物理存储空间在这方面的一致性却成了问题。这是因为,在多CPU结构中,系统中只有一条总线(例如,PCI总线),有多个CPU模块连接在系统总线上,每个CPU模块都有本地的物理内存,但是也可以通过系统总线访问其它CPU模块上的内存。另外,系统总线上还连接着一个公用的存储模块,所有的CPU模块都可以通过系统总线来访问它。因此,所有这些物理内存的地址可以互相连续而形成一个连续的物理地址空间。
显然,就某个特定的CPU而言,访问其本地的存储器速度是最快的,而穿过系统总线访问公用存储模块或其它CPU模块上的存储器就比较慢,而且还面临因可能的竞争而引起的不确定性。也就是说,在这样的系统中,其物理存储空间虽然地址连续,但因为所处“位置”不同而导致的存取速度不一致,所以称为“非一致存储结构(Non-Uniform Memory Architecture),简称NUMA。
事实上,严格意义上的UMA结构几乎不存在。就拿配置最简单的单CPU来说,其物理存储空间就包括了RAM、ROM(用于BIOS),还有图形卡上的静态RAM。但是,在UMA中,除主存RAM之外的存储器空间都很小,因此可以把它们放在特殊的地址上,在编程时加以特别注意就行,那么,可以认为以RAM为主体的主存是UMA结构。
由于NUMA的引入,就需要存储管理机制的支持,因此,Linux内核从2.4版本开始就提供了对NUMA的支持(作为一个编译可选项)。为了对NUMA进行描述,引入一个新的概念-“存储节点(或叫节点)”,把访问时间相同的存储空间就叫做一个“存储节点”。一般来说,连续的物理页面应该分配在相同的存储节点上。例如,如果CPU模块1要求分配5个页面,但是由于本模块上的存储空间已经不够,只能分配3个页面,那么此时,是把另外两个页面分配在其它CPU模块上呢,还是把5个页面干脆分配在一个模块上?显然,合理的分配方式因该是将这5个页面都分配在公用模块上。
Linux把物理内存划分为三个层次来管理:存储节点(Node)、管理区(Zone)和页面(Page),并用三个相应的数据结构来描述。
2.页面数据结构page
对一个物理页面的描述在/include/linux/mm.h中:
/* * Each physical page in the system has a struct page associated with * it to keep track of whatever it is we are using the page for at the * moment. Note that we have no way to track which tasks are using * a page. * * Try to keep the most commonly accessed fields in single cache lines * here (16 bytes or greater). This ordering should be particularly * beneficial on 32-bit processors. * * The first line is data used in page cache lookup, the second line * is used for linear searches (eg. clock algorithm scans). * * TODO: make this structure smaller, it could be as small as 32 bytes. */ typedef struct page { struct list_head list; /* ->mapping has some page lists. */ struct address_space *mapping; /* The inode (or ...) we belong to. */ unsigned long index; /* Our offset within mapping. */ struct page *next_hash; /* Next page sharing our hash bucket in the pagecache hash table. */ atomic_t count; /* Usage count, see below. */ unsigned long flags; /* atomic flags, some possibly updated asynchronously */ struct list_head lru; /* Pageout list, eg. active_list; protected by pagemap_lru_lock !! */ wait_queue_head_t wait; /* Page locked? Stand in line... */ struct page **pprev_hash; /* Complement to *next_hash. */ struct buffer_head * buffers; /* Buffer maps us to a disk block. */ void *virtual; /* Kernel virtual address (NULL if not kmapped, ie. highmem) */ struct zone_struct *zone; /* Memory zone we are in. */ } mem_map_t; extern mem_map_t * mem_map; |
源代码的注释中对这个数据结构给出了一定的说明,从中我们可以对此结构有一定的理解,后面还要对此结构中的每个域给出具体的解释。
内核中用来表示这个数据结构的变量常常是page或map。
当页面的数据来自一个文件时,index代表着该页面中的数据在文件中的偏移量;当页面的内容被换出到交换设备上,则index指明了页面的去向。结构中各个成分的次序是有讲究的,尽量使得联系紧密的若干域存放在一起,这样当这个数据结构被装入到高速缓存中时,联系紧密的域就可以存放在同一缓冲行(cache line)中。因为同一缓冲行(其大小为16字节)中的内容几乎可以同时存取,因此,代码注释中希望这个数据结构尽量地小到用32个字节可以描述。
系统中的每个物理页面都有一个page(或mem_map_t)结构。系统在初始化阶段根据内存的大小建立起一个page结构的数组mem_map,数组的下标就是内存中物理页面的序号。
3. 管理区zone
为了对物理页面进行有效的管理,Linux又把物理页面划分为三个区:
· 专供DMA使用的ZONE_DMA区(小于16BM)
· 常规的ZONE_NORMAL区(大于16MB小于896MB)
· 内核不能直接映射的区ZONE_HIGME区(大于896MB)。
这里进一步说明为什么对DMA要单独设置管理区。首先,DMA使用的页面是磁盘I/O所需的,如果在页面的分配过程中,所有的页面全被分配完,那么页面及盘区的交换就无法进行了,这是操作系统决不允许出现的现象。另外,在i386CPU中,页式存储管理的硬件支持是在CPU内部实现的,而不像有些CPU那样由一个单独的MMU来提供,所以DMA对内存的访问不经过MMU提供的地址映射。这样,外部设备就要直接访问物理页面的地址。可是,有些外设(特别是插在ISA总线上的外设接口卡)在这方面往往有些限制,要求用于DMA的物理地址不能过高。另一方面,当DMA所需的缓冲区超过一个物理页面的大小时,就要求两个物理页面在物理上是连续的,但因为此时DMA控制器不能依靠CPU内部的MMU将连续的虚存页面映射到物理上也连续的页面上,因此,用于DMA的物理页面必须加以单独管理。
关于管理区的数据结构zone_struct(或zone_t)将在后面进行描述。
4.存储节点的数据结构
存储节点的数据结构为pglist_data,定义于Include/linux/mmzone.h中:
typedef struct pglist_data { zone_t node_zones[MAX_NR_ZONES]; zonelist_t node_zonelists[GFP_ZONEMASK+1]; int nr_zones; struct page *node_mem_map; unsigned long *valid_addr_bitmap; struct bootmem_data *bdata; unsigned long node_start_paddr; unsigned long node_start_mapnr; unsigned long node_size; int node_id; struct pglist_data *node_next; } pg_data_t; |
显然,若干存储节点的pglist_data数据结构可以通过node_next形成一个单链表队列。每个结构中的node_mem_map指向具体节点的page结构数组,而数组node_zone[]就是该节点的最多三个页面管理区。
在pglist_data结构里设置了一个node_zonelists数组,其类型定义也在同一文件中:
typedef struct zonelist_struct {
zone_t *zone[MAX_NR_ZONE+1]; //NULL delimited
Int gfp_mast;
} zonelist_t
这里的zone[]是个指针数组,各个元素按特定的次序指向具体的页面管理区,表示分配页面时先试zone[0]所指向的管理区,如果不能满足要求就试zone[1]所指向的管理区,等等。这些管理区可以属于不同的存储节点。关于管理区的分配可以有很多种策略,例如,CPU模块1需要分配5个用于DMA的页面,可是它的ZONE_DMA只有三个页面,于是就从公用模块的ZONE_DMA中分配全部5个页面。就是说,每个zonelist_t规定了一种分配策略。然而,每个存储节点不应该只有一种分配策略,所以在pglist_data中提供的是一个zonelist_t数组,数组的大小NR_GFPINDEX为100。
6.2. 页面管机制的初步建立
为了对页面管理机制作出初步准备,Linux使用了一种叫bootmem分配器(bootmem allocator)的机制,这种机制仅仅用在系统引导时,它为整个物理内存建立起一个页面位图。这个位图建立在从start_pfn开始的地方,也就是说,内核映象终点_end上方的地方。这个位图用来管理低区(例如小于896MB),因为在0到896MB的范围内,有些页面可能保留,有些页面可能有空洞,因此,建立这个位图的目的就是要搞清楚哪一些物理页面是可以动态分配的。用来存放位图的数据结构为bootmem_data(在mm/numa.c中) :
typedef struct bootmem_data {
unsigned long node_boot_start;
unsigned long node_low_pfn;
void *node_bootmem_map;
unsigned long last_offset;
unsigned long last_pos;
} bootmem_data_t;
· node_boot_start表示存放bootmem位图的第一个页面(即内核映象结束处的第一个页面)。
· node_low_pfn表示物理内存的顶点,最高不超过896MB。
· node_bootmem_map指向bootmem位图
· last_offset 用来存放在前一次分配中所分配的最后一个字节相对于last_pos的位移量。
· last_pos 用来存放前一次分配的最后一个页面的页面号。这个域用在__alloc_bootmem_core()函数中,通过合并相邻的内存来减少内部碎片。
下面介绍与bootmem相关的几个函数,这些函数位于mm/bootmeme.c中。
1. init_bootmem()函数
unsigned long __init init_bootmem (unsigned long start, unsigned long pages)
{
max_low_pfn = pages;
min_low_pfn = start;
return(init_bootmem_core(&contig_page_data, start, 0, pages));
}
这个函数仅在初始化时用来建立bootmem分配器。这个函数实际上是init_bootmem_core()函数的封装函数。init_bootmem()函数的参数start表示内核映象结束处的页面号,而pages表示物理内存顶点所在的页面号。而函数init_bootmem_core()就是对contig_page_data变量进行初始化。下面我们来看一下对该变量的定义:
int numnodes = 1; /* Initialized for UMA platforms */
static bootmem_data_t contig_bootmem_data;
pg_data_t contig_page_data = { bdata: &contig_bootmem_data };
变量contig_page_data的类型就是前面介绍过的pg_data_t数据结构。每个pg_data_t数据结构代表着一片均匀的、连续的内存空间。在连续空间UMA结构中,只有一个节点contig_page_data,而在NUMA结构或不连续空间UMA结构中,有多个这样的数据结构。系统中各个节点的pg_data_t数据结构通过node_next连接在一起成为一个链。有一个全局量pgdat_list则指向这个链。从上面的定义可以看出,contig_page_data是链中的第一个节点。这里假定整个物理空间为均匀的、连续的,以后若发现这个假定不能成立,则将新的pg_data_t结构加入到链中。
pg_data_t结构中有个指针bdata,contig_page_data被初始化为指向bootmem_data_t数据结构。下面我们来看init_bootmem_core()函数的具体代码:
/* * Called once to set up the allocator itself. */ static unsigned long __init init_bootmem_core (pg_data_t *pgdat, unsigned long mapstart, unsigned long start, unsigned long end) { bootmem_data_t *bdata = pgdat->bdata; unsigned long mapsize = ((end - start)+7)/8; pgdat->node_next = pgdat_list; pgdat_list = pgdat; mapsize = (mapsize + (sizeof(long) - 1UL)) & ~(sizeof(long) - 1UL); bdata->node_bootmem_map = phys_to_virt(mapstart << PAGE_SHIFT); bdata->node_boot_start = (start << PAGE_SHIFT); bdata->node_low_pfn = end; /* * Initially all pages are reserved - setup_arch() has to * register free RAM areas explicitly. */ memset(bdata->node_bootmem_map, 0xff, mapsize); return mapsize; |
下面对这一函数给予说明:
· 变量mapsize存放位图的大小。(end - start)给出现有的页面数,再加个7是为了向上取整,除以8就获得了所需的字节数(因为每个字节映射8个页面)。
· 变量pgdat_list用来指向节点所形成的循环链表首部,因为只有一个节点,因此使pgdat_list指向自己。
· 接下来的一句使memsize成为下一个4的倍数(4为CPU的字长)。例如,假设有40个物理页面,因此,我们可以得出memsize为5个字节。所以,上面的操作就变为(5+(4-1))&~(4-1)即(00001000&11111100),最低的两位变为0,其结果为8。这就有效地使memsize变为4的倍数。
· phys_to_virt(mapstart << PAGE_SHIFT)把给定的物理地址转换为虚地址。
· 用节点的起始物理地址初始化node_boot_start(这里为0x00000000)
· 用物理内存节点的页面号初始化node_low_pfn。
· 初始化所有被保留的页面,即通过把页面中的所有位都置为1来标记保留的页面
· 返回位图的大小。
2. free_bootmem()函数
这个函数把给定范围的页面标记为空闲(即可用),也就是,把位图中某些位清0,表示相应的物理内存可以投入分配。
原函数为:
void __init free_bootmem (unsigned long addr, unsigned long size)
{
return(free_bootmem_core(contig_page_data.bdata, addr, size));
}
从上面可以看出,free_bootmem()是个封装函数,实际的工作是由free_bootmem_core()函数完成的:
static void __init free_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size) { unsigned long i; unsigned long start; /* * round down end of usable mem, partially free pages are * considered reserved. */ unsigned long sidx; unsigned long eidx = (addr + size - bdata->node_boot_start)/PAGE_SIZE; unsigned long end = (addr + size)/PAGE_SIZE; if (!size) BUG(); if (end > bdata->node_low_pfn) BUG(); /* * Round up the beginning of the address. */ start = (addr + PAGE_SIZE-1) / PAGE_SIZE; sidx = start - (bdata->node_boot_start/PAGE_SIZE); for (i = sidx; i < eidx; i++) { if (!test_and_clear_bit(i, bdata->node_bootmem_map)) BUG(); } } |
对此函数的解释如下:
· 变量edix被初始化为页面总数。
· 变量end被初始化为最后一个页面的页面号。
· 进行两个可能的条件检查.
· start初始化为第一个页面的页面号(向上取整),而sidx(start index)初始化为相对于node_boot_start.的页面号。
· 清位图中从sidx到eidx的所有位,即把这些页面标记为可用。
3. reserve_bootmem()函数
这个函数用来保留页面。为了保留一个页面,只需要在bootmem位图中把相应的位置为1即可。
原函数为:
void __init reserve_bootmem (unsigned long addr, unsigned long size)
{
reserve_bootmem_core(contig_page_data.bdata, addr, size);
}
reserve_bootmem()为封装函数,实际调用的是reserve_bootmem_core()函数:
static void __init reserve_bootmem_core(bootmem_data_t *bdata, unsigned long addr, unsigned long size) { unsigned long i; /* * round up, partially reserved pages are considered * fully reserved. */ unsigned long sidx = (addr - bdata->node_boot_start)/PAGE_SIZE; unsigned long eidx = (addr + size - bdata->node_boot_start + PAGE_SIZE-1)/PAGE_SIZE; unsigned long end = (addr + size + PAGE_SIZE-1)/PAGE_SIZE; if (!size) BUG(); if (sidx < 0) BUG(); if (eidx < 0) BUG(); if (sidx >= eidx) BUG(); if ((addr >> PAGE_SHIFT) >= bdata->node_low_pfn) BUG(); if (end > bdata->node_low_pfn) BUG(); for (i = sidx; i < eidx; i++) if (test_and_set_bit(i, bdata->node_bootmem_map)) printk("hm, page %08lx reserved twice.\n", i*PAGE_SIZE); } |
对此函数的解释如下:
· sidx (start index)初始化为相对于node_boot_start的页面号
· 变量eidx初始化为页面总数(向上取整)。
· 变量end初始化为最后一个页面的页面号(向上取整)。
· 进行各种可能的条件检查.
· 把位图中从sidx到eidx的所有位置1
4.__alloc_bootmem()函数
这个函数以循环轮转的方式从不同节点分配页面。因为在i386上只有一个节点,因此只循环一次。
函数原型为:
void * __alloc_bootmem (unsigned long size,
unsigned long align,
unsigned long goal);
void * __alloc_bootmem_core (bootmem_data_t *bdata,
unsigned long size,
unsigned long align,
unsigned long goal);
其中__alloc_bootmem()为封装函数,实际调用的函数为__alloc_bootmem_core (),因为__alloc_bootmem_core ()函数比较长,下面分片断来进行仔细分析:
unsigned long i, start = 0;
void *ret;
unsigned long offset, remaining_size;
unsigned long areasize, preferred, incr;
unsigned long eidx = bdata->node_low_pfn -
(bdata->node_boot_start >> PAGE_SHIFT);
把eidx初始化为本节点中现有页面的总数。
if (!size) BUG();
if (align & (align-1))
BUG();
进行条件检查
/*
* We try to allocate bootmem pages above 'goal'
* first, then we try to allocate lower pages.
*/
if (goal && (goal >= bdata->node_boot_start) &&
((goal >> PAGE_SHIFT) < bdata->node_low_pfn)) {
preferred = goal - bdata->node_boot_start;
} else
preferred = 0;
preferred = ((preferred + align - 1) & ~(align - 1)) >> PAGE_SHIFT;
开始分配后首选页的计算分为两步:
(1)如果goal为非0且有效,则给preferred赋初值,否则,其初值为0。
(2)根据参数align 来对齐preferred的物理地址。
areasize = (size+PAGE_SIZE-1)/PAGE_SIZE;
获得所需页面的总数(向上取整)
incr = align >> PAGE_SHIFT ? : 1;
根据对齐的大小来选择增加值。除非大于4K(很少见),否则增加值为1。
restart_scan:
for (i = preferred; i < eidx; i += incr) {
unsigned long j;
if (test_bit(i, bdata->node_bootmem_map))
continue;
这个循环用来从首选页面号开始,找到空闲的页面号。test_bit()宏用来测试给定的位,如果给定位为1,则返回1
for (j = i + 1; j < i + areasize; ++j) {
if (j >= eidx)
goto fail_block;
if (test_bit (j, bdata->node_bootmem_map))
goto fail_block;
}
这个循环用来查看在首次满足内存需求以后,是否还有足够的空闲页面。如果没有空闲页,就跳到fail_block。
start = i;
goto found;
如果一直到了这里,则说明从i开始找到了足够的页面,跳过fail_block并继续。
fail_block:;
}
if (preferred) {
preferred = 0;
goto restart_scan;
}
return NULL;
如果到了这里,从首选页面中没有找到满足需要的连续页面,就忽略preferred的值,并从0开始扫描。如果preferred为1,但没有找到满足需要的足够页面,则返回NULL。
found:
已经找到足够的内存,继续处理请求。
if (start >= eidx)
BUG();
进行条件检查。
/*
* Is the next page of the previous allocation-end the start
* of this allocation's buffer? If yes then we can 'merge'
* the previous partial page with this allocation.
*/
if (align <= PAGE_SIZE && bdata->last_offset
&& bdata->last_pos+1 == start) {
offset = (bdata->last_offset+align-1) & ~(align-1);
if (offset > PAGE_SIZE)
BUG();
remaining_size = PAGE_SIZE-offset;
if语句检查下列条件:
(1)所请求对齐的值小于页的大小(4k)。
(2)变量last_offset为非0。如果为0,则说明前一次分配达到了一个非常好的页面边界,没有内部碎片。
(3)检查这次请求的内存是否与前一次请求的内存是相临的,如果是,则把两次分配合在一起进行。
如果以上三个条件都满足,则用前一次分配中最后一页剩余的空间初始化remaining_size。
if (size < remaining_size) {
areasize = 0;
// last_pos unchanged
bdata->last_offset = offset+size;
ret = phys_to_virt(bdata->last_pos*PAGE_SIZE
+ offset + bdata->node_boot_start);
如果请求内存的大小小于前一次分配中最后一页中的可用空间,则没必要分配任何新的页。变量last_offset增加到新的偏移量,而last_pos保持不变,因为没有增加新的页。把这次新分配的起始地址存放在变量ret中。宏phys_to_virt()返回给定物理地址的虚地址。
} else {
remaining_size = size - remaining_size;
areasize = (remaining_size+PAGE_SIZE-1)/PAGE_SIZE;
ret = phys_to_virt(bdata->last_pos*PAGE_SIZE
+ offset + bdata->node_boot_start);
bdata->last_pos = start+areasize-1;
bdata->last_offset = remaining_size;
所请求的大小大于剩余的大小。首先求出所需的页面数,然后更新变量last_pos 和 last_offset。
例如,在前一次分配中,如果分配了9k,则占用3个页面,内部碎片为12k-9k=3k。因此,page_offset为1k,且剩余大小为3k。如果新的请求为1k,则第3个页面本身就能满足要求,但是,如果请求的大小为10k,则需要新分配((10 k- 3k) + PAGE_SIZE-1)/PAGE_SIZE,即2个页面,因此,page_offset为3k。
}
bdata->last_offset &= ~PAGE_MASK;
} else {
bdata->last_pos = start + areasize - 1;
bdata->last_offset