Linux 0.01 内存管理

源码下载

Linux 源码下载路径位于 https://mirrors.edge.kernel.org/pub/linux/kernel/,这篇博客所需要的 0.01 版本源码通过点击链接 https://mirrors.edge.kernel.org/pub/linux/kernel/Historic/linux-0.01.tar.gz 可下载。

说明

Linux 0.01 版本内核是一个 80386 平台版本的内核,所以需要了解 x86 平台的一些汇编知识以及 AT&T 汇编嵌入编程,不过没关系,我们可以边阅读代码边学习,遇到不懂的就暂停存档,然后就搜索学习,最后返回继续读档学习,这也是我学习 Linux 内核的基本思路。由于我的工作是在 ARM64 平台下,所以当之后的版本有了 ARM64 架构之后,将转移阵地到 ARM64 上面,影响应该不大。Linux 0.01 处理的物理内存大小是 8MB。

前置知识 – 分段和分页

分段和分页的很多理论知识在网上可以找到很多,这里不再赘述这部分内容,我们聚焦的点是在 Linux 0.01 版本中如何实现分段和分页。在保护模式下程序使用的是逻辑地址,逻辑地址映射到物理地址要经过线性地址,这部分的映射是分段的内容,而分页是将线性地址映射到物理地址。在 x86 中,分段是必须的,分页是可选的,Linux 中分页功能是开启的。
Linux 0.01 内存管理_第1张图片
如上图所示,在 x86 平台上,虚拟地址由两部分组成:16 位的段选择符和 32 位的偏移值,总共 48 位,我们称 32 位的偏移值位逻辑地址,在某些可辨清晰的情形下,可能也会将虚拟地址称为逻辑地址;线性地址和物理地址则都是 32 位。分段机制和分页机制都是通过查表实现的,这部分内容可以参考https://blog.csdn.net/leoufung/article/details/86770794 这篇文章,说的很详细。我们在这里将要说一下原理。

分段

分段有四个术语:段选择符、偏移值、段描述符和段描述符表。段选择符和偏移值构成了逻辑地址,段描述符表是一个数组,段描述符是构成段描述符表的一个个 entry,每个段描述符占 8 个字节,作用是提供一个段的基地址长度以及保护属性信息,这里我们主要关注的是段的基地址这个信息,这个基地址指的是一个段字节 0 在线性空间的位置。段描述符表有两种:全局描述符表(GDT)和局部描述符表(LDT)。在 Linux 中,每个任务都有一个 LDT。下面是逻辑地址转线性地址的参考图:
Linux 0.01 内存管理_第2张图片
刚才说段描述符表有两种,但具体选择哪种,依靠段选择符中的一个比特位是 0 还是 1 来决定,这里不细说。总结一下就是逻辑地址划分为两部分:段选择符和偏移值,通过段选择符定位到在段描述符表中的索引,拿到段描述符表中的内容,提取出段基地址,然后加上偏移值得到的线性地址就是该逻辑地址在线性空间映射的地址了。

分页

前提知识:每一页 4K。分页的做法和分段很类似,在 Linux 0.01 版本中,使用的是二级分页,有一个页目录和一个页表。具体做法如下:
Linux 0.01 内存管理_第3张图片
这边不详细展开说了,如果上图看不懂,可评论或搜索其他资料。

源码位置

mm路径.png
我们探索的源码位于内核根目录下的 mm 目录,该目录下有两个文件 memory.c 和 page.s,其中 memory.c 文件时我们关注的重点,里面的程序实现了对物理内存的管理。
mm内容.png

内存模型

在 Linux 0.01 版本内核中能处理的最大物理内存是 8MB,include/linux/config.h 中定义了该内存大小:

/* #define LASU_HD */
#define LINUS_HD

/*
 * Amount of ram memory (in bytes, 640k-1M not discounted). Currently 8Mb.
 * Don't make this bigger without making sure that there are enough page
 * directory entries (boot/head.s)
 */
#if	defined(LINUS_HD)
#define HIGH_MEMORY (0x800000)
#elif	defined(LASU_HD)
#define HIGH_MEMORY (0x400000)
#else
#error "must define hd"
#endif

/* End of buffer memory. Must be 0xA0000, or > 0x100000, 4096-byte aligned */
#if (HIGH_MEMORY>=0x600000)
#define BUFFER_END 0x200000
#else
#define BUFFER_END 0xA0000
#endif

这段代码中有两个宏 LASU_HDLINUS_HD,不清楚这两个宏是啥,知道的请不吝赐教。不过代码中默认定义了宏 LINUS_HD,我们就按这个宏来走接下来的流程就好。因此 HIGH_MEMORY8MBBUFFER_END2MBBUFFER_END 是缓冲区末端。
memory.c 文件中定义了 LOW_MEM 如下:

#if (BUFFER_END < 0x100000)
#define LOW_MEM 0x100000
#else
#define LOW_MEM BUFFER_END
#endif

因此,LOW_MEM 位于 2MB 处。所以整个内存模型如下:
Linux 0.01 内存管理_第4张图片
内存管理的区域是主内存区,分配和释放页面都是在主内存区。

地址映射 setup_paging

要清楚的一点是内核中直接访问的地址是线性地址,在这里没有逻辑地址,我们可以说线性地址是一个指针变量,我们可以对其解引用得到里面的值,但是我们不能说物理地址是一个指针变量,它就是一个整数值,我们不能用它来直接解引用访问内存。这里我们要做的是建立页表,内核中为了很简单就可以感知到物理内存,内核做的映射是 identity-mapping,即线性地址映射的值与物理地址的值相等。这段代码位于 boot/head.s 文件中:

.org 0x1000
pg0:

.org 0x2000
pg1:

setup_paging:
	movl $1024*3,%ecx                                                  ---------------------------- (1)
	xorl %eax,%eax                                                     ---------------------------- (2)
	xorl %edi,%edi			/* pg_dir is at 0x000 */                   ---------------------------- (3)
	cld;rep;stosl                                                      ---------------------------- (4)
	movl $pg0+7,_pg_dir		/* set present bit/user r/w */             ---------------------------- (5)
	movl $pg1+7,_pg_dir+4		/*  --------- " " --------- */
	movl $pg1+4092,%edi                                                ---------------------------- (6)
	movl $0x7ff007,%eax		/*  8Mb - 4096 + 7 (r/w user,p) */
	std
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax
	jge 1b
	xorl %eax,%eax		/* pg_dir is at 0x0000 */                      ---------------------------- (7)
	movl %eax,%cr3		/* cr3 - page directory start */
	movl %cr0,%eax                                                     ---------------------------- (8)
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			/* this also flushes prefetch-queue */
  1. 因为有 8M 的物理内存,而每个页表占一页,每个页表项占 4 个字节,所以每个页表总共有 4K / 4 = 1024 项,并且每一项表示物理页一页,因此一个页表可以表示 1024 * 4K = 4M 内存,所以 8M 内存需要有 2 个页表,加上页目录,总共需要 3 个页表。
  2. 用异或的方法使 eax 寄存器的值为 0,目的是后面的 stosl 指令会用到 eax 寄存器。
  3. 用异或的方法使 edi 寄存器的值为 0,目的是设置 stlos 指令从物理地址 0 的地方开始填充 eax,为什么是从 0 开始,因为 Linux 0.01 版本内核的页目录 pg_dir 设在 0 位置。
  4. cld 指令清除方向标志位 DF,即 DF=0.这个指令用于串操作指令中。先了解下串操作指令,在串操作指令中,源操作数和目的操作数分别使用寄存器 (e)si(e)di 进行间接寻址,每执行一次串操作,源指针 (e)si 和目的指针 (e)di 将自动进行修改:±1、±2、±4,其对应的分别是字节操作、字操作和双字操作。这里的 ±1、±2、±4 是加还是减就是取决于 DF 方向标志位,当 DF=1 时为减,当 DF=0 时为加(与 cld 指令相对的 std 指令)。rep 指令是重复执行后面的指令 ecx 次,也就是执行 stosl 指令 ecx 次,stosl 指令将 eax 中的值保存到 es:edi 指向的地址中,这里要说明 es:edies 表示的是将要访问哪一个数据段,在保护模式下只有一个数据段,所以 es 的值默认和 ds 一样,如果是在实模式中,就要自己设置 es 的值了,由于 eax 的值为0,所以这段代码目的是将这三个页表内容清零。
  5. 这里要说下页表项的结构,页目录项也是一样的,简单的想,一个页表项占 4 个字节,总共 32 位,里面要存的一个基本信息是映射的物理页面的页帧基地址,这个基地址是 4K 对齐的,也就是说这个页基地址总共 32 位,但它的低 12 位都是 0,那这样就只有高 10 位是有效信息了,那页表项中 10 位就可以表示物理页基地址了,那还剩 12 位,这 12 位用作页面属性控制,譬如这个页有没有映射了,可读可写这些属性,而页目录项可能是为了简单或者和页表项一致,它用了和页表项一样的结构:高 10 位表示页基地址,低 12 位用于属性控制。如下图:
    Linux 0.01 内存管理_第5张图片
    我们现在先关注其中 3 个属性位,低 3 位:P,R/W,U/S。
    • P:Present,表示该页面是否存在,也就是是否是有效页面,有没有映射了,值为 1则是有效页面,为 0 则是无效页面。 如果该位是 0,那么该页面无效,所以该项高 31 位可以用作其他用途,譬如用来存储在磁盘上的页面的序号。
    • R/W:Read/Write,如果等于 1,表示可读、可写、可执行;如果等于 0,表示只读、可执行。当处理器运行在特权级别(0、1、2环)时,R/W 位不起作用。页目录项中的 R/W 位对其映射的所有页面起作用。
    • U/S:User/Supervisor,如果等于 1,那么运行在任何特权级别的程序都可以访问该页面;如果等于 0,那么只有特权级别(0、1、2环)的程序可以访问。页目录项中的 U/S 位对其映射的所有页面起作用。
      还有一点要说明的是因为页目录项页只用了高 10 位表示页表的物理基地址,所以页表所在页面必须是 4K 对齐。
      所以这几行代码是在页目录中设置其余两个内核页表的机制,加 7(111b) 是为了设置低 3 位属性都为 1。
  6. 这段代码现在应该可以很方便理解了,就是建立 identity-mapping。
  7. 设置页目录起始地址为 0。(设置 cr3 控制寄存器)
  8. 启动分页管理,启动保护模式。(设置 cr0 控制寄存器)

管理方法 mem_map

Linux 0.01 版本内核通过一个数组来管理主内存区,这个数组名为 mem_map

/* these are not to be changed - thay are calculated from the above */
#define PAGING_MEMORY (HIGH_MEMORY - LOW_MEM)
#define PAGING_PAGES (PAGING_MEMORY/4096)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)

static unsigned short mem_map [ PAGING_PAGES ] = {0,};

这段代码的思想是将主内存区分为 4K 为一段来管理,mem_map 的一个 entry 项代表一个 4K 的页,当其值为 0 时,表示该页没有被占用,大于 0 表示被占用,大于 1 表示页面被共享。MAP_NR 求给定地址 addr 所在页面在 mem_map 数组的索引。

获取页面 get_free_page

通过 get_free_page 函数在主内存区申请一页空闲内存,申请成功返回物理内存的页基址,若无空闲内存页则返回 0。

/*
 * Get physical address of first (actually last :-) free page, and mark it
 * used. If no free pages left, return 0.
 */
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");                 ------------------------------- (1)

__asm__("std ; repne ; scasw\n\t"                       ------------------------------- (2)
	"jne 1f\n\t"                                        ------------------------------- (3)
	"movw $1,2(%%edi)\n\t"                              ------------------------------- (4)
	"sall $12,%%ecx\n\t"                                ------------------------------- (5)
	"movl %%ecx,%%edx\n\t"                              ------------------------------- (6)
	"addl %2,%%edx\n\t"                                 ------------------------------- (7)
	"movl $1024,%%ecx\n\t"                              ------------------------------- (8)
	"leal 4092(%%edx),%%edi\n\t"
	"rep ; stosl\n\t"
	"movl %%edx,%%eax\n"                                ------------------------------- (9)
	"1:"
	:"=a" (__res)
	:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
	"D" (mem_map+PAGING_PAGES-1)
	:"di","cx","dx");
return __res;
}
  1. 定义 __res 变量,register 关键字指示编译器将 __res 变量放在寄存器中,后面的 asm("ax") 指定寄存器为 ax 寄存器,这里使用寄存器变量是因为在嵌入汇编语言中想要将汇编指令的输出直接写到指定的寄存器中的话,使用寄存器变量会很方便。寄存器变量的一些知识参考 《Linux 内核完全注释》一书如下:
    Linux 0.01 内存管理_第6张图片
  2. __asm__ 表示这是嵌入汇编,关于这部分知识可以自己在网上搜索学习下格式。\n\t 是为了编译器预处理出来的汇编程序对齐好看,可以方便查看调试汇编程序。重点是 std ; repne ; scasw,这里有三个指令,std 指令使方向标志位 DF 置位,即 DF=1.这个指令用于串操作指令中。先了解下串操作指令,在串操作指令中,源操作数和目的操作数分别使用寄存器 (e)si(e)di 进行间接寻址,每执行一次串操作,源指针 (e)si 和目的指针 (e)di 将自动进行修改:±1、±2、±4,其对应的分别是字节操作、字操作和双字操作。这里的 ±1、±2、±4 是加还是减就是取决于 DF 方向标志位,当 DF=1 时为减,当 DF=0 时为加(与 std 指令相对的 cld 指令)。REPNE 指令一般用来扫描字符串,它用来重复后一个指令,这里是 scasw 指令,REPNE 的重复条件是 ZF=0 且 ecx > 0,每循环执行一次,ecx 的值自动减 1,所以如果当它后面的指令扫描字符串时,当它扫到字符串与需要检测的字符串相等时或者扫描了 ecx 次之后停止扫描,后一个条件好理解,因为扫描了 ecx 次之后,ecx 的值已经为 0,已经不满足重复条件了;而扫描到相等则会置 ZF=1,也会使条件不满足。最后一个指令 scasw,最后那个 w 表示字,两个字节,这个指令减 ax 寄存器和 di 寄存器的值对比,每比较一次,di 寄存器依赖 DF 的值自动增加或减小,增加或减小的值如果指令是 scasw 则为 2,如果是 scasb 则为 1。ax 寄存器的初始赋值在第 21 行,"0" (0)"0"(或者”“)表示使用输出操作数中同一位置的寄存器,即寄存器 ax(0) 表示赋值为 0;di 寄存器的初始赋值在第 22 行,"D" (mem_map+PAGING_PAGES-1) 中的 "D" 表示引用寄存器 edi,后面的 (mem_map+PAGING_PAGES-1) 表示赋值为 mem_map 数组的最后一个索引,所以查找空闲页面是从最后一页开始查找的。
  3. jne 1f 表示 ZF=0 则跳转到 1 标签,ZF=0 表示没有找到空闲页,跳转到 1 标签,结束嵌入汇编,然后直接返回 __res,也就是 0。
  4. 这条指令翻译为 1 => [2 + edi]edi 寄存器加 2 是因为执行完 scasw 指令之后 edi 会减 2,加 2 回到找到的空闲页的地址,这里找到空闲页,将对应的 mem_map 的值置为 1。
  5. ecx 左移 12 位,ecx 的值会随着 repne 指令变化,找到空闲页之后,ecx 为空闲页的 mem_map 数组索引,左移 12 位即是空闲页的相对于 LOW_MEM 的页面基址。
  6. ecx 移到 edx
  7. edx 加上 LOW_MEM 则为空闲页面的实际物理地址,由于内核是线性一一映射,所以 edx 也是线性地址。
  8. ecx 的值设为 1024,作用是为后面的 rep 指令充当计数器,循环 1024 次,leal 指令将 edi 指向空闲页的最后倒数第 4 个字节,stosl 指令将 eax 中的值保存到 es:edi 指向的地址中,这里要说明 es:edies 表示的是将要访问哪一个数据段,在保护模式下只有一个数据段,所以 es 的值默认和 ds 一样,如果是在实模式中,就要自己设置 es 的值了,由于 eax 的值为0,所以这段代码目的是将物理页面内容清零。
  9. 设置 eax 的值为空闲页面的物理地址,也就是设置了 __res 的值,最后返回 __res

释放页面 free_page

通过 free_page 释放一个页面。

/*
 * Free a page of memory at physical address 'addr'. Used by
 * 'free_page_tables()'
 */
void free_page(unsigned long addr)
{
	if (addr<LOW_MEM) return;                      ------------------------------------- (1)
	if (addr>HIGH_MEMORY)                          ------------------------------------- (2)
		panic("trying to free nonexistent page");
	addr -= LOW_MEM;                               ------------------------------------- (3)
	addr >>= 12;
	if (mem_map[addr]--) return;                   ------------------------------------- (4)
	mem_map[addr]=0;                               ------------------------------------- (5)
	panic("trying to free free page");
}
  1. 如果要释放的地址小于 LOW_MEM,表示要释放的地址不位于主内存区,对此不做处理。
  2. 如果要释放的地址大于 HIGH_MEMORY(等于 HIGH_MEMORY 也是不合法的,在 0.11 版本会更正),表示要释放的地址不在于我们所认知的物理地址范围之内,所以 panic,Linux 0.01 版本的 panic 函数实现很简单:
kernel/panic.c

volatile void panic(const char * s)
{
	printk("Kernel panic: %s\n\r",s);
	for(;;);
}

这里可以说下在这里 volatile 的作用,它修饰的是函数返回值,作用与修饰变量不同,它的作用是帮助编译器做优化,表示该函数不会返回了,所以如果在调用时也将返回地址压栈的话是没有意义的,volatile 可以提示编译器做这类优化,相当于 void panic(const char * s) __attribute__ ((noreturn));
3. 求出 addrmem_map 数组中的索引,也就是 MAP_NR 宏实现的功能,不清楚这里为啥不直接使用这个宏。
4. 将 mem_map 中对应的索引的值减 1,即该页面数的引用减少 1。
5. 如果跑到这一行则表示原本 mem_map[addr] == 0,但由于上一行 if 语句中将其减了 1, 所以这里将其直接还原成 0,然后 panic。

释放页表 free_page_tables

free_page_tables 函数释放整个页表,它有两个参数:

  1. 线性内存的起始地址 fromfrom 至少 4M 对齐,因为这个函数要释放的是整个页表,一个页表最大能表示 4M 的地址,所以要释放从 from 起的连续 4M 线性地址空间,那么 from 就要 4M 对齐。
  2. 释放空间大小 size:这个 sizeround_up 到 4M 对齐,譬如 0 的话就是 0,1.1 M 就是 4M,4.1M 是 8M。。。
    需求就是这样,代码注释说的是 exit() 函数会用到这个函数,先照着解读吧:
/*
 * This function frees a continuos block of page tables, as needed
 * by 'exit()'. As does copy_page_tables(), this handles only 4Mb blocks.
 */
int free_page_tables(unsigned long from,unsigned long size)
{
	unsigned long *pg_table;
	unsigned long * dir, nr;

	if (from & 0x3fffff)                                                  --------------- (1)
		panic("free_page_tables called with wrong alignment");
	if (!from)                                                            --------------- (2)
		panic("Trying to free up swapper memory space");
	size = (size + 0x3fffff) >> 22;                                       --------------- (3)
	dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */       --------------- (4)
	for ( ; size-->0 ; dir++) {                                          
		if (!(1 & *dir))                                                  --------------- (5)
			continue;
		pg_table = (unsigned long *) (0xfffff000 & *dir);                 --------------- (6)
		for (nr=0 ; nr<1024 ; nr++) {
			if (1 & *pg_table)                                            --------------- (7)
				free_page(0xfffff000 & *pg_table);
			*pg_table = 0;
			pg_table++;
		}
		free_page(0xfffff000 & *dir);                                     ---------------- (8)
		*dir = 0;
	}
	invalidate();                                                         ----------------- (9)
	return 0;
}
  1. from 要 4M 对齐。
  2. 虽说 4M 对齐,但是如果 from 是 0 的话,那就是相当于要释放掉内核段所在的 4M 线性空间,那当然是不行的了。
  3. size 向上取整到 4M 对齐,并且除以 4M,譬如 size 传参为 5M,那么向上取整就是 8M,就是 2 个 4M,所以 size 的值就是 2.
  4. 这行代码涉及到分页系统中,线性地址如何通过页目录和页表定位到物理地址。dir 最终得到的值是 from 对应的页目录的索引值,在 分页 一节中图中有看到线性地址的高 10 位指示页目录项,所以将 from 右移 22 位就得到了在页目录的第几项,又因每项占 4 个字节,所以还得乘以 4,也就是还要左移 2 位,才能得到 from 在页目录表中的对应索引的地址,总的来说,就是先右移 22 位,再左移 2 位,这个过程相当于 dir = (unsigned long *) ((from>>20) & 0xffc);。可能是省时吧。。。
  5. 页目录项最低位是 P 位,也就是表示这个页面有没有效,如果无效的话,就略过。
  6. *dir 取得页目录项里面的内容,也就是页表基址和页表属性,页表属性低 12 位,所以要与上 0xfffff000
  7. P 位为 1,则调用 free_page 释放该页。
  8. 释放页表所占页面。
  9. 因为修改了原本有效的页表,所以需要刷新 TLB:重新加载 cr3 控制寄存器。
#define invalidate() \
__asm__("movl %%eax,%%cr3"::"a" (0))

因为页目录的基地址就是 0,所以将 0 加载进 cr3 就可以了。

复制页表 copy_page_tables

这个函数主要是供 fork() 函数使用,给定映射的源线性地址 from 、目的线性地址 to 和线性空间大小 size,看懂了 free_page_tables 的代码,看这段代码也基本没啥问题,所以这段代码挑之前没涉及的点说。

int copy_page_tables(unsigned long from,unsigned long to,long size)
{
	unsigned long * from_page_table;
	unsigned long * to_page_table;
	unsigned long this_page;
	unsigned long * from_dir, * to_dir;
	unsigned long nr;

	if ((from&0x3fffff) || (to&0x3fffff))
		panic("copy_page_tables called with wrong alignment");
	from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */
	to_dir = (unsigned long *) ((to>>20) & 0xffc);
	size = ((unsigned) (size+0x3fffff)) >> 22;
	for( ; size-->0 ; from_dir++,to_dir++) {
		if (1 & *to_dir)
			panic("copy_page_tables: already exist");
		if (!(1 & *from_dir))
			continue;
		from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
		if (!(to_page_table = (unsigned long *) get_free_page()))
			return -1;	/* Out of memory, see freeing */
		*to_dir = ((unsigned long) to_page_table) | 7;
		nr = (from==0)?0xA0:1024;                                       -------------- (1)
		for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
			this_page = *from_page_table;
			if (!(1 & this_page))
				continue;
			this_page &= ~2;
			*to_page_table = this_page;
			if (this_page > LOW_MEM) {                                  --------------- (2)
				*from_page_table = this_page;
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;
			}
		}
	}
	invalidate();shua  
	return 0;
}

  1. 如果 from 是 0 的话,表示要复制的是内核段,这段最多只要 640K 内存肯定完全覆盖内核了,所以只要复制 640K / 4K = 160 = 0xA0 项,而其他的要复制 1024 项。
  2. 这里要了解写时复制(copy-on-write),简单的说, fork 的时候子进程和父进程数据段共享一个物理页,它们的页面属性设置为只读,只有当写的时候,重新分配一页,然后设置属性为可读写。低于 LOW_MEM 是内核使用的,一直都是共享的,不用修改这个属性。mem_map 数组是管理 LOW_MEM 以上内存的。

页面映射 put_page

put_page 函数将物理页面 page 映射到线性空间 address

unsigned long put_page(unsigned long page,unsigned long address)
{
	unsigned long tmp, *page_table;

/* NOTE !!! This uses the fact that _pg_dir=0 */

	if (page < LOW_MEM || page > HIGH_MEMORY)                     --------------1printk("Trying to put page %p at %p\n",page,address);
	if (mem_map[(page-LOW_MEM)>>12] != 1)                         --------------- (2)
		printk("mem_map disagrees with %p at %p\n",page,address);
	page_table = (unsigned long *) ((address>>20) & 0xffc);
	if ((*page_table)&1)
		page_table = (unsigned long *) (0xfffff000 & *page_table);
	else {
		if (!(tmp=get_free_page()))
			return 0;
		*page_table = tmp|7;
		page_table = (unsigned long *) tmp;
	}
	page_table[(address>>12) & 0x3ff] = page | 7;                 --------------- (3)
	return page;
}
  1. put_page 操纵的页面是从 get_free_page 中获取的,也就是来自于主内存区的,按理说,这里检查不通过应该直接返回的,因为运行下去 mem_map 数组有可能溢出,这里仅仅是打印出错信息,可能是因为内核中不会以这种不正确的页面来调用吧。。。
  2. 理由同上。
  3. (address>>12) & 0x3ff 是找到 address 在页表中的索引。
    注意这里不用刷新 TLB,因为是修改原本就无效的页表或原先就不存在的页面,这种原来就不在 TLB 中,所以修改了也不用刷新 TLB。

取消写保护 un_wp_pagedo_wp_page

un_wp_page 函数取消写保护,但是这个函数在 Linux 0.01 版本中实现有点问题,它在修改了页表项之后没有刷新 TLB,不过在之后的版本加了刷新。而且这个函数的参数是页表项的地址,这点与前面的函数都不一样。如果读懂了前面的函数,这个函数也挺好理解的,就不解析了。

#define copy_page(from,to) \
__asm__("cld ; rep ; movsl"::"S" (from),"D" (to),"c" (1024):"cx","di","si")

void un_wp_page(unsigned long * table_entry)
{
	unsigned long old_page,new_page;

	old_page = 0xfffff000 & *table_entry;
	if (old_page >= LOW_MEM && mem_map[MAP_NR(old_page)]==1) {  /* 页面不共享 */
		*table_entry |= 2; /* 这里之后应该 invalidate(); */
		return;
	}
	if (!(new_page=get_free_page()))        /* 有共享页面, mem_map[MAP_NR(old_page)] > 1 */
		do_exit(SIGSEGV);
	if (old_page >= LOW_MEM)
		mem_map[MAP_NR(old_page)]--;
	*table_entry = new_page | 7; /* 这里之后也应该 invalidate(); */
	copy_page(old_page,new_page);
}	

do_wp_page 函数的参数中的地址和 un_wp_page 中的地址参数不一样,do_wp_page 中是页面中的一个地址,并不是页表项地址。还有一个参数是错误码 error_code,这个错误码是进程在写写保护页面时由 CPU 自动产生的。要看懂这个函数,只要知道如何通过页面线性地址求得页表项线性地址就可以了。

/*
 * This routine handles present pages, when users try to write
 * to a shared page. It is done by copying the page to a new address
 * and decrementing the shared-page counter for the old page.
 */
void do_wp_page(unsigned long error_code,unsigned long address)
{
	un_wp_page((unsigned long *)
		(((address>>10) & 0xffc) + (0xfffff000 &
		*((unsigned long *) ((address>>20) &0xffc)))));

}

写页面验证 write_verify

这个函数的功能主要时验证页面可不可写,不可写就复制一个页面,取消写保护,这就是 un_wp_page 的功能,所以函数实现判断不可写,然后调用 un_wp_page 就好。

void write_verify(unsigned long address)
{
	unsigned long page;

	if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1)) /* 页面不存在,就没有共享和写保护可言,直接返回就好 */
		return;
	page &= 0xfffff000;
	page += ((address>>10) & 0xffc);
	if ((3 & *(unsigned long *) page) == 1)  /* non-writeable, present */
		un_wp_page((unsigned long *) page);
	return;
}

缺页处理 do_no_page

有两个参数,error_code 是由进程在访问页面时 CPU 因缺页产生,address 是产生异常地页面线性地址。

void do_no_page(unsigned long error_code,unsigned long address)
{
	unsigned long tmp;

	if (tmp=get_free_page())
		if (put_page(tmp,address))
			return;
	do_exit(SIGSEGV);
}

这里面有个 do_exit 函数,这个函数我归类在进程调度那块,所以现在不做介绍。

页异常中断处理程序

页异常有两种情况:

  1. 缺页异常,那么就调用 do_no_page 来处理。
  2. 写保护异常,则调用 do_wp_page 来处理。
    具体是哪种情况,由出错码的最低比特位来体现,如果是 0, 则是缺页导致异常,是 1 的话则是写保护异常。发生异常访问的线性地址保存在 cr2 寄存器中。页异常中断处理程序在 mm/page.s 文件中由汇编代码实现,如下:
/*
 * page.s contains the low-level page-exception code.
 * the real work is done in mm.c
 */

.globl _page_fault

_page_fault:
	xchgl %eax,(%esp)             ---------------- (1)
	pushl %ecx                    ---------------- (2)
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%edx              ----------------- (3)
	mov %dx,%ds
	mov %dx,%es
	mov %dx,%fs
	movl %cr2,%edx               ----------------- (4)
	pushl %edx                   ----------------- (5)
	pushl %eax
	testl $1,%eax                ----------------- (6)
	jne 1f
	call _do_no_page
	jmp 2f
1:	call _do_wp_page
2:	addl $8,%esp                 ----------------- (7)
	pop %fs                      ----------------- (8)
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret
  1. xchgl 指令交换 eax 寄存器的值和 esp 指向地址的内容,相当于在栈中保存了 eax 的值,并且将 esp 指向地址的内容交换到了 eax,也就是错误码 error_code
  2. 保存现场,因为待会儿要根据错误码最低位调用 memory.c 中的缺页处理函数。
  3. 使段寄存器指向内核数据段,在 boot/head.s 中的初始话中,内核代码段和内核数据段都设置成了长度 8MB 的段,应该使为了访问页目录表,内核数据段位于 gdt 表的第三个索引,每个索引项占 8 个字节,所以第三个就是 0x10。
_gdt:	.quad 0x0000000000000000	/* NULL descriptor,没有用的 */
	.quad 0x00c09a00000007ff	/* 8Mb,内核代码段 */
	.quad 0x00c09200000007ff	/* 8Mb,内核数据段 */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */
  1. 取出线性地址。
  2. 将 C 函数参数从右往左压栈,先入地址,再入错误码。
  3. 测试错误码最低位,如果不为 0,则是 1,那么就是写保护,跳到调用 do_wp_page 函数,否则转到调用 do_no_page。汇编代码中加了个下划线 ‘-’,这是因为 gcc 在编译 C 代码时会在函数名下加下划线,所以汇编代码也要这么做才能找到对应的函数。
  4. 函数返回,参数没用了,丢弃,这里不用两个 pop 指令,直接用一条 add 指令,提高效率。
  5. 还原现场,然后 iret 进行中断返回,使程序返回到原来发生中断的地方。

参考文献

资料站点:http://oldlinux.org/

你可能感兴趣的:(Linux,内存管理,linux,内核,内存管理)