Understanding The Linux Kernel --- Part2 Memory Addressing

内存寻址

操作系统自身不必完全了解物理内存,如今的微处理器包含的硬件线路使内存管理既高效又健壮,所以编程错误就不会对该程序之外的内存产生非法访问

  1. x86如何进行芯片级内存寻址
  2. Linux如何利用寻址硬件

x86 三种不同的地址术语

逻辑地址

逻辑地址是机器语言指令中用于表示一个操作数或者指令的地址

不同的机器架构的表现形式不一样,80X86的分段结构将每个逻辑地址都由一个段和一个偏移量来组成。

线性地址或称虚拟地址

线性地址是一个32位的无符号整数,可以用来表达高达4GB的地址,也是程序中实际应用的地址。每个进程都认为自己拥有4GB的地址空间可以使用。范围从0x0000_0000到0xffff_ffff

物理地址

用于内存芯片级内存单元的寻址,它们与微处理器的地址引脚发送到内存总线上的电信号对应

转换线路

内存控制单元MMU的分段单元硬件电路将机器语言中的逻辑地址转换成线性地址

分页单元硬件电路将线性地址转换为物理地址

对物理地址的访问,也就是对RAM芯片的访问,是并发的。多个CPU可以并发地像RAM芯片访问,但是由于对RAM芯片的读写是串行的,所以同一时间对RAM的读写必须得到仲裁:如果发现已经有一个CPU在访问RAM,那么此刻到来的其他CPU必须延迟读写操作知道正在读写的那个CPU读写完毕。

这是对于多处理器而言,但其实对于单处理器来说也是一样的,因为除了单独的那个CPU外,访问RAM芯片的还有DMA。

x86模式下的两种模式的不同硬件分段模型

x86的逻辑地址由分段模型提供,要有段地址和偏移地址

x86提供了段选择符提供段地址,并且为了快速定位到段选择子,处理器提供了段寄存器

段选择符的格式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EXqAsTrr-1677760557953)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301143659550.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aRXE72qt-1677760557953)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301145110077.png)]

段寄存器:

CS 代码段寄存器

SS 栈段寄存器

DS 数据段寄存器 通常作为数据源端

ES 额外的馈赠 通用目的的段寄存器 通常作为数据目的端

FS 通用目的的段寄存器

GS 通用目的的段寄存器

段描述符格式:

每个段由一个8字节的段描述符表示,它描述了一个段的所有信息。段描述符全都放在全局描述符表(GDT)或者局部描述符表(LDT)中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jrYtKt4g-1677760557954)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301144735619.png)]

根据type字段的不同,段描述符可以描述多种不同的段:

  1. 代码段描述符
  2. 数据段描述符
  3. 任务状态段描述符TSS
  4. 局部描述符表描述符

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6C5kJldU-1677760557954)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301145029556.png)]

实模式下寻址

x86:段寄存器值 << 4 + 偏移寄存器值

保护模式下寻址

宏观

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8EziBXOU-1677760557954)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301145134344.png)]

微观

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YMBLR4Ju-1677760557955)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301145320158.png)]

2.6版的Linux只有在80X86结构下才使用分段

运行在用户态,使用一对相同的段来寻址指令和数据

运行在内核态,使用另一对相同的段来寻址指令和数据

不需要给每个任务再单独分配段描述符

内核段描述符和用户段描述符虽然起始线性地址和长度都一样但是DPL不一样

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2NTU6M7I-1677760557955)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301195021762.png)]

(从图中可以看出:寻址上限是2^32-1 都从0x0000_0000说明Linux下逻辑地址和线性地址是一致的,也就是说逻辑地址的偏移量字段与相应的线性地址的值总是一致的)

段选择符使用宏定义:

用户代码段:__USER_CS

用户数据段:__USER_DS

内核代码段:__KERNEL_CS

内核数据段:__KERNEL_DS

为了寻址内核代码:

CS <-- __KERNEL_CS

单处理器使用单个GDT

多出来起使用多个GDT,并且使用数组cpu_gdt_table来组织他们

任务状态段TSS每个处理器有1个

每个TSS相应的线性地址空间都是内核数据段相应的线性地址空间的一个小子集

所有的TSS都顺序地存放在init_tss数组的第N个元素

GDT的布局示意图:

每个GDT包括18个段描述符和14个空的,未使用的保留项

插入未使用的项是为了使经常一起访问的描述符能够处于同一个32Byte的硬件高速缓存中

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zujSOWoG-1677760557955)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301204842859.png)]

Linux在X86的分段机制上运行,但是巧妙地绕开了分段,Linux主要以分页单元内存管理

分页机制

线性地址翻译成物理地址

其中一个关键任务是将所请求的访问类型与线性地址儿访问全新相比较,如果内存访问无效则产生一个缺页异常page fault

线性地址被分成以固定长度为单位的组 --> 页(page) 页内部连续的线性地址被映射到连续的物理连续地址中

物理地址RAM随之被划分成固定长度 --> 页框(page frame) 物理页

记录 线性地址 <–> 物理地址 映射 关系的数据结构叫 --> 页表(page table)

通过CR0寄存器的PG标志启用分页 PG=0则线性地址被解释成物理地址

常规分页:

Intel 80386 分页单元处理4KB的页

32位线性地址分成3个域

  high10	| mid10	|   low12

Directory  | Table  |  Offset

分级的目的是减少页表每个项(PTE)所要表达的信息量

如果使用一级页表 2^20个表项 每个表项4字节 需要2^20*4 = 4MB RAM

每个活动的进程都需要登记在页目录的其中一项

没必要马上为进程的所有页表都分配RAM,只有进程实际需要一个页表时才分配给它RAM会更有效率

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qBdpKLqN-1677760557956)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301214414398.png)]

CR3存放页目录所在的物理地址

页目录和页表项的格式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hi69zt6y-1677760557956)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302150222919.png)]

每个字段的含义:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZYzUmSXu-1677760557956)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301215158178.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pa3peMOJ-1677760557957)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301215207227.png)]

扩展分页

线性地址解释方式变为:

High10    |    Low22 高10位有效

Directory |    OFFSET

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f4a6gmfD-1677760557957)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301215345526.png)]

扩展分页用于将大段的连续的线性地址转换为相应的物理地址

设置CR4处理器的PSE标志 采用混合常规分页与扩展分页

硬件的保护

页表项和页目录项的格式中有USER/SUPERVISOR标志

该标志为0 只有CPL<3(内核态)时可以对页寻址

该标志为1 所有CPL登记都可以对页寻址

常规分页举例

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cIUpJcf8-1677760557958)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230301221529322.png)]

物理地址扩展PAE

物理地址扩展 2^32 --> 2^36 64GB

跟着分页机制也要更改

CR4的PAE标志激活PAE

页目录项中的页大小标志启用大尺寸页

PAE开启时页大小为2MB

改变后的分页机制:

64GB的RAM划分为2^24页框 2^36 / 2^24 = 2^12= 4kb 每页还是4K

页表项物理地址字段20bit --> 24bit

12标志位 + 24物理地址为 以至于原来总大小32bit --> 64bit

引入Page Directory pointer Table PDPT 页目录指针 表 CR3包含27位的PDPT基址字段

映射方式:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40UZ95Qt-1677760557959)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302150824664.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6VzIfQTz-1677760557960)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302150834503.png)]

Linux中的分页

Linux提供了一种同时兼容32位和64位系统的普通分页模型

对于32位系统来说 使用两级页表已经足够了

对于64位系统来说 需要更多级的页表 --> 三级甚至四级

2.6.1 采用四级分页模型:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BqDbOxqE-1677760557961)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302104825732.png)]

Linux通过使用页上级目录和页中间目录全为0,从根本上取消了页上级目录和页中间目录字段。同样的代码再32位系统下和64位系统下都可以使用

Linux内存管理对进程处理的贡献:

  1. 每个进程分配一块不同的物理地址空间,访问寻址错误
  2. 区分页面(数据组)和页面帧(主内存中的物理地址)。这允许将同一页面存储在页面帧中,然后保存到磁盘中,然后在其他页面帧中重新加载。

每一个进程有它自己的页全局目录和自己的页表集。当发生进程切换时,Linux把cr3控制寄存器的内容保存在前一个执行进程的描述符中,然后把下一个要执行进程的描述符的值装入cr3寄存器中。因此,当新进程重新开始在CPU上执行时,分页单元指向一组正确的页表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jZQqxSdU-1677760557961)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302131738714.png)]

Linux中分段分页相关的宏定义

线性地址部分

PAGE_OFFSET 线性地址的offset字段的位数 决定了页大小 X86架构下该宏值为12

#define PAGE_SHIFT 12
#define PAGE_SIZE  (_AC(1,UL) << PAGE_SHIFT)
#define PAGE_MASK	( ~(PAGE_SIZE-1) )

#define __AC(X,Y)	(X##Y)
#define _AC(X,Y)	__AC(X,Y)
#define _AT(T,X)	((T)(X))

PMD_SHIFT 线性地址的offset字段+Table字段的总和

#define PMD_SHIFT	(PAGE_SHIFT + (PAGE_SHIFT-3))
#define PMD_SIZE	(1UL << PMD_SHIFT)
#define PMD_MASK	(~(PMD_SIZE-1))

PUD_SHIFT 页上级目录项能映射的区域大小的对数

#define PUD_SHIFT		26
#define PTRS_PER_PUD		1
#define PUD_SIZE		(1UL << PUD_SHIFT)
#define PUD_MASK		(~(PUD_SIZE - 1))
#define PUE_SIZE		256

PDGIR_SHIFT 页全局目录能映射的区域大小的对数

#define PGDIR_SHIFT		26
#define PGDIR_SIZE		(1UL << PGDIR_SHIFT)
#define PGDIR_MASK		(~(PGDIR_SIZE - 1))
#define PTRS_PER_PGD		64

PTRS_PER_PTE/PTRS_PER_PMD/PTRS_PER_PUD/PTRS_PER_PGD

计算 页表 页中间目录 页上级目录 页全局目录 中表项的个数

禁止PAE 1024 1 1 1024

开启PAE 512 412 1 4

#define PTRS_PER_PTE		4096
#define PTRS_PER_PTE	(1UL << (PAGE_SHIFT-3))
#define PTRS_PER_PMD	(1UL << (PAGE_SHIFT-3))
#define PTRS_PER_PGD	(1UL << (PAGE_SHIFT-3))
页表处理相关
/*
 * These are used to make use of C type-checking..
 */
typedef struct { unsigned long pte; } pte_t;
typedef struct { unsigned long pmd; } pmd_t;
typedef struct { unsigned long pud; } pud_t;
typedef struct { unsigned long pgd; } pgd_t;
#define PTE_MASK	PHYSICAL_PAGE_MASK

typedef struct { unsigned long pgprot; } pgprot_t;

#define pte_val(x)	((x).pte)
#define pmd_val(x)	((x).pmd)
#define pud_val(x)	((x).pud)
#define pgd_val(x)	((x).pgd)
#define pgprot_val(x)	((x).pgprot)

#define __pte(x) ((pte_t) { (x) } )
#define __pmd(x) ((pmd_t) { (x) } )
#define __pud(x) ((pud_t) { (x) } )
#define __pgd(x) ((pgd_t) { (x) } )
#define __pgprot(x)	((pgprot_t) { (x) } )

修改页表表项:

#define pte_none(x)	(!pte_val(x))
#define pte_present(x)	(pte_val(x) & (_PAGE_PRESENT | _PAGE_PROTNONE))
#define pte_clear(mm,addr,xp)	do { set_pte_at(mm, addr, xp, __pte(0)); } while (0)
static inline void set_pte(pte_t *dst, pte_t val)
{
	pte_val(*dst) = pte_val(val);
}
set_pte_atomic
pte_same
pte_large	指向大页的检测
pte_bad		页是否存在主存中
pte_preset	页表的present字段

设置页标志的函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qKpH9Hhe-1677760557962)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302153749027.png)]

对页表操作的宏

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0DpYFZOh-1677760557963)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302153833219.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awMbG7kb-1677760557964)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302153905375.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IFHHoR4E-1677760557964)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302153947470.png)]

对页表项操作的宏

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DG9PfamB-1677760557965)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302154024033.png)]

物理内存布局

内核初始化阶段,内核必须简历一个物理地址的映射来指定哪些物理地址范围对于内核是可用的 哪些是不可用的

哪些是映射硬件设备I/O的 哪些是BIOS数据

不可以用的物理地址空间被记录为保留的页框

含有内核代码和已经初始化的数据结构的空间被记录为保留页框

保留页框的页绝不能被动态分配或者交换到磁盘上

作为一般规则,Linux内核从物理地址0x00100000开始安装,即从第二个兆字节开始安装。所需的页帧总数取决于内核的配置方式。一个典型的配置生成的内核可以在小于3 MB的RAM中加载。

启动过程的早期内核会询问BIOS以获取物理内存的实际大小

新近计算机中内核调用BIOS过程建立一组物理地址范围和其对应的内存类型

内核执行machine_specific_memory_setup() 函数 建立起物理地址映射

BIOS提供的物理地址映射举例

开始 结束 类型
0x0000_0000 0x0009_ffff Usable
0x0010_0000 0x07fe_ffff Usable
0x07ff_0000 0x07ff_ffff 加电自检阶段BIOS写入的硬件设备信息 ACPI data
0x07ff_3000 0x07ff_ffff 映射到硬件设备的ROM芯片 ACPI NVS
0xffff_0000 0xffff_ffff 由硬件映射到BIOS Reserved

随后执行setup_memory() 函数 分析物理内存区域表示初始化一些变量来描述内核的物理内存

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CFBDdYGw-1677760557965)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302160256060.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7luthg9w-1677760557965)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302160350509.png)]

可以在system.map中看到这些图中符号的线性地址,它是编译内核以后所创建的

避免kernel被装入一组不连续的页框中,Linux跳过RAM的第一个MB

_text physical addr = 0x00100000 kernel code start

_etext kernel code end

_etext ~ _edata initialized kernel data

_edata ~ _end uninitialized kernel data

图中出现的符号都是编译内核时产生的

进程页表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-REzRgDLi-1677760557966)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302161102248.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PzmQe3hE-1677760557966)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302161612022.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9nSYTOjp-1677760557966)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302161622170.png)]

内核页表:

内核自己维护一组页表,放在主内核页全局目录

系统初始化后这组页表从未被任何进程或者任何内核线程直接使用

内核如何初始化自己的页表,分两步

  1. 实模式下,内核创建一个有限的地址空间 128KB大小 (装载内核代码段数据段,初始页表,并用于存放动态数据结构) 这个空间只能将内核装到RAM 这个阶段称为临时内核页表
  2. 内核充分利用剩余的RAM并适当地建立 分页表

假设内核使用的段,临时页表和128KB的内存范围能够被RAM的前8MB容纳

分页的第一阶段 要允许实模式下和保护模式下都能够很容易地对这8MB寻址

由于Linux由BIOS加载后,起始阶段其实是运行在实模式,此时并没有开启分页机制。那Linux在开启分页机制之前需要先做哪些准备工作以支持分页机制?答案是页目录项及页表项,可以根据x86的硬件分页机制知道,此时的两级映射。

一般来说,Linux内核安装在RAM中从物理地址0x0010 0000开始的地方,也就是说,从第二个MB开始。
为什么内核没有安装在RAM第一个MB开始的地方?因为PC体系结构有几个独特的地方必须考虑到。例如:

  • 页框0由BIOS使用,存放加电自检(Power-On Self-Test, POST)期间检查到的系统硬件配置。
  • 物理地址从0x000a 0000到0x000f ffff的范围通常留给BIOS例程,并且映ISA图形卡上的内部内存。
  • 第一个MB内的其它页框可能由特定计算机模型保留

必须创建映射:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Hzk6kyXh-1677760557967)(C:\Users\EDZ\AppData\Roaming\Typora\typora-user-images\image-20230302173829395.png)]

临时页表,线性地址0的起始和0xC000_0000的起始,都指向同一片物理内存

这个过程的代码:

/* page_pde_offset/4 = 0x300,正好为页目录的0x300项(0xc000 0000的前10位) 
 * 0x0000 0000 与 0xC000 0000在页目录项正好偏移0x300项 */
page_pde_offset = (__PAGE_OFFSET >> 20);
  /* 页表是放在pg0开始的内存处 */
	movl $(pg0 - __PAGE_OFFSET), %edi
	movl $(swapper_pg_dir - __PAGE_OFFSET), %edx
	movl $0x007, %eax			/* 0x007 = PRESENT+RW+USER */
10:
	leal 0x007(%edi),%ecx			/* Create PDE entry */
  /* 填充线性地址0x0000 0000开始的目录项 */
	movl %ecx,(%edx)			/* Store identity PDE entry */
	/* 填充线性地址0xc000 0000开始的目录项 */
	movl %ecx,page_pde_offset(%edx)		/* Store kernel PDE entry */
	addl $4,%edx
	/* 一个页表包含1024项 */
	movl $1024, %ecx
11:
  /* 填充页表项,一个页表项映射4k物理内存,所以每次加0x1000 */
	stosl
	addl $0x1000,%eax
	loop 11b
	/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
	/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
  /* INIT_MAP_BEYOND_END=128*1024, 确定建立的页表是否能够映射“最后一个页表地址” + 128K的内存
   * 如果不行继续建立映射关系 */
	leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp
	cmpl %ebp,%eax
	jb 10b

正式启用分页以后 目录中低地址上的页表会被释放 清除

随后 第一阶段就可以通过与物理地址相同的线性地址 或者 通过从0xc0000000开始的8MB线性地址对RAM的前8MB进行寻址

内核通过把swapper_pg_dir所有项都填充为0来创建期望的映射:

// swapper_pg_dir,这个全局变量,在汇编 head.S 中定义了
// 编译时便被分配了,这是一个真正的全局变量,生命周期与内核相等
ENTRY(swapper_pg_dir)
	.fill 1024,4,0

临时的页面全局目录包含在swapper_pg_dir变量中。临时页面表从pg0开始存储,就在内核未初始化的数据段结束(图2-13中的符号_end)之后。

启用分页单元:

/*
 * Enable paging
 */
	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 */

最终映射的建立:

建立完临时映射大概8M的内存空间后,Linux内核就运行在保护模式了,此时就可以开始进行一些内核的基本初始化工作了。

最终内核页表的初始化主要是这个函数,在 x86 上,大概分了几种情况:

  • 内存小于 896M
  • 内存小于 4G
  • 内存大于 4G

主要都是在 init.c -> paging_init() 这个函数里实现的。
在这个函数里,会通过预定义(在配置环节)的宏定义、CPU的寄存器值等条件,完成不同情况的区分。

在这里我们不需要太纠结于Linux中页表之间的层次关系,比如觉得pgd一定要索引到pud,其实pud,pmd及pte只是Linux中对页表的一个抽象,只是一个命名而已,应该关注的是里面的内容,比如说常规分页模式下,pgd的目录项是初始化为pte的基地址及一些标志位,虽然没有pud,但是能找到下一级页表就行。下面我们从代码实现中就能看到:
函数的调用关系为:pagetable_init -> kernel_physical_mapping_init

void __init paging_init(void) // line: 499
static void __init pagetable_init (void) // line:310
static void __init kernel_physical_mapping_init(pgd_t *pgd_base) // line: 143
static void __init page_table_range_init (unsigned long start, unsigned long end, pgd_t *pgd_base) // line: 10

static void __init pagetable_init (void)
{
	unsigned long vaddr;
	pgd_t *pgd_base = swapper_pg_dir;

#ifdef CONFIG_X86_PAE
	int i;
	/* Init entries of the first-level page table to the zero page */
	/* 在扩展模式下,pgd的前三项映射线性地址空间为0-3G,这为用户空间访问的地址范围,所以用一个空页对它进行初始化 */
	for (i = 0; i < PTRS_PER_PGD; i++)
		set_pgd(pgd_base + i, __pgd(__pa(empty_zero_page) | _PAGE_PRESENT));
#endif

	/* Enable PSE if available */
	if (cpu_has_pse) {
		set_in_cr4(X86_CR4_PSE);
	}

	/* Enable PGE if available */
	if (cpu_has_pge) {
		set_in_cr4(X86_CR4_PGE);
		__PAGE_KERNEL |= _PAGE_GLOBAL;
		__PAGE_KERNEL_EXEC |= _PAGE_GLOBAL;
	}

	kernel_physical_mapping_init(pgd_base);
	remap_numa_kva();

	/*
	 * Fixed mappings, only the page table structure has to be
	 * created - mappings will be set by set_fixmap():
	 */
	vaddr = __fix_to_virt(__end_of_fixed_addresses - 1) & PMD_MASK;
	page_table_range_init(vaddr, 0, pgd_base);

	permanent_kmaps_init(pgd_base);

#ifdef CONFIG_X86_PAE
	/*
	 * Add low memory identity-mappings - SMP needs it when
	 * starting up on an AP from real-mode. In the non-PAE
	 * case we already have these mappings through head.S.
	 * All user-space mappings are explicitly cleared after
	 * SMP startup.
	 */
	pgd_base[0] = pgd_base[USER_PTRS_PER_PGD];
#endif
}

/*
 * This maps the physical memory to kernel virtual address space, a total 
 * of max_low_pfn pages, by creating page tables starting from address 
 * PAGE_OFFSET.
 */		// pfn 应该是 page frame number
static void __init kernel_physical_mapping_init(pgd_t *pgd_base)
{
	unsigned long pfn;
	pgd_t *pgd;
	pmd_t *pmd;
	pte_t *pte;
	int pgd_idx, pmd_idx, pte_ofs;

	pgd_idx = pgd_index(PAGE_OFFSET);	// 0xC000_0000 对应的页全局目录项在页全局目录中对应的位置
	pgd = pgd_base + pgd_idx;	// 页全局目录的一个结构体指针,指向的是页全局目录数组的某个元素
	pfn = 0;

	for (; pgd_idx < PTRS_PER_PGD; pgd++, pgd_idx++) {	// 页全局目录中有 1024 个元素,遍历一遍
		pmd = one_md_table_init(pgd);	// 最后返回的,是一个 pmd_t *;但是在二级分页的情况下,pmd 就等于 pgd
		if (pfn >= max_low_pfn)
			continue;
		for (pmd_idx = 0; pmd_idx < PTRS_PER_PMD && pfn < max_low_pfn; pmd++, pmd_idx++) {	// 二级分页下,PTRS_PER_PMD 为1,所以这个循环体只会被执行一次
			unsigned int address = pfn * PAGE_SIZE + PAGE_OFFSET;	// 这个明显是 PA,和 pfn 相关的应该都是 PA

			/* Map with big pages if possible, otherwise create normal page tables. */
			if (cpu_has_pse) {	// 如果支持大页表,也就是一页 4M;反正内核是需要连续空间,并且也不会被换出,所以使用大页表是合理的
				unsigned int address2 = (pfn + PTRS_PER_PTE - 1) * PAGE_SIZE + PAGE_OFFSET + PAGE_SIZE-1;

				// 不启用 PAE 的话,下面这两个 if 分支实际上是等价的
				// set_pmd 执行的是一个把值赋给指针的操作
				// 值=物理地址左移12位+标志位
				// 指针=页中间目录(数组)中的某个元素指针,note that:二级分页的情况下,页中间目录项=页全局目录项
				if (is_kernel_text(address) || is_kernel_text(address2))
					set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE_EXEC));
				else
					set_pmd(pmd, pfn_pmd(pfn, PAGE_KERNEL_LARGE));
				pfn += PTRS_PER_PTE;
			} else {	// 如果不支持大页表,页就是一页只能是 4k,那么需要把页中间目录中的每个页表项都初始化一遍,所以需要另一个循环
				pte = one_page_table_init(pmd);

				for (pte_ofs = 0; pte_ofs < PTRS_PER_PTE && pfn < max_low_pfn; pte++, pfn++, pte_ofs++) {
						// set_pte 的操作和 set_pmd 类似
						if (is_kernel_text(address))
							set_pte(pte, pfn_pte(pfn, PAGE_KERNEL_EXEC));
						else
							set_pte(pte, pfn_pte(pfn, PAGE_KERNEL));
				}
			}
		}
	}
}

当建立完内核最终的线性映射之后,线性地址0xc0000000 - 0xc0000000+896M, 对应的物理地址就为0x00000000-0x00000000+896M,这就是所谓的线性映射。

你可能感兴趣的:(操作系统,linux,网络,运维)