1. 概念
寫時復制技術最初產生于Unix系統,用于實現一種傻瓜式的進程創建:當發出fork(??)系統調用時,內核原樣復制父進程的整個地址空間并把復制的那一份分配給子進程。這種行為是非常耗時的,因為它需要:
- 為子進程的頁表分配頁面
- 為子進程的頁分配頁面
- 初始化子進程的頁表
- 把父進程的頁復制到子進程相應的頁中
創建一個地址空間的這種方法涉及許多內存訪問,消耗許多CPU周期,并且完全破壞了高速緩存中的內容。在大多數情況下,這樣做常常是毫無意義的,因為許多子進程通過裝入一個新的程序開始它們的執行,這樣就完全丟棄了所繼承的地址空間。
現在的Unix內核(包括Linux),采用一種更為有效的方法稱之為寫時復制(或COW)。這種思想相當簡單:父進程和子進程共享頁面而不是復制頁面。然而,只要頁面被共享,它們就不能被修改。無論父進程和子進程何時試圖寫一個共享的頁面,就產生一個錯誤,這時內核就把這個頁復制到一個新的頁面中并標記為可寫。原來的頁面仍然是寫保護的:當其它進程試圖寫入時,內核檢查寫進程是否是這個頁面的唯一屬主;如果是,它把這個頁面標記為對這個進程是可寫的。
?
2. Linux的fork()使用寫時復制
????? 傳統的fork()系統調用直接把所有的資源復制給新創建的進程。這種實現過于簡單并且效率低下,因為它拷貝的數據或許可以共享(This approach is significantly na?ve and inefficient in that it copies much data that might otherwise be shared.)。更糟糕的是,如果新進程打算立即執行一個新的映像,那么所有的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現。寫時拷貝是一種可以推遲甚至避免拷貝數據的技術。內核此時并不復制整個進程的地址空間,而是讓父子進程共享同一個地址空間。只用在需要寫入的時候才會復制地址空間,從而使各個進行擁有各自的地址空間。也就是說,資源的復制是在需要寫入的時候才會進行,在此之前,只有以只讀方式共享。這種技術使地址空間上的頁的拷貝被推遲到實際發生寫入的時候。在頁根本不會被寫入的情況下---例如,fork()后立即執行exec(),地址空間就無需被復制了。fork()的實際開銷就是復制父進程的頁表以及給子進程創建一個進程描述符。在一般情況下,進程創建后都為馬上運行一個可執行的文件,這種優化,可以避免拷貝大量根本就不會被使用的數據(地址空間里常常包含數十兆的數據)。由于Unix強調進程快速執行的能力,所以這個優化是很重要的。
COW技術初窺:
?
? ? ?在Linux程序中,fork()會產生一個和父進程完全相同的子進程,但子進程在此后多會exec系統調用,出于效率考慮,linux中引入了“寫時復制“技術,也就是只有進程空間的各段的內容要發生變化時,才會將父進程的內容復制一份給子進程。
????? 那么子進程的物理空間沒有代碼,怎么去取指令執行exec系統調用呢?
????? 在fork之后exec之前兩個進程用的是相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間,如果不是因為exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。而如果是因為exec,由于兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。??????
????? 在網上看到還有個細節問題就是,fork之后內核會通過將子進程放在隊列的前面,以讓子進程先執行,以免父進程執行導致寫時復制,而后子進程執行exec系統調用,因無意義的復制而造成效率的下降。
COW詳述:
???? 現在有一個父進程P1,這是一個主體,那么它是有靈魂也就身體的。現在在其虛擬地址空間(有相應的數據結構表示)上有:正文段,數據段,堆,棧這四個部 分,相應的,內核要為這四個部分分配各自的物理塊。即:正文段塊,數據段塊,堆塊,棧塊。至于如何分配,這是內核去做的事,在此不詳述。
1.????? 現在P1用fork()函數為進程創建一個子進程P2,
內核:
(1)復制P1的正文段,數據段,堆,棧這四個部分,注意是其內容相同。
(2)為這四個部分分配物理塊,P2的:正文段->PI的正文段的物理塊,其實就是不為P2分配正文段塊,讓P2的正文段指向P1的正文段塊,數據段->P2自己的數據段塊(為其分配對應的塊),堆->P2自己的堆塊,棧->P2自己的棧塊。如下圖所示:同左到右大的方向箭頭表示復制內容。
?
2.?????? 寫時復制技術:內核只為新生成的子進程創建虛擬空間結構,它們來復制于父進程的虛擬究竟結構,但是不為這些段分配物理內存,它們共享父進程的物理空間,當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間。
?
?
3.?????? vfork():這個做法更加火爆,內核連子進程的虛擬地址空間結構也不創建了,直接共享了父進程的虛擬空間,當然了,這種做法就順水推舟的共享了父進程的物理空間
?
通過以上的分析,相信大家對進程有個深入的認識,它是怎么一層層體現出自己來的,進程是一個主體,那么它就有靈魂與身體,系統必須為實現它創建相應的實體, 靈魂實體與物理實體。這兩者在系統中都有相應的數據結構表示,物理實體更是體現了它的物理意義。
???? 補充一點:Linux COW與exec沒有必然聯系
參考資料
1.?Linux進程管理——fork()和寫時復制