前言
在進程創建之初,父子進程的數據段和代碼段共享並且設置為只讀,直到他們之一要將代碼和數據段進行修改時才會進行復制即寫時復制。但是,這種判斷條件只能用於用戶態,因為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則表示只讀,這個是父進程的數據段,需要復制
參考: