先看看分页机制里面的页目录表、页表、页之间的关系。分页机制是用于将一个线性地址转换为一个物理地址。
在I32 CPU环境里面,首先通过设置CR0寄存器,打开保护模式、开启分页机制。然后将页目录表的物理地址基址给CR3寄存器。开启分页机制后,I32将全部的物理内存空间、线性地址空间划分为一个个的页。每个页可以是4KB或者4MB。
页目录表里面存放页目录表项,每个页目录表项指向页表。其中页目录表项的高20位为对应页表的物理地址的高20位。低12位为属性位。
页表里面存放着页表项,每个页表项指向页。其中页表项的高20位为对应页的物理地址的高20位,低12为属性位。
经过上面这三个结构,CPU就有了将线性地址转换为物理地址的基础。
当CPU拿到一个线性地址后,需要将其转换为物理地址。其中一个32位的线性地址分为三部分:最高10位,中10位,低12位。
其中高10位表示这个物理地址属于页目录表的那一项管辖,通过这个页目录表项就可以得到对应指向的页表的物理基址。
中10位表示这个物理地址属于上一步得到的页表里面第几项管辖。通过这个页表项就可以得到这个物理地址所在的页的物理基址。
低12位表示这个物理地址在所属的页(上一步确定了这个页的物理基址)里面的偏移。
OK,我们来看看这个copy_page_tables被第一次调用的场景:
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);//载入一个段选择子 0000 1111里面的段限制长;此时以字节为单位
data_limit=get_limit(0x17);//
old_code_base = get_base(current->ldt[1]);
old_data_base = get_base(current->ldt[2]);
if (old_data_base != old_code_base)
panic("We don't support separate I&D");
if (data_limit < code_limit)
panic("Bad data_limit");
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)) {
printk("free_page_tables: from copy_mem\n");
free_page_tables(new_data_base,data_limit);
return -ENOMEM;
}
return 0;
}
是在内存拷贝时的最后调用的。 我们注意看在copy_mem里面调用copy_page_tables时传入的参数:源:old_data_base,目的:new_data_base.大小:data_limit
其中old_data_base和new_data_base分别为0和0x4000000。
它把这个子进程的p->ldt[1]和p->ldt[2]的基地址都改成为0x4000000。这意味这什么?这意味着,之后的子进程的代码段、数据段里面的基地址为0x4000000。那么给定一个逻辑地址,它在转为线性地址时会将这个基地址跟偏移量相加。注意这里的0x4000000是线性地址。
那么问题来了,通过之前的内容我们了解到。子进程的eip为fork下一条的指令的偏移量,子进程的cs为0x0f。那么当切换到子进程的时候,发现刚进入子进程的指令的线性地址为0x4000000+eip。而这个0x4000000+eip的高10位是16,中10位为0,低12位由eip影响。这表示这个线性地址对应的物理地址属于第16个页目录表项对应的页表的第0个页表项所对应的页。然后页目录表的第16项还没有指定究竟挂着的那个页表,直接这样访问时要出问题的。所以在切换到子进程之前,需要将父进程的那一套页表到页的映射关系都先挪到子进程对应的页表。并让页目录表项指向页表。
所以这个函数其实就算想将父进程里面页表到页的映射关系拷贝到子进程去。
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;
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
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++) {
if (1 & *to_dir)
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);//*from_dir放着的是物理地址
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;
nr = (from==0)?0xA0:1024;
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;//只读不可写
*to_page_table = this_page;
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
invalidate();
return 0;
}
copy_page_tables 传入的是from=old_data_base也就是0,而to=new_data_base也就是0x4000000,size是父进程也就是进程0 的代码段大小 好像是640KB左右。
copy_page_tables是想把from所标识的页目录项指向的页表里面的各个页表项拷贝到to标识的页目录项指向的页表里面去。只要给定了页目录表项的地址,那么它对应的页表也就可以知道了。每个页目录表项的地址只需32位地址的高10位即可,因此其他位不可指定,都必须为0,或者说为了寻址到页目录表项对应的那个页表的基地址,所给的32位地址的中10位必须为0(这样做的话,就是对应页目录表项所指向页表的第0个页表项的基址,于是就可以把一个完整的页表的各个页表项从第0项一直往后遍历;如果中10位不为0,假设中10为取个0x10的值,那么拷贝页表项的时候,前0x10个页表项就莫名其妙的给漏了,这会导致子进程在做线性地址到物理地址映射的时候出大问题)。
这就是为什么一上来先看看from和to的低22位是否全部为0,0x3fffff转换为32位的二进制就是前10为0,后22为1.
if ((from&0x3fffff) || (to&0x3fffff))
panic("copy_page_tables called with wrong alignment");
如果有一个的低22位不全为0,那么这个线性地址就不是映射到某个页目录表项指向的页表的第0项,这就错误的。
panic是干啥的呢?
volatile void panic(const char * s)
{
printk("Kernel panic: %s\n\r",s);
if (current == task[0])
printk("In swapper task - not syncing\n\r");
else
sys_sync();
for(;;);
}
先打印错误信息,然后看当前进程是否为进程0,不是就刷新Buffer,然后进入死循环,一直耗空进程的时间片。这样这个进程之后就啥事情都干不了了,一直在转空圈圈。
接下来:
from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ //这里面放着的是源的页目录表项的地址(线性地址)
to_dir = (unsigned long *) ((to>>20) & 0xffc);//64Mb>>20 =64;64先是逻辑地址。当前代码段 数据段的基址为0.所以线性地址为64,二进制为0000 0000 0000 0000 0000 0000 0100 0000.表示第0个页目录项指向的页表的第0个页.
size = ((unsigned) (size+0x3fffff)) >> 22;
(from>>20) & 0xffc 是在干啥子哟?我们要时刻记住,现在我们想提取from和to的页目录表项!!!
from>>20 将from的高12位提取出来,然后再将这个结果与上一个0xffc。0xffc也是12位的,高10位为1,低2位为0。from>>20的高12位提取出来以后,高10位表示了是第几个页目录表项。但是我们要记住,每个页目录表项是4字节的!!!!所以(from>>20) & 0xffc 就是相当于:(from>>22) *4。把页目录表项的项数从高10位取出,然后再把项数乘以4,就是这个页目录表项在页目录表所在的页的偏移地址。
OK,我们再深入一点@@
根据这个道理,from_dir就是 ( 00000000000000000000000000000000 ) 2 (0000 0000 0000 0000 0000 0000 0000 0000)_{2} (00000000000000000000000000000000)2,而to_dir就是 ( 00000000000000000000000001000000 ) 2 (0000 0000 0000 0000 0000 0000 0100 0000)_{2} (00000000000000000000000001000000)2。对于这两个线性地址,我们这么看。from_dir的高10为0,中10位为0,低12位也为0,说明是0目录表项指向的页表的第0项的第0个字节。根据之前在head.S里面的分页内容,0目标表项指向第0个页表(物理基址为0x1000),第0个页表的第0项指向的页的物理基址为0(真的就是物理基址的开始,上面放着就是那个页目录表) ,而这个页目录表所在的页的偏移为0的位置就放着第0个页表的物理基址。
而to_dir的高10为0,中10位0,低12位为 0001000000 ) 2 00 0100 0000)_{2} 0001000000)2,这个二进制数就是十进制的64,这表示第0个页偏移64位。第0个页就是页目录表所在的页,这个页偏移64位其实就是第16个页表项的地址。
这说明,操作系统想把from指向的那个页表的各个页表项丢到第16项页目标表项指向的页表上面去。
size = ((unsigned) (size+0x3fffff)) >> 22;
就是把不足一个4MB(一个页表所能控制的内存长度)的size取整为一个4MB。size就标识着,应该复制多少个页表,注意是页表而不是页表项!!!!!,其实就是需要遍历多少个页目录表项,因为一个页目录表项对应着一个一个页表。
接下来就是一波的循环
for( ; size-->0 ; from_dir++,to_dir++) {
if (1 & *to_dir)
panic("copy_page_tables: already exist");
if (!(1 & *from_dir))
continue;
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);//*from_dir放着的是物理地址
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;
nr = (from==0)?0xA0:1024;
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;//只读不可写
*to_page_table = this_page;
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
}
首先,先看看to_dir这个地方有没有挂上一个页表。to_dir是目标页表项的线性地址。*to_dir就是取这个线性地址对应的内存上的值。这里面的值高20位理应存放着一个页表的物理基址的高20位,低12位是一些属性位。最后一个属性位是P位,即Present存在位。
if (1 & *to_dir)
panic("copy_page_tables: already exist");
如果这个 *to_dir指向的页表居然是存在的!!!这是很奇怪的,因为内存之前只初始化了页表项的第0,1,2,3 共四项,后面是还没有初始化的,然而这个居然说第16项居然被初始化了,这就是很大的错误了。
if (!(1 & *from_dir))
continue;
*from_dir指向的页表不存在就continue,这个是合理的。
from_page_table = (unsigned long *) (0xfffff000 & *from_dir);//*from_dir放着的是物理地址
现在就是要把*from_dir的高20位取出来了,其实就是第0个页目录表项指向的页表的高20位。
if (!(to_page_table = (unsigned long *) get_free_page()))
return -1; /* Out of memory, see freeing */
*to_dir = ((unsigned long) to_page_table) | 7;
to_page_table是通过get_free_page新申请的一个页的基址,虽然返回的线性地址,然而这个线性地址刚好也就会等于这个页的物理地址。
*to_dir = ((unsigned long) to_page_table) | 7; 就先把to_page_table或上7,就是或111,然后给 *to_dir .这个过程就是让页目录表项的第16项指向这个新申请的页,把这个新申请的页当做页表来使用,同时在这个页目录表里面置这个页表为存在,可读写。
for ( ; nr-- > 0 ; from_page_table++,to_page_table++) {
this_page = *from_page_table;
if (!(1 & this_page))
continue;
this_page &= ~2;//只读不可写
*to_page_table = this_page;
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
}
这个循环就是在把from_page_table指向的页表里面的各个页表项提取出来,修改一下特权级然后丢给to_page_table.
from_page_table一开始指向的是源页表所在页的物理地址的高20位,这个页的物理地址的低12位的偏移是先全为0的。之后from_page_table然后4个字节4个字节的增加,始终表示页表项在这个页表中的偏移量。这是为什么呢?注意到unsigned long * from_page_table;
这个声明,from_page_table是一个指针,每个指针是4个字节的。于是from_page_table++其实就是每次加4。
this_page = *from_page_table;
就是把每个页表项里面的内容取出来,给this_page。这个值是某个页的物理地址的高20位+低12位的属性位。
if (!(1 & this_page))
continue;
同样,如果这个页不存在就处理。
this_page &= ~2;//只读不可写
*to_page_table = this_page;
否则就修改这个this_page的倒数第2位为0,把读写位置为0。再把这个值给* to_page_table 目标页表项。这个时候目标页表项就跟源目标页表项指向同一个页了,但是目标页表项里面把这个页的属性解释为存在不可写。
if (this_page > LOW_MEM) {
*from_page_table = this_page;
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
}
为啥this_page 大于LOW_MEM的时候,父进程也不能写这个页了呢????
为啥this_page 大于LOW_MEM的时候,父进程也不能写这个页了呢????
为啥this_page 大于LOW_MEM的时候,父进程也不能写这个页了呢????
暂时没想到为什么。先占个坑。
这是让父进程在写这些页的时候也进行写时复制操作,不然当父子进程同时共享某个页时,父进程对页写了一波数据但是没法通知子进程,这会给子进程带来脏读的问题。所以除了进程0外,其他进程在创建子进程时都会在copy_processs中把父子进程的页表的各个项的属性设置为只读。
this_page -= LOW_MEM;
this_page >>= 12;
mem_map[this_page]++;
在mem_map里面把这个页的值加一,就是表示这个页又被新的一个进程使用着。
这样子,子进程就有了自己的页目标表项,也有了自己的页表,再有0x4000000+eip 这样的线性地址过来,它就可以正确的解析到物理地址上去。
这里看到,子进程建立自己的一套页表。页表里面的每一个项是从父进程拷贝过来然后修改属性位得到的。
可能会有疑问,为什么要这么做呢?为什么不让直接*to_dir=*from_dir,这样第16个页目录表项就指向一个有效的页表去。这样的确是使得线性地址解析没有问题了,但是父子进程会有不同的页表的管理,而且子进程也有申请新的页,创建新的页表项的需求,所以子进程是需要自己来维护这么一个页表的,虽然一开始这个页表里面各个页表项的指向的页是与父进程一致,但是子进程运行后期就不一定还是这样子,子进程完全有理由创建属于自己的新的代码段的诉求,而这个代码段肯定是要存在页里面的。