【Linux内存源码分析】内存布局探测

近期深入学习linux内核,先从内存管理下手吧,考虑到老版本的内核分析文章已经较多,于是找了一个较新的LTS内核版本尝试自行分析,这里选择了linux 3.14版本,环境主要是x86-32bit

Linux系统的内存管理是一个很复杂的工程,它不仅仅是物理内存管理,同时包括虚拟内存管理、内存交换和回收等,还有管理中的各式各样的算法。这也就表明了它的分析方法很多,因为切入点很多,这里分析内存管理采用了自底向上分析方法。

既然采用自底向上分析,那么内存的最底层莫过于就是物理内存了。物理内存管理的算法是buddy算法,一个很简单但是却意味深远的算法。不过这里暂不讲算法,毕竟系统启动并不是一开始就能够使用了buddy算法来管理物理内存的,心急吃不了热豆腐,总的有个循序渐进的过程。

废话不多说,既然说是内存管理,管理物理内存总得需要知道内存的大小吧?那么这里就先分析一下linux如何探测物理内存的。

探测物理内存布局的函数为detect_memory(),具体实现:

【file:/arch/x86/boot/memory.c】
int detect_memory(void)
{
	int err = -1;

	if (detect_memory_e820() > 0)
		err = 0;

	if (!detect_memory_e801())
		err = 0;

	if (!detect_memory_88())
		err = 0;

	return err;
}

可以清晰的看到上面分别调用了三个函数detect_memory_e820()detect_memory_e801()detect_memory_88()。较新的电脑调用detect_memory_e820()足矣探测内存布局,detect_memory_e801()detect_memory_88()则是针对较老的电脑进行兼容而保留的。

那么进一步看detect_memory_e820()的代码实现:

【file:/arch/x86/boot/memory.c】
static int detect_memory_e820(void)
{
	int count = 0;
	struct biosregs ireg, oreg;
	struct e820entry *desc = boot_params.e820_map;
	static struct e820entry buf; /* static so it is zeroed */

	initregs(&ireg);
	ireg.ax  = 0xe820;
	ireg.cx  = sizeof buf;
	ireg.edx = SMAP;
	ireg.di  = (size_t)&buf;

	/*
	 * Note: at least one BIOS is known which assumes that the
	 * buffer pointed to by one e820 call is the same one as
	 * the previous call, and only changes modified fields.  Therefore,
	 * we use a temporary buffer and copy the results entry by entry.
	 *
	 * This routine deliberately does not try to account for
	 * ACPI 3+ extended attributes.  This is because there are
	 * BIOSes in the field which report zero for the valid bit for
	 * all ranges, and we don't currently make any use of the
	 * other attribute bits.  Revisit this if we see the extended
	 * attribute bits deployed in a meaningful way in the future.
	 */

	do {
		intcall(0x15, &ireg, &oreg);
		ireg.ebx = oreg.ebx; /* for next iteration... */

		/* BIOSes which terminate the chain with CF = 1 as opposed
		   to %ebx = 0 don't always report the SMAP signature on
		   the final, failing, probe. */
		if (oreg.eflags & X86_EFLAGS_CF)
			break;

		/* Some BIOSes stop returning SMAP in the middle of
		   the search loop.  We don't know exactly how the BIOS
		   screwed up the map at that point, we might have a
		   partial map, the full map, or complete garbage, so
		   just return failure. */
		if (oreg.eax != SMAP) {
			count = 0;
			break;
		}

		*desc++ = buf;
		count++;
	} while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_map));

	return boot_params.e820_entries = count;
}

除去注释,实际代码量30余行,实现较为简单。主要实现的是一个循环调用BIOS0x15中断的功能。在intcall(0x15,
&ireg, &oreg);
0x15是中断向量,入参为ireg结构体,出参为oreg。再仔细看一下ireg的入参设置,ax赋值为0xe820,没错,这就是著名的e820的由来了。所谓的e820是指在x86的机器上,由BIOS提供的0x15中断去获取内存布局,其中中断调用时,AX寄存器必须为0xe820,中断调用后将会返回被BIOS保留内存地址范围以及系统可以使用的内存地址范围。所有通过中断获取的数据将会填充在boot_params.e820_map中,也就是著名的e820图了。

接下来通过0xe820的详细用法来理解这段代码:

【输入】

EAX=0xe820;

EBX=用来表示读取信息的Index,初始值为0,中断后返回该寄存器用来下次要获取的信号的序号;

ES:DI=用来保存信息的buffer地址;

ECX=buffer的空间大小;

EDX=入参签名,必须为“SMAP”

【输出】

CF=如果flag寄存中的CF被置位表示调用出错;

EAX=用来返回“SMAP”,否则表示出错;

ES:DI=对应的buffer,里面存放获取到的信息;

ECX=BIOS在buffer中存放数据的大小;

EBX=BIOS返回的下次调用的序号,如果返回为0,则表示无后续信息;

0xe820用法中,可以知道while循环就是用来连续调用0x15中断,根据每次的返回值通过ireg.ebx = oreg.ebx;设置,用来下一次探测内存布局信息,直至ebx返回0表示探测完毕。这样一来最终就可以得知该机器的整体内存布局了。

再顺道看一下buffer的内容究竟都有什么,根据代码定义,可以看到buffer的结构体为:

struct e820entry {
	__u64 addr;	/* start of memory segment */
	__u64 size;	/* size of memory segment */
	__u32 type;	/* type of memory segment */
} __attribute__((packed));

通过万能的谷歌查到Buffer中存放的数据格式说明:

Offset in bytes

Name

Description

0

BaseAddrLow

Low 32 bits of Base Address

4

BaseAddrHigh

High 32bits of Base Address

8

LengthLow

Low 32bits of Length in Bytes

12

LengthHigh

High 32bits of Length in Bytes

16

Type

Address type of this Length

类型含义:

Value

Pneumonic

Description

1

AddressRangeMemory

This run is available RAM usable by the
operating system

2

AddressRangeReserved

This run of Address is in use or reserved
by the system
and must not be used by the OS

Other

Undefined

Undefined —— Reserved for future use.Any
range of this type must be treated by the OS as if the type

最后顺便记录一下detect_memory()Linux系统中调用路径为:

main()                              
#/arch/x86/boot/main.c

└-> detect_memory()                 #/arch/x86/boot/main.c

└->detect_memory_e820()            #/arch/x86/boot/memory.c

这是在实模式下完成的内存布局探测,此时尚未进入保护模式。

对了,还有两个函数detect_memory_e801()detect_memory_88()没说呢,这里就不贴代码了,其实看一下它的实现,都是通过调用BIOS0x15中断来探测内存布局的,只是入参寄存器axah分别是0xe8010x88而已。这是对以前老式计算机表示兼容而保留的,现在的计算机都已经被0xe820取代了。

顺便附:BIOS 中断向量表(来自wikipedia.org

中断

描述

INT 00h

CPU: 除零错,或商不合法时触发

INT 01h

CPU: 单步陷阱,TF标记为打开状态时,每条指令执行后触发

INT 02h

CPU: 非可屏蔽中断, 开机自我测试时发生内存错误触发。

INT 03h

CPU: 第一个未定义的中断向量, 约定俗成仅用于调试程序

INT 04h

CPU: 算数溢出。通常由INTO指令在置溢出位时触发。

INT 05h

在按下Shift-Print ScreenBOUND指令检测到范围异常时触发。

INT 06h

CPU: 非法指令。

INT 07h

CPU: 没有数学协处理器时尝试执行浮点指令触发。

INT 08h

IRQ0: 可编程中断控制器每 55 毫秒触发一次,即每秒 18.2 次。

INT 09h

IRQ1: 每次键盘按下、按住、释放。

INT 0Ah

IRQ2:

INT 0Bh

IRQ3: COM2/COM4

INT 0Ch

IRQ4: COM1/COM3

INT 0Dh

IRQ5: 硬盘控制器(PC/XT 下)或 LPT2

INT 0Eh

IRQ6: 需要时由软碟控制器呼叫。

INT 0Fh

IRQ7: LPT1

INT 10h

显示服务BIOS或操作系统设定以供软件调用。

AH=00h

设定显示模式

AH=01h

设定游标形态

AH=02h

设定游标位置

AH=03h

获取游标位置与形态

AH=04h

获取光笔位置

AH=05h

设定显示页

AH=06h

清除或卷轴画面()

AH=07h

清除或卷轴画面(

AH=08h

读取游标处字符与属性

AH=09h

更改游标处字符与属性

AH=0Ah

更改游标处字符

AH=0Bh

设定边界颜色

AH=0Eh

TTY模式下写字符

AH=0Fh

取得目前显示模式

AH=13h

写字符串

INT 11h

返回设备列表。

INT 12h

获取常规内存容量。

INT 13h

低阶磁盘服务。

AH=00h

复位磁盘驱动器。

AH=01h

检查磁盘驱动器状态。

AH=02h

读扇区。

AH=03h

写扇区。

AH=04h

校验扇区。

AH=05h

格式化磁道。

AH=08h

取得驱动器参数。

AH=09h

初始化硬盘驱动器参数。

AH=0Ch

寻道。

AH=0Dh

复位硬盘控制器。

AH=15h

取得驱动器类型。

AH=16h

取得软驱中盘片的状态。

INT 14h

串口通信例程。

AH=00h

初始化串口。

AH=01h

写出字符。

AH=02h

读入字符。

AH=03h

状态。

INT 15h

其它(系统支持例程)。

AH=4FH

键盘拦截。

AH=83H

事件等待。

AH=84H

读游戏杆。

AH=85H

SysRq 键。

AH=86H

等待。

AH=87H

块移动。

AH=88H

获取扩展内存容量。

AH=C0H

获取系统参数。

AH=C1H

获取扩展 BIOS 数据区段。

AH=C2H

指针设备功能。

AH=E8h, AL=01h (AX = E801h)

获取扩展内存容量(自从 194

 年引入的新功能),可获取到 64MB 以上的内存容量。

AH=E8h, AL=20h (AX = E820h)

查询系统地址映射。该功能取代了 AX=E801h AH=88h

INT 16h

键盘通信例程。

AH=00h

读字符。

AH=01h

读输入状态。

AH=02h

Shift 键(修改键)状态。

AH=10h

读字符(增强版)。

AH=11h

读输入状态(增强版)。

AH=12h

Shift 键(修改键)状态(增强版)。

INT 17h

打印服务。

AH=00h

打印字符。

AH=01h

初始化打印机。

AH=02h

检查打印机状态。

INT 18h

执行磁带上的 BASIC 程序:真正的”IBM 兼容机在 ROM 里内置 BASIC 程序,当引导失败时由 BIOS 调用此例程解释执行。(例:打印“Boot disk error.
Replace disk and press any key to continue…”
这类提示信息)

INT 19h

加电自检之后载入操作系统。

INT 1Ah

实时钟服务。

AH=00h

读取实时钟。

AH=01h

设置实时钟。

AH=02h

读取实时钟时间。

AH=03h

设置实时钟时间。

AH=04h

读取实时钟日期。

AH=05h

设置实时钟日期。

AH=06h

设置实时钟闹铃。

AH=07h

重置实时钟闹铃。

INT 1Bh

Ctrl+Break,由 IRQ 9 自动调用。

INT 1Ch

预留,由 IRQ 8 自动调用。

INT 1Dh

不可调用:指向视频参数表(包含视频模式的数据)的指针。

INT 1Eh

不可调用:指向软盘模式表(包含关于软驱的大量信息)的指针。

INT 1Fh

不可调用:指向视频图形字符表(包含从 80h FFh  ASCII 字符的数据)的信息。

INT 41h

地址指针:硬盘参数表(第一硬盘)。

INT 46h

地址指针:硬盘参数表(第二硬盘)。

INT 4Ah

实时钟在闹铃时调用。

INT 70h

IRQ8: 由实时钟调用。

INT 74h

IRQ12: 由鼠标调用

INT 75h

IRQ13: 由数学协处理器调用。

INT 76h

IRQ14: 由第一个 IDE 控制器所呼叫

INT 77h

IRQ15: 由第二个 IDE 控制器所呼叫

发表评论

电子邮件地址不会被公开。 必填项已用*标注