前言
在进程创建之初,父子进程的数据段和代码段共享并且设置为只读,直到他们之一要将代码和数据段进行修改时才会进行复制即写时复制。但是,这种判断条件只能用于用户态,因为8086cpu, 在执行特权0代码时不会理会用户空间中页面是否为有保护,用户空间中数据页面保护标志不起任何作用的。这样将违背了进程的独立性。
用户态的写时复制:
在对页面进程修改时会受到用户空间页面标志的影响。在用户态上的写时复制是由硬件支持的,写时当你把页表项是的属性设为只读的话,如果对页表所指向的这段内存空间执行了写操作(具本说就是写数据的指令,比如说 mov ) ,CPU 就会自动发现,然后进行一个陷阱中,去执行你事先设定好的处理程序,在这个处理程序中你自己把数据拷一份给写数据的进程,给这个进程分配真正的物理空间, 然后再改页表,让内存可写,这时候重新执行这条写指令就行了~~~~~~~~这里纯粹是硬件的机制问题,只是软件利用了这种机制而已
内核态的写时复制:
为了保证进程的独立性,在内核态时,需要执行写前检测。[(verify_area(void &*addr, int size)) ],由于在实行写前验证是通过调用write_verify() 实现的,而对于该函数是以页为单位的,所以在verify_area需要得到addr所在的页面的首地址。
verify_area
void verify_area(void * addr,int size) { unsigned long start; start = (unsigned long) addr; // 由于start调整,size的大小也会变大 size += start & 0xfff; // start调整为所在页的起始地址 start &= 0xfffff000; // 这里的size是逻辑地址,线性地址需要加上段的起始地址,ldt[2]表示数据和堆栈段 start += get_base(current->ldt[2]); // 下面循环验证页,如果不可写,则复制页面 while (size>0) { size -= 4096; write_verify(start); start += 4096; } }
verify_area验证内存的起始位置和范围的调整示意图。主要是因为内存是以页为单位操作的
write_verify
void write_verify(unsigned long address) { unsigned long page; // 判断页目录项是否存在,这里为啥移动20,而不是22?下面解释 if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1)) return; page &= 0xfffff000; // 页表地址 page += ((address>>10) & 0xffc); // 页面不可写,写时复制 if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present */ un_wp_page((unsigned long *) page); return; }
- 分页机制
page = *((unsigned long *) ((address>>20) & 0xffc 要看懂这句,需要回顾一下分页机制
看上图,正常来说右移22位正好是目录号,移动20位是什么鬼?下面要搞清楚两个方面:
- 目录号是从1开始的,不能从0开始
- 目录号和指向目录的指针之间有个联系,实际上,看下图目录表从0x0000开始,可以一个指针占4个字节,目录号*4刚好等于指针地址,也是目录号左移动2位,这个等于 (address>>20) & 0xffc
- 页表项
P--位0是存在(Present)标志,用于指明表项对地址转换是否有效。P=1表示有效;P=0表示无效。在页转换过程中,如果说涉及的页目录或页表的表项无效,则会导致一个异常。如果P=0,那么除表示表项无效外,其余位可供程序自由使用,如图4-18b所示。例如,操作系统可以使用这些位来保存已存储在磁盘上的页面的序号。
所以(3 & *(unsigned long *) page) == 1这里是判断R/W标志位是否为0,为0则表示只读,这个是父进程的数据段,需要复制
参考: