深入理解Linux進(jìn)程管理
一、進(jìn)程基本概念
進(jìn)程是計(jì)算機(jī)里面最重要的概念之一。操作系統(tǒng)的目的就是為了運(yùn)行進(jìn)程。那么到底什么是進(jìn)程,操作系統(tǒng)又是如何實(shí)現(xiàn)進(jìn)程和管理進(jìn)程的呢?
1.1 進(jìn)程與程序
進(jìn)程是程序的執(zhí)行過程。程序是靜態(tài)的,是存在于外存之中的,電腦關(guān)機(jī)后依然存在。進(jìn)程是動態(tài)的,是存在于內(nèi)存之中的,是程序的執(zhí)行過程,電腦關(guān)機(jī)后就不存在進(jìn)程了。進(jìn)程的內(nèi)容來源于程序,進(jìn)程的啟動過程就是把程序從外存加載到內(nèi)存的過程。程序文件是有格式的,UNIX-Like操作系統(tǒng)的通用程序文件格式是ELF。程序文件是從源碼文件編譯過來的,源碼文件很多是用C或者C++書寫的。關(guān)于編譯系統(tǒng),請參看《深入理解編譯系統(tǒng)》,關(guān)于C和C++,請參看《深入理解C與C++》。
1.2 進(jìn)程與線程
進(jìn)程是操作系統(tǒng)分配和管理系統(tǒng)資源的基本單位。進(jìn)程本來也是程序執(zhí)行的基本單位,但是自從有了線程之后就不是了?,F(xiàn)在線程是程序執(zhí)行的基本單位,代表一個(gè)執(zhí)行流,一個(gè)進(jìn)程可以有多個(gè)執(zhí)行流。最初的時(shí)候,一個(gè)進(jìn)程就只有一個(gè)執(zhí)行流,也就是主線程,此時(shí)進(jìn)程就是線程,線程就是進(jìn)程。當(dāng)程序需要多個(gè)執(zhí)行流的時(shí)候,采取的都是多進(jìn)程的方式。但是創(chuàng)建一個(gè)新進(jìn)程是一個(gè)很耗費(fèi)資源的事情,而且多個(gè)進(jìn)程之間還要進(jìn)行進(jìn)程間通信也很費(fèi)事。于是人們便想到了開發(fā)進(jìn)程內(nèi)并發(fā)機(jī)制,也就是在一個(gè)進(jìn)程內(nèi)能同時(shí)存在多個(gè)執(zhí)行流(線程)。不同的人設(shè)計(jì)的進(jìn)程內(nèi)并發(fā)機(jī)制并不相同。按照線程的管理是否實(shí)現(xiàn)在內(nèi)核里,進(jìn)程內(nèi)并發(fā)機(jī)制可以分為兩大類,分別是內(nèi)核級線程(內(nèi)核級線程也被叫做輕量級進(jìn)程)和用戶級線程,注意這兩個(gè)名詞都帶個(gè)級,它們是進(jìn)程內(nèi)并發(fā)機(jī)制的兩個(gè)子類,并不是具體的線程。內(nèi)核級線程下的線程,按照運(yùn)行主體是在內(nèi)核空間還是在用戶空間可以分為內(nèi)核線程和用戶線程。用戶級線程下的線程,按照運(yùn)行主體是在內(nèi)核空間還是在用戶空間也可以分為內(nèi)核線程和用戶線程,但是由于用戶級線程實(shí)現(xiàn)在用戶空間,所以它的線程不可能存在于內(nèi)核空間。內(nèi)核級線程下的用戶線程一般被叫做用戶線程,簡稱線程。用戶級線程下的用戶線程如果再叫用戶線程或者線程就會產(chǎn)生混淆,于是就被叫做協(xié)程或者纖程。如下圖所示:
這兩種實(shí)現(xiàn)多線程的方法各有優(yōu)缺點(diǎn)。在用戶空間實(shí)現(xiàn)的話,優(yōu)點(diǎn)是簡單,不用改內(nèi)核,只需要實(shí)現(xiàn)一個(gè)庫就行了,創(chuàng)建線程開銷小,缺點(diǎn)是線程之間做不到真并發(fā),一個(gè)線程阻塞就會阻塞同一進(jìn)程的所有其它線程。在內(nèi)核空間實(shí)現(xiàn)的話,缺點(diǎn)是麻煩,需要改內(nèi)核,創(chuàng)建線程開銷大,但是優(yōu)點(diǎn)是能做到真并發(fā),一個(gè)進(jìn)程的多個(gè)線程可以同時(shí)在多個(gè)CPU上運(yùn)行,能充分利用CPU。當(dāng)然這兩者并不是對立的,它們可以同時(shí)實(shí)現(xiàn),一個(gè)進(jìn)程可以有多個(gè)內(nèi)核級線程,一個(gè)內(nèi)核級線程又可以有多個(gè)用戶級線程,編程者可以靈活選擇使用哪種多線程方式。
1.3 進(jìn)程與內(nèi)核
進(jìn)程與內(nèi)核在同一個(gè)虛擬地址空間中,但是在不同的子空間,進(jìn)程是在用戶空間,內(nèi)核是在內(nèi)核空間。整個(gè)系統(tǒng)只有一個(gè)內(nèi)核空間,但是卻有很多用戶空間,不過當(dāng)前用戶空間永遠(yuǎn)只有一個(gè)(對于一個(gè)CPU來說)。雖然內(nèi)核空間和用戶空間在同一個(gè)空間中,但是它們的權(quán)限并不相同。內(nèi)核空間處于特權(quán)模式,用戶空間處于非特權(quán)模式。內(nèi)核可以隨意訪問和操作用戶空間,但是用戶空間對內(nèi)核空間卻是看得見摸不著。內(nèi)核空間可以做很多特權(quán)操作,用戶空間沒有權(quán)限做,但是有些時(shí)候又需要做,所以內(nèi)核為用戶空間開了一個(gè)口子,就是系統(tǒng)調(diào)用,用戶空間可以通過系統(tǒng)調(diào)用來請求內(nèi)核的服務(wù)。關(guān)于系統(tǒng)調(diào)用請參看《深入理解Linux系統(tǒng)調(diào)用與API》。
下面我們用一張圖來總結(jié)內(nèi)核和進(jìn)程之間的關(guān)系:
這個(gè)圖是在講進(jìn)程調(diào)度的時(shí)候畫的,但是用在這里表示進(jìn)程和內(nèi)核的關(guān)系也很合適。
1.4 進(jìn)程與內(nèi)存
對于內(nèi)核來說,內(nèi)存是有虛擬內(nèi)存和物理內(nèi)存之分的。但是對于進(jìn)程來說,這些都是透明的,進(jìn)程只需要知道自己獨(dú)占一個(gè)用戶空間的內(nèi)存就可以了,它不知道也不需要知道自己是否運(yùn)行在虛擬內(nèi)存上。如果非要說進(jìn)程知道物理內(nèi)存和虛擬內(nèi)存,那么進(jìn)程也只能分配和管理虛擬內(nèi)存,它沒法分配管理物理內(nèi)存,因?yàn)槲锢韮?nèi)存對它來說是透明的。內(nèi)核在合適的時(shí)候會為進(jìn)程分配相應(yīng)的物理內(nèi)存,保證進(jìn)程在訪問內(nèi)存的時(shí)候一定會有對應(yīng)的物理內(nèi)存,但是進(jìn)程對此毫不知情,也管不了。
進(jìn)程需要內(nèi)存的時(shí)候可以通過系統(tǒng)調(diào)用brk、sbrk、mmap來向內(nèi)核申請分配虛擬內(nèi)存。但是直接使用系統(tǒng)調(diào)用來分配管理內(nèi)存顯然很麻煩效率也低,為此libc向進(jìn)程提供了malloc庫,malloc提供了malloc、free等幾個(gè)接口供進(jìn)程使用。這樣進(jìn)程需要內(nèi)存的時(shí)候就可以直接使用malloc去分配內(nèi)存,使用完了就用free去釋放內(nèi)存,不用考慮分配效率、內(nèi)存碎片等問題了。目前比較流行的malloc庫有ptmalloc、jemalloc、scudo等。
1.5 進(jìn)程運(yùn)行狀態(tài)
很多操作系統(tǒng)的書籍上都會講進(jìn)程的運(yùn)行狀態(tài),有的講的是三態(tài),有的講的是五態(tài)。其實(shí)兩者并不矛盾,三態(tài)只有進(jìn)程運(yùn)行時(shí)的狀態(tài),五態(tài)把進(jìn)程的新建和死亡狀態(tài)也算上去了,如下圖所示:
進(jìn)程剛創(chuàng)建之后處于新建態(tài),但是新建態(tài)不是持久狀態(tài),它會立馬轉(zhuǎn)變?yōu)榫途w狀態(tài)。然后進(jìn)程就會一直處于就緒、執(zhí)行、阻塞三態(tài)的循環(huán)之中。就緒態(tài)會由于進(jìn)程調(diào)度而轉(zhuǎn)為執(zhí)行態(tài);執(zhí)行態(tài)會由于時(shí)間片耗盡而轉(zhuǎn)為就緒態(tài),也會由于等待某個(gè)事件而轉(zhuǎn)為阻塞態(tài);阻塞態(tài)會由于某個(gè)事件的發(fā)生而轉(zhuǎn)為就緒態(tài)。最后進(jìn)程可能會由于主動退出或者發(fā)生異常而死亡。死亡態(tài)也不是一個(gè)持久態(tài),進(jìn)程死亡之后就不存在了。
1.6 進(jìn)程親緣關(guān)系
所有進(jìn)程都通過父子關(guān)系連接而構(gòu)成一顆親緣樹,這顆樹的樹根是init進(jìn)程(pid1)。Init進(jìn)程是第一個(gè)用戶空間進(jìn)程,所有的用戶空間進(jìn)程都是init進(jìn)程的子孫進(jìn)程。Init進(jìn)程的父進(jìn)程是零號進(jìn)程,零號進(jìn)程是在代碼中通過硬編碼創(chuàng)建的,其它所有的進(jìn)程都是通過fork創(chuàng)建的。這里為什么叫做零號進(jìn)程呢?因?yàn)榱闾栠M(jìn)程的職責(zé)發(fā)生過變化,在系統(tǒng)剛啟動的時(shí)候,零號進(jìn)程是BSP(bootstrapprocess),start_kernel函數(shù)就是在零號進(jìn)程中運(yùn)行的。當(dāng)系統(tǒng)初始化完成的時(shí)候,零號進(jìn)程退化為了idle進(jìn)程。當(dāng)我們只強(qiáng)調(diào)零號進(jìn)程的身份而不關(guān)心它的職責(zé)的時(shí)候,就叫它零號進(jìn)程。當(dāng)后面我們強(qiáng)調(diào)它的idle職責(zé)的時(shí)候,就叫它idle進(jìn)程。
零號進(jìn)程有兩個(gè)親兒子,除了init之外,還有一個(gè)是kthreadd(pid2)。Kthreadd是一個(gè)內(nèi)核線程,它是所有其它內(nèi)核線程的父進(jìn)程。內(nèi)核線程比較特殊的點(diǎn)在于它只運(yùn)行在內(nèi)核空間,所以所有的內(nèi)核線程都可以看做是同一個(gè)進(jìn)程下的線程,因?yàn)閮?nèi)核空間只有一個(gè)。但是每個(gè)內(nèi)核線程在邏輯意義上又是一個(gè)獨(dú)立的進(jìn)程,它們執(zhí)行獨(dú)立的任務(wù),有著獨(dú)立的進(jìn)程人格。所以當(dāng)我們說一個(gè)內(nèi)核線程的時(shí)候,心里也要明白它是一個(gè)單獨(dú)的進(jìn)程,是一個(gè)只有主線程的單線程進(jìn)程。
我們來畫一下進(jìn)程的親緣關(guān)系:
進(jìn)程除了父子這種血緣關(guān)系之外,還存在著家族關(guān)系。一個(gè)是大家族關(guān)系,會話組(session),一個(gè)是小家族關(guān)系,進(jìn)程組(process
group)。會話組的產(chǎn)生來源于早期的大型計(jì)算機(jī),當(dāng)時(shí)一個(gè)公司或者一個(gè)科研單位只能買得起一臺大型機(jī)。然后每個(gè)人都通過一個(gè)終端連接到這個(gè)大型機(jī),用自己的用戶名和密碼登錄上去。每個(gè)用戶都有自己的用戶id,一個(gè)用戶運(yùn)行的所有的程序構(gòu)成了一個(gè)會話組。有了會話組的概念,就可以方便我們把一個(gè)用戶運(yùn)行的所有進(jìn)程作為一個(gè)整體進(jìn)行管理。進(jìn)程組的產(chǎn)生來源于命令行操作的作業(yè)管理。什么是作業(yè)管理呢?就是把一行命令的執(zhí)行整體作為一個(gè)作業(yè)。一行命令的執(zhí)行不一定只有一個(gè)進(jìn)程,比如命令ps -ef | grepbash,就有兩個(gè)進(jìn)程,我們需要有個(gè)概念把這兩個(gè)進(jìn)程作為一個(gè)整體來處理,這個(gè)概念就是進(jìn)程組。有了進(jìn)程組的概念,作業(yè)管理就比較方便了,比如Ctrl+C就是給當(dāng)前正在執(zhí)行的命令(進(jìn)程組)發(fā)信號,進(jìn)程組中的每個(gè)進(jìn)程都會收到信號。
一個(gè)進(jìn)程誕生的時(shí)候默認(rèn)繼承父進(jìn)程的會話組和進(jìn)程組,但是進(jìn)程可以通過系統(tǒng)調(diào)用(setsid,setpgrp)創(chuàng)建新的會話組或者進(jìn)程組。會話組的第一個(gè)進(jìn)程叫做這個(gè)會話組的組長,進(jìn)程組的第一個(gè)進(jìn)程叫做這個(gè)進(jìn)程組的組長,會話組的id等于會話組組長的pid,進(jìn)程組的id等于進(jìn)程組組長的pid。一個(gè)進(jìn)程只有當(dāng)它不是某個(gè)進(jìn)程組組長的時(shí)候,它才可以調(diào)用setpgrp創(chuàng)建新的進(jìn)程組,同時(shí)它也成為了這個(gè)新建的進(jìn)程組的組長。這個(gè)也很好理解,只有臣子造反當(dāng)皇帝,哪有皇帝自己造自己的反重新創(chuàng)建一個(gè)朝代的。同理,只有不是會話組組長的進(jìn)程才能通過setsid創(chuàng)建新的會話組,并成為這個(gè)會話組組長。而且在這個(gè)新的會話組里也不能沒有進(jìn)程組啊,于是還會創(chuàng)建一個(gè)進(jìn)程組,這個(gè)會話組組長還會成為這個(gè)新建的進(jìn)程組的組長,這也要求了這個(gè)進(jìn)程之前不能是進(jìn)程組組長。所以只有既不是進(jìn)程組組長又不是會話組組長的進(jìn)程才能創(chuàng)建新的會話組。
任何一個(gè)進(jìn)程,它必然屬于某個(gè)進(jìn)程組,而且只能同時(shí)屬于一個(gè)進(jìn)程組。任何一個(gè)進(jìn)程,它必然屬于某個(gè)會話組,而且只能屬于一個(gè)會話組。任何一個(gè)進(jìn)程組,它的所有進(jìn)程必須都屬于同一個(gè)會話組。一個(gè)進(jìn)程所屬的會話組只有兩種來源,要么是繼承而來的,要么是自己創(chuàng)建的,進(jìn)程是不能轉(zhuǎn)會話組的。不過一個(gè)進(jìn)程是可以轉(zhuǎn)進(jìn)程組的,但是只能在同一個(gè)會話組中的進(jìn)程組之間轉(zhuǎn)。因此我們可以得出一個(gè)結(jié)論,一個(gè)會話組的所有進(jìn)程肯定都是其會話組組長的子孫進(jìn)程,一個(gè)進(jìn)程組的所有進(jìn)程一般情況下都是其進(jìn)程組組長的子孫進(jìn)程。
我們來畫一下進(jìn)程的家族關(guān)系:
二、進(jìn)程的實(shí)現(xiàn)
明白了進(jìn)程的基本概念之后,我們來看一看Linux是怎么實(shí)現(xiàn)進(jìn)程的。按照標(biāo)準(zhǔn)的操作系統(tǒng)理論,進(jìn)程是資源分配的單位,線程是程序執(zhí)行的單位,內(nèi)核里用進(jìn)程控制塊(PCB Process Control Block)來管理進(jìn)程,用線程控制塊(TCB Thread Control Block)來管理線程。那么Linux是按照這個(gè)邏輯來實(shí)現(xiàn)進(jìn)程的嗎?我們來看一下。
2.1 基本原理
Linux內(nèi)核并不是按照標(biāo)準(zhǔn)的操作系統(tǒng)理論來實(shí)現(xiàn)進(jìn)程的,在內(nèi)核里找不到典型的進(jìn)程控制塊和線程控制塊。內(nèi)核里只有一個(gè)task_struct結(jié)構(gòu)體,初學(xué)內(nèi)核的人會很疑惑這是代表進(jìn)程還是代表線程呢。之所以會這樣,是由于歷史原因造成的。Linux最開始的時(shí)候是不支持多線程的,也可以認(rèn)為此時(shí)一個(gè)進(jìn)程只能有一個(gè)線程就是主線程,因此線程就是進(jìn)程,進(jìn)程就是線程。所以最初的時(shí)候,task_struct既代表進(jìn)程又代表線程,因?yàn)檫M(jìn)程和線程沒有區(qū)別。但是后來Linux也要支持多線程了,我們在1.2節(jié)中討論過,多線程的實(shí)現(xiàn)方法可以在內(nèi)核實(shí)現(xiàn),也可以在用戶空間實(shí)現(xiàn),也可以同時(shí)實(shí)現(xiàn),Linux選擇的是在內(nèi)核實(shí)現(xiàn)。為了最大限度地利用已有的代碼,盡量不對代碼做大的改動,Linux選擇的方法是:task_struct既是線程又是進(jìn)程的代理。注意這句話,task_struct既是線程又是進(jìn)程的代理(不是進(jìn)程本身)。Linux并沒有設(shè)計(jì)單獨(dú)的進(jìn)程結(jié)構(gòu)體,而是用task_struct作為進(jìn)程的代理,這是因?yàn)檫M(jìn)程是資源分配的單位,線程是程序執(zhí)行的單位,同一個(gè)進(jìn)程的所有線程共享相同的資源,因此我們讓同一個(gè)進(jìn)程下的所有線程(task_struct)都指向相同的資源不就可以了嘛。線程在執(zhí)行的時(shí)候會通過task_struct里面的指針訪問資源,同一個(gè)進(jìn)程下的線程自然就會訪問到相同的資源,而且這么做還有很大的靈活性。
我們下面再來強(qiáng)調(diào)一下這句話,以加深對這句話的理解。
task_struct既是線程又是進(jìn)程的代理(不是進(jìn)程本身)。
2.2 進(jìn)程結(jié)構(gòu)體
當(dāng)我們明白了task_struct既是線程又是進(jìn)程的代理之后,再來理解task_struct就容易多了。task_struct的字段由兩部分組成,一部分是線程相關(guān)的,一部分是進(jìn)程相關(guān)的,線程相關(guān)的一般是直接內(nèi)嵌其它數(shù)據(jù),進(jìn)程相關(guān)的一般是用指針指向其它數(shù)據(jù)。線程代表的是執(zhí)行流,所以task_struct的線程相關(guān)部分是和執(zhí)行有關(guān)的,進(jìn)程代表的是資源分配,所以task_struct的進(jìn)程相關(guān)部分是和資源有關(guān)的。我們可以想一下和執(zhí)行有關(guān)的都有哪些,和資源有關(guān)的都哪些?可以很輕松地想到,和執(zhí)行有關(guān)的肯定是進(jìn)程調(diào)度相關(guān)的數(shù)據(jù)啊(進(jìn)程調(diào)度雖然叫進(jìn)程調(diào)度,但實(shí)際上調(diào)度的是線程)。和資源相關(guān)的,最重要的首先肯定是虛擬內(nèi)存啊,其次是文件系統(tǒng)。
下面我們來看一下task_struct的定義: linux-src/include/linux/sched.h
這個(gè)結(jié)構(gòu)體定義有700多行,本文把一些暫時(shí)用不到的都刪除了,現(xiàn)在還有70多行,我們來看一下大概都有哪些內(nèi)容。先看和進(jìn)程相關(guān)的,首先最重要的是虛擬內(nèi)存空間信息mm、active_mm,這兩個(gè)都是指針,對于用戶線程來說兩個(gè)指針的值永遠(yuǎn)都是相同的,同一個(gè)進(jìn)程的所有線程都指向相同的mm,這個(gè)值就表明了同一個(gè)進(jìn)程的線程都在同一個(gè)用戶空間。其次比較重要的是文件管理相關(guān)的兩個(gè)字段fs和files,也都是指針,fs代表的是文件系統(tǒng)掛載相關(guān)的,這個(gè)不僅是同進(jìn)程的所有線程都相同,而且整個(gè)系統(tǒng)默認(rèn)的值都一樣,除非使用了mount 命名空間,files代表的是打開的文件資源,這個(gè)是同進(jìn)程的所有線程都相同。然后我們再來看一下信號相關(guān)的,信號有的數(shù)據(jù)是進(jìn)程全局的,有的是線程私有的,信號的處理是進(jìn)程全局的,所以signal、sighand兩個(gè)字段都是指針,同進(jìn)程的所有線程都指向同一個(gè)結(jié)構(gòu)體,信號掩碼是線程私有的,所以blocked直接是內(nèi)嵌數(shù)據(jù)。進(jìn)程相關(guān)的數(shù)據(jù)基本就這些,下面我們來看一下線程相關(guān)的數(shù)據(jù)。首先是進(jìn)程的運(yùn)行退出狀態(tài),有幾個(gè)字段,__state、on_cpu、cpu、exit_state、exit_code、exit_signal。然后是和線程調(diào)度相關(guān)的幾個(gè)字段,有和優(yōu)先級相關(guān)的rt_priority、static_prio、normal_prio、prio,有和調(diào)度信息統(tǒng)計(jì)相關(guān)的兩個(gè)結(jié)構(gòu)體,se、sched_info。還有兩個(gè)非常重要的字段我們下一節(jié)講。
2.3 進(jìn)程標(biāo)識符
task_struct里面有兩個(gè)重要的字段pid、tgid。我們在用戶空間的時(shí)候也有pid、tid,那么用戶空間的pid是不是就是內(nèi)核的pid呢,那tgid又是啥呢。很多初學(xué)內(nèi)核的人會認(rèn)為用戶空間的pid就是內(nèi)核的pid,剛開始我也是這么認(rèn)為的,給我的內(nèi)核學(xué)習(xí)帶來了很大的困擾。實(shí)際上用戶空間的tid是內(nèi)核空間pid,用戶空間的pid是內(nèi)核空間的tgid,內(nèi)核空間的tgid是內(nèi)核里主線程的pid。為什么會這樣呢?主要還是前面講的問題,task_struct既是線程又是進(jìn)程的代理,沒有單獨(dú)的進(jìn)程結(jié)構(gòu)體。當(dāng)進(jìn)程創(chuàng)建時(shí),也就是進(jìn)程的第一個(gè)線程創(chuàng)建時(shí),會為task_struct分配一個(gè)pid,就是主線程的tid,然后進(jìn)程的pid也就是字段tgid會被賦值為主線程的tid。此后再創(chuàng)建的線程都會繼承父線程的tgid,所以在每個(gè)線程中都能直接獲取進(jìn)程的pid。
我們在這里畫個(gè)圖總結(jié)一下進(jìn)程與線程的關(guān)系、pid與tgid之間的關(guān)系:
Linux里面雖然沒有進(jìn)程結(jié)構(gòu)體,但是所有tgid相同、虛擬內(nèi)存等資源相同的線程構(gòu)成一個(gè)虛擬的進(jìn)程結(jié)構(gòu)體。創(chuàng)建進(jìn)程的第一個(gè)線程(task_struct)就是同時(shí)在創(chuàng)建進(jìn)程,其對應(yīng)的mm_struct、files_struct、signal_struct等資源都會被創(chuàng)建出來。創(chuàng)建進(jìn)程的第二個(gè)線程那就是純粹地創(chuàng)建線程了。
2.4 進(jìn)程的狀態(tài)
進(jìn)程的狀態(tài)在Linux中是如何表示的呢?task_struct中有兩個(gè)字段用來表示進(jìn)程的狀態(tài),__state和exit_state,前者是總體狀態(tài),后者是進(jìn)程在死亡時(shí)的兩個(gè)子狀態(tài)。
我們來看一下代碼中的定義: linux-src/include/linux/sched.h
其中TASK_RUNNING代表的是Runnable和Running狀態(tài)。在Linux中不是用flag直接區(qū)分Runnable和Running狀態(tài)的,它們都用TASK_RUNNING表示,區(qū)分它們的方法是進(jìn)程是否在運(yùn)行隊(duì)列的當(dāng)前進(jìn)程字段上。Blocked狀態(tài)有兩種表示,TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE,它們的區(qū)別是前者在睡眠時(shí)能被信號喚醒,后者不能被信號喚醒。表示死亡的狀態(tài)是TASK_DEAD,它有兩個(gè)子狀態(tài)EXIT_ZOMBIE、EXIT_DEAD,這兩個(gè)狀態(tài)在3.6中講解。
二、進(jìn)程的實(shí)現(xiàn)
明白了進(jìn)程的基本概念之后,我們來看一看Linux是怎么實(shí)現(xiàn)進(jìn)程的。按照標(biāo)準(zhǔn)的操作系統(tǒng)理論,進(jìn)程是資源分配的單位,線程是程序執(zhí)行的單位,內(nèi)核里用進(jìn)程控制塊(PCB Process Control Block)來管理進(jìn)程,用線程控制塊(TCB Thread Control Block)來管理線程。那么Linux是按照這個(gè)邏輯來實(shí)現(xiàn)進(jìn)程的嗎?我們來看一下。
2.1 基本原理
Linux內(nèi)核并不是按照標(biāo)準(zhǔn)的操作系統(tǒng)理論來實(shí)現(xiàn)進(jìn)程的,在內(nèi)核里找不到典型的進(jìn)程控制塊和線程控制塊。內(nèi)核里只有一個(gè)task_struct結(jié)構(gòu)體,初學(xué)內(nèi)核的人會很疑惑這是代表進(jìn)程還是代表線程呢。之所以會這樣,是由于歷史原因造成的。Linux最開始的時(shí)候是不支持多線程的,也可以認(rèn)為此時(shí)一個(gè)進(jìn)程只能有一個(gè)線程就是主線程,因此線程就是進(jìn)程,進(jìn)程就是線程。所以最初的時(shí)候,task_struct既代表進(jìn)程又代表線程,因?yàn)檫M(jìn)程和線程沒有區(qū)別。但是后來Linux也要支持多線程了,我們在1.2節(jié)中討論過,多線程的實(shí)現(xiàn)方法可以在內(nèi)核實(shí)現(xiàn),也可以在用戶空間實(shí)現(xiàn),也可以同時(shí)實(shí)現(xiàn),Linux選擇的是在內(nèi)核實(shí)現(xiàn)。為了最大限度地利用已有的代碼,盡量不對代碼做大的改動,Linux選擇的方法是:task_struct既是線程又是進(jìn)程的代理。注意這句話,task_struct既是線程又是進(jìn)程的代理(不是進(jìn)程本身)。Linux并沒有設(shè)計(jì)單獨(dú)的進(jìn)程結(jié)構(gòu)體,而是用task_struct作為進(jìn)程的代理,這是因?yàn)檫M(jìn)程是資源分配的單位,線程是程序執(zhí)行的單位,同一個(gè)進(jìn)程的所有線程共享相同的資源,因此我們讓同一個(gè)進(jìn)程下的所有線程(task_struct)都指向相同的資源不就可以了嘛。線程在執(zhí)行的時(shí)候會通過task_struct里面的指針訪問資源,同一個(gè)進(jìn)程下的線程自然就會訪問到相同的資源,而且這么做還有很大的靈活性。
我們下面再來強(qiáng)調(diào)一下這句話,以加深對這句話的理解。
task_struct既是線程又是進(jìn)程的代理(不是進(jìn)程本身)。
2.2 進(jìn)程結(jié)構(gòu)體
當(dāng)我們明白了task_struct既是線程又是進(jìn)程的代理之后,再來理解task_struct就容易多了。task_struct的字段由兩部分組成,一部分是線程相關(guān)的,一部分是進(jìn)程相關(guān)的,線程相關(guān)的一般是直接內(nèi)嵌其它數(shù)據(jù),進(jìn)程相關(guān)的一般是用指針指向其它數(shù)據(jù)。線程代表的是執(zhí)行流,所以task_struct的線程相關(guān)部分是和執(zhí)行有關(guān)的,進(jìn)程代表的是資源分配,所以task_struct的進(jìn)程相關(guān)部分是和資源有關(guān)的。我們可以想一下和執(zhí)行有關(guān)的都有哪些,和資源有關(guān)的都哪些?可以很輕松地想到,和執(zhí)行有關(guān)的肯定是進(jìn)程調(diào)度相關(guān)的數(shù)據(jù)啊(進(jìn)程調(diào)度雖然叫進(jìn)程調(diào)度,但實(shí)際上調(diào)度的是線程)。和資源相關(guān)的,最重要的首先肯定是虛擬內(nèi)存啊,其次是文件系統(tǒng)。
下面我們來看一下task_struct的定義: linux-src/include/linux/sched.h
這個(gè)結(jié)構(gòu)體定義有700多行,本文把一些暫時(shí)用不到的都刪除了,現(xiàn)在還有70多行,我們來看一下大概都有哪些內(nèi)容。先看和進(jìn)程相關(guān)的,首先最重要的是虛擬內(nèi)存空間信息mm、active_mm,這兩個(gè)都是指針,對于用戶線程來說兩個(gè)指針的值永遠(yuǎn)都是相同的,同一個(gè)進(jìn)程的所有線程都指向相同的mm,這個(gè)值就表明了同一個(gè)進(jìn)程的線程都在同一個(gè)用戶空間。其次比較重要的是文件管理相關(guān)的兩個(gè)字段fs和files,也都是指針,fs代表的是文件系統(tǒng)掛載相關(guān)的,這個(gè)不僅是同進(jìn)程的所有線程都相同,而且整個(gè)系統(tǒng)默認(rèn)的值都一樣,除非使用了mount 命名空間,files代表的是打開的文件資源,這個(gè)是同進(jìn)程的所有線程都相同。然后我們再來看一下信號相關(guān)的,信號有的數(shù)據(jù)是進(jìn)程全局的,有的是線程私有的,信號的處理是進(jìn)程全局的,所以signal、sighand兩個(gè)字段都是指針,同進(jìn)程的所有線程都指向同一個(gè)結(jié)構(gòu)體,信號掩碼是線程私有的,所以blocked直接是內(nèi)嵌數(shù)據(jù)。進(jìn)程相關(guān)的數(shù)據(jù)基本就這些,下面我們來看一下線程相關(guān)的數(shù)據(jù)。首先是進(jìn)程的運(yùn)行退出狀態(tài),有幾個(gè)字段,__state、on_cpu、cpu、exit_state、exit_code、exit_signal。然后是和線程調(diào)度相關(guān)的幾個(gè)字段,有和優(yōu)先級相關(guān)的rt_priority、static_prio、normal_prio、prio,有和調(diào)度信息統(tǒng)計(jì)相關(guān)的兩個(gè)結(jié)構(gòu)體,se、sched_info。還有兩個(gè)非常重要的字段我們下一節(jié)講。
2.3 進(jìn)程標(biāo)識符
task_struct里面有兩個(gè)重要的字段pid、tgid。我們在用戶空間的時(shí)候也有pid、tid,那么用戶空間的pid是不是就是內(nèi)核的pid呢,那tgid又是啥呢。很多初學(xué)內(nèi)核的人會認(rèn)為用戶空間的pid就是內(nèi)核的pid,剛開始我也是這么認(rèn)為的,給我的內(nèi)核學(xué)習(xí)帶來了很大的困擾。實(shí)際上用戶空間的tid是內(nèi)核空間pid,用戶空間的pid是內(nèi)核空間的tgid,內(nèi)核空間的tgid是內(nèi)核里主線程的pid。為什么會這樣呢?主要還是前面講的問題,task_struct既是線程又是進(jìn)程的代理,沒有單獨(dú)的進(jìn)程結(jié)構(gòu)體。當(dāng)進(jìn)程創(chuàng)建時(shí),也就是進(jìn)程的第一個(gè)線程創(chuàng)建時(shí),會為task_struct分配一個(gè)pid,就是主線程的tid,然后進(jìn)程的pid也就是字段tgid會被賦值為主線程的tid。此后再創(chuàng)建的線程都會繼承父線程的tgid,所以在每個(gè)線程中都能直接獲取進(jìn)程的pid。
我們在這里畫個(gè)圖總結(jié)一下進(jìn)程與線程的關(guān)系、pid與tgid之間的關(guān)系:
Linux里面雖然沒有進(jìn)程結(jié)構(gòu)體,但是所有tgid相同、虛擬內(nèi)存等資源相同的線程構(gòu)成一個(gè)虛擬的進(jìn)程結(jié)構(gòu)體。創(chuàng)建進(jìn)程的第一個(gè)線程(task_struct)就是同時(shí)在創(chuàng)建進(jìn)程,其對應(yīng)的mm_struct、files_struct、signal_struct等資源都會被創(chuàng)建出來。創(chuàng)建進(jìn)程的第二個(gè)線程那就是純粹地創(chuàng)建線程了。
2.4 進(jìn)程的狀態(tài)
進(jìn)程的狀態(tài)在Linux中是如何表示的呢?task_struct中有兩個(gè)字段用來表示進(jìn)程的狀態(tài),__state和exit_state,前者是總體狀態(tài),后者是進(jìn)程在死亡時(shí)的兩個(gè)子狀態(tài)。
我們來看一下代碼中的定義: linux-src/include/linux/sched.h
其中TASK_RUNNING代表的是Runnable和Running狀態(tài)。在Linux中不是用flag直接區(qū)分Runnable和Running狀態(tài)的,它們都用TASK_RUNNING表示,區(qū)分它們的方法是進(jìn)程是否在運(yùn)行隊(duì)列的當(dāng)前進(jìn)程字段上。Blocked狀態(tài)有兩種表示,TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE,它們的區(qū)別是前者在睡眠時(shí)能被信號喚醒,后者不能被信號喚醒。表示死亡的狀態(tài)是TASK_DEAD,它有兩個(gè)子狀態(tài)EXIT_ZOMBIE、EXIT_DEAD,這兩個(gè)狀態(tài)在3.6中講解。