一文詳解Linux中的fork機(jī)制
前言
在Linux系統(tǒng)中,進(jìn)程是操作系統(tǒng)最重要的執(zhí)行單元,而父子進(jìn)程的創(chuàng)建與管理更是系統(tǒng)資源分配和任務(wù)并行的關(guān)鍵。通過fork函數(shù),Linux能夠快速高效地復(fù)制一個(gè)進(jìn)程,使得父子進(jìn)程協(xié)同工作成為可能。理解父子進(jìn)程的運(yùn)行機(jī)制不僅有助于掌握系統(tǒng)編程的核心技能,更能為優(yōu)化資源利用與提高程序性能提供理論基礎(chǔ)。本文將帶你從基礎(chǔ)原理出發(fā),解析Linux父子進(jìn)程的運(yùn)行特性、fork的核心機(jī)制及其在實(shí)際開發(fā)中的應(yīng)用。
一、進(jìn)程PID
PID
是用來唯一標(biāo)識(shí)一個(gè)進(jìn)程的屬性,我們可以使用 ps
指令查看一個(gè)進(jìn)程的部分屬性。進(jìn)程的屬性信息是由操作系統(tǒng)來維護(hù)的,這些信息被存儲(chǔ)在一個(gè) task_struct
結(jié)構(gòu)體中,屬于操作系統(tǒng)內(nèi)核中的數(shù)據(jù)。由于操作系統(tǒng)本身是不相信用戶的,所以用戶無法直接去訪問 task_struct
對(duì)象中的成員,因此 ps
指令能夠顯示進(jìn)程的屬性信息,本質(zhì)上是通過系統(tǒng)調(diào)用接口去實(shí)現(xiàn)的。
1.1 通過系統(tǒng)調(diào)用接口查看進(jìn)程PID
獲取進(jìn)程的 PID
需要用到系統(tǒng)調(diào)用接口 getpid()
,該函數(shù)會(huì)返回調(diào)用該函數(shù)的進(jìn)程的 PID
,返回值類型為 pid_t
。如下圖我們使用 man getpid
指令去查看 getpid
的基礎(chǔ)文檔:
注意上圖中還有一個(gè) getppid
是什么呢?不難猜到,這應(yīng)該是用來獲取父進(jìn)程 PID
的系統(tǒng)調(diào)用接口,接下來我們寫段代碼來具象化 PID
吧。
注意上圖中還有一個(gè) getppid
是什么呢?不難猜到,這應(yīng)該是用來獲取父進(jìn)程 PID
的系統(tǒng)調(diào)用接口,接下來我們寫段代碼來具象化 PID
吧。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { while(1) { printf("I am a process, my id is: %d, parent id is: %d\n", getpid(), getppid()); sleep(1); } return 0; }
我們可以寫一個(gè)腳本來實(shí)時(shí)獲取上面這段代碼執(zhí)行起來后的進(jìn)程信息。
可以看到,我一個(gè)將這段代碼執(zhí)行了兩次,每一次的子進(jìn)程 PID
都在發(fā)生變化,但是父進(jìn)程的 PID
從未更改。
為了保證數(shù)據(jù)的準(zhǔn)確性,我們?cè)偈褂?ps
指令對(duì)比以下獲取到的進(jìn)程 PID
是否真的一樣。
while :; do ps axj | head -1 ; ps axj |grep process | grep -v grep ; sleep 1 ; done
結(jié)論:我們用 getpid
和 getppid
得到的父子進(jìn)程的 PID
和 ps
指令獲取到的進(jìn)程 PID
是一樣的
二、通過系統(tǒng)調(diào)用創(chuàng)建進(jìn)程-fork初識(shí)
之前我們自己創(chuàng)建進(jìn)程都是通過寫一份源代碼,然后去編譯運(yùn)行,最終得到一個(gè)進(jìn)程,今天給大家介紹另一種通過系統(tǒng)調(diào)用接口 fork
去創(chuàng)建進(jìn)程的方式。一樣的,我們使用 man fork
去查看一下 fork 的相關(guān)文檔:
大致意思就是:fork
函數(shù)會(huì)以調(diào)用該函數(shù)的進(jìn)程作為父進(jìn)程去創(chuàng)建一個(gè)子進(jìn)程.
創(chuàng)建成功時(shí),會(huì)在父進(jìn)程中返回子進(jìn)程的 PID
,在子進(jìn)程中返回 0 。否則就在父進(jìn)程中返回 -1 ,子進(jìn)程創(chuàng)建失敗。
2.1 調(diào)用fork函數(shù)后的現(xiàn)象
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { printf("before:only one line\n"); fork(); printf("after:only one line\n"); return 0; }
如上圖所示,fork 后面的代碼執(zhí)行了兩次!這是什么原因呢?我們?cè)賹懸欢未a跑跑。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> int main() { printf("begin:我是一個(gè)進(jìn)程,pid:%d, ppid:%d\n",getpid(), getppid()); pid_t id = fork(); if(id > 0) { while(1) { printf("我是父進(jìn)程,pid:%d,ppid:%d\n",getpid(),getppid()); sleep(1); } } else if(id == 0) { while(1) { printf("我是子進(jìn)程,pid:%d,ppid:%d\n",getpid(),getppid()); sleep(1); } } else { perror("子進(jìn)程創(chuàng)建失??!\n"); } return 0; }
通過結(jié)果我們可以得出,在上面的一份代碼中 id
大于0和 id
等于0同時(shí)存在, if
和 else if
同時(shí)滿足,并且有兩個(gè)死循環(huán)在同時(shí)跑。這個(gè)現(xiàn)象說明此時(shí)一定存在兩個(gè)進(jìn)程,即原來的 myprocess
進(jìn)程和在 myprocess
進(jìn)程中創(chuàng)建的子進(jìn)程,因?yàn)樵谝粋€(gè)進(jìn)程中 if
和 else if
是不可能同時(shí)滿足的。這也符合 fork
函數(shù)創(chuàng)建子進(jìn)程的目的,fork
函數(shù)創(chuàng)建子進(jìn)程后,會(huì)從原來的一個(gè)執(zhí)行流變成兩個(gè)執(zhí)行流。
2.2 為什么fork要給子進(jìn)程返回0,給父進(jìn)程返回子進(jìn)程 pid?
1. fork 返回值的設(shè)計(jì)目的
fork
是 UNIX 系統(tǒng)中用于創(chuàng)建新進(jìn)程的核心系統(tǒng)調(diào)用。調(diào)用一次 fork
,系統(tǒng)會(huì)“分 裂”出兩個(gè)進(jìn)程:父進(jìn)程和子進(jìn)程。它的返回值有以下特點(diǎn):
- 在父進(jìn)程中:
fork
返回新創(chuàng)建的子進(jìn)程的PID
,使得父進(jìn)程可以通過該PID
來管理和操作子進(jìn)程(如使用wait
或kill
等操作)。 - 在子進(jìn)程中:
fork
返回0
,標(biāo)識(shí)自己是子進(jìn)程,無需再通過PID
區(qū)分。
這種設(shè)計(jì)的核心目的正如您提到的,用于區(qū)分不同執(zhí)行流,即便父子共享同一套代碼,也可以根據(jù)返回值選擇性地執(zhí)行不同代碼。
2. 現(xiàn)實(shí)類比的深入解讀
- 父親喊“兒子”:如果不區(qū)分,所有子進(jìn)程都會(huì)響應(yīng),導(dǎo)致混亂。通過分配唯一的
PID
,每個(gè)子進(jìn)程可以被單獨(dú)識(shí)別。 - 子進(jìn)程喊“爸爸”:由于每個(gè)子進(jìn)程只能有一個(gè)父進(jìn)程,所以子進(jìn)程通過調(diào)用
getppid()
即可找到其唯一的父進(jìn)程。
3. 為什么子進(jìn)程返回值為 0
- 簡(jiǎn)單區(qū)分:子進(jìn)程無需知道自己的
PID
來執(zhí)行自己的任務(wù),而只需通過返回值0
知道自己是子進(jìn)程。效率和邏輯一致性:如果子進(jìn)程也返回自己的PID
,會(huì)引入額外的復(fù)雜性,而且父進(jìn)程需要一個(gè)單獨(dú)機(jī)制區(qū)分這些值。
2.3 一個(gè)函數(shù)是如何做到返回兩次的?如何理解?
在調(diào)用 fork
函數(shù)之前就只有一個(gè)進(jìn)程,我們先來回顧一下什么是進(jìn)程?進(jìn)程 = 內(nèi)核數(shù)據(jù)結(jié)構(gòu) + 代碼和數(shù)據(jù),其中的內(nèi)核數(shù)據(jù)結(jié)構(gòu)就是進(jìn)程對(duì)應(yīng)的 PCB
對(duì)象。
進(jìn)程的 PCB
對(duì)象會(huì)找到相應(yīng)的代碼和數(shù)據(jù),然后 CPU
就要去調(diào)度這個(gè)進(jìn)程,也就是找到該進(jìn)程的代碼和數(shù)據(jù)去執(zhí)行。調(diào)用 fork
函數(shù)創(chuàng)建子進(jìn)程,本質(zhì)上是操作系統(tǒng)多了一個(gè)進(jìn)程,因此 fork 函數(shù)創(chuàng)建出來的子進(jìn)程,它要先創(chuàng)建自己的 PCB
對(duì)象,子進(jìn)程的 PCB
對(duì)象大部分都是以父進(jìn)程的 PCB
對(duì)象為模板創(chuàng)建的,即從父進(jìn)程的 PCB
對(duì)象中拷貝過來,再對(duì)部分屬性稍作修改,子進(jìn)程的 PCB
對(duì)象就有了。但是它沒有自己的代碼和數(shù)據(jù),所以只能用父進(jìn)程的,所以 fork
函數(shù)之后,父子進(jìn)程的代碼共享,這就解釋了為什么上面 fork
函數(shù)之后的代碼輸出了兩次,其實(shí)就是父子進(jìn)程各自執(zhí)行了一次。
創(chuàng)建子進(jìn)程的目的就是為了幫助父進(jìn)程做不同的事情,但是父子進(jìn)程共享一份代碼,所以我們應(yīng)該在代碼中對(duì)它們加以區(qū)分。fork
函數(shù)就幫我們完成了這個(gè)需求,它會(huì)在父子進(jìn)程中返回不同的值,用戶只需要根據(jù)返回值的不同讓父子進(jìn)程執(zhí)行不同的代碼。fork
函數(shù)的實(shí)現(xiàn)過程:
pid_t fork():
- 創(chuàng)建子進(jìn)程
- 創(chuàng)建子進(jìn)程的PCB
- 填充PCB對(duì)應(yīng)的內(nèi)容
- 讓子進(jìn)程和父進(jìn)程指向同樣的代碼
- 此時(shí)父子進(jìn)程都有獨(dú)立的task_struct對(duì)象,可以被CPU調(diào)度運(yùn)行了
- return ret;
由于父子進(jìn)程會(huì)共享一份代碼,所以在 fork
函數(shù)執(zhí)行 return
語句之前,子進(jìn)程的 PCB
對(duì)象就已經(jīng)被創(chuàng)建出來了,CPU
已經(jīng)可以去同時(shí)調(diào)度父子進(jìn)程。由于 fork
函數(shù)中的 return
語句也是被共享的,所以 fork
函數(shù)有兩個(gè)返回值。
2.4 一個(gè)變量怎么會(huì)有不同的內(nèi)容?
1. fork 的返回值如何寫入不同的變量空間
當(dāng)調(diào)用 fork
時(shí),父進(jìn)程與子進(jìn)程會(huì)各自接收一個(gè)返回值,并且寫入同名變量 id
。但這并不意味著他們共享同一塊內(nèi)存,而是因?yàn)椋?/p>
- 獨(dú)立的進(jìn)程地址空間
每個(gè)進(jìn)程都有自己獨(dú)立的虛擬地址空間。在fork
之后,父進(jìn)程與子進(jìn)程的地址空間是彼此獨(dú)立的。盡管子進(jìn)程初始時(shí)看起來與父進(jìn)程完全相同,但實(shí)際上它們的數(shù)據(jù)是分離的。 - 寫時(shí)拷貝(COW)機(jī)制
操作系統(tǒng)為提高效率并節(jié)省資源,采用了寫時(shí)拷貝技術(shù)。在fork
之后:- 父子進(jìn)程共享同一份內(nèi)存數(shù)據(jù),直到有一方嘗試修改這些數(shù)據(jù)。
- 當(dāng)某個(gè)進(jìn)程試圖修改數(shù)據(jù)時(shí),操作系統(tǒng)會(huì)為該進(jìn)程分配新的物理內(nèi)存空間,并將被修改的數(shù)據(jù)復(fù)制到新分配的空間中。
2. fork 中變量 id 的本質(zhì)
在代碼中,變量 id
是存儲(chǔ) fork
返回值的地方。以下幾點(diǎn)解釋了為什么同名變量可以存儲(chǔ)不同的值:
- 父子獨(dú)立運(yùn)行
fork
返回后,父子進(jìn)程的執(zhí)行路徑分開。父進(jìn)程的id
變量存儲(chǔ)的是子進(jìn)程的 PID,而子進(jìn)程的id
變量存儲(chǔ)的是0
。 - 不同的內(nèi)存空間
由于父子進(jìn)程的地址空間獨(dú)立,id
實(shí)際上存在于兩塊不同的內(nèi)存區(qū)域,即父進(jìn)程的id
和子進(jìn)程的id
是完全獨(dú)立的變量。 - 賦值過程
fork
的返回值通過操作系統(tǒng)寫入到父子進(jìn)程各自的id
變量中: 父進(jìn)程在return
時(shí)向id
寫入子進(jìn)程的 PID。子進(jìn)程在return
時(shí)向id
寫入0
。
結(jié)語
Linux父子進(jìn)程的運(yùn)行機(jī)制展示了操作系統(tǒng)設(shè)計(jì)的高效性與靈活性。從fork的返回值設(shè)計(jì)到寫時(shí)拷貝(COW)的優(yōu)化方案,這一切都體現(xiàn)了Linux在性能與資源利用上的巧妙平衡。通過深入理解父子進(jìn)程的特性,不僅能夠提升系統(tǒng)編程的能力,還能為并發(fā)和并行程序設(shè)計(jì)提供堅(jiān)實(shí)的理論支持。希望本文能為你的學(xué)習(xí)和實(shí)踐帶來啟發(fā),在Linux系統(tǒng)的探索中邁向更高的層次。
以上就是一文詳解Linux中的fork機(jī)制的詳細(xì)內(nèi)容,更多關(guān)于Linux fork機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
簡(jiǎn)單實(shí)現(xiàn)linux聊天室程序
這篇文章主要介紹了簡(jiǎn)單實(shí)現(xiàn)linux聊天室程序的詳細(xì)代碼,幫助大家了解聊天室的實(shí)現(xiàn)原理,感興趣的小伙伴們可以參考一下2015-12-12CentOS7 配置Nginx支持HTTPS訪問的實(shí)現(xiàn)方案
這篇文章主要介紹了CentOS7 配置Nginx支持HTTPS訪問的實(shí)現(xiàn)方案的相關(guān)資料,這里實(shí)現(xiàn)該功能的步驟進(jìn)行了詳解,需要的朋友可以參考下2016-11-11ubuntu lamp(apache+mysql+php) 環(huán)境搭建及相關(guān)擴(kuò)展更新
ubuntu lamp(apache+mysql+php) 環(huán)境搭建及相關(guān)擴(kuò)展更新,需要的朋友可以參考下。2011-05-05CentOS下安裝python3.5+scrapy的方法步驟
本篇文章主要介紹了CentOS下安裝python3.5+scrapy的方法步驟,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2016-12-12Linux服務(wù)器如何查看每個(gè)用戶或者當(dāng)前用戶的磁盤占用量及文件同步
這篇文章主要介紹了Linux服務(wù)器如何查看每個(gè)用戶或者當(dāng)前用戶的磁盤占用量及文件同步問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-02-02