前言
MMU:内存管理单元(Memory Management Unit)完成的工作就是虚拟地址到物理地址的转换,可以让系统中的多个程序跑在自己独立的虚拟地址空间中,相互不会影响。程序可以对底层的物理内存一无所知,物理地址可以是不连续的,但是不妨碍映射连续的虚拟地址空间。
Linux 内核的内存管理程序采用了分页管理方式。它利用页目录和页表结构处理内核中其他部分代码对内存的申请和释放操作。内存的管理是以内存页面为单位进行的,一个内存页面是指地址连续的 4K 字节物理内存。通过页目录项和页表项,可以寻址和管理指定页面的使用情况。在 Linux 0.12 的内存管理目录中共有三个文件
其中,page.s 文件比较短,仅包含内存页异常的中断处理过程(int 14),主要实现了对缺页和页写保护的处理。memory.c是内存页面管理的核心文件,用于内存的初始化操作、页目录和页表的管理和内核其他部分对内存的申请处理过程。swap.c程序用于内存页面交换管理,其中主要包括交换映射位图管理函数和交换设备访问函数。
操作系统起始阶段如下:
前面三个汇编文件,其主要功能就是三张表的设置:全局描述符表、中断描述符表、页表;同时还设置了各种段寄存器,栈顶指针,并为后续的程序提供了设备信息。
在内核源代码的init/目录中只有一个 main.c文件。系统在执行完 boot/head.s程序后就会将执行权交给 main.c。该程序虽然不长,但却包括了内核初始化的所有工作。
main.c程序首先利用前面 setup.s程序取得的机器参数设置系统的根文件设备号以及一些内存全局变这些内存变量指明了主内存区的开始地址、系统所拥有的内存容量和作为高速缓冲区内存的末端地址。如果还定义了虚拟盘(RAMDISK),则主内存区将适当减少。高速缓冲部分还需要扣除被显示卡显存和其 BIOS 占用的部分。高速缓冲是用于磁盘等块设备临时存放数据的地方,以 1K(1024)字节为一个数据块单位。主内存区域的内存由内存管理模块mm通过分页机制进行管理分配,以 4K(4096)字节为一个内存页单位。内核程序可以自由访问高速缓冲中的数据,但需要通过 mm 才能使用分配到的内存页面。
系统中内存功能划分如下:
(本篇主要简单总结介绍操作系统在初始化内存管理都做了哪些关键工作。)
main方法如下:
void main(void) {
ROOT_DEV = ORIG_ROOT_DEV;
drive_info = DRIVE_INFO;
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;
mem_init(main_memory_start,memory_end);
trap_init();
blk_dev_init();
chr_dev_init();
tty_init();
time_init();
sched_init();
buffer_init(buffer_memory_end);
hd_init();
floppy_init();
sti();
move_to_user_mode();
if (!fork()) {
init();
}
for(;;) pause();
}
main方法主要包含了三个部分,第一部分是一些参数的取值和计算(包括根设备 ROOT_DEV,之前在汇编语言中获取的各个设备的参数信息 drive_info,以及通过计算得到的内存边 main_memory_start memory_end buffer_memory_end。都是由 setup.s 这个汇编程序调用 BIOS 中断获取的各个设备的信息,并保存在约定好的内存地址 0x90000 处。)
第二部分是各种初始化 init 操作包括内存初始化 mem_init,中断初始化 trap_init、进程调度初始化 sched_init 等等。第三部分是切换到用户态模式,并在一个新的进程中做一个最终的初始化 init。这个 init 函数里会创建出一个进程,设置终端的标准 IO,并且再创建出一个执行 shell 程序的进程用来接受用户的命令。随即就是死循环,如果没有任何任务可以运行,操作系统会一直陷入这个死循环无法自拔(死循环里的进程称为进程0(idle进程)),仅执行 pause系统调用,并又会调用调度函数。从而 Linux 系统进入正常运行阶段。
内存边界划分
针对不同的内存大小,设置不同的边界值
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;
假设总内存一共就 8M 大小。那么如果内存为 8M 大小,memory_end 就是8 * 1024 * 1024, 也就只会走倒数第二个分支,那么 buffer_memory_end 就为2 * 1024 * 1024, 那么 main_memory_start 也为2 * 1024 * 1024;边界划分后如下图:
(定了三个箭头所指向的地址的三个边界变量)
缓冲区管理和分配函数
buffer_init(buffer_memory_end);
主内存管理和分配
通过内存边界进行初始化
mem_init(main_memory_start, memory_end);
本质上是给一个 mem_map 数组的各个位置上赋了值,而且显示全部赋值为 USED (也就是 100),然后对其中一部分又赋值为了 0。 赋值为 100 的部分就是 USED,也就表示内存被占用,如果再具体说是占用了 100 次。剩下赋值为 0 的部分就表示未被使用,也即使用次数为零。
#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 这个数组的每个元素都代表一个 4K 内存是否空闲(准确说是使用次数)。
4K 内存通常叫做 1 页内存,而这种管理方式叫分页管理,就是把内存分成一页一页(4K)的单位去管理。 1M 以下的内存这个数组干脆没有记录,这里的内存是无需管理的,或者换个说法是无权管理的,也就是没有权利申请和释放,因为这个区域是内核所在的地方,不能被“污染”。 1M 到 2M 这个区间是缓冲区,2M 是缓冲区的末端,这些地方不是主内存区域,因此直接标记为 USED,产生的效果就是无法再被分配了。 2M 以上的空间是主内存区域,而主内存目前没有任何程序申请,所以初始化时统统都是零,未来等着应用程序去申请和释放这里的内存资源。
mem_map结构的使用
申请内存的过程中,使用到 mem_map 这个结构的。
在 memory.c 文件中有个函数 get_free_page(),用于在主内存区中申请一页空闲内存页,并返回物理内存页的起始地址。
比如在 fork 子进程的时候,会调用 copy_process 函数来复制进程的结构信息,其中有一个步骤就是要申请一页内存,用于存放进程结构信息 task_struct。
int copy_process(...) {
struct task_struct *p;
...
p = (struct task_struct *) get_free_page();
...
}
选择 mem_map 中首个空闲页面,并标记为已使用
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;
}
关于内存管理的关键操作,诸如写时拷贝机制、缺页异常中断等将在未来作几篇简要总结介绍!!!