内存管理 · 2014-08-12 0

【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余行,实现较为简单。主要实现的是一个循环调用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或操作系统设定以供软件调用。

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 控制器所呼叫