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

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

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

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

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



1. int detect_memory(void)
2. {
3.     int err = -1;
4.  
5.     if (detect_memory_e820() > 0)
6.         err = 0;
7.  
8.     if (!detect_memory_e801())
9.         err = 0;
10.  
11.     if (!detect_memory_88())
12.         err = 0;
13.  
14.     return err;
15. }
    <span style="line-height: 1.5; -ms-word-wrap: break-word;">&nbsp; &nbsp; 可以清晰的看到上面分别调用了三个函数</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">detect_memory_e820()</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">、</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">detect_memory_e801()</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">和</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">detect_memory_88()</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">。较新的电脑调用</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">detect_memory_e820()</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">足矣探测内存布局,</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">detect_memory_e801()</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">和</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">detect_memory_88()</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">则是针对较老的电脑进行兼容而保留的。</span>

    &nbsp; &nbsp;&nbsp;那么进一步看<span style="-ms-word-wrap: break-word;">detect_memory_e820()</span>的代码实现:

<div class="codeText" id="codeText" style="background: rgb(255, 255, 255); font: 12px/normal Consolas, monospace; margin: 0px 0px 1.1em; padding: 0px; border: 1px solid rgb(221, 221, 221); border-image: none; width: 1252.19px; letter-spacing: 0.1px; overflow: auto; -ms-word-break: break-all; -ms-word-wrap: break-word; font-size-adjust: none; font-stretch: normal;">
  1. static int detect_memory_e820(void)
  2. {
  3.     int count = 0;
  4.     struct biosregs ireg, oreg;
  5.     struct e820entry *desc = boot_params.e820_map;
  6.     static struct e820entry buf; / static so it is zeroed /
  7.  
  8.     initregs(&ireg);
  9.     ireg.ax = 0xe820;
  10.     ireg.cx = sizeof buf;
  11.     ireg.edx = SMAP;
  12.     ireg.di = (size_t)&buf;
  13.  
  14.     /*
  15.      * Note: at least one BIOS is known which assumes that the
  16.      * buffer pointed to by one e820 call is the same one as
  17.      * the previous call, and only changes modified fields. Therefore,
  18.      * we use a temporary buffer and copy the results entry by entry.
  19.      *
  20.      * This routine deliberately does not try to account for
  21.      * ACPI 3+ extended attributes. This is because there are
  22.      * BIOSes in the field which report zero for the valid bit for
  23.      * all ranges, and we don't currently make any use of the
  24.      * other attribute bits. Revisit this if we see the extended
  25.      * attribute bits deployed in a meaningful way in the future.
  26.      */
  27.  
  28.     do {
  29.         intcall(0x15, &ireg, &oreg);
  30.         ireg.ebx = oreg.ebx; / for next iteration... /
  31.  
  32.         /* BIOSes which terminate the chain with CF = 1 as opposed
  33.            to %ebx = 0 don't always report the SMAP signature on
  34.            the final, failing, probe. */
  35.         if (oreg.eflags & X86_EFLAGS_CF)
  36.             break;
  37.  
  38.         /* Some BIOSes stop returning SMAP in the middle of
  39.            the search loop. We don't know exactly how the BIOS
  40.            screwed up the map at that point, we might have a
  41.            partial map, the full map, or complete garbage, so
  42.            just return failure. */
  43.         if (oreg.eax != SMAP) {
  44.             count = 0;
  45.             break;
  46.         }
  47.  
  48.         *desc++ = buf;
  49.         count++;
  50.     } while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_map));
  51.  
  52.     return boot_params.e820_entries = count;
  53. }


    除去注释,实际代码量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=用来表示读取信息的<span style="-ms-word-wrap: break-word;">Index</span>,初始值为<span style="-ms-word-wrap: break-word;">0</span>,中断后返回该寄存器用来下次要获取的信号的序号;


ES:<span style="-ms-word-wrap: break-word;">DI=</span>用来保存信息的<span style="-ms-word-wrap: break-word;">buffer</span>地址;


ECX=buffer的空间大小;


EDX=入参签名,必须为&ldquo;<span style="-ms-word-wrap: break-word;">SMAP</span>&rdquo;;

    【输出】

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

EAX=用来返回&ldquo;<span style="-ms-word-wrap: break-word;">SMAP</span>&rdquo;,否则表示出错;


ES:<span style="-ms-word-wrap: break-word;">DI=</span>对应的<span style="-ms-word-wrap: break-word;">buffer</span>,里面存放获取到的信息;


ECX=BIOS在<span style="-ms-word-wrap: break-word;">buffer</span>中存放数据的大小;


EBX=BIOS返回的下次调用的序号,如果返回为<span style="-ms-word-wrap: break-word;">0</span>,则表示无后续信息;

    由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));

  • <span style="line-height: 1.5; -ms-word-wrap: break-word;">&nbsp; &nbsp; 通过万能的谷歌查到</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">Buffer</span><span style="line-height: 1.5; -ms-word-wrap: break-word;">中存放的数据格式说明:</span>
    



































    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

    &nbsp; &nbsp;&nbsp;类型含义:
    

























    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

    &nbsp; &nbsp;&nbsp;最后顺便记录一下<span style="-ms-word-wrap: break-word;">detect_memory()</span>在<span style="-ms-word-wrap: break-word;">Linux</span>系统中调用栈为:
    

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

    +&mdash;&mdash;<span style="-ms-word-wrap: break-word;">&gt; detect_memory()&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; #/arch/x86/boot/main.c</span>
    
    
    +&mdash;&mdash;<span style="-ms-word-wrap: break-word;">&gt;detect_memory_e820()&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; #/arch/x86/boot/memory.c</span>
    
    &nbsp; &nbsp;&nbsp;这是在实模式下完成的内存布局探测,此时尚未进入保护模式。
    
    &nbsp; &nbsp;&nbsp;对了,还有两个函数<span style="-ms-word-wrap: break-word;">detect_memory_e801()</span>和<span style="-ms-word-wrap: break-word;">detect_memory_88()</span>没说呢,这里就不贴代码了,其实看一下它的实现,都是通过调用<span style="-ms-word-wrap: break-word;">BIOS</span>的<span style="-ms-word-wrap: break-word;">0x15</span>中断来探测内存布局的,只是入参寄存器<span style="-ms-word-wrap: break-word;">ax</span>或<span style="-ms-word-wrap: break-word;">ah</span>分别是<span style="-ms-word-wrap: break-word;">0xe801</span>或<span style="-ms-word-wrap: break-word;">0x88</span>而已。这是对以前老式计算机表示兼容而保留的,现在的计算机都已经被<span style="-ms-word-wrap: break-word;">0xe820</span>取代了。
    
    &nbsp; &nbsp;&nbsp;顺便附:<span style="-ms-word-wrap: break-word;">BIOS </span>中断向量表(来自<span style="-ms-word-wrap: break-word;">wikipedia.org</span>):
    










































































































































































    中断



    描述



    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)



    获取扩展内存容量(自从 1944 年引入的新功能),可获取到 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 控制器所呼叫


    &nbsp;