先看看分页机制里面的页目录表、页表、页之间的关系。分页机制是用于将一个线性地址转换为一个物理地址。
在I32 CPU环境里面,首先通过设置CR0寄存器,打开保护模式、开启分页机制。然后将页目录表的物理地址基址给CR3寄存器。开启分页机制后,I32将全部的物理内存空间、线性地址空间划分为一个个的页。每个页可以是4KB或者4MB。
页目录表里面存放页目录表项,每个页目录表项指向页表。其中页目录表项的高20位为对应页表的物理地址的高20位。低12位为属性位。
页表里面存放着页表项,每个页表项指向页。其中页表项的高20位为对应页的物理地址的高20位,低12为属性位。
经过上面这三个结构,CPU就有了将线性地址转换为物理地址的基础。
当CPU拿到一个线性地址后,需要将其转换为物理地址。其中一个32位的线性地址分为三部分:最高10位,中10位,低12位。
其中高10位表示这个物理地址属于页目录表的那一项管辖,通过这个页目录表项就可以得到对应指向的页表的物理基址。
中10位表示这个物理地址属于上一步得到的页表里面第几项管辖。通过这个页表项就可以得到这个物理地址所在的页的物理基址。
低12位表示这个物理地址在所属的页(上一步确定了这个页的物理基址)里面的偏移。
页目录项和页表项
上图就是页目录项和页表项的格式。可以看出,由于页表或者页的物理地址都是4KB对齐的(低12位全是零),所以上图中只保留了物理基地址的高20位(bit[31:12])。低12位可以安排其他用途。
【P】:存在位。为1表示页表或者页位于内存中。否则,表示不在内存中,必须先予以创建或者从磁盘调入内存后方可使用。
【R/W】:读写标志。为1表示页面可以被读写,为0表示只读。当处理器运行在0、1、2特权级时,此位不起作用。页目录中的这个位对其所映射的所有页面起作用。
【U/S】:用户/超级用户标志。为1时,允许所有特权级别的程序访问;为0时,仅允许特权级为0、1、2的程序访问。页目录中的这个位对其所映射的所有页面起作用。
【PWT】:Page级的Write-Through标志位。为1时使用Write-Through的Cache类型;为0时使用Write-Back的Cache类型。当CR0.CD=1时(Cache被Disable掉),此标志被忽略。对于我们的实验,此位清零。
【PCD】:Page级的Cache Disable标志位。为1时,物理页面是不能被Cache的;为0时允许Cache。当CR0.CD=1时,此标志被忽略。对于我们的实验,此位清零。
【A】:访问位。该位由处理器固件设置,用来指示此表项所指向的页是否已被访问(读或写),一旦置位,处理器从不清这个标志位。这个位可以被操作系统用来监视页的使用频率。
【D】:脏位。该位由处理器固件设置,用来指示此表项所指向的页是否写过数据。
【PS】:Page Size位。为0时,页的大小是4KB;为1时,页的大小是4MB(for normal 32-bit addressing )或者2MB(if extended physical addressing is enabled).
【G】:全局位。如果页是全局的,那么它将在高速缓存中一直保存。当CR4.PGE=1时,可以设置此位为1,指示Page是全局Page,在CR3被更新时,TLB内的全局Page不会被刷新。
【AVL】:被处理器忽略,软件可以使用。
// 复制内存页表 // 新进程仅设置自己的页目录项和页表项,没有分配物理内存页面 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;
//选择器 //15 3 2 1 0 // 索引 TI RPL // 0x0f : 1111 11:用户级 1:局部描述符表 1:索引1 代码段选择符 // 0x0f : 10111 11:用户级 1:局部描述符表 10:索引2 数据段选择符 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]); // linux0.11内核不支持代码和数据段分立的情况 if (old_data_base != old_code_base) panic("We don‘t support separate I&D"); if (data_limit < code_limit) panic("Bad data_limit"); // 新进程的线性地址 = 64M * 任务号 new_data_base = new_code_base = nr * 0x4000000; 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_page_tables完成。
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; // 查看form和to对应的页表项是不是为第一项 if ((from&0x3fffff) || (to&0x3fffff)) panic("copy_page_tables called with wrong alignment"); // 获取from 和to页目录项地址 from_dir = (unsigned long *) ((from>>20) & 0xffc); /* _pg_dir = 0 */ to_dir = (unsigned long *) ((to>>20) & 0xffc); // size表示有多少个页表,不满4M的补足 size = ((unsigned) (size+0x3fffff)) >> 22; for( ; size-->0 ; from_dir++,to_dir++) { // P位:1表示页表存在 0表示不存在 if (1 & *to_dir) panic("copy_page_tables: already exist"); if (!(1 & *from_dir)) continue; // 页目录项高20位存放着页表项的起始地址,这里得到页表的物理地址 from_page_table = (unsigned long *) (0xfffff000 & *from_dir); // 申请新的页表,返回页表的物理地址 if (!(to_page_table = (unsigned long *) get_free_page())) return -1; /* Out of memory, see freeing */ // 填充to的页目录项内容 *to_dir = ((unsigned long) to_page_table) | 7; nr = (from==0)?0xA0:1024; // 取出from_page_table的内容,修改一下特权给to_page_table for ( ; nr-- > 0 ; from_page_table++,to_page_table++) { this_page = *from_page_table; // P位:1表示页存在 0表示不存在 if (!(1 & this_page)) continue; // 修改读写权限 只读不可写 this_page &= ~2; *to_page_table = this_page; // 当大于LOW_MEM,修改夫进程权限,防止夫进程修改,子进程读写脏数据 if (this_page > LOW_MEM) { *from_page_table = this_page; this_page -= LOW_MEM; this_page >>= 12; mem_map[this_page]++; } } } invalidate(); return 0; }
参考:
-
Linux 内核完全注释