近期深入学习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余行,实现较为简单。主要实现的是一个循环调用BIOS的0x15中断的功能。在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()没说呢,这里就不贴代码了,其实看一下它的实现,都是通过调用BIOS的0x15中断来探测内存布局的,只是入参寄存器ax或ah分别是0xe801或0x88而已。这是对以前老式计算机表示兼容而保留的,现在的计算机都已经被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 Screen或BOUND指令检测到范围异常时触发。 | ||||||||||||||||||||||||||||||
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或操作系统设定以供软件调用。
|
||||||||||||||||||||||||||||||
INT 11h | 返回设备列表。 | ||||||||||||||||||||||||||||||
INT 12h | 获取常规内存容量。 | ||||||||||||||||||||||||||||||
INT 13h | 低阶磁盘服务。
|
||||||||||||||||||||||||||||||
INT 14h | 串口通信例程。
|
||||||||||||||||||||||||||||||
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 | 键盘通信例程。
|
||||||||||||||||||||||||||||||
INT 17h | 打印服务。
|
||||||||||||||||||||||||||||||
INT 18h | 执行磁带上的 BASIC 程序:“真正的”IBM 兼容机在 ROM 里内置 BASIC 程序,当引导失败时由 BIOS 调用此例程解释执行。(例:打印“Boot disk error. Replace disk and press any key to continue…”这类提示信息) | ||||||||||||||||||||||||||||||
INT 19h | 加电自检之后载入操作系统。 | ||||||||||||||||||||||||||||||
INT 1Ah | 实时钟服务。
|
||||||||||||||||||||||||||||||
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 控制器所呼叫 |