内存区域划分 #
Linux 0.11 内核默认最多支持 16M 物理内存。它的内核模块在物理内存的最前端。然后是高速缓冲区,高速缓冲区是磁盘等块设备临时存放数据的地方,高速缓冲区的最高内存地址可以是 4M,下图中高速缓冲区还要扣除显存和 BIOS ROM 占用的部分。剩余部分才是主内存区,如果系统中还有 RAM 虚拟磁盘,主内存区的前段还要扣除虚拟盘所占的内存空间。如下图所示,
我们来看一下具体在代码中的实现, 下面代码中 ROOT_DEV 表示根设备号, buffer_memory_end 表示高速缓冲区的末端地址, memory_end 表示机器的内存数,main_memory_start 则表示主存的开始地址。
ROOT_DEV = ORIG_ROOT_DEV; // 0x901fc
drive_info = DRIVE_INFO; // 0x90080
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;
#ifdef RAMDISK
main_memory_start += rd_init(main_memory_start, RAMDISK*1024);
#endif
mem_init(main_memory_start,memory_end);
上面的代码首先初始化了根设备号和硬盘参数表,然后设置内存的大小为 1M + 扩展内存 * 1024 字节,memory_end &= 0xfffff000; 则是表示忽略不到 4K 的内存数。
然后是对内存的划分,如果发现包括扩展内存后的内存大小超过 16M 则按照 16 M 来算。如果发现内存大小超过 12M,则将缓冲区的末端地址设置到 4M 位置处,如果内存超过 6 M 但小于 12M, 则将缓冲区末端地址设置到 2M,否则就设置 1M 位置.
最后内存的起始位置就是高速缓冲区的末端地址。特别地,如果设置了虚拟内存的话主存的起始地址要往后推。rd_init 的第一个参数表示起始地址,第二个参数表示申请的长度。
最后调用 mem_init 进行主存的初始化,
内存区域索引 #
下面我们来看看这 16M 的内存是怎么进行索引的。首先 Linux 将内存划分成了一个一个的页,每个页大小为 4K。 然后建立对页的索引,用来构建索引的数据结构是页表。一个页表大小也是 4K,只不过它里面存的值是对每一个页的索引,页表里每一个页表项的结构如下图所示,
我们可以看到每一个页表项大小是 4个字节,其中高 20 位用来索引每一个页帧的地址,低 12 位是一些标志位,用来表示可以对这些页的操作。因为一个页表项是 4 个字节,页表的大小是 4K,所以我们也很容易得出一个页表能索引的页是 1024。所以这里笔者猜测当时 Linus 也是想着凑个整才把一个页的大小设置为 4K 而不是 1K,这样除以每一个页表项的大小刚好是 $2^{10}$。
下面我们来推一下要索引 16M 的内存需要多少个页表,已知一个页表能索引 1024 个页,一个页的大小是 4K, 所以一个页表能索引 4M 的物理内存,那 16 M 就需要 4个页表。这四个页表在 head.s 里被定义。
下面我们来看一下具体的代码实现,
/*
* I put the kernel page tables right after the page directory,
* using 4 of them to span 16 Mb of physical memory. People with
* more than 16MB will have to expand this.
*/
.org 0x1000
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000
我们可以看到第一个页表的起始位置不是0x0000,而是 0x1000。这是为什么呢?主要是因为当页表多了以后我们怎么知道哪个页表的位置呢,这就需要一个页目录来管理。页目录的结构和页表项的结构一样,只不过里面的索引是页表的索引,它的起始地址 pg_dir 就是0x0000。下面是对页表初始化的代码,
.align 2
setup_paging:
movl $1024*5,%ecx /* 5 pages - pg_dir+4 page tables */
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
cld;rep;stosl
movl $pg0+7,pg_dir /* set present bit/user r/w */
movl $pg1+7,pg_dir+4 /* --------- " " --------- */
movl $pg2+7,pg_dir+8 /* --------- " " --------- */
movl $pg3+7,pg_dir+12 /* --------- " " --------- */
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
第一行表示按照 4 字节方式对齐,然后对前 5 页的内存地址清零。然后是对每一个页表的设置,我们需要逐行阅读这段汇编代码。首先下面这行代码的 pg0+7 表示的是 0x1000 + 7,也就是二进制的0001 0000 0000 0111。我们拿它和 0xFFFFF000相与可以得到页表的起始地址,用它和 0XFFF项羽可以得到我们对这个页表的一些标志位。然后将这个值传送到0x0000就完成了对页表 0 的初始化。
movl $pg0+7,pg_dir /* set present bit/user r/w */
同样的方式可以完成对剩下三个页表的初始化
movl $pg1+7,pg_dir+4 /* --------- " " --------- */
movl $pg2+7,pg_dir+8 /* --------- " " --------- */
movl $pg3+7,pg_dir+12 /* --------- " " --------- */
然后从最后一个页表的最后一个页表项开始倒叙往前初始化页表
movl $pg3+4092,%edi
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
std
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax
jge 1b
完成之后需要初始化页目录表基址寄存器 eax,这里是 0x0000,然后 cr3中保存页目录表的物理地址。之后是启动分页处理,完成之后返回到 main 函数处真正开始执行启动操作系统的代码。
xorl %eax,%eax /* pg_dir is at 0x0000 */
movl %eax,%cr3 /* cr3 - page directory start */
movl %cr0,%eax
orl $0x80000000,%eax
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
Last modified on 2022-10-06