.net 門戶網(wǎng)站全網(wǎng)推廣代理
fork()
參考鏈接:鏈接
進(jìn)程控制原語(yǔ)包括:進(jìn)程的建立、進(jìn)程的撤銷、進(jìn)程的等待和進(jìn)程的喚醒。
fork,在英語(yǔ)用譯為叉子,形狀像Y,反過來(lái)就如下圖:
就是本來(lái)只有一個(gè)進(jìn)行app,然后它調(diào)用了fork()函數(shù),然后就產(chǎn)生了子進(jìn)程,原來(lái)的進(jìn)程叫父進(jìn)程。這個(gè)子進(jìn)程也是進(jìn)程,但凡是進(jìn)程,都有自己的虛擬地址空間。虛擬地址空間是從0到4G的大小,其中3-4G是屬于內(nèi)核的。創(chuàng)建完子進(jìn)程后,父進(jìn)程繼續(xù)運(yùn)行app(即原來(lái)的進(jìn)程)的代碼,剛創(chuàng)建出來(lái)的子進(jìn)程擁有和父進(jìn)程完全一樣的代碼段,數(shù)據(jù)段,也就是說(shuō)完完全全拷貝了一份父進(jìn)程,和父進(jìn)程完全一樣。即clone父進(jìn)程0-3G的內(nèi)容,而3-4G的kernel只需要重新映射一下到物理地址的kernel即可。但是操作系統(tǒng)要如何區(qū)分這兩個(gè)進(jìn)程呢?答案就是進(jìn)程ID,即pid。pid是存儲(chǔ)在PCB當(dāng)中的類似身份證的東西。子進(jìn)程會(huì)clone父進(jìn)程的PCB到子進(jìn)程,但是PCB里的pid會(huì)從操作系統(tǒng)中獲取,得到新的pid。PCB存儲(chǔ)在3-4G的內(nèi)核中。 如下圖:
fork()完以后,父進(jìn)程和子進(jìn)程由于有著同樣的數(shù)據(jù)段和代碼段,棧,PCB也大部分相同,所以兩個(gè)進(jìn)程就會(huì)干著同樣的事情,這樣對(duì)我們沒有意義,所以需要識(shí)別哪個(gè)是父進(jìn)程,哪個(gè)是子進(jìn)程,然后讓父進(jìn)程接著干原來(lái)的事,子進(jìn)程去干新的事情。
然后我們通過代碼來(lái)觀察:
這段代碼在linux中運(yùn)行,fork函數(shù)有兩次返回,即調(diào)用一次,返回兩次。在父進(jìn)程返回子進(jìn)程的pid,在子進(jìn)程返回0,如果返回負(fù)數(shù)則表明fork失敗。所以,我們根據(jù)返回值來(lái)判斷當(dāng)前進(jìn)程是父進(jìn)程還是子進(jìn)程。
結(jié)果就是:
然后ps aux顯示目前正在運(yùn)行的進(jìn)程:
pid為4296的是父進(jìn)程,4297的是子進(jìn)程。
解釋:這段程序本身就是一個(gè)進(jìn)程,然后它創(chuàng)建了一個(gè)子進(jìn)程,它本身變?yōu)橐粋€(gè)父進(jìn)程。但是兩個(gè)進(jìn)程運(yùn)行的都是同一段程序代碼。當(dāng)父進(jìn)程運(yùn)行時(shí),fork返回大于0的數(shù),那么我們就輸出相應(yīng)字符。而子進(jìn)程運(yùn)行時(shí),fork返回0,那么我們?cè)佥敵隽硗獾淖址K跃陀辛松厦娴默F(xiàn)象。
然后修改一下程序:
也就是讓父進(jìn)程休眠1s,一直打印,子進(jìn)程休眠3s,一直打印。
然后輸出如下:
明顯是父進(jìn)程打印得比較快。很符合所學(xué)知識(shí),
然后kill 掉子進(jìn)程,即 kill pid,然后就可以發(fā)現(xiàn)子進(jìn)程沒有輸出了,也可以確認(rèn)pid大1的是子進(jìn)程。
也就是說(shuō),我們只能通過fork的返回值來(lái)判斷當(dāng)前進(jìn)程是父進(jìn)程還是子進(jìn)程。
其實(shí),fork底層是調(diào)用了內(nèi)核的函數(shù)來(lái)實(shí)現(xiàn)fork的功能的,即先create()先創(chuàng)建進(jìn)程,此時(shí)進(jìn)程內(nèi)容為空,然后clone()復(fù)制父進(jìn)程的內(nèi)容到子進(jìn)程中,此時(shí)子進(jìn)程就誕生了,接著父進(jìn)程就return返回了。而子進(jìn)程誕生后,是直接運(yùn)行return返回的,然后接著執(zhí)行后面的程序,這里注意:子進(jìn)程是不會(huì)執(zhí)行前面父進(jìn)程已經(jīng)執(zhí)行過的程序了得,因?yàn)镻CB中記錄了當(dāng)前進(jìn)程運(yùn)行到哪里,而子進(jìn)程又是完全拷貝過來(lái)的,所以PCB的程序計(jì)數(shù)器也是和父進(jìn)程相同的,所以是從fork()后面的程序繼續(xù)執(zhí)行。此時(shí)就按照前面的規(guī)則進(jìn)行判斷返回。如下圖所示:
然后接下來(lái)介紹兩個(gè)進(jìn)程相關(guān)的函數(shù),getpid()和getppid()
getpid()返回的是當(dāng)前進(jìn)程的pid,getppid()返回的是當(dāng)前進(jìn)程的父進(jìn)程的pid。那前面說(shuō)的父進(jìn)程的父進(jìn)程是啥呢?是shell。因?yàn)槲覀兪亲影hell中 ./ 運(yùn)行程序才創(chuàng)建起剛才的父進(jìn)程的,所以shell是該父進(jìn)程的父進(jìn)程。
然后大家注意,如果此時(shí)在fork()前有變量n,那么創(chuàng)建子進(jìn)程后,父進(jìn)程和子進(jìn)程的n不是同一個(gè)n,但是虛擬地址是一樣的,因?yàn)橐彩峭耆截惛高M(jìn)程的,而進(jìn)程間的虛擬地址都是獨(dú)立的,對(duì)應(yīng)的實(shí)際物理地址肯定是不同的,當(dāng)你在兩個(gè)進(jìn)程中改變這個(gè)變量時(shí),也可以發(fā)現(xiàn)這兩個(gè)是不一樣的,對(duì)進(jìn)程線程有一定了解的都應(yīng)該很好了解。
fork()的時(shí)候,父進(jìn)程的虛擬地址映射著物理內(nèi)存的實(shí)際的物理地址,clone()的時(shí)候,并不是在物理地址中直接再?gòu)?fù)制一份和父進(jìn)程一樣的物理內(nèi)存塊,而是子進(jìn)程的虛擬地址也直接映射到同一物理內(nèi)存塊中,這就是讀時(shí)共享。那這樣的話不是就共享變量了嗎?不就和前面說(shuō)的矛盾了嗎? 關(guān)鍵:當(dāng)你操作這個(gè)物理內(nèi)存塊時(shí)(比如修改變量的值),再?gòu)?fù)制該部分的實(shí)際物理內(nèi)存到子進(jìn)程中,并不是全部復(fù)制。這就是寫時(shí)復(fù)制。所以,當(dāng)你在后面的程序中操作遍歷n時(shí),就會(huì)另辟內(nèi)存塊給子進(jìn)程,表示這兩者的獨(dú)立。這就是讀時(shí)共享,寫時(shí)復(fù)制。
優(yōu)點(diǎn):可以減少實(shí)際物理內(nèi)存的開銷,也減少了完全復(fù)制一份內(nèi)存塊時(shí)cpu等資源的開銷。同時(shí)減少使用的時(shí)間。所以linux引入了copy on write的機(jī)制。
程序功能就是:父進(jìn)程不斷地創(chuàng)建子進(jìn)程,子進(jìn)程經(jīng)過30s后就結(jié)束進(jìn)程。然后就看這引起的后果:
進(jìn)程不斷創(chuàng)建,ps aux和輸出都非常巨大,這樣由于pcb和變量的不斷產(chǎn)生,內(nèi)存消耗會(huì)很大,然后關(guān)鍵是cpu還要分配時(shí)間片給每個(gè)進(jìn)程中的線程(此進(jìn)程為一進(jìn)程對(duì)應(yīng)1線程),然后系統(tǒng)就會(huì)變得很卡,以至于其它不相關(guān)的進(jìn)程操作起來(lái)也非??D,因?yàn)閏pu要在海量的進(jìn)程中切換到你比較費(fèi)時(shí)。