個人主頁:🍝在肯德基吃麻辣燙
我的gitee:Linux倉庫
個人專欄:Linux專欄
分享一句喜歡的話:熱烈的火焰,冰封在最沉默的火山深處
文章目錄
- 前言
- 進程屬性
- 1.進程PID和PPID
- 2.fork函數創建子進程
- 1)為什么fork函數要給父進程返回子進程的pid,給子進程返回0?
- 2)fork函數究竟在干什么?
- 3)一個函數是如何做到返回兩次的?
- 寫時拷貝
- 總結
前言
本篇文章繼上文的進程概念后,現在對進程概念有了一定的理解。前面說過,操作系統管理進程實際上是管理描述進程的PCB對象,而PCB對象是一堆進程屬性的集合,那么進程都有哪些屬性?本篇文章會詳細寫出來。
進程屬性
我們知道,
進程 = 描述該進程的PCB結構體對象 + 對應的數據和代碼
,每一個進程都是由操作系統進行管理的,進程的PCB(process ctrl block)對象是該進程的所有屬性的集合,所以,一個進程的多種屬性,一定是放在PCB結構體里面的。
下面介紹進程的基本屬性。
1.進程PID和PPID
什么是PID?PID我們可能不知道,但是ID我們應該是知道的,ID就是身份識別碼。所以PID就是進程的身份識別碼(process ID)
在學校里面,每一個學生都有自己的學號,這個學號是獨一無二的,進程也是類似,每一個進程的PID
是獨一無二的。
我們可以通過下面的指令來查看進程的PID。
ps axj | head -1
ps axj 指令可以查看當前用戶下的所有進程,通過管道后,head指令提取管道文件的第一行并輸出到顯示器中。
結果如下:
第二個就是PID。
這里有一個注意的點,既然PID是該進程的唯一身份標識符,則該進程的PID一定是放在task_struct
結構體中的,因為PID
也是進程的屬性之一。PID的本質是一個int
類型。
這里有一個問題:我如何獲取自己進程的PID?
從上面的描述過程中可以畫出該圖,ps axj
指令能獲取用戶正在運行的所有進程,這些進程的信息本質上是ps axj
這條程序員寫的指令去調用操作系統開放的一個接口調用到的。
因為操作系統不相信任何人,它不敢也不給任何人訪問我的所有進程的PCB結構體和各種信息。
所以可以想到,要想獲取一個進程的PID,要通過一個系統調用接口來獲取,這個接口叫做getpid()
下面來通過代碼讓操作系統給我們分配一個小小的進程:
1 #include <stdio.h>2 #include <unistd.h>3 4 int main()5 {6 pid_t pid = getpid();7 8 while(1)9 {10 11 printf("I am a process,my pid is %d\n",pid);12 sleep(1);13 }14 15 return 0;16 }
運行后,再查詢該進程的pid
發現通過系統的接口函數返回的pid和我們運行程序時正在跑的進程的pid是一樣的。
getpid()
這個系統調用接口的工作原理是,我自己的進程調用getpid()
函數,獲取到我的PID后將結果返回給上層的一個變量。
不過這又有一個小細節,PPID是什么?
PPID,比PID多了一個P,這個P是parent的意思,也就是父進程的PID。
父進程就是該進程的父親進程,就相當于我這個進程是父進程分配下來的。
我們再重新執行程序會發現,我原來的進程的PID變了。這就像是我們上大學后,發現我這個學校并不如意,我決定回去復讀,第二年我比去年多考了幾十分,可天意難料,我又被去年的學校錄取了。這個過程中,去年我讀的這所學校分配給我一個學號,今年再來到這所學校,也有一個學號,這兩個學號肯定是不一樣的。
那么,我們這樣通過寫代碼的方式創建一個進程,它的父進程到底是誰?
我們查詢一下可以發現,每次執行程序,它的PID都不同,但是PPID都是一樣的,找到PID
為2215的那一行可以發現,它的COMMAND
就是對應的進程對象。
由此可知,每一個自己創建的進程的父進程都是bash
進程!
不過,bash進程的PID也是會變化的,重新啟動xhell腳本就得到不一樣的PID了。
2.fork函數創建子進程
fork函數的作用是:創建一個子進程。
這里是fork函數的基本說明,然而,重要的是fork函數的返回值:
- 如果創建成功:返回子進程的
pid
給父進程,返回0給子進程。 - 如果創建失敗,返回
-1
給父進程,子進程則什么都不返回。
下面給一段代碼演示一下fork函數。
1 #include<stdio.h> 2 #include<unistd.h>3 4 int main()5 {6 printf("begin:我是一個進程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());7 pid_t id = fork();8 9 if(id == 0)10 {11 //子進程12 while(1)13 {14 printf("我是子進程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());15 sleep(1);16 }17 }18 19 else if(id > 0)20 {21 //父進程22 while(1)23 {24 printf("我是父進程,我的pid是%d,我的ppid是%d\n",getpid(),getppid());25 sleep(1);26 }27 }28 29 return 0;30 }
運行后你會發現結果如上:
執行第一個printf
語句后,打印的pid和ppid就是當前正在運行進程的pid和ppid。
然后你會發現同時執行了if
和else if
兩個語句塊!
這到底是什么原因呢?
看運行結果可知,父進程的pid和ppid跟第一個printf打印出來的是一模一樣的!說明父進程就是當前這個程序的進程!而子進程是父進程的一個分支!
這里還有幾個問題需要解決:
1)為什么fork函數要給父進程返回子進程的pid,給子進程返回0?
舉個簡單的例子,每一個孩子一定只有一個親生父親,但是每一個父親可能會有多個孩子,這是毋庸置疑的。假如一個父親有5個孩子,父親說:孩子,你過來。然后5個孩子齊刷刷地跑過來,父親到底叫的是哪個孩子呢?
所以就必須讓父進程知道每一個子進程的標識符!
也就是要知道每一個子進程叫什么名字,返回子進程的pid
給父進程是最合理的。
而對于子進程來說,它的父進程只有一個且不花什么代價就能找到父進程pid
,所以只需要返回0給子進程作為標識即可。
2)fork函數究竟在干什么?
要知道,fork函數的功能是創建一個子進程,可是到底什么叫做創建一個子進程?
其實,創建子進程,無非就是系統中多了一個進程!
我們知道,進程 = PCB數據結構 + 自己的代碼和數據
,多一個進程就是在操作系統中多管理一個PCB數據結構和一段代碼罷了。
可是,子進程剛創建出來并沒有代碼和數據,所以子進程只能去找父進程的代碼和數據來執行。
這就是為什么,fork函數之后的所有代碼是父子進程共享的!
這就解釋了從fork函數之后下面的代碼,父進程和子進程都能跑的原因。
那為什么要創建子進程?
因為在不同的場景中,我們需要讓父子進程執行不同的代碼塊!
前面我們說過,fork函數之后父子進程共享代碼,雖然是共享,實際上就是為了讓父子進程執行不同的代碼塊,完成不同的工作從而協調起來。
3)一個函數是如何做到返回兩次的?
前面說過,fork函數之后的代碼父子共享。 但是,fork函數,也是一個函數,是在系統內部實現的,調用的時候會在fork函數內部創建進程,大致會做幾件事情:
1.創建子進程的PCB對象
2.初始化子進程的PCB
3.讓子進程指向父進程的數據和代碼
4.讓父子進程都能被CPU調度運行
前面說過,父子進程的代碼是共享的,所以在return id
這條代碼,一定是父子共享的! 因為在return id
語句執行之前,已經做好了創建子進程的工作。CPU可以單獨調用父子進程執行不同的工作。
所以return id
這條語句被執行了兩次!
可是前面說過,子進程是沒有任何數據和代碼的,子進程的代碼也是人家父進程的,何況只有一個pid變量,該怎么接收兩個返回值呢?
這里引出一個進程的性質:
任何平臺下,任何一個進程在運行時,都具有獨立性!
如何理解獨立性?
我們在windows系統下面,我現在打開網頁版csdn和xhell還有qq,突然我的qq崩潰了,但這并不影響我的網頁運行,也不影響我在聽音樂,這就是獨立性,各個進程運行互不干擾。
既然進程有獨立性,這就保證了每個進程之間不能有任何瓜葛,必須讓它們割裂!
所以,父子進程一定不能訪問同一份數據!
在這個前提下,子進程要想運行起來,必須要有自己的一份數據,所以,子進程只能想辦法把父進程的數據拷貝下來!
這樣就能夠保證父子進程既能夠保持父子進程的代碼共享,又能保證父進程的數據不能被修改。保證父子共享代碼的同時,又保證了進程的獨立性。
可是,如果我的父進程有很多很多個變量,而子進程拷貝了父進程的數據,又不會去改這些變量,甚至不訪問這些變量,就會造成在內存中有兩份冗余的數據!為了解決這個問題,程序員想出一個好辦法:寫時拷貝
寫時拷貝
寫時拷貝是指:子進程在執行了return id
這一條語句后,不會立刻去拷貝父進程的所有數據,而是先看子進程需要什么數據,再根據這些數據開辟需要的空間,這樣就能避免數據冗余的情況。
后續如果子進程還需要數據,操作系統再給子進程空間并拷貝過去即可。
通過寫時拷貝,實現父子進程的獨立性,保證父進程的數據不會被修改,又能保證父子進程的代碼共享!
總結
1.本篇文章講述了進程的最基本的屬性:進程的PID和PPID,PID是每個進程獨有的標識序號,PPID是該進程的父進程的標識序號。
2.通過fork函數創建出來子進程。什么是創建子進程,以及給了一個案例,運行后發現了令人震驚的結果,提出了fork函數之后父子進程的代碼是共享的,但是每個進程都具有獨立性,父進程的數據絕對不能讓子進程修改,從而產生寫時拷貝的做法,來保證父子進程既能夠具有獨立性,也能讓父子進程代碼共享。