面試官,您確定要讓我展開講進(jìn)程嗎?
本文轉(zhuǎn)載自微信公眾號「CS指南」,作者大白。轉(zhuǎn)載本文請聯(lián)系CS指南公眾號。
大家好呀,我是大白!準(zhǔn)備了好久,我的 Linux 系列文章終于和大家見面了。最近為了籌備 Linux 的文章,以程序員的視角看城市系列都沒時間寫了,最近不光讀者催我,一些喜歡我城市系列文章的號主也在催我,哈哈。現(xiàn)在我的第一篇寫 Linux 的文章終于寫出來了,第二篇、第三篇會越來越快,質(zhì)量也會越來越高,希望大家多多支持我呀!
面試中經(jīng)常會被問到進(jìn)程和線程的區(qū)別,你給面試官回答一個進(jìn)程里包含許多線程他又不太高興,嫌你答的太少了,要掛你。這個系列我們就以 Linux 中的進(jìn)程和線程為例,好好的拆開講一講里面的細(xì)節(jié)。今天這篇文章先展開講一講 Linux 的進(jìn)程是如何創(chuàng)建的。這個系列還會不斷更新,歡迎持續(xù)關(guān)注呀!
Linux的進(jìn)程是怎樣創(chuàng)建的
Linux系統(tǒng)創(chuàng)建進(jìn)程都是由已存在的進(jìn)程創(chuàng)建的(除了0號進(jìn)程),被創(chuàng)建的進(jìn)程叫做子進(jìn)程,創(chuàng)建子進(jìn)程的進(jìn)程就做父進(jìn)程。這句話是不是有點熟悉,沒錯,Linux進(jìn)程串起來也是一顆樹的結(jié)構(gòu)。就像下面這樣:
在Linux中,為了創(chuàng)建一個子進(jìn)程,父進(jìn)程用系統(tǒng)調(diào)用fork來創(chuàng)建子進(jìn)程。fork()其實就是把父進(jìn)程復(fù)制了一份(子進(jìn)程有自己的特性,比如標(biāo)識、狀態(tài)、數(shù)據(jù)空間等;子進(jìn)程和父進(jìn)程共同使用程序代碼、共用時間片等)。
可以看下面這段代碼:
- #include<stdio.h>
- #include<unistd.h>
- int main()
- {
- int p_num = 0;
- int c_num = 0;
- int pid = fork();
- if(pid == 0) //返回的pid為0為子進(jìn)程
- {
- c_num++;
- }
- else
- {
- p_num++; //返回的pid大于0為父進(jìn)程
- }
- printf("p_num=%d, c_num=%d\n",p_num,c_num);
- printf("pid=%d\n",pid);
- return 0;
- }
- //運行結(jié)果如下所示
- p_num=1, c_num=0
- pid=36101
- p_num=0, c_num=1
- pid=0
大家看,代碼中調(diào)用了fork以后,之后的程序被執(zhí)行了兩遍。子進(jìn)程和父進(jìn)程各自的變量互相沒有受到干擾。不過子進(jìn)程和父進(jìn)程執(zhí)行的是相同的代碼,子進(jìn)程和父進(jìn)程資源占用情況如下圖所示:
大家可以看出,通過fork后,子進(jìn)程并沒有和父進(jìn)程獨立開,用的是相同的代碼。另外還有一個問題時,這個時候子進(jìn)程的時間片是和父進(jìn)程一分為二來共享的。這樣我創(chuàng)建子進(jìn)程還有什么意義?為了徹底將父進(jìn)程和子進(jìn)程分離開來,就要用到一個系統(tǒng)調(diào)用 execv()。
看下面這段代碼:
- //process.c
- #include<stdio.h>
- #include<unistd.h>
- int main()
- {
- int pid = fork();
- if(pid == 0)
- {
- execv("./test.o",NULL); //test.o是一個經(jīng)過編譯的c語言文件,這里記得要放test.o的絕對路徑
- }
- printf("This is parent process\n");
- return 0;
- }
- //test.c
- #include<stdio.h>
- int main()
- {
- printf("This is child process");
- return 0;
- }
- //運行結(jié)果如下所示
- This is parent process
- This is child process
通過上面的代碼可以看出,從系統(tǒng)調(diào)用 execv() 后,子進(jìn)程直接走自己的代碼了,沒有像前一段代碼一樣把后面的代碼執(zhí)行了兩次。通過調(diào)用 execv(),子進(jìn)程和父進(jìn)程就基本分離開了。
結(jié)合系統(tǒng)繼續(xù)看Linux的進(jìn)程樹是什么樣的
好了,通過上面的介紹,大家應(yīng)該對進(jìn)程是怎么創(chuàng)建的有一定的了解。想繼續(xù)學(xué)習(xí)的我們來接著上強度。
我們在 Linux 系統(tǒng)上通過 ps - ef 命令查看系統(tǒng)目前的進(jìn)程:
- /[root@localhost lucas]# ps -ef
- UID PID PPID C STIME TTY TIME CMD
- root 1 0 3 21:41 ? 00:02:38 /usr/lib/systemd/systemd --s
- root 2 0 0 21:41 ? 00:00:07 [kthreadd]
- root 3 2 0 21:41 ? 00:00:00 [rcu_gp]
- root 4 2 0 21:41 ? 00:00:00 [rcu_par_gp]
- ...
- rtkit 1151 1 0 21:41 ? 00:00:14 /usr/libexec/rtkit-daemon
- root 1152 1 0 21:41 ? 00:00:00 /usr/sbin/ModemManager
- avahi 1155 1 0 21:41 ? 00:00:06 avahi-daemon: running [linux
- root 1159 1 0 21:41 ? 00:00:02 /usr/lib/systemd/systemd-mac
我來解釋上表是什么意思。
首先,每一個進(jìn)程都要所屬一個用戶,UID 就是用戶的標(biāo)識符(通過 root 用戶創(chuàng)建的進(jìn)程 UID 就是 root,如果我自己創(chuàng)建的話就應(yīng)該是我的用戶名,比如我的名字 "dabai")。
其次每一個進(jìn)程都要有一個 ID 來表示這個進(jìn)程,PID 就表示的是當(dāng)前進(jìn)程的 id。
最后,上文提到除了 0 號進(jìn)程,每一個進(jìn)程都是由他的父進(jìn)程創(chuàng)建的,PPID 就表示當(dāng)前進(jìn)程的父進(jìn)程 id。
通過 0 號進(jìn)程創(chuàng)建 1 號進(jìn)程和 2 號進(jìn)程,然后通過 1 號進(jìn)程去創(chuàng)建用戶態(tài)進(jìn)程,再通過 2 號進(jìn)程創(chuàng)建內(nèi)核態(tài)進(jìn)程,就生成了 Linux 進(jìn)程樹。
「什么是0號進(jìn)程、1號進(jìn)程以及2號進(jìn)程?」。
0號進(jìn)程:在內(nèi)核初始化的過程中,會先通過指令 struct task_struct init_task = INIT_TASK(init_task) 創(chuàng)建 0 號進(jìn)程。這是唯一一個沒有通過 fork 或者 kernel_thread 產(chǎn)生的進(jìn)程。是進(jìn)程列表的第一個。但是這個進(jìn)程不是實際意義上的進(jìn)程,類似與鏈表頭。所以雖然 0 號進(jìn)程是在內(nèi)核態(tài)創(chuàng)建的,但不能說 0 號進(jìn)程是內(nèi)核態(tài)的第一個進(jìn)程,反而要說 2 號進(jìn)程是內(nèi)核態(tài)的第一個進(jìn)程。
1號進(jìn)程:通過調(diào)用指令 kernel_thread(kernel_init, NULL, CLONE_FS) 從內(nèi)核態(tài)切換到用戶態(tài)來創(chuàng)建的,1號進(jìn)程是所有用戶態(tài)的祖先。
2號進(jìn)程:通過調(diào)用指令 kernel_thread(kthreadd, NULL, ClONE_FS | CLONE_FILES) 來創(chuàng)建,2號進(jìn)程負(fù)責(zé)所有內(nèi)核態(tài)的進(jìn)程的調(diào)度和管理,是內(nèi)核態(tài)所有進(jìn)程的祖先。(注意,內(nèi)核態(tài)不區(qū)分線程和進(jìn)程,所以說進(jìn)程和線程都可以,都是任務(wù))
「為什么要先創(chuàng)建 0 號進(jìn)程,而不直接創(chuàng)建 1 號進(jìn)程?」
現(xiàn)在對于為什么要先創(chuàng)建 0 號進(jìn)程而不直接創(chuàng)建1號和2號進(jìn)程有許多討論。我認(rèn)為...算了,我不認(rèn)為了,一展開講這篇文章又收不了尾了,以后可以專門寫一篇文章來論述這里。簡單來說就是Linux 的第一個進(jìn)程不適合是一個真進(jìn)程,需要一個沒有數(shù)據(jù)之類東西的假進(jìn)程。
「為什么要區(qū)分用戶態(tài)和內(nèi)核態(tài)?」
因為有了多個進(jìn)程,對于關(guān)鍵資源來說,就會產(chǎn)生爭用以及誤操作破壞資源等情況。這時就需要對資源的訪問權(quán)限進(jìn)行一定的限制。x86 提供了分層的權(quán)限機(jī)制,內(nèi)核態(tài)具有最高的訪問權(quán)限,而用戶態(tài)訪問核心資源時必須要切換到內(nèi)核態(tài)才可以訪問。
好了,我看了下字?jǐn)?shù),這篇文章已經(jīng)不少了,接下來我還會繼續(xù)去分享進(jìn)程和線程的更多細(xì)節(jié),也會根據(jù)讀者的反饋在已完成的文章上不斷完善,歡迎大家持續(xù)關(guān)注呀!
參考資料:
【1】Linux進(jìn)程的創(chuàng)建與管理:https://blog.csdn.net/qq_38410730/article/details/81193118
【2】極客時間:《趣談Linux操作系統(tǒng)》