《深入理解Linux内核(第三版)》笔记(一),第二章内存寻址

本文介绍了如何将经典《深入理解Linux内核》第三版中的内存寻址理论应用到ARM架构上,并详细剖析了页框、页目录、页表转换的过程,包括转换表的实现和相关宏定义。作者还分享了页描述符和页表项的创建过程,以及内核页表初始化的关键步骤,特别关注了swapper_pg_dir变量的作用和内核物理内存映射的初始化策略。
摘要由CSDN通过智能技术生成

《深入理解Linux内核(第三版)》,如此经典;以下简称《第三版》
不过这本书是基于 x86 硬件讲的;虽然和 ARM 移植的需求有些出入,但是目前没找到更适合上手的资料
针对 ARM 的 Linux 讲解,可以参考《奔跑吧,Linux 内核》系列的书籍

对应版本的内核源码的快照:
linux-2.6.11源码
基于源码的对应的节点,自己维护了一个阅读版的仓库。
在代码轻量的添加了一些笔记。该专栏中的代码行数均和该版本代码对应。

单独一个仓库的好处是代码下载比较快,坏处是没法跟踪每行代码最初是谁写的。

笔记(一),记录对《第三版》第二章“内存寻址”的学习
PS:
笔记写的再好,也取代不了书;如果想详细理解这书中内容,还是要抱着本子看的
笔记是对原书的扩展,把自己的理解备注到原文上,为以后的阅读和理解节省时间
而不是精简;把书的内容或者目录抄一遍到博客上,没太多意义

开篇讲内存是有意义的。
——第一感觉总是从 init 进程开始;但是,内存的操作,是进程开始的前置条件。
——可以通过简单的阅读 page.h 相关的代码,稍微的熟悉下内核的编码风格。

由虚拟地址(VA)到物理地址(PA)的寻址流程

几个概念:页框,页目录转换表,页目录,页表转换表,页表

  • 页框可以理解为物理实体,4K大小;物理内存被划分为一个一个的页框
  • 所有的页目录放到一个特定的页框(即“页目录转换表”)里
    • 这个页框的物理地址是被内核知道的
    • 并且在内核的整个生命周期中不会改变
  • “页目录转换表”里有 1024 个页目录,每个页目录4Byte,索引一个“页表转换表”
  • 理论上有 1024 个页框被作为“页表转换表”,可以放到内存的各个地方,但是要 4K 地址对齐
  • 一个页表转换表内有 1024 个页表,每个页表4Byte,索引一个物理页框

然后说这两级转换表的指向的实现,是 field 字段和线性地址的联合运用:

  • 两个转换表都有1024个元素,正好,各对应线性地址中的 10 个 bit
  • 页目录和页表的 4 个 Byte 分为两段:present 字段和 field 字段
  • present 字段在低 12bit,包含标志位,后续展开
  • field 字段有 20 bit
  • 首先,要在“页目录转换表”中找到对应的页目录
    • “页目录转换表”的基地址是已知的
    • 线性地址的 bit31:22 提供索引,找到目标页目录在“页目录转换表”中的位置
  • 然后,基于页目录找到“页表转换表”的物理地址
    • 页目录的 field,贡献“页表转换表”物理地址的高 20bit (bit31:12)
    • “页表转换表” 4KB 对齐,也就是说它的物理地址的低 12bit 本来就是0
    • 所以,这高 20bit 就是“页表转换表“的物理地址
  • 线性地址的 bit21:12 提供索引,找到目标页表在“页表转换表”中的位置
  • 页表的 field,贡献目标页框的物理地址的高 20bit (bit31:12);其实也就是目标页框的物理地址了
  • 最后,找到目标存储空间的物理地址
    • 目标页框的物理地址已经知道了
    • 线性地址的 bit11:0 提供准确的偏移
    • 页框物理地址加上偏移,就得到了要找的物理地址

相关宏

这一章,分解一些比较复杂的宏定义。

分页相关的头文件的位置:include->asm-xxx

  • page.h:描述物理层面的页
  • pgtable.h:描述物理层面的页表

现在基本确定了一件事情,一个页表项是由:页框地址(高20位,对应准确的物理地址)+若干标志位(利用低12位)
组成的。

present 相关的低 12 位的标志位。
这些标志是和硬件相关的,目前的理解是根据 x86 进行的,对于 AMR 会有若干不同。

#define _PAGE_PRESENT	0x001	// 标志该页(或页表)存在于主存中,为1的话
								// 不为1却被访问,会产生缺页中断:把线性地址放到一个地方,并抛中断
#define _PAGE_RW	0x002		// 为1,表示该页可读可写;为0,标志该页只读
#define _PAGE_USER	0x004		// 若为1,用户可访问(内核当然更可以访问了);若为0,用户不可访问,只有内核可以访问

// 在 Linux 中,下面这两个标志位恒为 0,所以总是回写策略和启用高速缓存
#define _PAGE_PWT	0x008		// Page Write-Throuth,若为1,则要求硬件启用通写策略
#define _PAGE_PCD	0x010		// Page Cache Disable,若为1,则要求硬件关闭该页的高速缓存

#define _PAGE_ACCESSED	0x020	// 该页被分页单元访问过,就会置1;由操作系统按策略清零
#define _PAGE_DIRTY	0x040		// 只应用于页表项,对应的页框被写过后,就会置1
#define _PAGE_PSE	0x080	/* 4 MB (or 2MB) page, Pentium+, if present.. */
#define _PAGE_GLOBAL	0x100	/* Global TLB entry PPro+ */ // 置1可以保持该页一直存在于 TLB 中
#define _PAGE_UNUSED1	0x200	/* available for programmer */
#define _PAGE_UNUSED2	0x400
#define _PAGE_UNUSED3	0x800

关于通写(write-through)和回写(write-back):参考《第三版》P60


pmd_bad(x) 解析

#define	pmd_bad(x)	((pmd_val(x) & (~PAGE_MASK & ~_PAGE_USER)) != _KERNPG_TABLE)
// 0x0FFF & 0xFFB = 0x0FFB
// x.pmd & 0x0FFB != 0x063

// 以下是相关引用
typedef struct { unsigned long pmd; } pmd_t;
#define pmd_val(x)	((x).pmd)

#define PAGE_SHIFT	12
#define PAGE_SIZE	(1UL << PAGE_SHIFT)		// 0x1000
#define PAGE_MASK	(~(PAGE_SIZE-1))		// 0xF000

#define _PAGE_USER	0x004

#define _PAGE_PRESENT	0x001
#define _PAGE_RW	0x002
#define _PAGE_ACCESSED	0x020
#define _PAGE_DIRTY	0x040
#define _KERNPG_TABLE	(_PAGE_PRESENT | _PAGE_RW | _PAGE_ACCESSED | _PAGE_DIRTY)	// 0x063

pte_modify:把 pte 的标志按规则设置为 newprot

static inline pte_t pte_modify(pte_t pte, pgprot_t newprot)
{
	pte.pte_low &= _PAGE_CHG_MASK;	// 需要保留被访问标志和被写标志
	pte.pte_low |= pgprot_val(newprot);
#ifdef CONFIG_X86_PAE
	/*
	 * Chop off the NX bit (if present), and add the NX portion of
	 * the newprot (if present):
	 */
	pte.pte_high &= ~(1 << (_PAGE_BIT_NX - 32));
	pte.pte_high |= (pgprot_val(newprot) >> 32) & \
					(__supported_pte_mask >> 32);
#endif
	return pte;
}

#define _PAGE_CHG_MASK	(PTE_MASK | _PAGE_ACCESSED | _PAGE_DIRTY)
typedef struct { unsigned long pgprot; } pgprot_t;	// 表示与一个单独表项相关的保护标志
#define pgprot_val(x)	((x).pgprot)

#define pgd_index(address) (((address) >> PGDIR_SHIFT) & (PTRS_PER_PGD-1))
// PTRS_PER_PGD - 1 = 1023 = 0x03FF
// 一个线性地址,右移掉对齐位后,再清理一下
// Page Global Directory,每个进程都有一个,是一个数组,有1024个元素,以上操作可以直接由线性地址计算出对应的下级目录项的索引

#define PGDIR_SHIFT	22
#define PTRS_PER_PGD	1024

由线性地址,得到目标页框在 pgd 中的索引

struct mm_struct {
	...
	pgd_t * pgd;
	...
};	// from sched.h

#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))
// 数组地址加数组索引,得到数组元素的地址;类型是 (pgd_t *)

// 这个宏起到了减少依赖的作用!!
// 如果上面是函数的话,mm 的结构体需要被包含到这个头文件里;但是现在只需要要求使用这个宏的文件包含结构体头文件就可以了

页描述符指针 + 标志位组,生成一个 pte

#define mk_pte(page, pgprot)	pfn_pte(page_to_pfn(page), (pgprot))
struct page *mem_map;
// 这里使用了指针相减,只有在同一个数组中的两个元素指针相减,才是有意义的
// 得到的结果是被减数和减数之间差几个元素;特别的,如果减数是数组起始地址,那得到的是被减数在数组中的位置
#define page_to_pfn(page)	((unsigned long)((page) - mem_map))
// 得到了对应的页描述符在页描述符数组中的位置
// 页描述符和页表项(PTE)不是同一个概念,要到第八章才详细描述;总之,它是一个数据抽象,是一个结构体

// 页描述符数组是物理内存的一个线性压缩,压缩比正好是页的大小
// 所以得到页描述符,左移,就得到了页表项的 field 域(物理地址的高20位,如果页大小是12位的话)
// field 加上保护标志位组,就得到了一个完美的页表项啦
#define pfn_pte(pfn, prot)	__pte(((pfn) << PAGE_SHIFT) | pgprot_val(prot))

一开始没有看明白,看完“内核页表”以后才大概了解了。
内核的 PA 在内存的开始,内核的 VA 在 0xC000_0000 (3G)的位置
首先要解决的疑惑是:dir 和 address,是什么?

// 目的是得到页表项的指针,这个页表项在页表中,页表是一个数组;返回的是页表数组的元素的指针
// 但是现在不确定的是,不知道得到的到底是 PA,还是 VA
// 按照理解,pmd_page_kernal 返回的必然是 VA;也就是说,可以认为得到了这个页表项的 VA 的结构体指针
#define pte_offset_kernel(dir, address)  ((pte_t *) pmd_page_kernel(*(dir)) +  pte_index(address))


// 内核,在物理内存的开始位置,在虚拟内存的 3G+ 的位置
#define __PAGE_OFFSET		(0xC0000000UL)
#define __va(x)			((void *)((unsigned long)(x)+PAGE_OFFSET))	// PA 到 VA

// 页中间目录里面放的是什么?是页中间目录表项,也就是某个页表的物理地址
// pmd_val(pmd) & PAGE_MASK),把地址的低 12 位置 0 了
// 再经过 va 处理,就得到了内核页表的虚拟地址。
#define pmd_page_kernel(pmd)  ((unsigned long) __va(pmd_val(pmd) & PAGE_MASK))
// 现在的困惑是,内核页表放到哪里了?得到它的VA有什么用?

/*
 * the pte page can be thought of an array like this: pte_t[PTRS_PER_PTE]
 *
 * this macro returns the index of the entry in the pte page which would
 * control the given virtual address
 */
#define pte_index(address)	(((address) >> PAGE_SHIFT) & (PTRS_PER_PTE - 1))
// 所以说,这和宏会返回一个数组的索引值
// 不管是 PA 还是 VA,这个 address 都能获得预期的 index

这个也超纲了,等回来看

#define pte_offset_map(dir, address)  ((pte_t *)page_address(pmd_page(*(dir))) + pte_index(address))

#define pte_page(x)		pfn_to_page(pte_pfn(x))		// 页表项对应的页描述符的地址

#define pfn_to_page(pfn)	(mem_map + (pfn))		// 页描述符数组起始地址 + 索引
#define pte_pfn(x)		((unsigned long)(((x).pte_low >> PAGE_SHIFT)))	// 得到页框的物理地址在页描述符数组中的索引

相关底层函数

这一节分析一些相关的、比较复杂的函数。
分页相关的 .c 文件在 arch -> xxx -> mm

  • pgtable.c
// 貌似 mm 都没有用上
pgd_t *pgd_alloc(struct mm_struct *mm)
{
	int i;
	pgd_t *pgd = kmem_cache_alloc(pgd_cache, GFP_KERNEL);	// 这个函数暂时不深究,应该是申请了一整个页全局目录的页框

	if (PTRS_PER_PMD == 1 || !pgd)
		return pgd;

	for (i = 0; i < USER_PTRS_PER_PGD; ++i) {
		// 同时,将页中间目录的空间也都申请出来了;这里和理解的不太一致,原则上,页中间目录的页框可以不用分配的啊
		pmd_t *pmd = kmem_cache_alloc(pmd_cache, GFP_KERNEL);
		if (!pmd)
			goto out_oom;

		// 原子的给一个 64 位的指针赋值
		// 指针,是页全局目录里的表项的地址;值,没看明白,看起来是线性地址转成物理地址了,但是不应该啊
		set_pgd(&pgd[i], __pgd(1 + __pa(pmd)));
	}
	return pgd;

out_oom:
	for (i--; i >= 0; i--)
		kmem_cache_free(pmd_cache, (void *)__va(pgd_val(pgd[i])-1));
	kmem_cache_free(pgd_cache, pgd);
	return NULL;
}

#define set_pgd(pgdptr, pgdval)	set_pud((pud_t *)(pgdptr), (pud_t) { pgdval })
#define set_pud(pudptr,pudval) 	set_64bit((unsigned long long *)(pudptr),pud_val(pudval))	// 原子的给一个 64 位的指针赋值

看了下后面的几个函数,有点超前,先往后看吧。


相关上层函数

内核页表的初始化

首先是一个比较重要的变量:swapper_pg_dir

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

当看明白上面这个变量的时候,突然有了一种豁然开朗的感觉。原来如此。但是现在还有一些细节是模糊的,还不能清晰表述。
内核页表的初始化分为两个阶段,首先是临时内核页表阶段,然后再进入最终内核页表阶段。
临时页表,线性地址0的起始和0xC000_0000的起始,都指向同一片物理内存。
当正式启用分页以后,也许目录中低地址的页表会被释放掉?


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

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

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

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: 103

代码要看,其中有若干 #ifdef 和 if 语句,完成了一套代码对各种情况的适配。


解析下 kernel_physical_mapping_init 这个函数


/*
 * 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));
				}
			}
		}
	}
}
为了透彻理解linux的工作机理,以及为何它在各种系统上能顺畅运行,你需要深入到内核的心脏。cpu与外部世界的所有交互活动都是由内核处理的,哪些程序会分享处理器的时间,以什么样的顺序来分享。内核不遗余力地管理有限的内存,以使数以千计的进程有效地共享系统资源。内核还精心组织数据传送,使得cpu不再受限于慢速硬盘。    《深入理解linux内核第三版将引领你畅游内核中用到的最主要数据结构、算法和编程技巧。如果你的确想了解计算机内部的实现机理,那么作者透过现象探寻本质,提供了颇有价值的深入分析。本书针对具体的intel平台,讨论了其重要特征,逐行剖析了相关的代码片段。但是,本书涵盖的内容不仅仅局限于代码的机理,还解释了linux运作方式的理论支撑。    本书第三版涵盖linux 2.6,从中可以看到几乎内核每个子系统都有相当大的变化,首当其冲的是内存管理和块设备部分。本书集中讨论了如下内容:    内存管理,包括文件缓冲、进程交换以及直接内存访问(dma)    虚拟文件系统层和第二及第三扩展文件系统    进程创建及调度   信号、中断及设备驱动程序的主要接口   定时   内核中的同步   进程间通信(ipc)   程序执行   本书将使你熟悉linux所有的内在工作机理,但本书不仅仅是一种学术演练。你将了解到什么条件会促使linux产生最佳性能,你还会看到,linux在各种环境下如何满足进程调度、文件访问及内存管理期间系统提出的快速响应要求。本书有助于你充分展现linux系统的魅力。
当着手翻译第三版时,我不由得回想起开始接触Linux 的那投日子。 几年前,当我们拿到Linux 内核代码开始研究时,可以说茫然无措。其规模之大,叫“覆 压三百余里,隔离天日”似乎不为过;其关系错综复杂,叫"廊腰线回,檐牙高啄,各 抱地势,勾心斗角”也非言过其实。阿房宫在规模和结构上给人的震撼可能与Linux 有 异曲同工之妙。“楚人一炬,可怜焦土”,可能正是因为它的结构和规模,阿房宫在中国 两十多年矗极的计建历史中终于没有再现,只能叫后人扼腕叹息;但是, Linux 却实实 在在地矗立在我们面前,当我们徘徊在这宏伟宫殿之前时,攻许,我们也需要火炬 不是用来效灭,而是为了照亮勇者脚下的征途。 Linus Torvalds 在我们面前展现的Linux 魔法卷轴,让我们的视野进入一个自由而开放 的新世界。自由意味着自我价值的实现,开放代表着团结协作的理想,这对于从没把握 过核心操作系统的中国人来说,无疑燃起了心中的梦想。于是,许多人毫不犹豫地走进 来了,希望深入到那散发自由光彩、由众人团结协力搭造起的殿堂。但是很快,不少人 迻缩了。面对这样一个汪洋大诲,有的人迷惑了,出诲的航道在哪里?有的人倒下了. 漫漫征途何时是尽头?我常常想,如果那时他们手中就有这本书的话…… Daniel P.Bovet 和Marco Cesati 携手为我们打造了这本浅无巨著,自此我们有了火把, 有了航诲图,于是我们就有了彼岸,有了航道,也有了补给码头。不是吗?中断虽繁, 但笫四、六两章切中肯紧的剖析,肯定能让你神清气爽;内存管双虽淮, 但多达三章细 致入微的说理一定会让你茅塞顿开。内容的组织更是别具匠心,每章开始部分一般性原 理的描述打破了知识的局限,将每个部分的全景展现在你面前。而针对每个知识点芯到 实处的独到分析,又会使你沉迷于知识的社会贯通之中。第三版Linux 2.6 的全面描 述会使你为2.4 与2.6 之间的沟壑而感叹`但请放心,你曾从Linux 旧版本中荻取的点滴 依然是你前进的基石。总之,你面对的不再是赤裸裸的代码,而是真正能雅俗共赏的艺 术。 对整
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值