1 進(jìn)程
進(jìn)程指的是處于執(zhí)行期的程序。但是需要注意的是進(jìn)程并不僅僅包括一段可執(zhí)行程序的代碼举瑰,它同時(shí)還包括其他資源此迅,例如打開的文件旧巾,掛起的信號(hào)鲁猩,內(nèi)核內(nèi)部數(shù)據(jù)廓握,處理器狀態(tài)隙券,具有內(nèi)存映射的地址空間和執(zhí)行線程以及數(shù)據(jù)段等娱仔。
1.1 進(jìn)程描述符
一個(gè)操作系統(tǒng)如果想管理好進(jìn)程,那么操作系統(tǒng)就需要這個(gè)進(jìn)程的所有信息薪铜,Linux內(nèi)核成功抽象了進(jìn)程這一概念隔箍,然后使用task_struct即進(jìn)程描述符來對進(jìn)程進(jìn)行管理蜒滩,同時(shí)內(nèi)核使用雙向鏈表(即任務(wù)隊(duì)列)對進(jìn)程描述符進(jìn)行了相應(yīng)的組織。(task_struct結(jié)構(gòu)體定義在<linux/sched.h>)捡遍。
task_struct在32位計(jì)算機(jī)中占有1.7KB画株。包含一個(gè)進(jìn)程所有的信息谓传,包含打開的文件续挟,進(jìn)程地址空間诗祸,掛起的信號(hào)轴总,進(jìn)程狀態(tài)等怀樟,具體可以參照在Linux內(nèi)核代碼中定義的task_struct結(jié)構(gòu)體代碼漂佩。Linux在分配進(jìn)程描述符時(shí)投蝉,使用了slab機(jī)制(可以查看進(jìn)程創(chuàng)建一節(jié))瘩缆。當(dāng)進(jìn)程描述符task_struct分配完畢之后庸娱,需要對其進(jìn)行存放。
1.2 內(nèi)核進(jìn)程操作
對于一個(gè)進(jìn)程來說归露,在內(nèi)存中會(huì)分配一段內(nèi)存空間剧包,一般來說這個(gè)空間為1或者2個(gè)頁往果,這個(gè)內(nèi)存空間就是進(jìn)程的內(nèi)核棧陕贮。在進(jìn)程內(nèi)核棧的棧底有一個(gè)結(jié)構(gòu)體變量為thread_info掉缺,這個(gè)結(jié)構(gòu)體變量中包含了一個(gè)指向該進(jìn)程描述符task_struct的指針攀圈,這個(gè)變量的存在峦甩,可以使內(nèi)核快速地獲得某一個(gè)進(jìn)程的進(jìn)程描述符凯傲,從而提高響應(yīng)速度。在x86體系結(jié)構(gòu)中幌缝,內(nèi)核中的current宏就是通過對于這個(gè)結(jié)構(gòu)體的訪問來實(shí)現(xiàn)的涵卵,而在其他寄存器豐富的體系結(jié)構(gòu)中看轿偎,可能會(huì)沒有使用thread_info結(jié)構(gòu)體被廓,而是直接使用某一個(gè)寄存器來完成例如PPC體系結(jié)構(gòu)。
/*x86中thread_info的定義*/
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
unsigned long flags; /* low level flags */
unsigned long status; /* thread-synchronous flags */
__u32 cpu; /* current CPU */
int preempt_count; /* 0 => preemptable, <0 => BUG */
mm_segment_t addr_limit; /* thread address space:
* 0-0xBFFFFFFF for user-thead
* 0-0xFFFFFFFF for kernel-thread
*/
struct restart_block restart_block;
__u8 supervisor_stack[0];
};
1.3 進(jìn)程PID
Linux的內(nèi)核使用PID來對進(jìn)程進(jìn)行唯一標(biāo)識(shí)球碉。PID是pid_t的隱含類型,PID的值受到<linux/threads.h>頭文件中規(guī)定的最大值的限制多律,但是為了和傳統(tǒng)的Unix操作系統(tǒng)兼容狼荞,PID會(huì)被默認(rèn)設(shè)置為32768即short int短整型的最大值相味。PID的最大值是系統(tǒng)中允許同時(shí)存在的進(jìn)程的最大數(shù)目丰涉。PID 的最大值可以通過/proc/sys/kernel/pid_max來修改一死。
1.4 進(jìn)程家族樹
Linux和Unix系統(tǒng)一樣投慈,進(jìn)程之間存在明顯的繼承關(guān)系伪煤。所有的進(jìn)程都是PID為1的init進(jìn)程的后代抱既。內(nèi)核會(huì)在系統(tǒng)啟動(dòng)的最后階段啟動(dòng)init進(jìn)程防泵,這個(gè)進(jìn)程回去讀取并且執(zhí)行系統(tǒng)的初始化腳本(initscript)執(zhí)行相關(guān)程序捷泞,完成整個(gè)系統(tǒng)的啟動(dòng)肚邢。
在Linux操作系統(tǒng)中骡湖,每個(gè)進(jìn)程都會(huì)有父進(jìn)程响蕴,每個(gè)進(jìn)程都會(huì)有0到n個(gè)子進(jìn)程。同一個(gè)父進(jìn)程的所有進(jìn)程被稱為兄弟辖试。進(jìn)程描述符中罐孝,包含了指向父進(jìn)程的指針莲兢,還包含了一個(gè)children子進(jìn)程鏈表(init進(jìn)程的進(jìn)程描述符是靜態(tài)分配的)改艇。所以通過簡單的遍歷就可訪問到系統(tǒng)中的所有進(jìn)程谒兄。在代碼中特意提供了for_each_process(task)宏來進(jìn)行對整個(gè)進(jìn)程隊(duì)列(或稱任務(wù)隊(duì)列)的訪問能力。
2 進(jìn)程創(chuàng)建
2.1 創(chuàng)建過程
在Linux進(jìn)程創(chuàng)建不同于其他操作系統(tǒng)瘦穆,Linux操作系統(tǒng)提供了兩個(gè)單獨(dú)的函數(shù)完成進(jìn)程的創(chuàng)建工作纪隙。其中fork()函數(shù)通過拷貝完成子進(jìn)程的創(chuàng)建,子進(jìn)程會(huì)完全拷貝父進(jìn)程中的絕大多數(shù)資源扛或,(除了PID和PPID绵咱,以及一些敏感資源和統(tǒng)計(jì)量)。然后在使用exec()函數(shù)完成可執(zhí)行文件的讀取熙兔,并且將其載入地址空間運(yùn)行悲伶。而其他操作系統(tǒng)一般只使用一個(gè)函數(shù)完成上述的兩步操作。
fork()函數(shù)是通過clone()系統(tǒng)調(diào)用實(shí)現(xiàn)的住涉。此調(diào)用會(huì)通過一系列參數(shù)標(biāo)志指明父子進(jìn)程需要共享的資源麸锉。庫函數(shù)根據(jù)參數(shù)標(biāo)志調(diào)用clone()。clone()調(diào)用do_fork()函數(shù)柳爽。do_fork()函數(shù)在kernel/fork.c中定義,并且完成了創(chuàng)建中的大部分工作。然后該函數(shù)會(huì)去調(diào)用copy_process()函數(shù)。copy_process()函數(shù)完成了下述工作:
1) 調(diào)用duo_task_struct()函數(shù)為新進(jìn)程創(chuàng)建內(nèi)核棧诡蜓、thread_info脚粟、和task_struct,但是這些值都和當(dāng)前進(jìn)程的相同,只是一份簡單的復(fù)制。
2) 檢查當(dāng)前用戶的進(jìn)程總數(shù)是否超過限制
3) 將進(jìn)程描述符中關(guān)于目前進(jìn)程的統(tǒng)計(jì)信息清零,使得子進(jìn)程和父進(jìn)程能夠進(jìn)行區(qū)分
4) 將子進(jìn)程狀態(tài)設(shè)為TASK_UNINTERRUPTIBLE,使其不能運(yùn)行
5) 調(diào)用copy_flag()函數(shù)更新task_struct的flags成員术幔。將超級用戶權(quán)限標(biāo)志符PF_SUPERPRIV清零,然后將進(jìn)程未調(diào)用exec()函數(shù)標(biāo)志位PF_FORKNOEXEC置位。
6) 調(diào)用alloc_pid()為新進(jìn)程分配一個(gè)有效PID
7) 根據(jù)傳遞給clone()的參數(shù)標(biāo)志兜畸,該函數(shù)(即copy_process()函數(shù))拷貝或者共享打開的文件煞躬、文件系統(tǒng)信息、信號(hào)處理函數(shù)、進(jìn)程地址空間和命名空間皱卓。
8) 掃尾,然后返回一個(gè)指向子進(jìn)程的指針
當(dāng)然還有其他形式的fork()函數(shù)實(shí)現(xiàn)方式颅和。例如vfork()函數(shù)功能和fork()函數(shù)相同,但是vfork()函數(shù)不會(huì)拷貝父進(jìn)程的頁表項(xiàng)彼绷。vfork()生成的子進(jìn)程作為一個(gè)單獨(dú)的線程在其地址空間內(nèi)運(yùn)行猜旬,父進(jìn)程會(huì)被阻塞熟嫩,直到子進(jìn)程退出或者調(diào)用exec()函數(shù)柠逞,子進(jìn)程不允許向地址空間內(nèi)寫入數(shù)據(jù)个束。但是在使用了寫時(shí)拷貝技術(shù)之后,這一項(xiàng)技術(shù)其實(shí)已經(jīng)無關(guān)緊要了。
2.2 進(jìn)程創(chuàng)建優(yōu)化
由于進(jìn)程描述符task_struct是一個(gè)在進(jìn)程創(chuàng)建時(shí)必須的數(shù)據(jù)結(jié)構(gòu),所以進(jìn)程的創(chuàng)建速度可以通過加速進(jìn)程描述符的創(chuàng)建來提高糠悯,有鑒于此讯泣,內(nèi)核使用了slab機(jī)制來對其進(jìn)行處理。所謂slab機(jī)制棋凳,就是對于頻繁被使用的數(shù)據(jù)結(jié)構(gòu),會(huì)進(jìn)行緩存骄噪,而不是使用完畢之后直接進(jìn)行釋放。這樣做的好處是,如果需要頻繁創(chuàng)建某一數(shù)據(jù)結(jié)構(gòu)變量,只是直接使用即可橱脸,而不需要進(jìn)行內(nèi)存的申請,使用完畢也不需要釋放惠拭,大大減少了分配內(nèi)存和回收內(nèi)存的時(shí)間条霜。使用slab機(jī)制后,進(jìn)程描述符可以被快速地建立抹沪,同時(shí)進(jìn)程銷毀時(shí)也不需要去進(jìn)行進(jìn)程描述符的內(nèi)存釋放噪馏。
當(dāng)然Linux內(nèi)核在其他方面也使用了加快進(jìn)程創(chuàng)建的方法。上面講到封豪,Linux創(chuàng)建進(jìn)程使用fork()函數(shù)來完成廓推,而fork()函數(shù)又使用clone()系統(tǒng)調(diào)用來實(shí)現(xiàn)涝婉,但是需要注意的是桥温,創(chuàng)建一個(gè)新進(jìn)程時(shí),Linux內(nèi)核加入了寫時(shí)拷貝機(jī)制來加速進(jìn)程的創(chuàng)建遵湖,而不是完整地對進(jìn)程所有內(nèi)容進(jìn)行簡單的復(fù)制捌蚊。所謂寫時(shí)拷貝就是在新進(jìn)程創(chuàng)建時(shí)赴涵,子進(jìn)程和父進(jìn)程共享一個(gè)進(jìn)程地址空間拷貝杨拐,當(dāng)子進(jìn)程或者父進(jìn)程對這個(gè)拷貝執(zhí)行寫入操作后鳍徽,數(shù)據(jù)才會(huì)被復(fù)制瑰剃,然后進(jìn)行各自的修改,所以資源在未進(jìn)行寫入時(shí),以只讀方式共享梯醒。這種寫時(shí)拷貝的方式诀蓉,將進(jìn)程的創(chuàng)建開銷從子進(jìn)程對父進(jìn)程資源的大量復(fù)制,簡化為復(fù)制父進(jìn)程的頁表和子進(jìn)程唯一進(jìn)程描述符的創(chuàng)建脑豹。
2.3 進(jìn)程終結(jié)
進(jìn)程終結(jié)時(shí)筑悴,內(nèi)核必須要釋放他所占用的資源定躏,然后通知父進(jìn)程。進(jìn)程的析構(gòu)發(fā)生在exit()系統(tǒng)調(diào)用時(shí)液茎,可以是顯式的逞姿,也可以是隱式的辞嗡,例如從某個(gè)程序的主函數(shù)返回(對于C語言來說其實(shí)會(huì)在main()函數(shù)的返回點(diǎn)后面設(shè)置exit()代碼)。當(dāng)進(jìn)程收到不能處理但是又不能忽視的信號(hào)或者出現(xiàn)異常時(shí)滞造,也可能會(huì)被動(dòng)終結(jié)欲间。但是進(jìn)程在終結(jié)是,大部分還是會(huì)調(diào)用do_exit()完成(在kernel/exit.c中定義)断部。
(1) 將task_struct中的標(biāo)志成員設(shè)置為PF_EXITING
(2) 調(diào)用del_timer_sync()刪除任意內(nèi)核定時(shí)器猎贴。根據(jù)返回的結(jié)果確認(rèn)沒有任何定時(shí)器在排隊(duì),同時(shí)也沒有任何定時(shí)器處理程序在運(yùn)行蝴光。
(3) 若開啟了BSD的進(jìn)程記賬功能她渴,那么還需要調(diào)用acct_update_integrals()來輸出記賬信息
(4) 調(diào)用exit_mm()釋放進(jìn)程占用的mm_struct,若是沒有其他進(jìn)程使用這個(gè)地址空間蔑祟,那么就徹底釋放此地址空間
(5) 調(diào)用sem_exit()函數(shù)趁耗,若進(jìn)程排隊(duì)等待IPC信號(hào),則離開隊(duì)列
(6) 調(diào)用exit_file()和exit_fs()疆虚,分別遞減文件描述符苛败、文件系統(tǒng)數(shù)據(jù)的引用計(jì)數(shù)。若釋放后引用計(jì)數(shù)為0径簿,則直接釋放罢屈。
(7) 將存放在task_struct的exit_code成員中的任務(wù)退出代碼置為由exit()提供的任務(wù)退出代碼,或者完成任何其他由內(nèi)核機(jī)制規(guī)定的退出動(dòng)作篇亭。退出代碼的存放是為了供父進(jìn)程檢索
(8) 調(diào)用exit_notify()函數(shù)向自己的父進(jìn)程發(fā)送信號(hào)缠捌,并且給自己的子進(jìn)程重新尋找養(yǎng)父,養(yǎng)父為線程組中的其他線程或者為init進(jìn)程译蒂,然后將進(jìn)程狀態(tài)置為EXIT_ZOMBLE曼月。
(9) do_exit()調(diào)用schedule()切換到新進(jìn)程。這是do_exit()執(zhí)行的最后代碼柔昼,退出后就不再返回哑芹。
2.3.1 刪除進(jìn)程描述符
進(jìn)程在執(zhí)行完do_exit()函數(shù)調(diào)用之后,會(huì)處于EXIT_ZOMBIE退出狀態(tài)捕透,其所占有的內(nèi)存就是內(nèi)核棧聪姿、thread_info結(jié)構(gòu)和task_struct結(jié)構(gòu)體。處于這個(gè)狀態(tài)的進(jìn)程唯一目的就是向父進(jìn)程提供信息激率。父進(jìn)程檢索到信息或者通知內(nèi)核那是無關(guān)的信息后咳燕,由進(jìn)程所持有的剩余的內(nèi)存釋放勿决。
調(diào)用do_exit()之后乒躺,雖然線程已經(jīng)僵死不再運(yùn)行,但是系統(tǒng)還保留了它的進(jìn)程描述符低缩。這樣做可以使系統(tǒng)能夠在子進(jìn)程終結(jié)后仍獲取其信息嘉冒。所以進(jìn)程的終結(jié)清理操作可以和進(jìn)程描述符的刪除操作分開運(yùn)行曹货。
在刪除進(jìn)程描述符的時(shí)候,會(huì)調(diào)用release_task()讳推,完成以下操作:
(1)調(diào)用__exit_signal()顶籽,由次函數(shù)調(diào)用_unhash_process(),后者又調(diào)用detach_pid()從pidhash上刪除該進(jìn)程银觅,同時(shí)從任列表中刪除該進(jìn)程
2)__exit_signal()釋放目前僵死進(jìn)程所使用的所有剩余資源礼饱,并進(jìn)行最終的統(tǒng)計(jì)和記錄。
3)如果這個(gè)進(jìn)程是進(jìn)程組最后一個(gè)進(jìn)程究驴,并且領(lǐng)頭進(jìn)程已經(jīng)死掉镊绪,那么release_task()通知僵死的領(lǐng)頭進(jìn)程的父進(jìn)程
4)調(diào)用put_task_struct()釋放進(jìn)程內(nèi)核棧和thread_info結(jié)構(gòu)所占用的頁,釋放task_struct所占的slab高速緩存洒忧。
若父進(jìn)程在子進(jìn)程之前退出蝴韭,則首先會(huì)為子進(jìn)程在當(dāng)前進(jìn)程組內(nèi)宣召一個(gè)進(jìn)程作為父親,若不行熙侍,就讓init進(jìn)程作為父進(jìn)程榄鉴。
3 線程
線程是指在進(jìn)程中活動(dòng)的對象,相對而言蛉抓,線程僅僅局限在進(jìn)程內(nèi)部庆尘,線程擁有的資源遠(yuǎn)遠(yuǎn)比進(jìn)程小,僅僅包括獨(dú)立的程序計(jì)數(shù)器和進(jìn)程棧以及一組進(jìn)程寄存器巷送。在其他操作系統(tǒng)中進(jìn)程和線程的概念往往會(huì)被嚴(yán)格區(qū)分减余,但是對于Linux操作系統(tǒng)內(nèi)核而言,它對線程和進(jìn)程并不進(jìn)行區(qū)分惩系,線程通常被視為一個(gè)與其他進(jìn)程共享某些資源的進(jìn)程位岔。每個(gè)線程都擁有自己的task_struct,所以線程在Linux內(nèi)核中也被視為一個(gè)進(jìn)程堡牡,這是和其他操作系統(tǒng)截然不同的抒抬。
線程的創(chuàng)建和進(jìn)程是類似的但是在調(diào)用clone()的時(shí)候,會(huì)傳遞一些特殊的標(biāo)志位晤柄,例如CLONE_VM擦剑,CLONE_FS,CLONE_FILES芥颈,CLONE_SIGHAND惠勒,這些值都是由下表定義的。
內(nèi)核很多時(shí)候還需要在后臺(tái)執(zhí)行一些操作爬坑,這些都是由內(nèi)核線程(kernel thread)完成纠屋。內(nèi)核線程獨(dú)立于內(nèi)核進(jìn)程運(yùn)行,同時(shí)內(nèi)核線程沒有獨(dú)立的地址空間盾计,并且不會(huì)切換到用戶空間售担,其他和普通線程一樣赁遗,沒有區(qū)別。
內(nèi)核線程一般是自動(dòng)從內(nèi)核進(jìn)程中衍生而出族铆,同樣內(nèi)核線程也是通過clone()系統(tǒng)調(diào)用實(shí)現(xiàn)岩四,并且需要調(diào)用wake_up_process()函數(shù)來進(jìn)行明確地喚醒。kthread_run()可以完成線程的喚醒和運(yùn)行哥攘,但是本質(zhì)上只是調(diào)用了kthread_create()和wake_up_process()剖煌。內(nèi)核線程可以使用do_exit()函數(shù)退出,也可以由內(nèi)核其他部分調(diào)用kthread_stop()函數(shù)來進(jìn)行退出逝淹。
4 進(jìn)程和線程的區(qū)別
對于Linux內(nèi)核而言末捣,進(jìn)程和線程沒有區(qū)別。對于Linux內(nèi)核而言创橄,并沒有對線程進(jìn)行特殊處理箩做,而是將線程與進(jìn)程同等對待,這與其他操作系統(tǒng)完全不同妥畏。其他操作系統(tǒng)都提供了專門的機(jī)制去實(shí)現(xiàn)多線程機(jī)制邦邦,由于Linux強(qiáng)大輕便快速的進(jìn)程創(chuàng)建手段,所以Linux僅僅將線程看作是進(jìn)程共享了進(jìn)程資源的多個(gè)進(jìn)程醉蚁,對于Linux內(nèi)核來說創(chuàng)建線程等價(jià)于創(chuàng)建一個(gè)進(jìn)程燃辖。通過Linux內(nèi)核可以得知,一個(gè)進(jìn)程的多線程其實(shí)只是共享了很多資源网棍,例如地址空間等黔龟。由此產(chǎn)生了“Linux沒有多線程機(jī)制“”這一說法,但是本質(zhì)上來說滥玷,并不是Linux沒有多線程機(jī)制氏身,只是其實(shí)現(xiàn)方式和其他操作系統(tǒng)不同而已。
這是個(gè)人在閱讀《Linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》時(shí)候的一點(diǎn)心得惑畴,里面加入了一些自己關(guān)于操作系統(tǒng)的理解蛋欣,對自己的現(xiàn)有的知識(shí)進(jìn)行梳理,如有錯(cuò)誤敬請指正如贷。