淺析深拷貝與淺拷貝以及寫時拷貝
今天分享一個高頻面試問題:深拷貝與淺拷貝以及寫時拷貝
假設(shè)B復(fù)制了A,當(dāng)修改A時,看B是否會發(fā)生變化。如果B也跟著變了,說明這是淺拷貝;如果B沒變,那就是深拷貝。
1、淺拷貝:將原對象的引用直接賦給新對象,新對象只是原對象的一個引用。
2、深拷貝:創(chuàng)建一個新的對象和數(shù)組,將原對象的各項屬性的“值”(數(shù)組的所有元素)拷貝過來,是“值”而不是“引用”。
淺拷貝只是對指針的拷貝,拷貝后兩個指針指向同一個內(nèi)存空間,深拷貝不但對指針進(jìn)行拷貝,而且對指針指向的內(nèi)容進(jìn)行拷貝,經(jīng)深拷貝后的指針是指向兩個不同地址的指針。
3、寫時復(fù)制技術(shù):最初產(chǎn)生于Unix系統(tǒng),用于實現(xiàn)一種傻瓜式的進(jìn)程創(chuàng)建:當(dāng)發(fā)出fork( )系統(tǒng)調(diào)用時,內(nèi)核原樣復(fù)制父進(jìn)程的整個地址空間并把復(fù)制的那一份分配給子進(jìn)程。這種行為是非常耗時的,因為它需要:
· 為子進(jìn)程的頁表分配頁面
· 為子進(jìn)程的頁分配頁面
· 初始化子進(jìn)程的頁表
· 把父進(jìn)程的頁復(fù)制到子進(jìn)程相應(yīng)的頁中
創(chuàng)建一個地址空間的這種方法涉及許多內(nèi)存訪問,消耗許多CPU周期,并且完全破壞了高速緩存中的內(nèi)容。在大多數(shù)情況下,這樣做常常是毫無意義的,因為許多子進(jìn)程通過裝入一個新的程序開始它們的執(zhí)行,這樣就完全丟棄了所繼承的地址空間。
現(xiàn)在的Unix內(nèi)核(包括Linux),采用一種更為有效的方法稱之為寫時復(fù)制(或COW)。這種思想相當(dāng)簡單:父進(jìn)程和子進(jìn)程共享頁面而不是復(fù)制頁面。然而,只要頁面被共享,它們就不能被修改。無論父進(jìn)程和子進(jìn)程何時試圖寫一個共享的頁面,就產(chǎn)生一個錯誤,這時內(nèi)核就把這個頁復(fù)制到一個新的頁面中并標(biāo)記為可寫。原來的頁面仍然是寫保護(hù)的:當(dāng)其它進(jìn)程試圖寫入時,內(nèi)核檢查寫進(jìn)程是否是這個頁面的唯一屬主;如果是,它把這個頁面標(biāo)記為對這個進(jìn)程是可寫的。
Linux的fork()使用寫時復(fù)制
傳統(tǒng)的fork()系統(tǒng)調(diào)用直接把所有的資源復(fù)制給新創(chuàng)建的進(jìn)程。這種實現(xiàn)過于簡單并且效率低下,因為它拷貝的數(shù)據(jù)或許可以共享。更糟糕的是,如果新進(jìn)程打算立即執(zhí)行一個新的映像,那么所有的拷貝都將前功盡棄。Linux的fork()使用寫時拷貝(copy-on-write)頁實現(xiàn)。
寫時拷貝是一種可以推遲甚至避免拷貝數(shù)據(jù)的技術(shù)。內(nèi)核此時并不復(fù)制整個進(jìn)程的地址空間,而是讓父子進(jìn)程共享同一個地址空間。只用在需要寫入的時候才會復(fù)制地址空間,從而使各個進(jìn)行擁有各自的地址空間。也就是說,資源的復(fù)制是在需要寫入的時候才會進(jìn)行,在此之前,只有以只讀方式共享。這種技術(shù)使地址空間上的頁的拷貝被推遲到實際發(fā)生寫入的時候。在頁根本不會被寫入的情況下—例如,fork()后立即執(zhí)行exec(),地址空間就無需被復(fù)制了。fork()的實際開銷就是復(fù)制父進(jìn)程的頁表以及給子進(jìn)程創(chuàng)建一個進(jìn)程描述符。在一般情況下,進(jìn)程創(chuàng)建后都為馬上運(yùn)行一個可執(zhí)行的文件,這種優(yōu)化,可以避免拷貝大量根本就不會被使用的數(shù)據(jù)(地址空間里常常包含數(shù)十兆的數(shù)據(jù))。由于Unix強(qiáng)調(diào)進(jìn)程快速執(zhí)行的能力,所以這個優(yōu)化是很重要的。
COW技術(shù)初窺:
在Linux程序中,fork()會產(chǎn)生一個和父進(jìn)程完全相同的子進(jìn)程,但子進(jìn)程在此后多會exec系統(tǒng)調(diào)用,出于效率考慮,linux中引入了“寫時復(fù)制“技術(shù),也就是只有進(jìn)程空間的各段的內(nèi)容要發(fā)生變化時,才會將父進(jìn)程的內(nèi)容復(fù)制一份給子進(jìn)程。
那么子進(jìn)程的物理空間沒有代碼,怎么去取指令執(zhí)行exec系統(tǒng)調(diào)用呢?
在fork之后exec之前兩個進(jìn)程用的是相同的物理空間(內(nèi)存區(qū)),子進(jìn)程的代碼段、數(shù)據(jù)段、堆棧都是指向父進(jìn)程的物理空間,也就是說,兩者的虛擬空間不同,但其對應(yīng)的物理空間是同一個。當(dāng)父子進(jìn)程中有更改相應(yīng)段的行為發(fā)生時,再為子進(jìn)程相應(yīng)的段分配物理空間,如果不是因為exec,內(nèi)核會給子進(jìn)程的數(shù)據(jù)段、堆棧段分配相應(yīng)的物理空間(至此兩者有各自的進(jìn)程空間,互不影響),而代碼段繼續(xù)共享父進(jìn)程的物理空間(兩者的代碼完全相同)。而如果是因為exec,由于兩者執(zhí)行的代碼不同,子進(jìn)程的代碼段也會分配單獨的物理空間。
在網(wǎng)上看到還有個細(xì)節(jié)問題就是,fork之后內(nèi)核會通過將子進(jìn)程放在隊列的前面,以讓子進(jìn)程先執(zhí)行,以免父進(jìn)程執(zhí)行導(dǎo)致寫時復(fù)制,而后子進(jìn)程執(zhí)行exec系統(tǒng)調(diào)用,因無意義的復(fù)制而造成效率的下降。