詳解linux內(nèi)核進(jìn)程
進(jìn)程是UNIX操作系統(tǒng)抽象概念中最基本的一種,其中涉及進(jìn)程的定義以及相關(guān)的概念,比如線程;它們?cè)趦?nèi)核中如何被列舉?如何創(chuàng)建?最終又如何消亡?讓我們通過下面的分析,一步步解開內(nèi)核進(jìn)程的神秘面紗。
1. 進(jìn)程和線程
進(jìn)程和線程是程序運(yùn)行時(shí)狀態(tài),是動(dòng)態(tài)變化的,進(jìn)程和線程的管理操作(比如,創(chuàng)建,銷毀等)都是有內(nèi)核來實(shí)現(xiàn)的。
Linux中的進(jìn)程于Windows相比是很輕量級(jí)的,而且不嚴(yán)格區(qū)分進(jìn)程和線程,線程不過是一種特殊的進(jìn)程。
所以下面只討論進(jìn)程,只有當(dāng)線程與進(jìn)程存在不一樣的地方時(shí)才提一下線程。
進(jìn)程提供2種虛擬機(jī)制:虛擬處理器和虛擬內(nèi)存
每個(gè)進(jìn)程有獨(dú)立的虛擬處理器和虛擬內(nèi)存,
每個(gè)線程有獨(dú)立的虛擬處理器,同一個(gè)進(jìn)程內(nèi)的線程有可能會(huì)共享虛擬內(nèi)存。
內(nèi)核中進(jìn)程的信息主要保存在task_struct中(include/linux/sched.h)
進(jìn)程標(biāo)識(shí)PID和線程標(biāo)識(shí)TID對(duì)于同一個(gè)進(jìn)程或線程來說都是相等的。
Linux中可以用ps命令查看所有進(jìn)程的信息:
ps -eo pid,tid,ppid,comm
2. 進(jìn)程的生命周期
進(jìn)程的各個(gè)狀態(tài)之間的轉(zhuǎn)化構(gòu)成了進(jìn)程的整個(gè)生命周期。
3. 進(jìn)程的創(chuàng)建
Linux中創(chuàng)建進(jìn)程與其他系統(tǒng)有個(gè)主要區(qū)別,Linux中創(chuàng)建進(jìn)程分2步:fork()和exec()。
fork: 通過拷貝當(dāng)前進(jìn)程創(chuàng)建一個(gè)子進(jìn)程
exec: 讀取可執(zhí)行文件,將其載入到內(nèi)存中運(yùn)行
創(chuàng)建的流程:
調(diào)用dup_task_struct()為新進(jìn)程分配內(nèi)核棧,task_struct等,其中的內(nèi)容與父進(jìn)程相同。
check新進(jìn)程(進(jìn)程數(shù)目是否超出上限等)
清理新進(jìn)程的信息(比如PID置0等),使之與父進(jìn)程區(qū)別開。
新進(jìn)程狀態(tài)置為 TASK_UNINTERRUPTIBLE
更新task_struct的flags成員。
調(diào)用alloc_pid()為新進(jìn)程分配一個(gè)有效的PID
根據(jù)clone()的參數(shù)標(biāo)志,拷貝或共享相應(yīng)的信息
做一些掃尾工作并返回新進(jìn)程指針
創(chuàng)建進(jìn)程的fork()函數(shù)實(shí)際上最終是調(diào)用clone()函數(shù)。
創(chuàng)建線程和進(jìn)程的步驟一樣,只是最終傳給clone()函數(shù)的參數(shù)不同。
比如,通過一個(gè)普通的fork來創(chuàng)建進(jìn)程,相當(dāng)于:clone(SIGCHLD, 0)
創(chuàng)建一個(gè)和父進(jìn)程共享地址空間,文件系統(tǒng)資源,文件描述符和信號(hào)處理程序的進(jìn)程,即一個(gè)線程:clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0)
在內(nèi)核中創(chuàng)建的內(nèi)核線程與普通的進(jìn)程之間還有個(gè)主要區(qū)別在于:內(nèi)核線程沒有獨(dú)立的地址空間,它們只能在內(nèi)核空間運(yùn)行。
這與之前提到的Linux內(nèi)核是個(gè)單內(nèi)核有關(guān)。
4. 進(jìn)程的終止
和創(chuàng)建進(jìn)程一樣,終結(jié)一個(gè)進(jìn)程同樣有很多步驟:
子進(jìn)程上的操作(do_exit)
設(shè)置task_struct中的標(biāo)識(shí)成員設(shè)置為PF_EXITING
調(diào)用del_timer_sync()刪除內(nèi)核定時(shí)器, 確保沒有定時(shí)器在排隊(duì)和運(yùn)行
調(diào)用exit_mm()釋放進(jìn)程占用的mm_struct
調(diào)用sem__exit(),使進(jìn)程離開等待IPC信號(hào)的隊(duì)列
調(diào)用exit_files()和exit_fs(),釋放進(jìn)程占用的文件描述符和文件系統(tǒng)資源
把task_struct的exit_code設(shè)置為進(jìn)程的返回值
調(diào)用exit_notify()向父進(jìn)程發(fā)送信號(hào),并把自己的狀態(tài)設(shè)為EXIT_ZOMBIE
切換到新進(jìn)程繼續(xù)執(zhí)行
子進(jìn)程進(jìn)入EXIT_ZOMBIE之后,雖然永遠(yuǎn)不會(huì)被調(diào)度,關(guān)聯(lián)的資源也釋放掉了,但是它本身占用的內(nèi)存還沒有釋放,
比如創(chuàng)建時(shí)分配的內(nèi)核棧,task_struct結(jié)構(gòu)等。這些由父進(jìn)程來釋放。
父進(jìn)程上的操作(release_task)
父進(jìn)程受到子進(jìn)程發(fā)送的exit_notify()信號(hào)后,將該子進(jìn)程的進(jìn)程描述符和所有進(jìn)程獨(dú)享的資源全部刪除。
從上面的步驟可以看出,必須要確保每個(gè)子進(jìn)程都有父進(jìn)程,如果父進(jìn)程在子進(jìn)程結(jié)束之前就已經(jīng)結(jié)束了會(huì)怎么樣呢?
子進(jìn)程在調(diào)用exit_notify()時(shí)已經(jīng)考慮到了這點(diǎn)。如果子進(jìn)程的父進(jìn)程已經(jīng)退出了,那么子進(jìn)程在退出時(shí),exit_notify()函數(shù)會(huì)先調(diào)用forget_original_parent(),然后再調(diào)用find_new_reaper()來尋找新的父進(jìn)程。
find_new_reaper()函數(shù)先在當(dāng)前線程組中找一個(gè)線程作為父親,如果找不到,就讓init做父進(jìn)程。(init進(jìn)程是在linux啟動(dòng)時(shí)就一直存在的)