Linux0.11源码趣读

用源码打开操作系统“黑盒子”

Linux,Linux 0.11,内核,源码,操作系统,

闪客 “低并发编程”公号作者

12|内存划分:如何借助边界值划分内存?

讲述:闪客 大小:6.63MB 时长:00:06:55
00:00
1.0×

你好,我是闪客。

书接上回,上一讲,我们回顾了一下 main.c 函数之前我们做的全部工作,给进入 main 函数做了充分的准备。

这一讲,我们将看到一个初步的内存管理方案,并通过一个巧妙的数据结构将全部内存井井有条地管理起来。

为什么要给内存划分边界

让我们从 main 函数的第一行代码开始读。

// init/main.c
void main(void) {
    ROOT_DEV = ORIG_ROOT_DEV;
    drive_info = DRIVE_INFO;
    ...
}

首先,ROOT_DEV 为系统的根文件设备号,drive_info 为之前 setup.s 程序获取并存储在内存 0x90000 处的设备信息,我们先不管这俩变量,等之后用到了再说。

我们看后面这一“坨”很影响整体画风的代码。

// init/main.c
void main(void) {
    ...
    memory_end = (1<<20) + (EXT_MEM_K<<10);
    memory_end &= 0xfffff000;
    if (memory_end > 16*1024*1024)
        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;
    ...
}

这一坨代码和后面规规整整的 xxx_init 处在平级的位置。要是我们这么写代码,肯定被老板批评,被同事鄙视了。但没办法,这是人家 Linus 写的代码,不论你是否认可,人家已经是经典了,所以看就完事了。

这一坨代码虽然很乱,但仔细看,就会发现它只是为了计算出三个变量罢了。

  • main_memory_start
  • memory_end
  • buffer_memory_end

我们继续观察最后一行代码还会发现,其实有两个变量是相等的。

main_memory_start = buffer_memory_end;

所以,这部分代码其实仅仅计算出了两个变量。

  • main_memory_start
  • memory_end

推测出了代码的总体目标,我们再具体分析这个逻辑。其实就是一堆 if else 判断而已,判断的标准都是 memory_end ,也就是内存最大值的大小。而这个内存最大值,根据第一行代码就可以看出,等于 1M + 扩展内存大小。

那 ok 了,其实就只是针对不同的内存大小,设置不同的边界值罢了,为了更好理解,我们完全没必要考虑这么周全,可以代入具体数值来推算,这里就假设总内存一共就 8M 大小吧。

那么如果内存为 8M 大小,memory_end 就是:

8 * 1024 * 1024

也就是,代码逻辑只会走倒数第二个分支,那么 buffer_memory_end 就为:

2 * 1024 * 1024

那么 main_memory_start 和它相等,也为:

2 * 1024 * 1024

那这些值有什么用呢?一张图就给你说明白了。

你看,其实三个变量对应的,就是图里三个箭头所指向的地址的三个边界变量。有了这三个变量,内存就进一步划分成了三个部分。

具体主内存区是如何管理的,要看 mem_init 方法。

// init/main.c
void main(void) {
    ...
    mem_init(main_memory_start, memory_end);
    ...
}

而缓冲区是如何管理的,要看 buffer_init 方法。

// init/main.c
void main(void) {
    ...
    buffer_init(buffer_memory_end);
    ...
}

是不是非常清晰?

主内存如何管理

好,我们再讨论一下,主内存是如何管理的,很简单,放轻松。

我们进入 mem_init 函数,代码是后面这样。

// mm/memory.c
#define LOW_MEM 0x100000
#define PAGING_MEMORY (15*1024*1024)
#define PAGING_PAGES (PAGING_MEMORY>>12)
#define MAP_NR(addr) (((addr)-LOW_MEM)>>12)
#define USED 100

static long HIGH_MEMORY = 0;
static unsigned char mem_map[PAGING_PAGES] = { 0, };

// start_mem = 2 * 1024 * 1024
// end_mem = 8 * 1024 * 1024
void mem_init(long start_mem, long end_mem)
{
    int i;
    HIGH_MEMORY = end_mem;
    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;
}

看到代码,我们会发现也没几行,而且并没有更深的方法调用,看来读懂它不是难事。

仔细一看这个方法,其实折腾来折腾去,就是给一个 mem_map 数组的各个位置上赋了值,先是全部赋值为 USED 也就是 100,然后对其中一部分又赋值为了 0。

赋值为 100 的部分就是 USED,也就表示内存被占用,如果再具体说是占用了 100 次,这个之后再说。剩下赋值为 0 的部分就表示未被使用,也即使用次数为零。

是不是很简单?就是准备了一个表,记录了哪些内存被占用了,哪些内存没被占用。这就是所谓的“管理”,并没有那么神乎其神。

那接下来自然有两个问题。

  1. 数组中的每个元素表示内存是否空闲,那么是表示多大的内存是否空闲呢?

  2. 初始化时哪些地方是占用的,哪些地方又是未占用的?

内存划分

我们结合后面这张图就能看明白了,我们仍然假设内存总共只有 8M。


可以看出,初始化完成后,其实就是 mem_map 这个数组的每个元素都代表一个 4K 内存是否空闲(准确说是使用次数)。

4K 内存通常叫做 1 页内存,而这种管理方式叫分页管理,就是把内存分成一页一页(4K)的单位去管理。

1M 以下的内存这个数组干脆没有记录,这里的内存是无需管理的,或者换个说法是无权管理的,也就是没有权利申请和释放,因为这个区域是内核代码所在的地方,不能被“污染”。

1M 到 2M 这个区间是缓冲区,2M 是缓冲区的末端,缓冲区的开始在哪里我们之后再说,这些地方不是主内存区域,因此直接标记为 USED,产生的效果就是无法再被分配了。

2M 以上的空间是主内存区域,而主内存目前没有任何程序申请,所以初始化时统统都是零,未来等着应用程序去申请和释放这里的内存资源。

mem_map结构

那应用程序如何申请内存呢?我们这一讲先不展开,不过我们可以简单展望一下,看看申请内存的过程中,是如何使用 mem_map 这个结构的。

memory.c 文件中有个函数 get_free_page(),用于在主内存区中申请一页空闲内存页,并返回物理内存页的起始地址。

比如我们在 fork 子进程的时候,会调用 copy_process 函数来复制进程的结构信息,其中有一个步骤就是要申请一页内存,用于存放进程结构信息 task_struct。

// kernel/fork.c
int copy_process(...) {
    struct task_struct *p;
    ...
    p = (struct task_struct *) get_free_page();
    ...
}

我们继续看 get_free_page 的具体实现,是内联汇编代码。哪怕你看不懂也不要紧,只需要注意,它里面就用到了 mem_map 结构。

// mm/memory.c
unsigned long get_free_page(void) {
    register unsigned long __res asm("ax");
    __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");
    return __res;
}

这部分代码的作用,其实就是选择 mem_map 中首个空闲页面,并标记为已使用。

总结时刻

总结一下今天的内容。

为了之后的内存划分,首先需要计算出两个边界值,将内存划分成了三个部分,分别是内核程序、缓冲区和主内存。

主内存的管理初始化工作是 mem_init 函数做的,而缓冲区的管理初始化工作是 buffer_init 函数做的。

主内存管理方面,我们重点讲了 mem_map 结构。后面的内存申请与释放等骚操作,就是跟这张大表 mem_map 打交道而已,你一定要记住它哦。

欲知后事如何,且听下回分解。