0.11 内核 在内核态上如何实现‘写时复制’ verify_area write_verify

如果大家学习了内核中进程的部分就会听说过‘写时复制’ 。
但是从来没有想过在用户态和内核态上‘写时复制’判断的前提条件都不一样。

在进程创建之初,父子进程的数据段和代码段共享并且设置为只读,直到他们之一要将代码和数据段进行修改时才会进行复制即写时复制。但是,这种判断条件只能用于用户态,因为8086cpu, 在执行特权0代码时不会理会用户空间中页面是否为有保护,用户空间中数据页面保护标志不起任何作用的。这样将违背了进程的独立性。
用户态的写时复制:
          在对页面进程修改时会受到用户空间页面标志的影响。在用户态上的写时复制是由硬件支持的,写时当你把页表项是的属性设为只读的话,如果对页表所指向的这段内存空间执行了写操作(具本说就是写数据的指令,比如说 mov ) ,CPU 就会自动发现,然后进行一个陷阱中,去执行你事先设定好的处理程序,在这个处理程序中你自己把数据拷一份给写数据的进程,给这个进程分配真正的物理空间, 然后再改页表,让内存可写,这时候重新执行这条写指令就行了~~~~~~~~这里纯粹是硬件的机制问题,只是软件利用了这种机制而已

内核态的写时复制:
        为了保证进程的独立性,在内核态时,需要执行写前检测。[(verify_area(void &*addr, int size)) ],由于在实行写前验证是通过调用write_verify() 实现的,而对于该函数是以页为单位的,所以在verify_area需要得到addr所在的页面的首地址。

  页表项:
             页框地址   |     可见 |  0 0 | D | A  | 0 0 | u/s | r/w |p
             31--------12                                 
  p : 是否在内存中
  r/w 表示可读/可写
  verify_area:
  1. verify_area(void * addr, int size)
  2. {
  3. unsigned long start = (unsigned long)addr; #对于页面来讲是以4k为单位的
  4. size+= start&0xfff;   #获取addr的后12位,即业内偏移,再加上大小.
  5. start& = 0xfffff000;#获得页面的首地址
  6. start+=get_base(current->ldt[2]);#get_base()获得描述符中的线性基址,start加上之后变成线性地址  
  7. while(size>0)  
  8. {
  9.   size-=4096;
  10.   write_verify(start);
  11.   start+=4096;
  12. }
 write_verify:
  
  1. void write_verify(unsigned long address)  
  2. {
  3.      unsigned long page;
  4.      /*判断指定地址所对应的页面目录项的页表是否存在*/
  5.       if (!( (page = *((unsigned long *) ((address>>20) & 0xffc)) )&1))
  6.              return;
  7.       page &= 0xfffff000;
  8.       page += ((address>>10) & 0xffc);/*页号,page此时表示的是页表的地址*/
  9.       if ((3 & *(unsigned long *) page) == 1) /* non-writeable, present 页面不可写*/
  10.             un_wp_page((unsigned long *) page);//写实复制
  11.      return;
  12. }
  在该函数中传递的参数地址是线性地址,而且在linux OS中使用的页机制,因此我们需要从线性地址中获得页表 的索引值。而线性地址是针对4G空间,而每个页面为4k , 则可以有2^20个页面则:(对于页机制的线性地址分为: 页号(20位) 和 页内偏移量(12)构成
                        xxx...xx.xxx     yy.....yy
                             20                  12
      因为页面有2^20个,而且每个页表项是4k,则需要 2^22k空间。如果将所有的页表项存储在一起十分麻烦,为了解决该问题,可以将表项进行分页存储-----》多级页表。在80386以上,intel都采用两级页表。
     32位被分为三部分。
               Directory(目录)   最高10位
               Table      (页表)   中间10位
               Offset      (偏移量)  最低12位
  1. //将描述符的基址存储于edx中,在地址描述符中关于基址的存储有3部分,24-31, 16-23,0-1
  2. #define _get_base(addr) ({\
  3. unsigned long __base; \
  4. __asm__("movb %3,%%dh\n\t" \//24-31
  5. "movb %2,%%dl\n\t" \
  6. "shll $16,%%edx\n\t" \ /* 基地址高16 位移到edx 中高16 位处。*/
  7. "movw %1,%%dx" \
  8. :"=d" (__base) \
  9. :"m" (*((addr)+2)), \
  10. "m" (*((addr)+4)), \
  11. "m" (*((addr)+7))); \
  12. __base;})
分析:对于段描述符来讲,其实将基地址分成了4部分。
                       7              6                 5                 4                 3                  2                  1              0
字节7                                                             段基址31-24
字节6              G             X                 0               AVX                                    段长19-16
字节5              P              DPL            S                                 段类型和保护                                      A                  字节4                                                              段基址23-16
字节3                                                              段基址15-8
字节2                                                              段基址7-0
字节1                                                              段长 15-8  
字节0                                                              段长 7-0

其中:(addr)+7 表示的意思是: addr 表示描述的首地址,(addr)+7 表示以addr为基址,向高位偏移7个字节。其他的同理。

注: dx 16位  edx 32位
set_base与之相对应,将基址赋值给对应的描述符。
  1. //将地址描述符的基址存储于edx中,对于描述符的基址分为了3部分,
  2. #define _set_base(addr,base) \
  3. __asm__("movw %%dx,%0\n\t" \
  4. "rorl $16,%%edx\n\t" \
  5. "movb %%dl,%1\n\t" \
  6. "movb %%dh,%2" \
  7. ::"m" (*((addr)+2)), \
  8. "m" (*((addr)+4)), \
  9. "m" (*((addr)+7)), \
  10. "d" (base) \
  11. :"dx")



上一篇:【Android应用开发】EasyDialog 源码解析(二)


下一篇:简述负载均衡&CDN技术