阅读 63

【Linux 0.11】第十三章 内存管理

@[toc] 在这里插入图片描述

内存管理内容导图

Linux 内核管理的核心是采用分页管理方式,利用页目录和页表结构处理内核中其它部分代码对内存的申请和释放。内存的管理以内存页面(地址连续的4K字节物理内存)为单位进行,通过页目录项和页表项,可以寻址和管理指定页面的使用情况。

内存管理相关的代码文件表
文件名称位置功能
head.slinux/boot/head.s初始化内存页目录表以及页表。
main.clinux/init/main.c物理内存初始化。
memory.clinux/mm/memory.c内存页面管理核心,内存初始化、页目录表和页表管理等。
page.slinux/mm/page.s内存页异常中断处理过程(int14),对缺页和页写保护处理。
swap.c(linux0.12)linux/mm/swap.c虚拟内存交换功能。
graph LR
o((start)) -- 0xffff0 --> A
A[ROM BIOS] -- 0x7c00 --> B[bootsect.s]
B -- 1. 0x90000 --> B
B -- 2. 0x90200 --> C[setup.s]
B -- 3. 0x10000 --> D[system-head.s]
C -- 0x00000 --> D
boot 引导阶段

13.1 内存管理相关的数据结构和全局变量

13.1.1 全局变量 mem_map[]

linux/mm/memory.c

mem_map 是一个字节数组,其数组长度为主存区域的页面数量,其数组内容表示页面占用的次数,当每申请一页物理内存时,就将占用次数加 1,0 表示页面空闲。主存区域的数组内容被初始化为 0。

static unsigned char mem_map [ PAGING_PAGES ] = {0,}; # PAGING_PAGES = 15M /4K
复制代码

13.1.2 交换映射位图 swap_bitmap

linux/mm/swap.c(Linux0.12)

管理交换设备上的交换页面,每个比特位表示一个交换页面,若某比特位为 0,则表示对应设备上交换页面已被占用,若比特位为 1,则表示页面可用。

static char * swap_bitmap=NULL;
复制代码

13.2 内存管理初始化

13.2.1 分页机制初始化

linux/boot/head.s

head.s中关于页表的建立过程,即分页机制初始化。

head.s主要任务:

  1. 初始化 gdt,idt。
  2. 检测 A20 地址线是否打开(在实模式下,内核只能使用物理内存空间中 1MB 以下的部分,为了能够寻址 16MB 的内存空间,需要开启 A20 地址线)。
  3. 初始化内存页目录表(为内存的分页管理做准备工作)以及页表。该项为本节所具体讲解的内容。

head.s 代码段位于内核模块开头处,代码处于物理内存起始处。分页处理机制将页目录表放在物理地址开始处,然后连续放置 4 个页表,每个页表(页面)大小为 4KB,每个页表均由 1024 个页表项(1024*4B)构成。使用 5 个页面((1+4)*4K)的内存空间,实现了通过页目录表索引 4 个页表,每个页表又索引 4MB 内存空间的过程,最终实现对物理内存 16MB 空间的管理,如下图所示。

在这里插入图片描述

页表设置及映射效果图

注意:由于物理地址起始处还放置了 head.s 中的部分代码,分页机制建立后代码开头至.org 0x1000(116行)前的内容均被页表所覆盖,head.s中剩余的代码和数据均在0x5000起始的位置。head.s 代码起始处,pg_dir:标识符(16行),标示了该处是页目录表起始位置,也即.org 0x0000处是页目录起始位置(所有进程共用).org 0x1000(116行)开始定义了 4 个页表(内核专用)—— pg0、pg1、pg2、pg3。

linux/boot/head.s
/*
 *  linux/boot/head.s
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 *  head.s contains the 32-bit startup code.
 *
 * NOTE!!! Startup happens at absolute address 0x00000000, which is also where
 * the page directory will exist. The startup code will be overwritten by
 * the page directory.
 */
.text
.globl idt,gdt,pg_dir,tmp_floppy_area
pg_dir:		# 页目录位置
.globl startup_32
startup_32:
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
	call setup_idt
	call setup_gdt
	movl $0x10,%eax		# reload all the segment registers
	mov %ax,%ds		# after changing gdt. CS was already
	mov %ax,%es		# reloaded in 'setup_gdt'
	mov %ax,%fs
	mov %ax,%gs
	lss stack_start,%esp
	xorl %eax,%eax
1:	incl %eax		# check that A20 really IS enabled
	movl %eax,0x000000	# loop forever if it isn't
	cmpl %eax,0x100000
	je 1b

/*
 * NOTE! 486 should set bit 16, to check for write-protect in supervisor
 * mode. Then it would be unnecessary with the "verify_area()"-calls.
 * 486 users probably want to set the NE (#5) bit also, so as to use
 * int 16 for math errors.
 */
	movl %cr0,%eax		# check math chip
	andl $0x80000011,%eax	# Save PG,PE,ET
/* "orl $0x10020,%eax" here for 486 might be good */
	orl $2,%eax		# set MP
	movl %eax,%cr0
	call check_x87
	jmp after_page_tables

/*
 * We depend on ET to be correct. This checks for 287/387.
 */
check_x87:
	fninit
	fstsw %ax
	cmpb $0,%al
	je 1f			/* no coprocessor: have to set bits */
	movl %cr0,%eax
	xorl $6,%eax		/* reset MP, set EM */
	movl %eax,%cr0
	ret
.align 2
1:	.byte 0xDB,0xE4		/* fsetpm for 287, ignored by 387 */
	ret

/*
 *  setup_idt
 *
 *  sets up a idt with 256 entries pointing to
 *  ignore_int, interrupt gates. It then loads
 *  idt. Everything that wants to install itself
 *  in the idt-table may do so themselves. Interrupts
 *  are enabled elsewhere, when we can be relatively
 *  sure everything is ok. This routine will be over-
 *  written by the page tables.
 */
setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea idt,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr
	ret

/*
 *  setup_gdt
 *
 *  This routines sets up a new gdt and loads it.
 *  Only two entries are currently built, the same
 *  ones that were built in init.s. The routine
 *  is VERY complicated at two whole lines, so this
 *  rather long comment is certainly needed :-).
 *  This routine will beoverwritten by the page tables.
 */
setup_gdt:
	lgdt gdt_descr
	ret

/*
 * I put the kernel page tables right after the page directory,
 * using 4 of them to span 16 Mb of physical memory. People with
 * more than 16MB will have to expand this.
 */
.org 0x1000
pg0:	# 从偏移0x1000处开始是第一个页表(偏移0处将存放页表目录)

.org 0x2000
pg1:

.org 0x3000
pg2:

.org 0x4000
pg3:

.org 0x5000	# 定义下面的内存数据块从偏移0x5000处开始
/*
 * tmp_floppy_area is used by the floppy-driver when DMA cannot
 * reach to a buffer-block. It needs to be aligned, so that it isn't
 * on a 64kB border.
 */
tmp_floppy_area:
	.fill 1024,1,0

after_page_tables:
	pushl $0		# These are the parameters to main :-)
	pushl $0
	pushl $0
	pushl $L6		# return address for main, if it decides to.
	pushl $main
	jmp setup_paging
L6:
	jmp L6			# main should never return here, but
				# just in case, we know what happens.

/* This is the default interrupt "handler" :-) */
int_msg:
	.asciz "Unknown interrupt\n\r"
.align 2
ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg
	call printk
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret


/*
 * Setup_paging
 *
 * This routine sets up paging by setting the page bit
 * in cr0. The page tables are set up, identity-mapping
 * the first 16MB. The pager assumes that no illegal
 * addresses are produced (ie >4Mb on a 4Mb machine).
 *
 * NOTE! Although all physical memory should be identity
 * mapped by this routine, only the kernel page functions
 * use the >1Mb addresses directly. All "normal" functions
 * use just the lower 1Mb, or the local data space, which
 * will be mapped to some other place - mm keeps track of
 * that.
 *
 * For those with more memory than 16 Mb - tough luck. I've
 * not got it, why should you :-) The source is here. Change
 * it. (Seriously - it shouldn't be too difficult. Mostly
 * change some constants etc. I left it at 16Mb, as my machine
 * even cannot be extended past that (ok, but it was cheap :-)
 * I've tried to show which constants to change by having
 * some kind of marker at them (search for "16Mb"), but I
 * won't guarantee that's all :-( )
 */
.align 2
setup_paging:
	movl $1024*5,%ecx		/* 5 pages - pg_dir+4 page tables */
	xorl %eax,%eax
	xorl %edi,%edi			/* pg_dir is at 0x000 */
	cld;rep;stosl			// eax 内容存到 es:edi 所指内存位置处;edi增4
	movl $pg0+7,pg_dir		/* set present bit/user r/w */
	movl $pg1+7,pg_dir+4		/*  --------- " " --------- */
	movl $pg2+7,pg_dir+8		/*  --------- " " --------- */
	movl $pg3+7,pg_dir+12		/*  --------- " " --------- */
	movl $pg3+4092,%edi 
	movl $0xfff007,%eax		/*  16Mb - 4096 + 7 (r/w user,p) */ // 每项内容:当前映射的物理内存地址+该页标志
	std				// edi 值减4B
1:	stosl			/* fill pages backwards - more efficient :-) */
	subl $0x1000,%eax
	jge 1b
	cld
	xorl %eax,%eax		/* pg_dir is at 0x0000 */
	movl %eax,%cr3		/* cr3 - page directory start */
	movl %cr0,%eax
	orl $0x80000000,%eax
	movl %eax,%cr0		/* set paging (PG) bit */
	ret			/* this also flushes prefetch-queue */

.align 2
.word 0
idt_descr:
	.word 256*8-1		# idt contains 256 entries
	.long idt
.align 2
.word 0
gdt_descr:
	.word 256*8-1		# so does gdt (not that that's any
	.long gdt		# magic number, but it works for me :^)

	.align 8
idt:	.fill 256,8,0		# idt is uninitialized

gdt:	.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */

复制代码

head.s 中与页目录及页表初始化相关的核心函数段为 setup_paging (200行处)。

页目录及页表初始化步骤:

  1. 为页目录表和 4 个页表(为了能够索引 16MB 内存空间)在内核中申请空间(代码 116 行开始处定义完毕)。

  2. 清空页目录表和 4 个页表的内容。如代码 201~204 行所示。

  3. 设置页目录表中的项。内核中我们设置了 4 个页表,故我们需要在页目录表中设置4个页表项来索引它们,如代码 205~208 行所示。第一个页表所在的线性地址为 0x1000,赋予第一个页表的属性为 0x7,表示该页存在、可读可写。由于每个页表项大小为 4B,故 pg_dir+4 跳转到下一个页表项。

  4. 设置每个页表中的页表项。每个页表大小为 4*1024B(标识物理页号范围:0-0xfff),如代码 209~214 行所示。pg3+4092 表示从最后一页的最后一个页表项开始填起,填写的内容为该页表项所映射的物理内存页号以及该页的属性0x7,将循环判断变量 eax 减去 4K,继续设置下一页表项,直至零,表示已将 4096 个页表项填写完毕,即 16M 内存分页完毕。

如图所示,展示了如何将一个线性地址定位到内存最后一个页表开头位置的过程。将32位线性地址分为三个部分:页目录表项(10位)、页表项(10位)、页内偏移(12位)。通过将线性地址分块,实现了线性地址到有限物理地址的映射,如图所示。

在这里插入图片描述

线性地址各字段的分解
  1. 设置页目录表的起始地址。将页目录表起始地址赋予 cr3 寄存器,如代码 216~217 行所示。
  2. 开启分页机制。如代码 218~221 行所示,将 cr0 寄存器的最高位设置为 1 来开启分页机制。

页目录表占用一页内存(4KB),每个表项占 4B,可以寻址 1024 个页表,每个页表也占用一页内存,因此一个页表可以寻址 1024 个页面,一个页目录表可以寻址 4G 内存空间,Linux 内核中,所有进程都使用一个页目录表,内核代码和数据段长度都是 16MB,使用 4 个页表,映射 16MB 物理内存,对于内核段来讲,其线性地址就是物理地址。

13.2.2 物理内存空间的规划

linux/init/main.c

main() 中关于物理内存空间的规划。

main.c 部分代码
// main.c 部分代码
#define EXT_MEM_K (*(unsigned short *)0x90002) // 1MB以后的扩展内存大小(KB)
static long memory_end = 0; // 机器具有的物理内存容量(B)
static long buffer_memory_end = 0; // 高速缓冲区末端地址
static long main_memory_start = 0; // 主内存开始的位置
void main(void)		/* This really IS void, no error here. */
{			/* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
 	ROOT_DEV = ORIG_ROOT_DEV;	 
 	drive_info = DRIVE_INFO;
	memory_end = (1<<20) + (EXT_MEM_K<<10);	// 内存大小=1MB+扩展内存(K)*1024字节	
	memory_end &= 0xfffff000;	// 忽略不到4KB的内存数
	if (memory_end > 16*1024*1024)	// 如果内存超过16MB,则按照16MB计
		memory_end = 16*1024*1024;
	if (memory_end > 12*1024*1024) 
		buffer_memory_end = 4*1024*1024;	// 设置缓冲区末端地址
	else if (memory_end > 6*1024*1024)
		buffer_memory_end = 2*1024*1024;
	else
		buffer_memory_end = 1*1024*1024;
	main_memory_start = buffer_memory_end;	// 主内存起始位置=缓冲区末端
#ifdef RAMDISK_SIZE
	main_memory_start += rd_init(main_memory_start, RAMDISK_SIZE*1024);	// 占用主内存空间,定义内存虚拟盘
#endif
	mem_init(main_memory_start,memory_end);
}
复制代码

head.s 执行完毕后就会跳转到 main.c 继续执行,main.c 文件主要做了内核初始化的工作,包括了块设备、字符设备等,以及人工设置第一个任务的工作。上面的代码展现了 main() 中关于内存的规划部分。主要内容是规范内存大小、确定主存区起始位置、设置虚拟盘空间和调用 memory.c 中的主内存初始化函数。

物理内存空间的规划步骤:

  1. 规范内存大小。代码第 14 行,求出内存大小,通过 1MB 内核区域+扩展内存区域方式求解。扩展内存(EXT_MEM_K,定义在代码第 2 行)大小为 0x90002,内存大小(memory_end,定义在代码第 3 行)计算结果为 0x241007ff。代码第 15 行,忽略不到 4K 的内存数,求解出结果 0x24100000。代码第 16~17 行,通过分支语句,判断出内存容量超过了16MB,将内存大小记为 16MB。

  2. 确定主存起始位置。代码第 18~23 行,通过内存大小,来设置高速缓冲区末端地址(buffer_memory_end,定义在代码第4行),具体分支判断过程可参考下方图片。代码第 24 行,将高速缓冲区末端地址赋值给主内存起始地址(main_memory_start,定义在代码第 5 行),至此,完成主内存区域始址的标记工作。

  3. 设置虚拟盘空间。代码第 25~27 行,通过 kernel/blk_drv/ramdisk.c 文件中的变量,来设置虚拟盘所占用的空间。

  4. 调用 memory.c 中的主内存初始化函数。代码第 28 行,调用 mm/memory.c 程序中的函数 mem_init,进一步将主内存区初始化,调用形式为 mem_init(main_memory_start,memory_end);

在这里插入图片描述

内存初始化示意图

13.2.3 全局变量 mem_map[] 的初始化

mem_init() 中对全局变量 mem_map[] 的初始化。

mem_init() 函数主要针对主内存区域进行管理分配。mem_map[] 数据结构则表示了物理内存页面的状态,每个字节描述一个物理内存页的占用状态,其中的值表示被占用的次数,0 表示物理内存空闲,当申请一页物理内存时,就将对应的字节值变为1。

memory.c 部分代码
// memory.c 部分代码
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define USED 100
static unsigned char mem_map [ PAGING_PAGES ] = {0,};
void mem_init(long start_mem, long end_mem)
{
	int i;
	HIGH_MEMORY = end_mem;	// 设置内存高端 16MB
	for (i=0 ; i<PAGING_PAGES ; i++)
		mem_map[i] = USED;
	i = MAP_NR(start_mem);
	end_mem -= start_mem;
	end_mem >>= 12;
	while (end_mem-->0)
		mem_map[i++]=0;
}
复制代码

该变量的初始化过程为:

  1. 计算非内核空间内存所需要的页面数(PAGING_PAGES,代码第 2 行)。
  2. 将高速缓冲区域以及虚拟盘区域(如果有)全部初始化为 100(代码 11~12 行)。
  3. 将主内存区域的项清零(代码 16~17 行)。

以 16MB 内存大小为例。除去内核空间 1MB,mem_map 需要管理剩余 15MB 空间的页面,一共有(16MB-1MB)/4KB=3840 项,即 PAGING_PAGES 为 3840,主内存区域具有(16MB-4.5MB)/4KB=2944 项(此 4.5MB 空间还包括了高速缓冲区域及虚拟盘区域),故前 896 项在数组 mem_map 中均被置为 100,而剩余 2944 项均被置为 0,等待内存分页管理程序的分配,如图展示了 mem_map 初始化结果。

在这里插入图片描述

mem_map 初始化

13.3 物理内存

13.3.1 申请

linux/mm/swap.c

涉及的函数:get_free_page() (swap.c-linux 0.12 memory.c Linux0.11)

get_free_page() 函数的目的是在主内存区找到一个空闲物理页面,需要注意的是其只是寻找有无空闲物理页面,而非映射分配,如果存在空闲物理页面,则返回空闲物理页面起始地址,否则返回 0,该函数使用 C/C++ 的内联汇编语法设计,对于某些需要被经常调用的代码,使用汇编来写可以提高性能。

swap.c/get_free_page()
unsigned long get_free_page(void)
{
register unsigned long __res asm("ax");
repeat:
	__asm__("std ; repne ; scasb\n\t"
		"jne 1f\n\t"
		"movb $1,1(%%edi)\n\t"
		"sall $12,%%ecx\n\t"
		"addl %2,%%ecx\n\t"
		"movl %%ecx,%%edx\n\t"
		"movl $1024,%%ecx\n\t"
		"leal 4092(%%edx),%%edi\n\t"
		"rep ; stosl\n\t"
		"movl %%edx,%%eax\n"
		"1:"
		:"=a" (__res)
		:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
		"D" (mem_map+PAGING_PAGES-1)
		:"di","cx","dx");
	if (__res >= HIGH_MEMORY)
		goto repeat;
	if (!__res && swap_out())	// 若没得到空闲页面则执行交换处理,并重新查找
		goto repeat;
	return __res;	// 返回空闲物理页面地址
}
复制代码

函数的输入(代码 17~18 行)

格式为: "操作约束(寄存器约束、内存约束、立即数约束、通用约束1)"(输入表达式)

  • "0" (0):表示与第 0 个操作表达式使用相同的寄存器/内存。第 0 个寄存器即 ax,也即将 ax 赋值为 0。
  • "i" (LOW_MEM):表示使用一个整数类型的立即数,申明内存低端地址,不需要借助任何寄存器
  • "c" (PAGING_PAGES):表示使用 %ecx/%cx/%cl,即 cx=PAGING_PAGES(0xf00)。
  • "D" (mem_map+PAGING_PAGES-1) :表示使用 %edi/%di。即 edi=mem_map+PAGING_PAGES-1,指向内存字节位图的最后一个字节。

引申:

r:I/O,表示使用一个通用寄存器,由GCC在%eax/%ax/%al、%ebx/%bx/%bl、%ecx/%cx/%cl、%edx/%dx/%dl中选取一个GCC认为是合适的; q:I/O,表示使用一个通用寄存器,与r的意义相同; g:I/O,表示使用寄存器或内存地址; m:I/O,表示使用内存地址; a:I/O,表示使用%eax/%ax/%al; b:I/O,表示使用%ebx/%bx/%bl; c:I/O,表示使用%ecx/%cx/%cl; d:I/O,表示使用%edx/%dx/%dl; D:I/O,表示使用%edi/%di; S:I/O,表示使用%esi/%si;

函数的输出(代码第 16 行)

  • __res=ax。物理页面起始地址,函数返回值保存在 eax 中(代码第 16 行)。

函数执行流程(不包括20~23行代码反汇编)

1. 函数反汇编结果如下图所示。从开头直到行标志 51 处,完成了函数输入参数赋值的步骤和判断页面为 0(从而向前判断)的情况,往后直到函数结束,完成了函数的主体功能以及返回。
2. scas 指令是用 al (或 ax )中的值对目的串( es:di 或 edi)中的字节(或字)进行扫描,常与 repnz (不相等继续)或 repz (相等继续)连用(行标志 52 处),ecx(物理页数)减 1,edi(mem_map中从后往前最后一个字节),串比较相符的某个字节。
3. 如果找到空闲页后,将对应页面的 mem_map 项置为 1(行标志 56 处)
4. 该页面号 \*4K 得到页面起始地址,再加上基址,得到物理起始地址(行标志 5a、5d)
5. 页面清零(行标志 63~70 处)
6. 将物理页面起始地址保存到 eax 中(行标志 72 处)
复制代码

在这里插入图片描述

内联汇编反编译结果(不包括20~23行代码反汇编结果)

13.3.2 释放

linux/mm/memory.c

涉及的函数:free_page()

free_page(addr) 函数的主要作用是释放**物理地址 addr **开始的一个页面内存,即将 mem_map 对应 addr 位置的引用次数减一。

memory.c/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;	// 用于内核与缓冲(1MB)
	if (addr >= HIGH_MEMORY)
		panic("trying to free nonexistent page");
	addr -= LOW_MEM;
	addr >>= 12;	// 页面号
	if (mem_map[addr]--) return;
	mem_map[addr]=0;
	panic("trying to free free page");
}
复制代码

函数输入:物理地址。

函数执行流程

  1. 判断参数合法性。内核、缓冲以及高于系统所含物理内存最高端均为不合法请求。其中 addr 小于 1MB,则表示请求释放内核程序处的页面,对此请求不予理睬(代码第 7 行所示),对于请求释放高于系统物理内存的请求,显示出错信息,并宕机(代码 8~9 行所示),代码可能存在问题。
  2. 将物理地址 addr 转换为以 4K 每单位大小的页号(代码 10~11 行所示)。
  3. 如果以页号为下标对应的 mem_map 项不等于 0,则减少引用次数 1 次,返回(代码 12 行所示)。
  4. 如果 mem_map[addr] 已经为 0,mem_map[addr]-- 则将值变为 -1,代码继续执行,将 mem_map[addr] 重新赋值为 0 ,然后报错宕机(代码 13~14 行所示),表示代码可能存在问题。

13.3.3 统计

linux/mm/memory.c

涉及的函数:show_mem()(Linux0.12)

show_mem() 函数显示内存信息。

memory.c/show_mem()
void show_mem(void)
{
	int i,j,k,free=0,total=0;
	int shared=0;
	unsigned long * pg_tbl;
	printk("Mem-info:\n\r");
	for(i=0 ; i<PAGING_PAGES ; i++) {
		if (mem_map[i] == USED)	// 跳过不能用于分配的内存页面
			continue;
		total++;	// 统计主内存区页面总数 total,以及其中空闲页面数 free 和被共享页面数 shared。
		if (!mem_map[i])
			free++;	// 主内存区空闲页面统计
		else
			shared += mem_map[i]-1;	// 共享的页面数
	}
	printk("%d free pages of %d\n\r",free,total);
	printk("%d pages shared\n\r",shared);
	k = 0;	// 一个进程占用页面统计值
	for(i=4 ; i<1024 ;) {	// 页目录表前 4 项供内核代码使用
		if (1&pg_dir[i]) {
			if (pg_dir[i]>HIGH_MEMORY) {
				printk("page directory[%d]: %08X\n\r",
					i,pg_dir[i]);
				continue;
			}
			if (pg_dir[i]>LOW_MEM)
				free++,k++;
			pg_tbl=(unsigned long *) (0xfffff000 & pg_dir[i]);
			for(j=0 ; j<1024 ; j++)
				if ((pg_tbl[j]&1) && pg_tbl[j]>LOW_MEM)
					if (pg_tbl[j]>HIGH_MEMORY)
						printk("page_dir[%d][%d]: %08X\n\r",
							i,j, pg_tbl[j]);
					else
						k++,free++;
		}	// 每个任务线性空间大小为 64MB,所以一个任务占用16个目录下,每统计16个目录项就会把进程的任务结构占用的页表统计进来。
		i++;
		if (!(i&15) && k) {	// k != 0 表示相应进程存在
			k++,free++;	/* one page/process for task_struct */
			printk("Process %d: %d pages\n\r",(i>>4)-1,k);	// 显示对应进程号和其占用的物理内存页统计值k后,将k清零,用于统计下一个进程占用的内存页面数。
			k = 0;
		}
	}
	printk("Memory found: %d (%d)\n\r",free-shared,total);
}
复制代码

13.4 页表

13.4.1 释放

linux/mm/memory.c

涉及的函数:free_page_tables() Linux0.12

为了能够连续释放页面空间,故编写此函数,该函数要求每个页表映射 4MB 的空闲页面,输入页表所映射页面的线性地址始址以及需要释放的空间大小(字节)即可释f放页表空间,并置表项空闲。

memory.c/free_page_tables()
/*
 * 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)	// 判断参数from的线性基地址是否在4MB处、8MB、12MB.
		panic("free_page_tables called with wrong alignment");
	if (!from)
		panic("Trying to free up swapper memory space");
	size = (size + 0x3fffff) >> 22;	// 释放的页表个数,页目录项数
	dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ // 页表项索引内容
	for ( ; size-->0 ; dir++) {
		if (!(1 & *dir))
			continue;
		pg_table = (unsigned long *) (0xfffff000 & *dir);	// 取页表地址
		for (nr=0 ; nr<1024 ; nr++) {
			if (*pg_table){	// 若所指页表项内容不为0
				if (1 & *pg_table) // 若该项有效,则释放对应页
					free_page(0xfffff000 & *pg_table);
				else	// 否则释放交换设备中对应页
					swap_free(*pg_table >> 1);
               	*pg_table = 0; // 该页表项内容清零
            }
			pg_table++;	// 指向页表下一项
		}
		free_page(0xfffff000 & *dir);	// 释放该表所占内存页面
		*dir = 0;
	}
	invalidate();
	return 0;
}
复制代码

函数执行流程:

  1. 首先检查参数合法性(代码 10~13 行)。检查 from 参数,其值是否是 4MB、8MB、12MB、16MB...,同时还检查其是否是 0,如果是 0 则出错,说明试图释放内核和缓冲所在空间。
  2. 然后进行转换(代码 14~15 行)。将 size(步长)转换为所需要页表项个数(4MB 的进位整数倍,进一法),将 from 转变为起始目录项指针。由于线性地址一共 32 位,高 10 位为页目录项号,中间 10 位为页表项号,低 12 位为页内偏移,将 size+0x3fffff(4MB-1) 可得到 size 中所包含的 4MB 个数(进一法),即将 size 转变为页表数量,也即页目录项数。在页目录表中,每个页目录项大小为 4B,一共有 1024 个页目录项,即目录页的大小为 4KB,dir 变量的大小为 4B,即每次自增 1 均会增加 4 个字节的位移,为了能够遍历 1024 个页目录项,需要遍历的位移长度为 4*1024,需要 12 位的循环判断变量(dir)。故如代码 15 行所示,将线性地址 from 右移 20 位,同时与 0xffc 相与来屏蔽低两位内容,最终得到起始目录项指针。
  3. 遍历页目录表,依次释放每个页表中的页表项。代码 17~18 行,跳过了无效的页目录项。如果页目录项有效,则从 19 行开始释放清空页表。代码第 19 行,取出页表地址,同时屏蔽低三位页表属性,进入代码第 20 行,循环清空页表中的 1024 个页表项,每个页表项对应 4KB 内存空间,如果某个页表项内容不为零,则判断该页表项是否有效,如果有效则调用 free_page() 函数,否则,释放交换设备中对应页,然后将页表项内容清零(代码第26行),指向页表中下一项(代码第 28 行)。
  4. 将页表所占用的页面释放,代码第 30 行。
  5. 将对应页表的目录项清零,代码第 31 行。

dir=((address>>22)<<2) & 0xffc:线性地址对应的目录项(address>>22:页目录项索引)

pg_table = *dir & 0xfffff000:该目录项中存放的页表的起始地址

*pg_table:该页表第 0 个页帧的起始地址

*(pg_table+1):该页表第 1 个页帧的起始地址

13.4.2 拷贝

linux/mm/memory.c

涉及的函数:copy_page_tables()

通过复制内存页面来拷贝一定范围线性地址中的内容。输入参数是源页表所映射的线性地址始址,目的线性地址,需要复制的字节数。根据源和目的线性地址处所对应的页目录表,填写(拷贝到)目的页表中页表项。

memory.c/copy_page_tables()
/*
 *  Well, here is one of the most complicated functions in mm. It
 * copies a range of linerar addresses by copying only the pages.
 * Let's hope this is bug-free, 'cause this one I don't want to debug :-)
 *
 * Note! We don't copy just any chunks of memory - addresses have to
 * be divisible by 4Mb (one page-directory entry), as this makes the
 * function easier. It's used only by fork anyway.
 *
 * NOTE 2!! When from==0 we are copying kernel space for the first
 * fork(). Then we DONT want to copy a full page-directory entry, as
 * that would lead to some serious memory waste - we just copy the
 * first 160 pages - 640kB. Even that is more than we need, but it
 * doesn't take any more memory - we don't copy-on-write in the low
 * 1 Mb-range, so the pages can be shared with the kernel. Thus the
 * special case for nr=xxxx.
 */
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;
		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) {
				*from_page_table = this_page;
				this_page -= LOW_MEM;
				this_page >>= 12;
				mem_map[this_page]++;
			}
		}
	}
	invalidate();
	return 0;
}
复制代码

函数执行流程:

  1. 首先检查参数,代码 26~27 行。源地址和目的地址都需要为 4MB、8MB、12MB...,否则宕机。这样可以保证从第一个页表的第 1 项开始复制页表项。
  2. 然后取得源地址和目的地址的起始目录项指针(from_dir和to_dir),在求出需要拷贝的页表数量 size,如代码 28~30 行。
  3. 判断目的起始目录项指针所指向页面是否存在以及源目录项是否有效。如果目的目录项指针指向页面已经存在,则宕机报错。如代码 32~35 行所示。
  4. 取源目录项中页表地址(代码 36 行),为保存目的目录项对应的页表,在主内存区中申请 1 个空闲页,如果get_free_page() 返回 0,则说明内存不足,代码 37~38 行。
  5. 更改次目录项对应页表的属性为用户级,代码 39 行所示。
  6. 针对当前目录项对应的页表,设置需要复制的页面数,如果是在内核空间,则复制 160 页(640KB 内存),物理内存的逻辑划分如图所示,否则复制一个页表中所有的 1024 项,映射 4MB 空间,如代码 40行 所示。

在这里插入图片描述

内存划分
  1. 开始循环赋值指定的 nr 个内存页面表项。如果源页表项没有使用,则不复制该表项,否则让页表项对应的内存页面为只读,如代码 41~46 行所示。
  2. 如果源页表所对应的内存页在 1MB 以上,则需要设置内存页面映射数组 mem_map,为索引的页面对应的 mem_map 项增加引用次数(如代码 47~51 行所示)。

13.4.3 建立线性页和物理页的映射关系

linux/mm/memory.c

涉及的函数:put_page(),及调用该函数的函数。

将一实际物理页面(page)映射到指定的线性地址(address)处,返回页面的物理地址,具体内容是设置页表中的页表项。

memory.c/put_page()
/*
 * This function puts a page in memory at the wanted address.
 * It returns the physical address of the page gotten, 0 if
 * out of memory (either when trying to access page-table or
 * page.)
 */
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)
		printk("Trying to put page %p at %p\n",page,address);
	if (mem_map[(page-LOW_MEM)>>12] != 1)	// page是否是新申请页面
		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;
/* no need for invalidate */
	return page;
}
复制代码

函数执行流程:

  1. 首先判断传入参数合法性。判断物理内存页面指针page是否低于1MB或者高于内存最高端物理地址,如果是则发出警告。代码 13~14 行。
  2. 检查该 page 页面是否是新申请的页面?否则警告。代码 15~16 行。
  3. 根据 address 线性地址,求出对应的目录项指针(代码第 17 行)。
  4. 判断目录项有效性。如果有效,则屏蔽第 12 位页内偏移地址,将结果保存到 page_table 变量中;如果无效(指定的页表不在内存中),则调用 get_free_page() 函数申请调用一个页面来保存页表,在对应目录项中设置标志,将该页表地址放到 page_table 中,默认页目录基址为 0。代码 18~25 行。
  5. 设置页表项 page_table 中的表项内容,代码第 26 行。屏蔽线性地址 address 低 12 位页内偏移地址,旋转剩余 20 位中的低 10 位(页表地址),填入物理内存页地址 page(xxx000号page),同时写入属性7。
  6. 第 27 行所示:不需要刷新页变换高速缓冲。原因是:此函数会被缺页异常函数 do_no_page() 调用,对于缺页引起的异常,不需要刷新 CPU 的页变换缓冲,不需要调用 invalidate() 函数。

13.4.4 物理内存共享

linux/mm/memory.c

涉及的函数:try_to_share(), share_page()

try_to_share() 将 p 进程 address 处的页面与当前进程共享。share_page() 函数尝试去寻找一个能够与当前进程共享页面的进程,参数 address 是当前进程空间中期望共享的某页面地址。

memory.c/try_to_share()、share_page()
/*
 * try_to_share() checks the page at address "address" in the task "p",
 * to see if it exists, and if it is clean. If so, share it with the current
 * task.
 *
 * NOTE! This assumes we have checked that p != current, and that they
 * share the same executable.
 */
static int try_to_share(unsigned long address, struct task_struct * p)
{
	unsigned long from;
	unsigned long to;
	unsigned long from_page;
	unsigned long to_page;
	unsigned long phys_addr;

	from_page = to_page = ((address>>20) & 0xffc);
	from_page += ((p->start_code>>20) & 0xffc);	// p进程页目录项
	to_page += ((current->start_code>>20) & 0xffc);	// 当前进程目录项
/* is there a page-directory at from? */
	from = *(unsigned long *) from_page;	// p进程目录项内容
	if (!(from & 1))
		return 0;
	from &= 0xfffff000;	// 页表地址
	from_page = from + ((address>>10) & 0xffc);	// 页表项指针
	phys_addr = *(unsigned long *) from_page;	// 页表项内容
/* is the page clean and present? */
	if ((phys_addr & 0x41) != 0x01)
		return 0;
	phys_addr &= 0xfffff000;
	if (phys_addr >= HIGH_MEMORY || phys_addr < LOW_MEM)
		return 0;
	to = *(unsigned long *) to_page;	// current 进程页目录项内容
	if (!(to & 1)) {	// 判断页表是否存在
		if ((to = get_free_page()))	// 申请一空闲页面来存放页表
			*(unsigned long *) to_page = to | 7;
		else
			oom();
	}
	to &= 0xfffff000;
	to_page = to + ((address>>10) & 0xffc);
	if (1 & *(unsigned long *) to_page)
		panic("try_to_share: to_page already exists");
/* share them: write-protect */
	*(unsigned long *) from_page &= ~2;	// 只读
	*(unsigned long *) to_page = *(unsigned long *) from_page;	// p对应页表项赋值给current对应页表项
	invalidate();
	phys_addr -= LOW_MEM;
	phys_addr >>= 12;
	mem_map[phys_addr]++;	// p进程被引用页面引用次数加1
	return 1;
}

/*
 * share_page() tries to find a process that could share a page with
 * the current one. Address is the address of the wanted page relative
 * to the current data space.
 *
 * We first check if it is at all feasible by checking executable->i_count.
 * It should be >1 if there are other tasks sharing this inode.
 */
static int share_page(unsigned long address)
{
	struct task_struct ** p;

	if (!current->executable)
		return 0;	// 本进程无对应执行文件
	if (current->executable->i_count < 2)	// 引用次数为1
		return 0;	// 无共享条件
	for (p = &LAST_TASK ; p > &FIRST_TASK ; --p) {
		if (!*p)
			continue;
		if (current == *p)
			continue;
		if ((*p)->executable != current->executable)
			continue;
		if (try_to_share(address,*p))
			return 1;
	}
	return 0;
}
复制代码

try_to_share(address, p)

输入参数:address-进程中的逻辑地址。p-被共享的进程。

函数执行流程:

  1. 求出逻辑地址 address 在当前进程current和进程p中对应的实际页目录项 from_page 和 to_page。首先求出指定逻辑地址 address 处在进程空间(64MB)中的页目录项,代码 17 行所示。该逻辑页目录项号+进程在 4G 线性空间中起始地址对应的页目录项=4G线性空间中实际页目录项 from_page 和 to_page,代码 18~19 行所示。
  2. 取出 p 进程页表项内容。首先判断 p 进程目录项是否有效,只需要判断第一位是否为 1,如果为 1,则说明页表存在。如果页表存在,则取出对应的页表项内容(对应的物理页面地址),代码 24~26 行所示。
  3. 判断物理页面是否存在且是否被修改。使用 0x41 来检查 D 位和 P 位,然后取出对应物理页面地址的有效性,不该低于1MB,不该高于内存最高端,代码 28~32 行。至此,找到了进程 p 中对应逻辑地址 address 处符合要求的物理页面。
  4. 确定 current 进程中逻辑地址 address 处对应的页表项地址,代码 33~38 行所示。判断 current 进程对应页目录项内容 to_page 是否有效,如果无效,即目录项对应的页表不存在,则申请一空闲页面来存放页表,否则报错。
  5. 然后通过页目录项中的页表地址(代码 40 行所示),加上页表项在页表中的偏移(代码 41 行所示),便得到了页表项地址,检查是否有效,如果有效则出错宕机(表示在未映射操作的前提下,已经映射了该页面)。
  6. 然后当前进程 current 复制 p 进程的页表项,实现当前进程逻辑地址 address 处页面被映射到 p 进程逻辑地址 address 处页面映射的物理页面上,代码 45~50 行,45~46 行实现了写保护以及页表项拷贝,47 行刷新页变换高速缓冲,再将该页面号对应的引用次数加1。
  7. 程序返回 1,说明共享成功。

在这里插入图片描述

页表项内容

share_page(address)

函数执行流程:

  1. 检查当前进程是否符合共享条件。进程任务数据结构中的 executable 字段可以判断进程是否存在对应的执行文件。如果有则进一步查询对应文件节点的引用数值,如果为 1,则说明该执行文件当前只有一个进程在运行,不符合共享条件,如代码 66~69 行所示。
  2. 若存在共享条件。那么就遍历任务数组,来寻找运行相同执行文件的另一个进程,进行页面共享。如代码 70~78 行所示。第 70 行处的 for 循环,遍历了一个任务数组(从后向前),71 行表示该任务项空闲的情况,如果任务项空闲则继续寻找,第 73 行表示如果是当前任务的情况,也继续循环,代码 75 行表示了如果存在一个进程运行的文件与当前进程所执行的文件不同时的情况,也继续遍历,第 77 行,则调用 try_to_share 函数,尝试共享。

13.5 进程地址空间

head.s中gdt中的内核代码和数据段。

head.s
gdt_descr:
	.word 256*8-1	# so does gdt (not that that's any
	.long gdt		# magic number, but it works for me :^)
gdt:.quad 0x0000000000000000	/* NULL descriptor */
	.quad 0x00c09a0000000fff	/* 16Mb */
	.quad 0x00c0920000000fff	/* 16Mb */
	.quad 0x0000000000000000	/* TEMPORARY - don't use */
	.fill 252,8,0			/* space for LDT's and TSS's etc */
复制代码
  • 代码第 1 行:加载全局描述符表寄存器 gdtr 要求的操作数。

  • 代码第 2 行:设置前 2 字节,表示 gdt 表长度。

  • 代码第 3 行:gdt 表的线性基地址,每 8 字节组成一个描述符项,共有 256 项。

  • 代码第 4 行:全局表,此项为空项。

  • 代码第 5 行:代码段描述符。

    • 0x08
    • 在这里插入图片描述
  • 代码第 6 行:数据段描述符。

    • 0x10
    • 在这里插入图片描述
  • 代码第 7 行:系统调用段描述符。

  • 代码第 8 行:预留空间。

在 0.11 中每个进程最大可用虚拟地址空间为 64MB,全局描述符表有 256 个表项,2 项空闲 2 项系统使用,每个进程使用 2 项,也就是此时系统最多容纳 (256-4)/2=126 个任务,虚拟地址范围 126x64MB=8GB。但 0.11 中人工定义的最大任务数是 64 个,所以全部线性地址空间为 64x64MB=4GB。

在这里插入图片描述

进程地址空间划分

heap 是堆空间区域,用于分配进程在执行过程中动态申请的内存空间。 bss 是进程的未初始化数据区,用于存放静态的未初始化数据。 每个任务都有两个堆栈,分被用于用户态和内核态程序的执行,分别称为用户态堆栈和内核态堆栈(线性地址的位置由该任务的 TSS 段中 ss0 和 esp0 两个字段决定,其位置在任务数据结构所在页面的末端),内核态堆栈很小(大约 3K 字节),任务的用户态堆栈却可以在用户的 64MB 空间延伸。

13.6 页错误处理

write_verify()、un_wp_page() page.s中的页错误中断处理程序 page_fault()。

write_verify()——写页面验证,该函数处理的是页面存在且页面不可写的情况,传入参数是指定页面的线性地址。 un_wp_page()——取消写保护函数,页异常中断过程中写保护异常的处理,传入参数是页表项指针。

graph LR
A[物理页面是否被共享] -- 是 --> B[重新申请新页面并复制页面内容]
A -- 否 --> C[设置页面可写]
page_default() 函数功能 write_verify()、un_wp_page() 函数
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; // 1:可写,改写属性即可,不需要重新申请
		invalidate();
		return;
	}
	if (!(new_page=get_free_page()))
		oom();
	if (old_page >= LOW_MEM)
		mem_map[MAP_NR(old_page)]--;	// 取消页面共享
	*table_entry = new_page | 7;
	invalidate();
	copy_page(old_page,new_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;
}
复制代码

write_verify()

写页面验证函数。仅处理页面不可写且存在的情况,调用取消写保护函数,(R/W=0)页面不可写

函数执行流程:

  1. 根据线性地址判断页表是否存在,如果不存在则返回,如代码 23~24 行所示。因为对于不存在的页面没有共享和写时复制而言。
  2. 如代码 25 行所示,得到页表项基址,再加上代码26行所示的地址偏移得到页表项地址。
  3. 如代码 27 行所示,判断该页表项内容,如果页面不可写且存在,那么调用 un_wp_page() 函数,否则返回。

un_wp_page() 在内核创建进程时,新进程与父进程被设置成共享代码和数据内存页面,并且所有这些页面均被设置成只读页面。而当新进程或原进程需要向内存页面写数据时,就会产生页面写保护异常,然后判断该页面是否被共享,如果是,那么重新申请一个新页面并复制页面内容,如果没有被共享,那就只需要设置页面读写标记即可。

函数执行流程:

  1. 如代码第 5 行所示,取出指定页表项中物理页面地址。
  2. 判断页面是否在主内存区,判断页面对应的引用次数是否为 1(如果为1则说明该页面无共享),如果符合要求,那么就只需要改变页面页表项中的 R/W 标志为可写即可(代码第 7 行所示),刷新页变换高速缓冲(代码第 8 行所示)。
  3. 否则在主内存区内申请一个空闲页给执行写操作的进程单独使用(如代码 11~12 行所示)。
  4. 如果 mem_map>1,则将原页面的页面映射字节数组值减1,如代码 13~14 行所示。
  5. 将指定的页表项内容更新为页面地址,并设置可读写等标志,如代码 15 行所示。
  6. 刷新页变换高速缓冲(代码第 16 行所示),然后调用 copy_page() 函数,将原页面内容复制到新页面上,代码 17 行所示。

page.s中的页错误中断处理程序page_fault()

graph LR
A[中断int14] --> B[取出引起页面异常的线性地址cr2, 出错码]
B --> C[测试页存在位]
C -- 存在 --> D[取消写保护 do_wp_page]
C -- 不存在 --> E[调用缺页处理程序 do_no_page]
page.s 功能

page.s 程序包含底层页异常处理代码,实际工作在 memory.c 中完成。page.s 文件中的中断处理程序 page_fault(中断14),分为两种情况:一是由于缺页引起的页异常中断,通过调用 do_no_page(error_code, address) 来处理;二是由页写保护引起的页异常(当前进程没有访问指定页面的权限),此时调用页写保护处理函数 do_wp_page(error_code, address) 进行处理。出错码 error_code 是 CPU 自动产生并压入堆栈的。

控制寄存器:CR0~CR3。

  • CR0:含有控制器操作模式和状态的系统控制标志。
  • CR1:保留。
  • CR2:含有导致页错误的线性地址。
  • CR3:含有页目录表物理内存基地址(页目录基地址寄存器 PDBR)。
page.s
/*
 *  linux/mm/page.s
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * 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)	# 取出错码到 eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%edx	# 置内核数据段选择符
	mov %dx,%ds
	mov %dx,%es
	mov %dx,%fs
	movl %cr2,%edx	# 取引起页面异常的线性地址
	pushl %edx	# 将该线性地址和出错码压入栈中,作用将调用函数的参数
	pushl %eax
	testl $1,%eax	# 测试页存在标志 P,如果不是缺页引起的异常则跳转
	jne 1f
	call do_no_page	# 调用缺页处理函数
	jmp 2f
1:	call do_wp_page	# 调用写保护处理函数
2:	addl $8,%esp
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret

复制代码

页异常描述符将在 traps.c 中设置。

函数执行流程:

  1. 第 15 行:取出错码保存到 eax 中。
  2. 第 21 行:设置内核数据段选择符。
  3. 第 25 行:将线性地址和出错码压入栈中,作为将调用函数的参数。
  4. 第 28 行:测试 页存在标志 P,如果不是缺页引起的异常则跳转。
  5. 第 30 行:调用缺页处理函数 do_no_page()
  6. 第 32 行:调用写保护处理函数 do_wp_page()

两个页出错调用函数:

1. do_wp_page()

执行写保护页面处理。

do_wp_page() 函数
/*
 * 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.
 *
 * If it's in code space we exit with a segment error.
 */
void do_wp_page(unsigned long error_code,unsigned long address)
{
#if 0
/* we cannot do this yet: the estdio library writes to code space */
/* stupid, stupid. I really want the libc.a from GNU */
	if (CODE_SPACE(address))
		do_exit(SIGSEGV);
#endif
	un_wp_page((unsigned long *)
		(((address>>10) & 0xffc) + (0xfffff000 &
		*((unsigned long *) ((address>>20) &0xffc)))));

}
复制代码

当用户往一共享页面上写数据时,会触发页异常中断,然后调用此函数来处理,将页面复制到一个新地址上并且递减原页面的共享计数值实现(un_wp_page()),我认为该函数只是调用 do_wp_page( ) 函数的接口,仅仅判断出错码以及构造页表项指针。 函数传入的参数是 error_code——CPU 自动产生,address 是页面线性地址。

代码 10~15 行来判断 address 是否位于代码空间,否则终止执行程序。

为了调用 do_wp_page() 函数,需要构造传入参数。((address>>10) & 0xffc):计算指定线性地址中页表项在页表中的偏移地址。(0xfffff000 &* ((address>>20) &0xffc)):取目录项中页表的地址值。由页表项在页表中偏移地址加上目录项内容中对应页表的物理地址即可得到页表项的指针。

2. do_no_page()

graph LR
A[缺页处理] --> B[1. 进程动态申请内存页面 映射一页物理页]
A  --> C[2. 尝试与已加载的相同文件进行页面共享]
A --> D[3. 从文件中读取所缺的数据页面到指定线性地址处]
do_no_page() 函数功能 memory.c/do_no_page()
void do_no_page(unsigned long error_code,unsigned long address)
{
	int nr[4];
	unsigned long tmp;
	unsigned long page;
	int block,i;

	address &= 0xfffff000;	// address 处缺页页面地址
	tmp = address - current->start_code;	// 进程线性地址空间对应偏移地址
	if (!current->executable || tmp >= current->end_data) {
		get_empty_page(address);	// 申请一个物理内存页面,映射到进程页面逻辑地址address处
		return;
	}
	if (share_page(tmp))	// 尝试逻辑地址 tmp 处页面共享
		return;
	if (!(page = get_free_page()))
		oom();
/* remember that 1 block is used for header */
	block = 1 + tmp/BLOCK_SIZE;	// 执行文件中起始数据块号
	for (i=0 ; i<4 ; block++,i++)
		nr[i] = bmap(current->executable,block);	// 设备上对应的逻辑块号
	bread_page(page,current->executable->i_dev,nr);	// 读设备上4个逻辑块
	i = tmp + 4096 - current->end_data;	// 超过执行文件 end_data 的部分清零
	tmp = page + 4096;
	while (i-- > 0) {
		tmp--;
		*(char *)tmp = 0;
	}
	if (put_page(page,address))	// 引起缺页异常的一页物理页面映射到线性地址address处
		return;
	free_page(page);
	oom();
}
复制代码

do_no_page() 函数由 page.s 程序中调用,有两种情况,一是进程申请一个干净页面存放堆或栈中的数据,那么直接为进程申请一物理内存并映射到指定线性地址处即可;二是所缺页面在进程执行映像文件范围内,那么就尝试共享页面,如果不成功,则申请一页物理内存页面,然后从设备上读取执行文件中的相应页面映射到进程逻辑页面逻辑地址 tmp 处。

函数执行流程:

  1. 取线性地址空间 address 处在进程空间中相对于进程基址的偏移长度值 tmp(逻辑地址),代码 8~9 行,其中 tmp 指的是缺页页面对应的逻辑地址。
  2. 若当前进程的 executable 节点指针为空(executable 是进程正在运行的执行文件的 i 节点结构)或者指定地址超出(代码+数据)长度,则申请一页物理内存,并映射到指定的线性地址处(表明进程在申请新的内存页面存放堆或栈中数据),如代码 10~13 行所示。
  3. 否则就说明所缺页面并不是干净页面,那么就尝试共享页面,若成功则退出,否则申请一个新物理页,然后从设备上读取执行文件中的相应页面并放置到进程页面逻辑地址 tmp 处,如代码 14~17 行所示。
  4. 如代码 18~28 所示,展示了如何从设备上读取执行文件中的相应页面。
  5. 如代码 29~33 行所示,将引起缺页异常的物理页面 page 映射到指定线性地址 address 处(代码第 29 行),若操作成功就返回,否则就释放内存页(代码第 31 行),显示内存不够(代码第 32 行)。

13.7 交换机制

0.12 内核中实现

Linux从0.12 版本开始,在内核中增加了虚拟内存交换功能——将暂时不用的内存页面临时保存到磁盘上,如果需要使用保存到磁盘上的页面内容,再将其放到内存中去。内存交换管理使用了与主内存区管理相同的位图映射技术,使用比特位图来确定被交换的内存页面具体的保存位置和映射位置。在编译内核时,若我们定义过交换设备号 SWAP_DEV,那么编译出的内核就具有内存交换功能。对于 Linux0.12 来说,交换设备使用硬盘上的一个单独交换分区(即虚拟内存分区,大小和实际物理内存有关系),分区上不含文件系统。

一个页面有 SWAP_BITS=4096*8=32768 个比特位,交换分区可管理页面数不超过 32768 个页面。

系统块设备分区程序把交换分区初始化位大小位 swap_size 个交换页面,第 0 个页面作为交换管理页面(保存有交换位图映射信息),实际个数少于 SWAP_BITS 数目。

数据结构——交换映射位图swap_bitmap

位图中的比特位:0,1,1,1,1,    ...   1,0      ...   ,0
对应交换页号:	 0,1,2,3,4,swap_size-1,swap_size,...,SWAP_BITS-1
复制代码

在这里插入图片描述

交换分区示意图

13.7.1 交换处理初始化

若系统定义了交换设备号 SWAP_DEV,内核就会执行交换处理初始化函数 init_swapping()

内存页面初始化函数。1. 检查是否有交换设备,交换设备是否有效。2. 取一个内存页来存放页面位映射数组。3. 设置管理页面在页面位映射数组的标志,1即可用,0即被占用。4. 检查页面位映射数组中的比特值。

swap.c
int SWAP_DEV = 0;	// 内核初始化时设置的交换设备号
static char * swap_bitmap = NULL;	// 页面位映射数组
void init_swapping(void)
{// 检测是否有交换设备
	extern int *blk_size[];	// 指向指定主设备号的块设备的块数数组。它的每一项对应一个子设备上所拥有的数据块总数,每个子设备对应设备的一个分区,blk_drv/ll_rw_blk.c
	int swap_size,i,j;

	if (!SWAP_DEV)
		return;
	if (!blk_size[MAJOR(SWAP_DEV)]) { //#define MAJOR(a) (((unsigned)(a))>>8) 取设备主设备号(高字节)
		printk("Unable to get size of swap device\n\r");
		return;
	}
	swap_size = blk_size[MAJOR(SWAP_DEV)][MINOR(SWAP_DEV)];	// 取得并检查指定交换设备号的交换分区数据块总数 #define MINOR(a) ((a)&0xff) 取子设备号(低字节)
	if (!swap_size)
		return;
	if (swap_size < 100) {
		printk("Swap device too small (%d blocks)\n\r",swap_size);
		return;
	}
	swap_size >>= 2;	// 将交换数据块总数转换成对应可交换页面总数
	if (swap_size > SWAP_BITS)
		swap_size = SWAP_BITS;
	swap_bitmap = (char *) get_free_page();	// 申请一物理内存用来存放交换页面位映射数组
	if (!swap_bitmap) {
		printk("Unable to start swapping: out of memory :-)\n\r");
		return;
	}
	read_swap_page(0,swap_bitmap);	// 将设备交换分区上的页面0(交换区管理页面)读到swap_bitmap页面中
	if (strncmp("SWAP-SPACE",swap_bitmap+4086,10)) {	// 根据4086字节开始处的设备特征字符串来判断是否是有效交换设备
		printk("Unable to find swap-space signature\n\r");
		free_page((long) swap_bitmap); // 将物理地址转化为线性地址
		swap_bitmap = NULL;
		return;
	}
	memset(swap_bitmap+4086,0,10);	// 特征字符串清0
	for (i = 0 ; i < SWAP_BITS ; i++) {	// 检查位图比特
		if (i == 1)
			i = swap_size;
		if (bit(swap_bitmap,i)) {
			printk("Bad swap-space bit-map\n\r");
			free_page((long) swap_bitmap);
			swap_bitmap = NULL;
			return;
		}
	}
	j = 0;
	for (i = 1 ; i < swap_size ; i++)
		if (bit(swap_bitmap,i))
			j++;
	if (!j) {
		free_page((long) swap_bitmap);
		swap_bitmap = NULL;
		return;
	}
	printk("Swap device ok: %d pages (%d bytes) swap-space\n\r",j,j*4096);
}
复制代码

SWAP_DEV:在内核初始化时,若系统定义了交换设备号 SWAP_DEV,内核就会执行交换处理初始化函数 init_swapping()。

函数执行流程:

  1. 根据设备的分区数组(块数数组)检查系统是否有交换设备,如代码 5~13 行所示。代码第 8~9 行,根据交换设备号来判断是否定义了交换设备,如果没有则返回;代码 10~13 行,查看交换设备有没有设置块数数组,如果没有,则显示警告信息并返回。代码第 10、14 行:blk_size[MAJOR][MINOR],含有所有块设备的块总数,如果 !blk_size[MAJOR] 则不必检测子设备的块总数。blk_size[MAJOR] 表示该主设备号所包含的总块数数组,blk_size[MAJOR][MINOR] 取该设备上交换分区中的数据块总数。
  2. 检查交换设备是否有效,根据数据块总数来判断。如代码 14~20 行所示,若总块数小于 100 块,则显示信息“交换设备太小”,退出。
  3. 申请物理页来存放页面位映射数组。如代码 21~28 行所示,代码第 21 行,将交换数据块总数转换成对应可交换页面总数,该值不能大于 32768(SWAP_BITS,4K*8,表示一个页面中的比特数),如果大于的话,设置为最大值 32768,然后申请一物理页来存放该数组,如代码 24 行所示。代码第 24 行,swap_bitmap = (char *) get_free_page();
  4. 然后把设备交换分区上的页面 0 读到 swap_bitmap 页面中。即设置交换区管理页面,代码第 29 行所示,该宏定义在 linux/mm.h 中,其中第 4086 字节开始处有特征字符串 “SWAP-SPACE”,如果没有,则说明该交换设备无效,显示出错信息,复位所有设置,如代码 30~35 行所示,否则将特征字符串字节清零,如代码 36 行所示。
  5. 检查页交换页面映射数组 swap_bitmap。仅是检查而非赋值。位图中比特位 0,表明对应交换页面已使用,否则表示交换页面空闲。第一个交换页面默认位管理页面,存放页面映射数组,故第 0 位为 0,交换页面的 [1 -- swap_size-1] 都可用(设备上实际可用的交换页面数为 swap_size-1 个),它们在位图中对应的比特位为 1,而位图中 [swap_size -- SWAP_BITS] 围内的比特位因为无对应交换页面,故设置为 0(不可用),以上检查步骤如代码 3750 行。代码 5155 行表示,如果统计得出没有空闲的交换页面,则表示交换功能有问题,释放位图占用的页面并退出;代码 57 行表示,如果工作正常,则显示交换设备所具有的交换页面数和交换空间总字节数。

13.7.2 比特位操作宏以及内嵌函数

通过不同的 op 操作,定义对指定比特位进行测试、设置或清除三种操作。

  • 参数 addr 是指定线性地址;nr 是指定地址处开始的比特位偏移。
  • 根据代码第 5 行,随 op 字符的不同构成不同的指令:
    • 当 op="" 时,形成指令 bt —— 测试并用原值设置进位位。
    • 当 op="s" 时,形成指令 bts —— 设置比特位值并用原值设置进位位。
    • 当 op="r" 时,形成指令 btr —— 复位比特位值并用原值设置进位位。
  • 输入:%0 - (返回值) %1 - 位偏移nr %2 - 基址addr %3 - 操作寄存器初值0
  • 内嵌汇编代码会将基地址(%2)和比特偏移值(%1)所指定的比特位值保存到进位标志 CF 中。
  • adcl 指令是带进位加。
  • 如果 CF=1,返回寄存器值为 1,否则返回寄存器值为 0。
swap.c 头部定义过程
#define bitop(name,op) \
static inline int name(char * addr,unsigned int nr) \
{ \
int __res; \
__asm__ __volatile__("bt" op " %1,%2; adcl $0,%0" \
:"=g" (__res) \
:"r" (nr),"m" (*(addr)),"0" (0)); \
return __res; \
}
// 根据不同的 op 字符定义3个内嵌函数
bitop(bit,"")	// 定义函数 bit(char* addr, unsigned int nr) 测试并用原值设置进位位,获取该位?
bitop(setbit,"s")	// 定义函数 setbit(char* addr, unsigned int nr) 置位
bitop(clrbit,"r")	// 定义函数 clrbit(char* addr, unsigned int nr) 复位
复制代码

13.7.3 申请与释放交换页面

linux/mm/swap.c

基于交换位图申请一交换页面和释放交换设备中的指定页面

swap.c/get_swap_page()
static int get_swap_page(void)
{
	int nr;

	if (!swap_bitmap)
		return 0;
	for (nr = 1; nr < 32768 ; nr++)	// 扫描整个交换映射位图
		if (clrbit(swap_bitmap,nr))	// 复位第一个为1的比特位
			return nr;	//返回目前空闲的交换页面号
	return 0;
}

void swap_free(int swap_nr)	// 释放交换设备中指定的交换页面
{
	if (!swap_nr)
		return;
	if (swap_bitmap && swap_nr < SWAP_BITS)
		if (!setbit(swap_bitmap,swap_nr))
			return;
	printk("Swap-space bad (swap_free())\n\r");
	return;
}
复制代码

get_swap_free() —— 申请取得一交换页面号 扫描整个交换映射位图,复位值为1的第一个比特,并返回值位置值(空闲的交换页面号),否则则返回 0,如代码 111 行所示。 代码第 7 行,32768 表示总共能映射的页面数。 代码第 8 行,调用函数 clrbit(),复位值为 1 的第一个比特位。 swap_free() —— 释放交换设备中指定的交换页面 在交换位图中设置指定的页面号对应的比特位(置为空闲),若原来比特位就等于 1,则表示出错,显示出错信息。 函数传入参数位指定交换页面号。 代码 1719 行,判断页面映射数组和指定交换页面号是否有效,如果有效,则调用函数 setbit() 将位置为 1,表示该页面被释放。

13.7.4 交换页换入与换出

linux/mm/swap.c

从交换设备上把指定页面交换进内存中和内存页面信息输出到交换设备上

swap.c/swap_in()
void swap_in(unsigned long *table_ptr)	// 把指定页面交换进内存中,页表项指针
{
	int swap_nr;
	unsigned long page;

	if (!swap_bitmap) {
		printk("Trying to swap in without swap bit-map");
		return;
	}
	if (1 & *table_ptr) {
		printk("trying to swap in present page\n\r");
		return;
	}
	swap_nr = *table_ptr >> 1;	// 页表项内容/2得到页表号,页号*2
	if (!swap_nr) {
		printk("No swap page in swap_in\n\r");
		return;
	}
	if (!(page = get_free_page()))	// 物理内存中申请新物理页
		oom();
	read_swap_page(swap_nr, (char *) page);	// 从交换设备中读入页面号为swap_nr的页面
	if (setbit(swap_bitmap,swap_nr))	// 页面位映射数组置位,表示该交换设备上的对应交换页面空闲
		printk("swapping in multiply from same page\n\r");
	*table_ptr = page | (PAGE_DIRTY | 7);	// 页表项指向对应物理页面
}
复制代码

swap_in() —— 把指定页面交换进内存中 把指定页表项对应的内存页面从交换设备中读入到新申请的内存页面中。同时修改交换位图中对应比特位,修改页表项内容,让它指向该内存页面,并设置相应标志。 传入参数 table_ptr 是页表项指针。

函数执行流程:

  1. 代码 6~10 行,检查交换位图和参数的有效性。判断交换位图是否存在以及指定页表项对应的页面是否存在于内存中,如果出错,则显示警告信息。
  2. 代码第 14~18 行,判断交换区是否有符合要求的交换页,只有存在位为 0,并且页表项内容不为 0 的页面才会在交换设备中。
  3. 代码 19~24 行,申请一页物理内存并从交换设备中读入页面号为 swap_nr 的页面,在用 read_swap_page() 把页面交换进来后,就把交换位图中对应比特位置位。如果原本就是置位的,说明此次是再次从交换设备中读入相同的页面。最后让页表项指向该物理页面(如代码 24 行所示),设置页表属性——修改位、页面特权级、用户可读写和存在标志。

为什么换入时不需要调用 invalidate() 刷新快表?

因为在换出的时候会调用,导致换出的页表并不会影响到快表,而换入时,由于是新换入页表,所以内存中还没有使用到,故不需要。

swap.c/try_to_swap_out()、swap_out()
int try_to_swap_out(unsigned long * table_ptr)
{
	unsigned long page;
	unsigned long swap_nr;

	page = *table_ptr;
	if (!(PAGE_PRESENT & page))	// PAGE_PRESENT:0x01 0000 0001 linux/mm.h
		return 0;
	if (page - LOW_MEM > PAGING_MEMORY) // 判断页表项指定的物理页面地址是否大于内存高端(15MB)
		return 0;
	if (PAGE_DIRTY & page) {	// PAGE_DIRTY:0x40 0100 0000 linux/mm.h 该页面被修改过
		page &= 0xfffff000;	// 取物理页面地址
		if (mem_map[MAP_NR(page)] != 1)	// 被共享的页面不需要被交换出去
			return 0;
		if (!(swap_nr = get_swap_page()))	// 申请交换页面号,空出页表项存在位,只有存在位为0并且页表项内容不为0的页面才会在交换设备中
			return 0;
		*table_ptr = swap_nr<<1;
		invalidate();	// 刷新CPU页变换高速缓冲
		write_swap_page(swap_nr, (char *) page);	// 调用写交换页面函数,将内存中的页面写到交换设备中。
		free_page(page);
		return 1;
	}
	*table_ptr = 0;
	invalidate();
	free_page(page);
	return 1;
}

/*
 * Ok, this has a rather intricate logic - the idea is to make good
 * and fast machine code. If we didn't worry about that, things would
 * be easier.
 */
int swap_out(void)	// 搜索整个4GB线性空间,期间尝试将对应物理内存页面交换到交换设备中,一旦成功则返回1.
{
	static int dir_entry = FIRST_VM_PAGE>>10;	// 任务1的第1个目录项索引 #define FIRST_VM_PAGE (TASK_SIZE>>12) 64MB/4KB
	static int page_entry = -1;
	int counter = VM_PAGES;	// 页面总数 #define LAST_VM_PAGE (1024*1024) 4GB/4KB #define VM_PAGES (LAST_VM_PAGE - FIRST_VM_PAGE) 1032192
	int pg_table;

	while (counter>0) {	// 循环遍历页目录表,查找有效页目录项
		pg_table = pg_dir[dir_entry];	// 页目录表项的内容
		if (pg_table & 1)
			break;
		counter -= 1024;	// 一个页表对应1024个页表帧
		dir_entry++;		// 下一个目录项
		if (dir_entry >= 1024)
			dir_entry = FIRST_VM_PAGE>>10;	// 重新遍历
	}
	pg_table &= 0xfffff000;	// 页表指针
	while (counter-- > 0) {	// 针对该页表中所有的1024个页面,逐一调用交换函数尝试交换出去,如果该页表项1024个页面均遍历完毕,则继续处理下一个页表
		page_entry++;	// 页表项索引
		if (page_entry >= 1024) {
			page_entry = 0;
		repeat:
			dir_entry++;	// 处理下一个页表
			if (dir_entry >= 1024)
				dir_entry = FIRST_VM_PAGE>>10; // 此判断可以删除(查看Linux1.2此操作如实现?)
			pg_table = pg_dir[dir_entry];	// 页目录项内容
			if (!(pg_table&1))
				if ((counter -= 1024) > 0)
					goto repeat;
				else
					break;
			pg_table &= 0xfffff000;	// 页表指针
		}
		if (try_to_swap_out(page_entry + (unsigned long *) pg_table))
			return 1;
	}
	printk("Out of swap-memory\n\r");
	return 0;
}
复制代码

try_to_swap_out() —— 尝试把内存页面输出到交换设备上 若页面没有被修改过,那么就没有必要写入交换设备,可以直接在主内存区释放;若页面被修改过,那就申请一个交换页面号,然后把页面交换出去,交换页面号保存在对应的页表项中,并且保持页表项存在位 P=0。 传入参数是页表项指针。 函数执行流程:

  1. 检查参数有效性。如代码第 6~10 行所示。取页表项内容 page,判断交换出去的页面是否存在,然后判断页表项指定的物理页面地址是否大于分页管理的内存高端 PAGING_MEMORY(15MB)。
  2. 如果页面被修改过。如代码 11~22 行所示。再判断该页面是否被共享,如果该页面被共享,则该页面不会被释放出去,如果该页面未被共享,则申请一交换页面号,并保存到页表项中,然后把页面交换出去并释放对应物理内存页面。代码 13~14 行,根据 mem_map 内存管理数组中保存的对应物理页面的引用次数,判断该页面是否被共享,如果不等于 1,则说明该页面被共享,故直接返回 0 退出。否则,申请一交换页面号(swap_nr),准备将该页换出,对应页表项中存放的是 swap_n r乘 2 的结果,如代码 17 行所示,之所以乘 2,是因为为了空出原来页表项的存在位(P),只有存在位为 0 并且页表项内容不为 0 的页面才会在交换设备中。将对应的内存页写到交换设备中,如代码 19 行所示,然后释放内存页面。
  3. 如果页面没有被修改过,那么就直接释放即可,如代码 23~26 号所示。

代码 9 行以及 36 行处的变量 LOW_MEM、PAGING_MEMORY 等变量的定义如下:

需要说明的是:不交换任务 0(task[0] 内核页面)。第一个可供交换的页面是任务 0 末端 64MB 处开始的虚拟内存页面。

#define FIRST_VM_PAGE (TASK_SIZE>>12) // TASK_SIZE:0x04000000 64MB 64MB/4KB=16384
#define LAST_VM_PAGE (1024*1024) // 4GB/4KB=1048576
#define VM_PAGES (LAST_VM_PAGE - FIRST_VM_PAGE) // 1032192
复制代码

swap_out() —— 把内存页面放到交换设备中

  1. 从线性地址 64MB 对应的页目录项(FIRST_VM_PAGE >> 10)开始,搜索整个 4GB 线性空间。期间尝试把对应的物理内存页面交换到交换设备中去,一旦成功地换出一个页面,就返回 1,否则返回 0。函数中两个静态变量用于暂存当前搜索点,用于下次搜索时地起始位置。
  2. 查找包含有效的页表内容的页目录项 pg_table,找到则退出,否则减少 counter 变量置,然后继续检测下一页目录项。如代码 41~49 行所示,循环取出页目录项内容,然后判断该页表内容是否有效。
  3. 在取得当前目录项中的页表指针后,再查看该页表所对应的 1024 个页面,逐一调用交换函数 try_to_swap_out(),如果成功将页面交换到交换设备中,就返回 1,若对应所有目录项的所有页表都已尝试失败,则显示报错信息,返回 0,如代码 5071 行所示。代码 50 行 page_table 变量为页表指针,代码第 52 行 page_entry 为为页表项索引,代码第 56 行所示 dir_entry++ 表示处理下一个页表,每找到一个有效页面就尝试调用交换函数,如代码 6768 行所示,如果调用成功,就退出返回 1,否则继续寻找下一页面。

13.7.5 两个低级块读写函数

#define read_swap_page(nr,buffer) ll_rw_page(READ,SWAP_DEV,(nr),(buffer)); #define write_swap_page(nr,buffer) ll_rw_page(WRITE,SWAP_DEV,(nr),(buffer)); 以上两个宏定义在linux/mm.h中 ll_rw_page() 函数在kernel/blk_drv/ll_rw_blk.c中实现

ll_rw_page() 函数(指定了设备号的设备页面访问函数)以页面为单位的块设备低级读写函数,以 4KB 为单位访问块设备数据,即每次读/写 8 个扇区(512B)。

ll_rw_blk.c 是所有块设备(硬盘、软盘和 Ram 虚拟盘)与系统其他部分之间的接口程序。通过调用该程序的低级块读写函数 ll_rw_block(),系统中的其他程序可以异步读写块设备中的数据。实际的读写操作是由设备的请求项处理函数 request_fn()完成(对于硬盘操作——do_hd_request()、对于软盘操作——do_fd_request()、对于虚拟盘操作——do_rd_request())。

ll_rw_blk.c/ll_rw_page()
void ll_rw_page(int rw, int dev, int page, char * buffer)
{
	struct request * req;
	unsigned int major = MAJOR(dev);
	// 检查参数
    // 检查主设备号以及设备的请求操作函数是否存在
	if (major >= NR_BLK_DEV || !(blk_dev[major].request_fn)) {
		printk("Trying to read nonexistent block-device\n\r");
		return;
	}
    // 检查参数命令是否是 READ 或者 WRITE
	if (rw!=READ && rw!=WRITE)
		panic("Bad block dev command, must be R/W");
	// 建立请求项
repeat:
	req = request+NR_REQUEST;
	while (--req >= request)
		if (req->dev<0)
			break;
	if (req < request) {
		sleep_on(&wait_for_request);
		goto repeat;
	}
    // 想空闲请求项中填写请求信息, 并将其加入队列中
/* fill up the request-info, and add it to the queue */
	req->dev = dev;	// 设备号
	req->cmd = rw;	// 命令(READ/WRITE)
	req->errors = 0;	// 读写操作错误计数
	req->sector = page<<3;	// 起始读扇区
	req->nr_sectors = 8;	// 读写扇区数 1 页
	req->buffer = buffer;	// 数据缓冲区
	req->waiting = current;	// 当前进程进入该请求等待队列
	req->bh = NULL;	// 无缓冲块头指针(不用高速缓冲)
	req->next = NULL;	// 下一个请求项指针
	current->state = TASK_UNINTERRUPTIBLE;	// 置为不可中断状态
	add_request(major+blk_dev,req);	// 将请求项加入队列中
	schedule();
}
复制代码

13.7.6 申请空闲物理页面

在主内存区申请取得一空闲物理页面,如果以及没有可用的物理页面,则交换处理,再次申请页面,函数具体注释可以参考 memory.c 中 get_free_page() 函数。

  • 输入:%1(ax=0)- 0;%2(LOW_MEM)字节位图管理的内存起始位置;%3(cx=PAGING_AGES);%4(edi=mem_map+PAGING_PAGES-1)
  • 输出:%0(ax=物理页面起始地址)
/*
 * 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");

repeat:
	__asm__("std ; repne ; scasb\n\t"
		"jne 1f\n\t"
		"movb $1,1(%%edi)\n\t"
		"sall $12,%%ecx\n\t"
		"addl %2,%%ecx\n\t"
		"movl %%ecx,%%edx\n\t"
		"movl $1024,%%ecx\n\t"
		"leal 4092(%%edx),%%edi\n\t"
		"rep ; stosl\n\t"
		"movl %%edx,%%eax\n"
		"1:"
		:"=a" (__res)
		:"0" (0),"i" (LOW_MEM),"c" (PAGING_PAGES),
		"D" (mem_map+PAGING_PAGES-1)
		:"di","cx","dx");
	if (__res >= HIGH_MEMORY)	// 页面地址大于实际内存容量重新寻址
		goto repeat;
	if (!__res && swap_out())	// 若没得到空闲页面则执行交换处理,并重新寻找
		goto repeat;
	return __res;	// 返回空闲物理页面地址
}
复制代码

代码实际指向 mem_map[] 的最后一个字节,本函数从位图末端开始向前扫描所有页面标志。代码会在内存映射字节位图中查找值为 0 的字节项,然后把对应的物理页面清零,如果得到的页面地址大于实际物理内存容量则重新寻找,如果没有找到空闲页面则去调用执行交换处理,并重新查找,如代码 25~28 行所示。

附录

文章分类
代码人生
文章标签