Linux0.11 内存管理
页表初始化
将_pg_dir
放在head.s
文件的开头,而head.s
在内存的0x0000 0000
处,所以_pg_dir
页目录在0x0000 0000
处。
在head.s
中,在设置完idt和
gdt`前,需要设置页目录表。
jmp after_page_tables
...
.org 0x1000 # 从偏移0x1000 处开始是第1 个页表(偏移0 开始处将存放页表目录)。
pg0:
.org 0x2000
pg1:
.org 0x3000
pg2:
.org 0x4000
pg3:
.org 0x5000 # 定义下面的内存数据块从偏移0x5000 处开始。
...
after_page_tables:
pushl $0
pushl $0 # 这些是调用main 程序的参数(指init/main.c)。
pushl $0
pushl $L6 # return address for main, if it decides to.
pushl $_main # '_main'是编译程序对main 的内部表示方法。
jmp setup_paging # 跳转至第198 行。
...
.align 2 # 按4 字节方式对齐内存地址边界。
setup_paging: # 首先对5 页内存(1 页目录 + 4 页页表)清零
#总共5页,每页4096字节,按照每次增长4字节,所以是1024*5
movl $1024*5,%ecx
xorl %eax,%eax
xorl %edi,%edi /* pg_dir is at 0x000 */
# 页目录从0x000 地址开始。
#下面这三句:cld将edi或esi设为递增方向,rep做重复(%ecx)次,
#stosl表示将edx中的值保存到es:edi指向的地址中,edi每次递增4,
#这三句实现了按四字节清空前5*1024*4字节的目的
cld;rep;stosl
# 下面4 句设置页目录中的项,我们共有4 个页表所以只需设置4 项。
# 页目录项的结构与页表中项的结构一样,4 个字节为1 项。
# "$pg0+7"表示:0x00001007,是页目录表中的第1 项。
# 则第1 个页表所在的地址 = 0x00001007 & 0xfffff000 = 0x1000;
# 第1 个页表的属性标志 = 0x00001007 & 0x00000fff = 0x07,表示该页存在、用户可读写。
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 /* --------- " " --------- */
# 下面6 行填写4 个页表中所有项的内容,共有:4(页表)*1024(项/页表)=4096 项(0 - 0xfff),
# 也即能映射物理内存 4096*4Kb = 16Mb。
# 每项的内容是:当前项所映射的物理内存地址 + 该页的标志(这里均为7)。
# 使用的方法是从最后一个页表的最后一项开始按倒退顺序填写。一个页表的最后一项在页表中的
# 位置是1023*4 = 4092。因此最后一页的最后一项的位置就是$pg3+4092。
movl $pg3+4092,%edi # edi??最后一页的最后一项。
movl $0xfff007,%eax /* 16Mb - 4096 + 7 (r/w user,p) */
# 最后1 项对应物理内存页面的地址是0xfff000,
# 加上属性标志7,即为0xfff007.
std # 方向位置位,edi 值递减(4 字节)。
1: stosl /* fill pages backwards - more efficient :-) */
subl $0x1000,%eax # 每填写好一项,物理地址值减0x1000。
jge 1b # 如果小于0 则说明全添写好了。
# 设置页目录基址寄存器cr3 的值,指向页目录表。
xorl %eax,%eax /* pg_dir is at 0x0000 */ # 页目录表在0x0000 处。
movl %eax,%cr3 /* cr3 - 保存pg_table基地址 */
# 设置启动使用分页处理(cr0 的PG 标志,位31)
movl %cr0,%eax
orl $0x80000000,%eax # 添上PG 标志。
movl %eax,%cr0 /* set paging (PG) bit */
ret /* this also flushes prefetch-queue */
# 在改变分页处理标志后要求使用转移指令刷新预取指令队列,这里用的是返回指令ret。
# 该返回指令的另一个作用是将堆栈中的main 程序的地址弹出,并开始运行/init/main.c 程序。
# 本程序到此真正结束了。
进程创建时内存的分配
在子进程建立时,通过copy_mem
函数来分配内存,建立页表:
// 设置新任务的代码和数据段基址、限长并复制页表。
// nr 为新任务号;p 是新任务数据结构的指针。
int
copy_mem (int nr, struct task_struct *p)
{
unsigned long old_data_base, new_data_base, data_limit;
unsigned long old_code_base, new_code_base, code_limit;
code_limit = get_limit (0x0f); // 取局部描述符表中代码段描述符项中段限长。
data_limit = get_limit (0x17); // 取局部描述符表中数据段描述符项中段限长。
old_code_base = get_base (current->ldt[1]); // 取原代码段基址。
old_data_base = get_base (current->ldt[2]); // 取原数据段基址。
...
new_data_base = new_code_base = nr * 0x4000000; // 新基址=任务号*64Mb(任务大小)。
p->start_code = new_code_base;
set_base (p->ldt[1], new_code_base); // 设置代码段描述符中基址域。
set_base (p->ldt[2], new_data_base); // 设置数据段描述符中基址域。
if (copy_page_tables (old_data_base, new_data_base, data_limit))
{ // 复制代码和数据段。
free_page_tables (new_data_base, data_limit); // 如果出错则释放申请的内存。
return -ENOMEM;
}
return 0;
}
在完成了子进程的ldt设置以后,就给子进程分配虚拟内存,并且为虚拟内存对应建立页目录项、页表,并且使子进程的页表指向与父进程相同的页,同时将this_page的权限设置为只读,直到之后需要write时,再改变指向的物理内存,这就是copy on write,通过copy_page_tables
来实现:
int copy_page_tables (unsigned long from, unsigned long to, long size)
{
unsigned long *from_page_table;
unsigned long *to_page_table;
unsigned long this_page;
unsigned long *from_dir, *to_dir;
unsigned long nr;
// 源地址和目的地址都需要是在4Mb 的内存边界地址上。否则出错,死机。
if ((from & 0x3fffff) || (to & 0x3fffff))
panic ("copy_page_tables called with wrong alignment");
// 取得源地址和目的地址的目录项(from_dir 和to_dir)。参见对115 句的注释。
from_dir = (unsigned long *) ((from >> 20) & 0xffc); /* _pg_dir = 0 */
to_dir = (unsigned long *) ((to >> 20) & 0xffc);
// 计算要复制的内存块占用的页表数(也即目录项数)。
size = ((unsigned) (size + 0x3fffff)) >> 22;
// 下面开始对每个占用的页表依次进行复制操作。
for (; size-- > 0; from_dir++, to_dir++)
{
// 如果目的目录项指定的页表已经存在(P=1),则出错,死机。
if (1 & *to_dir)
panic ("copy_page_tables: already exist");
// 如果此源目录项未被使用,则不用复制对应页表,跳过。
if (!(1 & *from_dir))
continue;
// 取当前源目录项中页表的地址??from_page_table。
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);
// 为目的页表取一页空闲内存,如果返回是0 则说明没有申请到空闲内存页面。返回值=-1,退出。
if (!(to_page_table = (unsigned long *) get_free_page ()))
return -1; /* Out of memory, see freeing */
// 设置目的目录项信息。7 是标志信息,表示(Usr, R/W, Present)。
*to_dir = ((unsigned long) to_page_table) | 7;
// 针对当前处理的页表,设置需复制的页面数。如果是在内核空间,则仅需复制头160 页,否则需要
// 复制1 个页表中的所有1024 页面。
nr = (from == 0) ? 0xA0 : 1024;
// 对于当前页表,开始复制指定数目nr 个内存页面。
for (; nr-- > 0; from_page_table++, to_page_table++)
{
this_page = *from_page_table; // 取源页表项内容。
if (!(1 & this_page)) // 如果当前源页面没有使用,则不用复制。
continue;
// 复位页表项中R/W 标志(置0)。(如果U/S 位是0,则R/W 就没有作用。如果U/S 是1,而R/W 是0,
// 那么运行在用户层的代码就只能读页面。如果U/S 和R/W 都置位,则就有写的权限。)
this_page &= ~2;
*to_page_table = this_page; // 将该页表项复制到目的页表中。
// 如果该页表项所指页面的地址在1M 以上,则需要设置内存页面映射数组mem_map[],于是计算
// 页面号,并以它为索引在页面映射数组相应项中增加引用次数。
if (this_page > LOW_MEM)
{
// 下面这句的含义是令源页表项所指内存页也为只读。因为现在开始有两个进程共用内存区了。
// 若其中一个内存需要进行写操作,则可以通过页异常的写保护处理,为执行写操作的进程分配
// 一页新的空闲页面,也即进行写时复制的操作。
*from_page_table = this_page; // 令源页表项也只读。
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate (); // 刷新页变换高速缓冲。
return 0;
}
from_dir, to_dir
对应页目录表中页目录项的物理地址,并且都是4字节对齐的,因为页目录表的基址为0,页目录项的索引虚拟地址的高10位,from_dir =(unsigned long*)(((from)>>22)<<2) = (unsigned long *) ((from >> 20) & 0xffc)
现在就完成了子进程的建立。