最近在回想一些知識(shí)點(diǎn)的時(shí)候,覺(jué)得對(duì)進(jìn)程這一塊有些模糊,特別寫(xiě)一篇隨筆對(duì)進(jìn)程信息進(jìn)行鞏固和復(fù)習(xí)。
以我個(gè)人的理解就是,程序是一段二進(jìn)制編碼甚至是一個(gè)簡(jiǎn)單的可執(zhí)行文件,而當(dāng)程序這段二進(jìn)制編碼放入內(nèi)存運(yùn)行時(shí),它就會(huì)產(chǎn)生一個(gè)或多個(gè)進(jìn)程。
對(duì)于CPU來(lái)說(shuō),它的工作就是不停地執(zhí)行指令,而由于CPU執(zhí)行指令的速度非常快,它可以用5ms的時(shí)間專(zhuān)門(mén)用于執(zhí)行進(jìn)程A,5ms的時(shí)間專(zhuān)門(mén)用于執(zhí)行進(jìn)程B,5ms的時(shí)間專(zhuān)門(mén)用于執(zhí)行進(jìn)程C,然后這樣不停交替執(zhí)行進(jìn)程A、B、C。在我們看來(lái)就像進(jìn)程A、B、C在同時(shí)執(zhí)行一樣,而實(shí)際上同一個(gè)時(shí)間點(diǎn)只有一個(gè)進(jìn)程正在CPU上運(yùn)行。
就是用于描述一個(gè)進(jìn)程的結(jié)構(gòu)體,每個(gè)進(jìn)程有且只有一個(gè)進(jìn)程描述符,它里面包含了這個(gè)進(jìn)程相關(guān)的所有信息。
這里只截取了部分之后需要說(shuō)明的字段。在內(nèi)核中,會(huì)有一個(gè)進(jìn)程鏈表通過(guò)使用進(jìn)程描述符中的tasks結(jié)構(gòu)把所有進(jìn)程的進(jìn)程鏈表鏈接起來(lái)。
我們?cè)诰幊痰臅r(shí)候知道,在進(jìn)程地址空間中有個(gè)棧,用于程序的順利執(zhí)行,而當(dāng)程序陷入內(nèi)核態(tài)之后,就不能夠使用應(yīng)用態(tài)的棧了,所以,對(duì)于每個(gè)進(jìn)程,它在內(nèi)核中也有一個(gè)內(nèi)核態(tài)的棧區(qū),在內(nèi)核中,把棧和thread_info(線程描述符)結(jié)構(gòu)結(jié)合起來(lái)放在一起,這塊存儲(chǔ)區(qū)域通常為8192字節(jié),也就是兩個(gè)頁(yè)框。thread_info結(jié)構(gòu)大小為52字節(jié),也就是說(shuō),進(jìn)程的可用的棧大小為8140個(gè)字節(jié)。因?yàn)檫M(jìn)程在內(nèi)核態(tài)中所需要執(zhí)行的代碼量并不算多,所以這個(gè)8K的內(nèi)核棧已經(jīng)足夠使用。在編譯內(nèi)核時(shí)也可以設(shè)置整個(gè)內(nèi)核棧為一個(gè)頁(yè)框大小(4KB),不過(guò)在這種情況下,內(nèi)核在處理硬中斷和軟中斷時(shí)就不使用進(jìn)程的內(nèi)核棧棧,而是使用額外的兩個(gè)個(gè)棧:硬中斷請(qǐng)求棧(每個(gè)CPU一個(gè),大小4K),軟中斷請(qǐng)求棧(每個(gè)CPU一個(gè),大小4K)。不過(guò)值得注意的是,在進(jìn)行異常處理時(shí)還是會(huì)使用進(jìn)程的內(nèi)核棧。
如上圖可以看到,進(jìn)程的內(nèi)核棧是向下增長(zhǎng)的,也就是棧底在高位地址,棧頂在低位地址。對(duì)于這個(gè)內(nèi)核棧的作用,我們可以總結(jié)一下:
linux使用輕量級(jí)進(jìn)程對(duì)多線程應(yīng)用提供支持,其實(shí)它的創(chuàng)建也是基于fork()系統(tǒng)調(diào)用,只是在進(jìn)程描述符的初始化當(dāng)中有所區(qū)別。首先,輕量級(jí)進(jìn)程也是一個(gè)進(jìn)程,它有它自己的pid,有它自己的內(nèi)核棧和進(jìn)程描述符,甚至還有它自己的調(diào)度策略,而輕量級(jí)進(jìn)程和普通進(jìn)程不同的就是它沒(méi)有自己的進(jìn)程地址空間,并且要響應(yīng)線程組內(nèi)其他線程接收到的信號(hào)(但可以通過(guò)修改信號(hào)屏蔽字屏蔽某些信號(hào))。輕量級(jí)進(jìn)程使用的是父進(jìn)程的內(nèi)存地址空間,也就是在task_struct結(jié)構(gòu)中的mm和active_mm指針都指向父進(jìn)程的mm指針?biāo)傅刂?。而信?hào)描述符指針signal會(huì)指向父進(jìn)程指向的地址。而在應(yīng)用層,線程有自己的棧,我想這個(gè)應(yīng)該是由glibc實(shí)現(xiàn)的。
輕量級(jí)進(jìn)程和普通進(jìn)程區(qū)別:
還有兩個(gè)狀態(tài)是既可以存放在進(jìn)程描述符的state字段中,也可以存放在exit_state字段中。從這兩個(gè)字段可以看出,只有當(dāng)進(jìn)程執(zhí)行被終止時(shí),進(jìn)程的狀態(tài)才會(huì)為這兩種狀態(tài)中的一種:
對(duì)于一個(gè)普通進(jìn)程,它的執(zhí)行狀態(tài)如下圖所示:
我們使用一個(gè)簡(jiǎn)單地例子說(shuō)明這種狀態(tài)的轉(zhuǎn)變,我們有個(gè)程序A,它的工作就是做一些計(jì)算,然后把計(jì)算結(jié)構(gòu)寫(xiě)入磁盤(pán)文件中。我們?cè)趕hell中運(yùn)行它,起初它就是TASK_RUNNING狀態(tài),也就是運(yùn)行態(tài),CPU會(huì)不停地分配時(shí)間片供我們的進(jìn)程A運(yùn)行,每次時(shí)間片耗盡后,進(jìn)程A都會(huì)轉(zhuǎn)變到就緒態(tài)(實(shí)際上還是TASK_RUNNING狀態(tài),只是此時(shí)在等待CPU分配時(shí)間片,暫時(shí)不在CPU上運(yùn)行)。當(dāng)進(jìn)程A使用fwrite或write將數(shù)據(jù)寫(xiě)入磁盤(pán)文件時(shí),就會(huì)進(jìn)入阻塞態(tài)(TASK_INTERRUPTIBLE狀態(tài)),而磁盤(pán)將數(shù)據(jù)寫(xiě)入完畢后,會(huì)通過(guò)一個(gè)中斷告知內(nèi)核,內(nèi)核此時(shí)會(huì)將進(jìn)程A的狀態(tài)由阻塞態(tài)(TASK_INTERRUPTIBLE)轉(zhuǎn)變?yōu)榫途w態(tài)(TASK_RUNNING)等待CPU分配時(shí)間片運(yùn)行。而最后當(dāng)進(jìn)程A需要退出時(shí),內(nèi)核先會(huì)將其設(shè)置為僵死狀態(tài)(EXIT_ZOMBIE),這時(shí)候它所使用的內(nèi)存已經(jīng)被釋放,只保留了一個(gè)進(jìn)程描述符供父進(jìn)程使用,最后當(dāng)父進(jìn)程(也就是我們起初啟動(dòng)它的shell)通過(guò)wait()類(lèi)系統(tǒng)調(diào)用通知內(nèi)核后,內(nèi)后會(huì)將進(jìn)程A設(shè)置為僵死撤銷(xiāo)狀態(tài)(EXIT_DEAD),并釋放其進(jìn)程描述符。到這里進(jìn)程A的整個(gè)運(yùn)行周期完整結(jié)束。
PID是一個(gè)數(shù)字,用于標(biāo)識(shí)一個(gè)進(jìn)程,就像學(xué)生的學(xué)號(hào)一樣,每個(gè)進(jìn)程都有一個(gè)唯一的編號(hào),保存在進(jìn)程描述符的pid字段中。一般的,在系統(tǒng)運(yùn)行期間,PID都是被順序編號(hào),比如進(jìn)程A的PID為10,那下個(gè)創(chuàng)建的進(jìn)程的PID則為11。不過(guò)PID的值有一個(gè)上限,當(dāng)內(nèi)核使用的PID達(dá)到這個(gè)上限后就會(huì)循環(huán)開(kāi)始找已閑置的小PID號(hào)。在缺省狀態(tài)下,最大PID值為32767(PID_MAX_DEFAULT - 1);可以通過(guò)修改/proc/sys/kernel/pid_max這個(gè)文件來(lái)減小PID上限值。而在64位系統(tǒng)中,PID可擴(kuò)大到4194303。
內(nèi)核是通過(guò)一個(gè)叫pidmap的位圖來(lái)管理已分配的PID號(hào)和閑置的PID號(hào)。在32位系統(tǒng)中,pidmap的大小就是一個(gè)頁(yè)框的大小(4KB),而一個(gè)頁(yè)框大小為32768位,也就是每一位代表一個(gè)PID號(hào),1代表此PID已經(jīng)被分配,0代表此PID號(hào)未被使用;而在64位系統(tǒng)下,pidmap會(huì)使用多個(gè)頁(yè)框。
在POSIX標(biāo)準(zhǔn)中規(guī)定了一個(gè)多線程應(yīng)用程序中所有的線程都必須有相同的PID,在linux內(nèi)核中,是使用輕量級(jí)進(jìn)程實(shí)現(xiàn)線程的功能,但是輕量級(jí)進(jìn)程也是一個(gè)進(jìn)程,他們的PID都不相同,為了實(shí)現(xiàn)這一點(diǎn),內(nèi)核在進(jìn)程描述符中引入了tgid字段。在linux的線程組概念中,一個(gè)線程組中所有線程使用的該線程組領(lǐng)頭線程相同的PID,也就是該組第一個(gè)輕量級(jí)進(jìn)程的PID,并保存到進(jìn)程描述符的tgid字段中,如下圖:
在編程過(guò)程中,我們使用的getpid()函數(shù)返回的值其實(shí)是當(dāng)前進(jìn)程的tgid而不是pid的值,而由于線程組中領(lǐng)頭線程和pid和tgid相同,因而getpid()對(duì)這類(lèi)進(jìn)程所起到的作用和一般進(jìn)程是一樣的。
接下來(lái)說(shuō)說(shuō)內(nèi)核如何將所有的PID和進(jìn)程描述符組織在一起,方便系統(tǒng)查找和使用。在系統(tǒng)運(yùn)行過(guò)程中,可能會(huì)有成百上千的進(jìn)程在運(yùn)行,這時(shí)候進(jìn)程的查找效率就至關(guān)重要了,比如系統(tǒng)管理員使用kill 1024命令去終止PID=1024的進(jìn)程,內(nèi)核會(huì)從這個(gè)PID導(dǎo)出對(duì)應(yīng)的進(jìn)程描述符進(jìn)行處理。內(nèi)核為了提高查找效率,專(zhuān)門(mén)使用了4個(gè)哈希表用于索引進(jìn)程描述符。為什么要4個(gè),因?yàn)槲覀兛梢杂胮id、tgid、pgrp、session去找進(jìn)程,這幾個(gè)哈希表說(shuō)明如下:
當(dāng)我們使用kill 29384命令時(shí),內(nèi)核會(huì)根據(jù)29384處理得出199,然后以199為下標(biāo),獲取PID哈希表中對(duì)應(yīng)的鏈表頭,并在此鏈表中找出PID=29384的進(jìn)程。進(jìn)程描述符中使用struct pid_link pids[PIDTYPE_MAX]鏈入這四個(gè)哈希表。對(duì)于另外三個(gè)哈希表,道理一樣。
在系統(tǒng)中,除了進(jìn)程0,一個(gè)進(jìn)程是由另一個(gè)進(jìn)程創(chuàng)建,它們都具有父子關(guān)系。如果一個(gè)進(jìn)程創(chuàng)建多個(gè)子進(jìn)程,則子進(jìn)程之間有兄弟關(guān)系。在整個(gè)系統(tǒng)啟動(dòng)期間,會(huì)初始化系統(tǒng)的第一個(gè)進(jìn)程init_task,這個(gè)進(jìn)程屬于內(nèi)核中的一個(gè)進(jìn)程,它算是所有進(jìn)程的祖先,之后它會(huì)啟動(dòng)PID為1的init進(jìn)程和PID為2的kthreadd,這兩個(gè)進(jìn)程之后啟動(dòng)的所有進(jìn)程,而init_task之后會(huì)轉(zhuǎn)變?yōu)橐粋€(gè)idle進(jìn)程用于CPU空閑時(shí)運(yùn)行。在進(jìn)程描述符中,使用real_parent、parent、children、sibling這幾個(gè)指針將進(jìn)程關(guān)系組織在一起,我們看看這幾個(gè)指針的說(shuō)明:
所有處于TASK_RUNNING狀態(tài)的進(jìn)程都會(huì)被放入CPU的運(yùn)行隊(duì)列,它們有可能在不同CPU的運(yùn)行隊(duì)列中。
系統(tǒng)沒(méi)有為T(mén)ASK_STOPED、EXIT_ZOMBIE和EXIT_DEAD狀態(tài)的進(jìn)程建立專(zhuān)門(mén)的鏈表,因?yàn)樘幱谶@些狀態(tài)的進(jìn)程訪問(wèn)比較簡(jiǎn)單,可通過(guò)PID和通過(guò)特定父進(jìn)程的子進(jìn)程鏈表進(jìn)行訪問(wèn)。
所有TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE都會(huì)被放入相應(yīng)的等待隊(duì)列,系統(tǒng)中有很多種等待隊(duì)列,有些是等待磁盤(pán)操作的終止,有些是等待釋放系統(tǒng)資源,有些是等待時(shí)間經(jīng)過(guò)固定的間隔,每個(gè)等待隊(duì)列它的喚醒條件不同,比如等待隊(duì)列1是等待系統(tǒng)釋放資源A的,等待隊(duì)列2是等待系統(tǒng)釋放資源B的。因此,等待隊(duì)列表示一組睡眠進(jìn)程,當(dāng)某一條件為真時(shí),由內(nèi)核喚醒這條等待隊(duì)列上的進(jìn)程。我們看看內(nèi)核中一個(gè)簡(jiǎn)單的sleep_on()函數(shù):
聯(lián)系客服