Linux内存管理之地址映射

elvalad posted @ Aug 16, 2012 04:39:46 PM in Default , 1660 阅读

写在前面:由于地址映射涉及到各种寄存器的设置访问,Linux对于不同体系结构处理器的地址映射采用不同的方法,例如对于i386及后来的32位的Intel的处理器在页式映射时采用的是2级页表映射,而对于IA64的处理器则采用3级分页。对于其他类型的处理器,例如MK68000等其他许多处理器,在地址映射时则忽略了段式映射,只是因为Intel的X86系列需要兼容早期的段式映射,才在后来的设计中即使用了段式映射,也采用了页式映射。以后关于Linux的笔记,除特别说明外,均是在i386体系结构之上,笔记中所有源码除特别说明外均摘自linux-2.4.0源码树。

 

现代操作系统在内存管理上均使用高效的页式管理,Linux也不例外。对于i386处理器则有些例外,为了兼容早期的处理器,Intel强制要求必须先经过段式映射。在地址映射时,虚拟地址被划分成固定的页面大小,由MMU将虚拟地址映射到实际的物理地址。在访问一个虚拟地址表示的内存空间中,CPU必须经过若干次的内存访问才能完成映射,具体访问次数为N+1(N为页表级数),同时还需要N次加法运算。

 

在Linux进行段式映射和页式映射之前,需要搞清楚X86系列的地址描述方式:

0)逻辑地址:出现在机器指令中,用来制定操作数的地址。

1)线性地址:逻辑地址经过分段单元处理后得到线性地址,这是一个32位的无符号整数,可用于定位4G个存储单元。

2)物理地址:线性地址经过页表查找后得出物理地址,这个地址将被送到地址总线上指示所要访问的物理内存单元。
 
段式映射即为将逻辑地址与线性地址映射起来,而页式映射则为将线性地址和物理地址对应起来。
 
段式映射阶段:i386CPU选择代码段寄存器CS的当前值作为段描述符表中的下标,段式寄存器的第2位为0时使用GDT,为1时使用LDT。Intel设计为内核使用GDT,各个进程使用自己的LDT,寄存器的最低两位表示特权级别。在Linux内核中其实只使用GDT,在4个权限级别中只是用了0代表kernel级,3代表用户级。内核在创建一个新进程时都会先设定其段寄存器,对i386处理器中,段寄存器的设置代码位于include/asm-i386/processor.h
#define start_thread(regs, new_eip, new_esp) do {	\
	__asm__("movl %0,%%fs ; movl %0,%%gs": :"r" (0));\
	set_fs(USER_DS);				\
	regs->xds = __USER_DS;				\
	regs->xes = __USER_DS;				\
	regs->xss = __USER_DS;				\
	regs->xcs = __USER_CS;				\
	regs->eip = new_eip;				\
	regs->esp = new_esp;				\
} while (0)
 
从代码可以看出,Linux将i386处理器的DS,ES,SS寄存器均设置为USER_DS,这表示在Linux中对于进程的代码段,数据段和堆栈段是不区分的。__USER_CS和__USER_DS的设置位于include/asm-i386/segment.h
#ifndef _ASM_SEGMENT_H
#define _ASM_SEGMENT_H

#define __KERNEL_CS	0x10
#define __KERNEL_DS	0x18

#define __USER_CS	0x23
#define __USER_DS	0x2B

#endif
 
由以上代码可以看出,CS寄存器中的内容是0x23,通过段寄存器各位的含义可知,CPU以4作为下标,从全局描述符表GDT中寻找段描述选项,GDT的内容在arch/i386/kernel/head.S中定义
ENTRY(gdt_table)
	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x0000000000000000	/* not used */
	.quad 0x00cf9a000000ffff	/* 0x10 kernel 4GB code at 0x00000000 */
	.quad 0x00cf92000000ffff	/* 0x18 kernel 4GB data at 0x00000000 */
	.quad 0x00cffa000000ffff	/* 0x23 user   4GB code at 0x00000000 */
	.quad 0x00cff2000000ffff	/* 0x2b user   4GB data at 0x00000000 */
	.quad 0x0000000000000000	/* not used */
	.quad 0x0000000000000000	/* not used */
	/*
	 * The APM segments have byte granularity and their bases
	 * and limits are set at run time.
	 */
	.quad 0x0040920000000000	/* 0x40 APM set up for bad BIOS's */
	.quad 0x00409a0000000000	/* 0x48 APM CS    code */
	.quad 0x00009a0000000000	/* 0x50 APM CS 16 code (16 bit) */
	.quad 0x0040920000000000	/* 0x58 APM DS    data */
	.fill NR_CPUS*4,8,0		/* space for TSS's and LDT's */

 

到此段式映射已经完成,实际上对于Linux的页式映射来说这一步完全没用但又不得不做。通过段映射,进程的逻辑地址已经映射到了线性地址,但实际上通过段描述符的意义来看,二者是相同的。
 
页式映射阶段:在页式存储中,每个进程都有其自身的PGD,指向PGD的指针存在进程的mm_struct中,每当进程运行的时候,内核需要设定好控制寄存器CR3,MMU是从CR3中取得页面目录指针的。我们知道CPU在执行程序时使用的是虚拟的地址,而MMU硬件在映射时使用的是实际的物理内存地址,其具体实现是由include/asm-i386/mmu_context.h中的函数实现
static inline void switch_mm(struct mm_struct *prev, struct mm_struct *next, struct task_struct *tsk, unsigned cpu)
{
	if (prev != next) {
		/* stop flush ipis for the previous mm */
		clear_bit(cpu, &prev->cpu_vm_mask);
		/*
		 * Re-load LDT if necessary
		 */
		if (prev->context.segments != next->context.segments)
			load_LDT(next);
#ifdef CONFIG_SMP
		cpu_tlbstate[cpu].state = TLBSTATE_OK;
		cpu_tlbstate[cpu].active_mm = next;
#endif
		set_bit(cpu, &next->cpu_vm_mask);
		/* Re-load page tables */
		asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));
	}
#ifdef CONFIG_SMP
	else {
		cpu_tlbstate[cpu].state = TLBSTATE_OK;
		if(cpu_tlbstate[cpu].active_mm != next)
			BUG();
		if(!test_and_set_bit(cpu, &next->cpu_vm_mask)) {
			/* We were in lazy tlb mode and leave_mm disabled 
			 * tlb flush IPI delivery. We must flush our tlb.
			 */
			local_flush_tlb();
		}
	}
#endif
}

#define activate_mm(prev, next) \
	switch_mm((prev),(next),NULL,smp_processor_id())

#endif

 

重点关注其中的asm volatile("movl %0,%%cr3": :"r" (__pa(next->pgd)));它实现的功能即为将页目录指针读入CR3寄存器中。通过线性地址的最高10位可以从页面目录中知道具体的目录项,在找到进程的目录项之后,在目录项中,高20位指向页面表,在得到页面表之后,CPU再从线性地址的中间10位得到页表中的表项。在32位处理器上页表中的高20位指向物理内存的初始地址,在其后添加12个0,然后加上线性地址中的低12位(即为线性地址中的偏移量),这样就得到了一个具体的物理地址了。

 

在地址映射这个问题上,内核只提供页表,实际的转换是由硬件去完成的。那么内核如何生成这些页表呢?这就有两方面的内容,虚拟地址空间的管理和物理内存的管理。实际上只有用户态的地址映射才需要管理,内核态的地址映射是写死的即为[0xC000 0000] (3 GB)到[0xFFFF FFFF] (4 GB)。在这一部分中,内核要实现的一个重要功能就是通过高速缓存来提高查找速度。

 

参考资料:

(0)http://book.douban.com/subject/1231584/

(1)http://hi.baidu.com/_kouu/item/4c73532902a05299b73263d0

(2)http://docs.huihoo.com/joyfire.net/3-1.html

(3)http://baike.baidu.com/view/3289301.htm

(4)http://www.eefocus.com/article/09-06/74736s.html


登录 *


loading captcha image...
(输入验证码)
or Ctrl+Enter