Linux内存分段分页

随着集成技术越来越精细,内存存储量从字节,千字节,兆字节,GB,…容量越来越大。如何有效地管理内存是一门艺术。在80X86体系中通过分段部件和分页部件提供内存管理的支持,由此从换分出实地址模式,保护模式。实地址模式下一般是裸机程序,Linux启动起始内核代码载入内存运行并没有内存管理机制,因此内核是以裸机程序运行,接着通过创建自举分配器创建一个零时的内存分配器来支持系统进一步构建内存管理子系统,待内存管理子系统构建完成以后便抛弃这个自举分配器,同时内核由实地址模式进入到保护模式。保护模式下的内存又分为分段管理,分页管理。分页管理是分段管理的进化版。在硬件上对分段管理有段寄存器来支持,分页管理有MMU支持。

一.分段管理

1.16位CPU

Intel 8086是16位CPU,它只有16位寄存器、16位数据总线和20位地址总线,它只能运行在实模式,16位的CPU如何访问20位的地址范围(2的20次方=1 048 576=1M)的内存空间。为了能够访问到整个地址空间,在CPU里添加了4个段寄存器,寄存器位数为16位,分别为CS(代码段寄存器)DS(数据段寄存器) SS(堆栈段寄存器)ES(扩展段寄存器)。所以段寄存器就是为了解决CPU位数和地址总线不同的问题而诞生的。在实模式下通过 物理地址=段值*16+偏移,乘以16(2的4次方)意味着左移四位,加上段寄存器本身的16位,凑成20位,段值和偏移都是16位的 具有1MB(2^16 * 2^4 + offset)的寻址能力。

2.32位的CPU

Intel的CPU发展到80386时,CPU变成了32位,地址总线变成32位,寻址空间达到了2的32次方=4GB。然而寄存器大小为了兼容之前体系下的版本,寄存器依旧是16比特位宽,这下寻址能力不足了。于是新增了GDTR(全局的段的描述附表寄存器),LDTR(局部的描述附表寄存器)两个32位的寄存器
当x86 CPU 工作在保护模式时,可以使用全部32根地址线访问4GB的内存,因为80386的所有通用寄存器都是32位的,所以用任何一个通用寄存器来间接寻址,不用分段就可以访问4G空间中任意的内存地址。但是一个地址空间是否可以被写入,可以被多少优先级的代码写入,是不是允许执行等等涉及保护的问题就出来了。要解决这些问题,必须对一个地址空间定义一些安全上的属性。段寄存器这时就派上了用场。但是设计属性和保护模式下段的参数,要表示的信息太多了,要用64位长的数据才能表示。
我们把着64位的属性数据叫做段描述符,它包含3个变量:段物理基地址、段界限、段属性 80386的段寄存器是16位的,无法放下保护模式下64位的段描述符。把所有段的段描述符顺序存放在内存中的指定位置,组成一个段描述符表(Descriptor Table);而段寄存器中的16位用来做索引信息,这时,段寄存器中的信息不再是段地址了,而是段选择子(Selector)。可以通过它在段描述符表中“选择”一个项得到段的全部信息。

段寄存器存放着段选择子,段选择子是段的一个16位标识符。它并不直接指向段,而是指向段选择符表中定义段的段描述符。它有三个字段内容:请求特权级RPL(Request Privilege Level)、表指示标志TI(Table Index)、索引值(Index),描述符索引数量为2的13次方=8192,12个表项被系统预留了。用户只能用8080个表项,TI代表着从GDTR还是LDTR索引,RPL代表内核态。

在这里插入图片描述
段描述符是GDT和LDT表中的一个数据结构项,用来向处理器提供一个有关段的位置和大小信息以及访问控制的状态信息。包含三个主要字段:段基地址、段限长、和段属性。段描述符通常由编译器、连接器、加载器或者操作系统来创建
在这里插入图片描述
段限长为16+4=20位,段限长由G位控制,G=0代表单位1B,段长1M,G=1代表单位4KB,段长4KB*1M(2的20次方)=4G,刚好一个物理内存大小。段基地址长16+8+8=32位,与地址总线长度等。
保护模式下分段映射:GDT [DS>>3].BaseAddr + IP(逻辑地址32位) = 线性地址,GDT[DS >>3]这个是段选择符高13位(15-3)保存着所以向左移动3个位对齐。

段描述符表: 是段描述符的一个数组。
在这里插入图片描述
LDT可以看做是GDT中的一个段,不过这个段里的内容是一张段描述符表,通过TI来标识到哪张表选择描述符。于是寻址过程变成了:

在这里插入图片描述
1、段寄存器中存放段选择子Selector
2、GDTR/LDTR中存放着GDT/LDT段描述符表的基地址
3、由段寄存器中的段选择符的TI位决定到GDT/LDT表寻找描述符
4、通过选择子根据GDT/LDT中的基地址,就能找到对应的段描述符 具体来说是:选择子*8 (段描述符64位=8字节)
5、段描述符中有段的物理基地址,就得到段在内存中的基地址
6、加上偏移量(IP),就找到在这个段中存放的数据的线性地址(只有分段机制情况下,就是真实物理地址)。

在这里插入图片描述
在系统运行多个任务的时候,多个任务共享的段存放由GDT表中的段描述符标识,不共享的段由LDT表中的段描述符标识。在切换任务时只需重新装在LDTR。

3.GDT初始化

linux-5.4.80/arch/x86/boot/header.S

start_of_setup:
   ......
# Jump to C code (should not return)
	calll	main

linux-5.4.80/arch/x86/boot/main.c

void main(void)
{
    ......
    //进入保护模式
	go_to_protected_mode();
}

linux-5.4.80/arch/x86/boot/pm.c

void go_to_protected_mode(void)
{
	//离开实地址模式,禁用中断
	realmode_switch_hook();
    //使能A20
	if (enable_a20()) {
		puts("A20 gate not responding, unable to boot...\n");
		die();
	}
	//重置协处理器
	reset_coprocessor();

	/* Mask all interrupts in the PIC */
	mask_all_interrupts();

	//设置IDT,GDT
	setup_idt();
	setup_gdt();
	//跳转到code32_start指定的入口即startup_32或startup_64
	protected_mode_jump(boot_params.hdr.code32_start,
			    (u32)&boot_params + (ds() << 4));
}

linux-5.4.80/arch/x86/boot/pm.c
设置GDT

static void setup_gdt(void)
{
    //初始化GDT,设置CS,DS,TSS描述符表项
	static const u64 boot_gdt[] __attribute__((aligned(16))) = {
		/* CS: code, read/execute, 4 GB, base 0 */
		[GDT_ENTRY_BOOT_CS] = GDT_ENTRY(0xc09b, 0, 0xfffff),
		/* DS: data, read/write, 4 GB, base 0 */
		[GDT_ENTRY_BOOT_DS] = GDT_ENTRY(0xc093, 0, 0xfffff),
		/* TSS: 32-bit tss, 104 bytes, base 4096 */
		/* We only have a TSS here to keep Intel VT happy;
		   we don't actually use it for anything. */
		[GDT_ENTRY_BOOT_TSS] = GDT_ENTRY(0x0089, 4096, 103),
	};
	static struct gdt_ptr gdt;

	gdt.len = sizeof(boot_gdt)-1;
	gdt.ptr = (u32)&boot_gdt + (ds() << 4);

	asm volatile("lgdtl %0" : : "m" (gdt));
}
//gdt指针指向gdt表
struct gdt_ptr {
	u16 len;
	u32 ptr;
} __attribute__((packed));


linux-5.4.80/arch/x86/include/asm/segment.h
构造GDT/LDT段描述符表项

#define GDT_ENTRY(flags, base, limit)			\
	((((base)  & _AC(0xff000000,ULL)) << (56-24)) |	\
	 (((flags) & _AC(0x0000f0ff,ULL)) << 40) |	\
	 (((limit) & _AC(0x000f0000,ULL)) << (48-16)) |	\
	 (((base)  & _AC(0x00ffffff,ULL)) << 16) |	\
	 (((limit) & _AC(0x0000ffff,ULL))))

linux-5.4.80/arch/x86/kernel/head_32.S
protected_mode_jump跳转到此处

__HEAD
ENTRY(startup_32)
	movl pa(initial_stack),%ecx
	
	/* test KEEP_SEGMENTS flag to see if the bootloader is asking
		us to not reload segments */
	testb $KEEP_SEGMENTS, BP_loadflags(%esi)
	jnz 2f
     
    //读取GDTR寄存器
	lgdt pa(boot_gdt_descr)
	movl $(__BOOT_DS),%eax
	movl %eax,%ds
	movl %eax,%es
	movl %eax,%fs
	movl %eax,%gs
	movl %eax,%ss
	......
	//调用i386_start_kernel
    call *(initial_code)
    ......

ENTRY(initial_code)
	.long i386_start_kernel

	.data
.globl boot_gdt_descr

	ALIGN
# early boot GDT descriptor (must use 1:1 address mapping)
	.word 0				# 32 bit align gdt_desc.address
boot_gdt_descr:
	.word __BOOT_DS+7
	.long boot_gdt - __PAGE_OFFSET

# boot GDT descriptor (later on used by CPU#0):
	.word 0				# 32 bit align gdt_desc.address
ENTRY(early_gdt_descr)
	.word GDT_ENTRIES*8-1
	.long gdt_page			/* Overwritten for secondary CPUs */
	.align L1_CACHE_BYTES
//段限长4GB
ENTRY(boot_gdt)
	.fill GDT_ENTRY_BOOT_CS,8,0
	.quad 0x00cf9a000000ffff	/* kernel 4GB code at 0x00000000 */
	.quad 0x00cf92000000ffff	/* kernel 4GB data at 0x00000000 */

linux-5.4.80/arch/x86/kernel/head32.c

asmlinkage __visible void __init i386_start_kernel(void)
{
	/* Make sure IDT is set up before any exception happens */
	idt_setup_early_handler();
    ......
	start_kernel(); //开始内核其他项的启动
}

linux-5.4.80/arch/x86/include/asm/desc_defs.h
Linux通过desc_struct来表示GDT段描述符表项

struct desc_struct {
	u16	limit0;
	u16	base0;
	u16	base1: 8, type: 4, s: 1, dpl: 2, p: 1;
	u16	limit1: 4, avl: 1, l: 1, d: 1, g: 1, base2: 8;
} __attribute__((packed));

linux-5.4.80/arch/x86/include/asm/desc.h
每个处理器都对应一个gdt

struct gdt_page {
	struct desc_struct gdt[GDT_ENTRIES];
} __attribute__((aligned(PAGE_SIZE)));

DECLARE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page);

linux-5.4.80/arch/x86/kernel/cpu/common.c

DEFINE_PER_CPU_PAGE_ALIGNED(struct gdt_page, gdt_page) = { .gdt = {
#ifdef CONFIG_X86_64
	[GDT_ENTRY_KERNEL32_CS]		= GDT_ENTRY_INIT(0xc09b, 0, 0xfffff),
	[GDT_ENTRY_KERNEL_CS]		= GDT_ENTRY_INIT(0xa09b, 0, 0xfffff),
	[GDT_ENTRY_KERNEL_DS]		= GDT_ENTRY_INIT(0xc093, 0, 0xfffff),
	[GDT_ENTRY_DEFAULT_USER32_CS]	= GDT_ENTRY_INIT(0xc0fb, 0, 0xfffff),
	[GDT_ENTRY_DEFAULT_USER_DS]	= GDT_ENTRY_INIT(0xc0f3, 0, 0xfffff),
	[GDT_ENTRY_DEFAULT_USER_CS]	= GDT_ENTRY_INIT(0xa0fb, 0, 0xfffff),
#else
	......
	[GDT_ENTRY_ESPFIX_SS]		= GDT_ENTRY_INIT(0xc092, 0, 0xfffff),
	[GDT_ENTRY_PERCPU]		= GDT_ENTRY_INIT(0xc092, 0, 0xfffff),
	GDT_STACK_CANARY_INIT
#endif
} };
EXPORT_PER_CPU_SYMBOL_GPL(gdt_page);

linux-5.4.80/arch/x86/include/asm/desc_defs.h
使用GDT_ENTRY_INIT宏来填充表项

#define GDT_ENTRY_INIT(flags, base, limit)			\
	{							\
		.limit0		= (u16) (limit),		\
		.limit1		= ((limit) >> 16) & 0x0F,	\
		.base0		= (u16) (base),			\
		.base1		= ((base) >> 16) & 0xFF,	\
		.base2		= ((base) >> 24) & 0xFF,	\
		.type		= (flags & 0x0f),		\
		.s		= (flags >> 4) & 0x01,		\
		.dpl		= (flags >> 5) & 0x03,		\
		.p		= (flags >> 7) & 0x01,		\
		.avl		= (flags >> 12) & 0x01,		\
		.l		= (flags >> 13) & 0x01,		\
		.d		= (flags >> 14) & 0x01,		\
		.g		= (flags >> 15) & 0x01,		\
	}

4.任务切换

linux-5.4.80/include/linux/mm_types.h
通过mm_context_t来关联任务

struct mm_struct {
	......
	mm_context_t context;
	......
}

linux-5.4.80/arch/x86/include/asm/mmu.h

typedef struct {
	......
	struct ldt_struct	*ldt;
	......
} mm_context_t;

linux-5.4.80/arch/x86/include/asm/mmu_context.h

struct ldt_struct {
	struct desc_struct	*entries;  //段表项结构
	unsigned int		nr_entries;
	int			slot;
};

linux-5.4.80/arch/x86/include/asm/mmu_context.h
切换任务时进行ldt切换

static inline void switch_ldt(struct mm_struct *prev, struct mm_struct *next)
{
#ifdef CONFIG_MODIFY_LDT_SYSCALL
	if (unlikely((unsigned long)prev->context.ldt |
		     (unsigned long)next->context.ldt))
		load_mm_ldt(next);
#endif
}

linux-5.4.80/arch/x86/include/asm/mmu_context.h

static inline void load_mm_ldt(struct mm_struct *mm)
{
#ifdef CONFIG_MODIFY_LDT_SYSCALL
	struct ldt_struct *ldt;
	ldt = READ_ONCE(mm->context.ldt);
	if (unlikely(ldt)) {
		set_ldt(ldt->entries, ldt->nr_entries);
	} else {
		clear_LDT();
	}
#else
	clear_LDT();
#endif
}

二.分页管理

内存分段管理,可以以段为单位有效利用内存,缺点是无法利用碎片,必须搬移内存,造成性能损失。Linux通过分页管理来有效利用内存碎片,通过CR0寄存器的PE位来开启分页管理。如果不开启分页管理,那么上述分段产生的线性地址就是物理内存地址,如果开启了分页管理,那么这个32位线性地址经过页表转换以后得到物理地址。

在这里插入图片描述

1.页表和分页

以32位CPU来看,32位CPU可寻址物理内存为4G。那么如何通过页目录和页表组织4G的寻址空间转换,首先通过分段产生的线性地址是32位的,这32位线性地址划分为3段,[31:22]这10位为页目录(PGD)项索引,[21:12]这10位为页表(PTE)项索引,[11:0]这12位为页内偏移(OFFSET)。由此PGD目录项总共1024项=2的10次方,PTE表项总共1024项=2的10次方,内存页长度为4K=2的12次方。所以1024 x 1024 x 4K = 4G。32位的线性地址通过分页打散为4K的内存页。

在这里插入图片描述
由上述线性地址由页目录页索引,表索引,页偏移组成。每个进程都有各自的页表,CR3寄存器存放当前进程的页目录地址。那么Linux中目录和页表,特别是目录项和表项如何组织的呢。
在这里插入图片描述
页目录和页表项是32位的,由于内存以4K为一页的,所以页表项[12:0]被用作表项属性标记用,如P位表示该表项的有效性。R/W用于分页级读写保护。
在这里插入图片描述
最后,32位CPU,逻辑地址,线性地址,物理地址的转换如上图示。

2.32位分页

linux-5.4.80/arch/x86/include/asm/pgtable-2level_types.h
32位CPU两级分页:[31:22] - [21:12] - [11:0]

#ifndef __ASSEMBLY__
#include <linux/types.h>

typedef unsigned long	pteval_t;
typedef unsigned long	pmdval_t;
typedef unsigned long	pudval_t;
typedef unsigned long	p4dval_t;
typedef unsigned long	pgdval_t;
typedef unsigned long	pgprotval_t;

typedef union {
	pteval_t pte;
	pteval_t pte_low;
} pte_t;
#endif	/* !__ASSEMBLY__ */

#define SHARED_KERNEL_PMD	0   //两级分页时PMD目录项为0
#define PGDIR_SHIFT	22          //页目录偏移
#define PTRS_PER_PGD	1024    //页目录项1024项
#define PTRS_PER_PTE	1024    //页表项1024项
#define PGD_KERNEL_START	(CONFIG_PAGE_OFFSET >> PGDIR_SHIFT) //页目录位置[32:22]移动 32 >> 22

#endif /* _ASM_X86_PGTABLE_2LEVEL_DEFS_H */

linux-5.4.80/arch/x86/include/asm/pgtable-3level_types.h
32位CPU三级分页: [31:30] - [39:21] - [20:12] - [11:0]

#ifndef __ASSEMBLY__
#include <linux/types.h>

typedef u64	pteval_t;
typedef u64	pmdval_t;
typedef u64	pudval_t;
typedef u64	p4dval_t;
typedef u64	pgdval_t;
typedef u64	pgprotval_t;

typedef union {
	struct {
		unsigned long pte_low, pte_high;
	};
	pteval_t pte;
} pte_t;
#endif	/* !__ASSEMBLY__ */

#ifdef CONFIG_PARAVIRT_XXL
#define SHARED_KERNEL_PMD	((!static_cpu_has(X86_FEATURE_PTI) &&	\
				 (pv_info.shared_kernel_pmd)))
#else
#define SHARED_KERNEL_PMD	(!static_cpu_has(X86_FEATURE_PTI))
#endif

#define PGDIR_SHIFT	30    //页目录偏移
#define PTRS_PER_PGD	4  //页目录4项
#define PMD_SHIFT	21     //中间级页目录偏移
#define PTRS_PER_PMD	512  //中间级页目录512项
#define PTRS_PER_PTE	512  //页表512项

#define MAX_POSSIBLE_PHYSMEM_BITS	36
#define PGD_KERNEL_START	(CONFIG_PAGE_OFFSET >> PGDIR_SHIFT)

#endif /* _ASM_X86_PGTABLE_3LEVEL_DEFS_H */

3.64位分页

linux-5.4.80/arch/x86/include/asm/pgtable_64_types.h
四级页表: [48:39] - [38:30] - [29:21] - [20:12] - [11:0]
寻址范围: 0x000000000000-0xFFFFFFFFFFFF,256TB
五级页表: [55-48] - [47:39] - [38:30] - [29:21] - [20:12] - [11:0]
寻址范围0x00000000000000-0xFFFFFFFFFFFFFF,128PB

#ifndef __ASSEMBLY__
#include <linux/types.h>
#include <asm/kaslr.h>

typedef unsigned long	pteval_t;
typedef unsigned long	pmdval_t;
typedef unsigned long	pudval_t;
typedef unsigned long	p4dval_t;
typedef unsigned long	pgdval_t;
typedef unsigned long	pgprotval_t;

typedef struct { pteval_t pte; } pte_t;

extern unsigned int pgdir_shift;
extern unsigned int ptrs_per_p4d;

#endif	/* !__ASSEMBLY__ */

#define SHARED_KERNEL_PMD	0

//五级页表
#ifdef CONFIG_X86_5LEVEL
#define PGDIR_SHIFT	pgdir_shift    //偏移
#define PTRS_PER_PGD	512        //PGD表512项

//五级页表下的四级页表
#define P4D_SHIFT		39         //P4D偏移
#define MAX_PTRS_PER_P4D	512    //P4D表512项
#define PTRS_PER_P4D		ptrs_per_p4d
#define P4D_SIZE		(_AC(1, UL) << P4D_SHIFT)
#define P4D_MASK		(~(P4D_SIZE - 1))

#define MAX_POSSIBLE_PHYSMEM_BITS	52

#else /* CONFIG_X86_5LEVEL */

//四级页表
#define PGDIR_SHIFT		39
#define PTRS_PER_PGD		512
#define MAX_PTRS_PER_P4D	1

#endif /* CONFIG_X86_5LEVEL */

//三级页表
#define PUD_SHIFT	30
#define PTRS_PER_PUD	512

//二级页表
#define PMD_SHIFT	21
#define PTRS_PER_PMD	512

//一级页表
#define PTRS_PER_PTE	512

#define PMD_SIZE	(_AC(1, UL) << PMD_SHIFT)
#define PMD_MASK	(~(PMD_SIZE - 1))
#define PUD_SIZE	(_AC(1, UL) << PUD_SHIFT)
#define PUD_MASK	(~(PUD_SIZE - 1))
#define PGDIR_SIZE	(_AC(1, UL) << PGDIR_SHIFT)
#define PGDIR_MASK	(~(PGDIR_SIZE - 1))
......
#endif /* _ASM_X86_PGTABLE_64_DEFS_H */

Linux内核通过分段分页机制来划分内存,将程序的逻辑地址转换为线性地址,线性地址为多任务模型提供了统一的内存模型,再将线性地址转换为物理地址,使得较小的物理内存也可以拥有较大的线性地址空间。这样上层应用就可以不用考虑多样化的复杂的内存管理问题,同样这个模型也使得Linux具有极好的兼容性,可以适应多样化的硬件。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值