init目錄下的main.c文件的start_kerne函數(shù)相當(dāng)于普通C程序的main函數(shù),是內(nèi)核的初始化的起點。
Linux內(nèi)核的核心代碼在kernel目錄中:
- ipc目錄:進(jìn)程間通信
- mm:內(nèi)存管理
- net:網(wǎng)絡(luò)相關(guān)
...
rest_init從start_kernel一啟動的時候便一直存在,稱為0號進(jìn)程。0號進(jìn)程即最終的idle進(jìn)程(init task即手工創(chuàng)建的PCB)。當(dāng)系統(tǒng)沒有進(jìn)程需要執(zhí)行時就調(diào)度到idle進(jìn)程。不管分析內(nèi)核的哪一部分都會涉及到 start_kernel,所有的模塊都需要調(diào)用start_kernel來完成初始化。0號進(jìn)程創(chuàng)建了1號進(jìn)程kernel_thread,是第一個用戶態(tài)進(jìn)程,即 init 進(jìn)程,是用戶態(tài)所有進(jìn)程的祖先,然后,新建
kthreadd進(jìn)程,是內(nèi)核態(tài)所有進(jìn)程的祖先。最后,通過cpu_startup_entry函數(shù)啟動 0 號進(jìn)程。
一般現(xiàn)代CPU都有幾種不同的指令執(zhí)行級別:
在高執(zhí)行級別下,代碼可以執(zhí)行特權(quán)指令,訪問任意的物理地址,這種CPU執(zhí)行級別就對應(yīng)著內(nèi)核態(tài),而在相應(yīng)的低級別執(zhí)行狀態(tài)下,代碼的掌控范圍會受到限制。只能在對應(yīng)級別允許的范圍內(nèi)活動。
舉例:
intel x86 CPU有四種不同的執(zhí)行級別0-3(ring0-ring3),Linux只使用了其中的0級和3級分別來表示內(nèi)核態(tài)和用戶態(tài)。
Q:為什么有權(quán)限級別的劃分?
A:為了保護(hù)系統(tǒng),為了程序員把系統(tǒng)搞崩潰。資深程序員寫的更健壯。
- cs寄存器的最低兩位表明了當(dāng)前代碼的特權(quán)級
- CPU每條指令的讀取都是通過cs:eip這兩個寄存器:其中cs是代碼段選擇寄存器,eip是偏移量寄存器。
- 上述判斷由硬件完成,一般來說在Linux中,地址空間是一個顯著的標(biāo)志:
0xc0000000以上的地址空間只能在內(nèi)核態(tài)下訪問,0x00000000-0xbfffffff的地址空間在兩種狀態(tài)下都可以訪問- 注意:這里所說的地址空間是邏輯地址而不是物理地址
中斷處理是用戶態(tài)進(jìn)入內(nèi)核態(tài)的主要方式。
系統(tǒng)調(diào)用時一種特殊的中斷。中斷發(fā)生后的第一件事就是保存現(xiàn)場。處理完后的第一件事就是恢復(fù)現(xiàn)場。
系統(tǒng)調(diào)用和API(應(yīng)用編程接口)的關(guān)系:
- API只是一個函數(shù)的定義
- 系統(tǒng)調(diào)用通過軟中斷(trap)向內(nèi)核發(fā)出一個明確的請求(是一個中斷)
不是每個API都對應(yīng)一個特定的系統(tǒng)調(diào)用:
API可能直接提供用戶態(tài)的服務(wù)
如,一些數(shù)學(xué)函數(shù)一個單獨的API可能調(diào)用幾個系統(tǒng)調(diào)用
不同的API可能調(diào)用了同一個系統(tǒng)調(diào)用
系統(tǒng)調(diào)用返回值:大部分封裝例程返回一個整數(shù),其值的含義依賴于相應(yīng)的系統(tǒng)調(diào)用,-1在多數(shù)情況下表示內(nèi)核不能滿足進(jìn)程的請求。
系統(tǒng)調(diào)用的三層皮:
- API(函數(shù)接口,e.g:xyz())
- system_call(系統(tǒng)調(diào)用)
- sys_xyz(中斷服務(wù)程序)
當(dāng)用戶態(tài)進(jìn)程調(diào)用一個系統(tǒng)調(diào)用時,CPU切換到內(nèi)核態(tài)并開始執(zhí)行一個內(nèi)核函數(shù)。 在Linux中是通過執(zhí)行int $ 0x80來執(zhí)行系統(tǒng)調(diào)用的, 這條匯編指令產(chǎn)生向量為128的編程異常。
傳參: 內(nèi)核實現(xiàn)了很多不同的系統(tǒng)調(diào)用, 進(jìn)程必須指明需要哪個系統(tǒng)調(diào)用,這需要傳遞一個名為系統(tǒng)調(diào)用號的參數(shù)——使用eax寄存器。
寄存器傳遞參數(shù)具有如下限制:
- 每個參數(shù)的長度不能超過寄存器的長度,即32位
- 在系統(tǒng)調(diào)用號(eax)之外,參數(shù)的個數(shù)不能超過6個(ebx, ecx,edx,esi,edi,ebp)
超過6個怎么辦?會將某一個寄存器作為一個指針,指向一塊內(nèi)存,進(jìn)入內(nèi)核態(tài)之后就可以訪問所有的地址空間,將通過這塊內(nèi)存來傳遞。
初始化的時候就把int 0x80與system_call綁定起來啦,通過中斷向量來匹配起來的。
系統(tǒng)調(diào)用機制的初始化:
SAVE_ALL:保存現(xiàn)場
call *sys_call_table(, %eax,4)作用為調(diào)用系統(tǒng)調(diào)用處理函數(shù) ,eax傳遞系統(tǒng)調(diào)用號
irq_return:系統(tǒng)調(diào)用過程結(jié)束
call schedule:進(jìn)程調(diào)度的代碼
restore_all:恢復(fù)現(xiàn)場
OS的三大功能:
- 進(jìn)程管理(核心)
- 內(nèi)存管理
- 文件系統(tǒng)
進(jìn)程控制塊 :PCB——task_struct
為了管理進(jìn)程,內(nèi)核必須對每個進(jìn)程進(jìn)行清晰的描述,進(jìn)程描述符提供了內(nèi)核所需了解的進(jìn)程信息。
struct task_struct數(shù)據(jù)結(jié)構(gòu)很龐大
Linux進(jìn)程的狀態(tài)與操作系統(tǒng)原理中的描述的進(jìn)程狀態(tài)似乎有所不同,比如就緒狀態(tài)和運行狀態(tài)都是TASK_RUNNING,為什么呢?
進(jìn)程的標(biāo)示pid
所有進(jìn)程鏈表struct list_head tasks; (雙向鏈表)
內(nèi)核的雙向循環(huán)鏈表的實現(xiàn)方法
程序創(chuàng)建的進(jìn)程具有父子關(guān)系,在編程時往往需要引用這樣的父子關(guān)系。進(jìn)程描述符中有幾個域用來表示這樣的關(guān)系
Linux為每個進(jìn)程分配一個8KB大小的內(nèi)存區(qū)域,用于存放該進(jìn)程兩個不同的數(shù)據(jù)結(jié)構(gòu):Thread_info和進(jìn)程的內(nèi)核堆棧
進(jìn)程處于內(nèi)核態(tài)時使用,不同于用戶態(tài)堆棧,即PCB中指定了內(nèi)核棧,那為什么PCB中沒有用戶態(tài)堆棧?用戶態(tài)堆棧是怎么設(shè)定的?
內(nèi)核控制路徑所用的堆棧很少,因此對棧和Thread_info來說,8KB足夠了struct thread_struct thread; //CPU-specific state of this task
文件系統(tǒng)和文件描述符
內(nèi)存管理——進(jìn)程的地址空間
再談系統(tǒng)調(diào)用:
fork()是在用戶態(tài)創(chuàng)建子進(jìn)程的一個系統(tǒng)調(diào)用。 fork系統(tǒng)調(diào)用在父進(jìn)程和子進(jìn)程各返回一次。 所以下面的else if和else可能都會被執(zhí)行,這并不是不遵循if-else結(jié)構(gòu),而是對應(yīng)了多個進(jìn)程。
創(chuàng)建新進(jìn)程是通過復(fù)制當(dāng)前進(jìn)程來實現(xiàn)的(父進(jìn)程的部分信息),復(fù)制完父進(jìn)程之后再進(jìn)行必要的修改(修改PCB,堆棧等),為子進(jìn)程初始化。初始化時拷貝內(nèi)核堆棧數(shù)據(jù)和指定新進(jìn)程的第一條指令地址(修改ip和sp)。系統(tǒng)調(diào)用內(nèi)核函數(shù)sys_fork,sys_clone(fork使用的系統(tǒng)調(diào)用),sys_vfork來創(chuàng)建新的進(jìn)程,他們都是調(diào)用的do_fork。do_fork里面有一個copy_process,里面就是創(chuàng)建進(jìn)程內(nèi)容的主要代碼。
fork出的子進(jìn)程是從哪里開始執(zhí)行的?
fork()函數(shù)通過系統(tǒng)調(diào)用創(chuàng)建一個與原來進(jìn)程幾乎完全相同的進(jìn)程,也就是兩個進(jìn)程可以做完全相同的事,但如果初始參數(shù)或者傳入的變量不同,兩個進(jìn)程也可以做不同的事。
一個進(jìn)程調(diào)用fork()函數(shù)后,系統(tǒng)先給新的進(jìn)程分配資源,例如存儲數(shù)據(jù)和代碼的空間。然后把原來的進(jìn)程的所有值都復(fù)制到新的新進(jìn)程中,只有少數(shù)值與原來的進(jìn)程的值不同。相當(dāng)于克隆了一個自己。
由fork函數(shù)創(chuàng)建的新進(jìn)程被稱為子進(jìn)程。fork函數(shù)被調(diào)用一次,但是返回兩次。父進(jìn)程返回的值是新進(jìn)程的進(jìn)程ID,而子進(jìn)程返回的值是0。
子進(jìn)程執(zhí)行代碼開始位置
fork確實創(chuàng)建可一個子進(jìn)程并完全復(fù)制父進(jìn)程,但是子進(jìn)程是從fork后面到那個指令開始執(zhí)行。如果子進(jìn)程也從main開頭到尾執(zhí)行所有指令,那么它執(zhí)行到fork指令時也必定會創(chuàng)建一個個子子進(jìn)程,子子孫孫無窮盡。
常見的兩種應(yīng)用場景
- 一個父進(jìn)程希望復(fù)制自己,使父、子進(jìn)程同時執(zhí)行不同的代碼段。這在網(wǎng)絡(luò)服務(wù)中是常見的——父進(jìn)程等待客戶端的服務(wù)請求,當(dāng)這種請求到達(dá)時,父進(jìn)程調(diào)用fork,使子進(jìn)程處理此請求。父進(jìn)程則繼續(xù)等待下一個服務(wù)請求的到達(dá)
- 一個進(jìn)程要執(zhí)行一個不同的程序。這是shell中常見的情況,子進(jìn)程從fork返回后立即調(diào)用exec
fork函數(shù)返回值的三種情況
返回子進(jìn)程Id給父進(jìn)程
因為一個進(jìn)程的子進(jìn)程可能有多個,并且沒有一個函數(shù)可以獲得一個進(jìn)程的所有子進(jìn)程ID。
返回給子進(jìn)程值為0
一個進(jìn)程只會有一個父進(jìn)程,所以子進(jìn)程總是可以調(diào)用getpid以獲得當(dāng)前進(jìn)程Id以及調(diào)用getppid獲得父進(jìn)程Id.
出現(xiàn)錯誤,返回負(fù)值
- 當(dāng)前進(jìn)程數(shù)已經(jīng)達(dá)到系統(tǒng)規(guī)定的上限,這時errno的值被設(shè)置為EAGAIN
- 系統(tǒng)內(nèi)存不足,這時errno的值被設(shè)置為ENOMEM
創(chuàng)建新進(jìn)程成功后,系統(tǒng)中出現(xiàn)兩個基本完全相同的進(jìn)程,這兩個進(jìn)程執(zhí)行沒有固定的先后順序,哪個進(jìn)程先執(zhí)行要看系統(tǒng)的進(jìn)程調(diào)度策略
子進(jìn)程運行之后且返回用戶態(tài)之前會發(fā)生進(jìn)程調(diào)度嗎?
當(dāng)程序執(zhí)行完成,子進(jìn)程使用exit()系統(tǒng)調(diào)用終止。exit()會釋放進(jìn)程的大部分?jǐn)?shù)據(jù)結(jié)構(gòu),并且把這個終止的消息通知給父進(jìn)程。這時候,子進(jìn)程被稱為zombie process(僵尸進(jìn)程)。直到父進(jìn)程通過wait()系統(tǒng)調(diào)用知悉子進(jìn)程終止之前,子進(jìn)程都不會被完全的清除。一旦父進(jìn)程知道子進(jìn)程終止,它會清除子進(jìn)程的所有數(shù)據(jù)結(jié)構(gòu)和進(jìn)程描述符。
僵尸進(jìn)程和孤兒進(jìn)程有什么區(qū)別、如何處理?
區(qū)別:僵尸進(jìn)程占用一個進(jìn)程ID號,占用資源,危害系統(tǒng)。但孤兒進(jìn)程與僵尸進(jìn)程不同的是,由于父進(jìn)程已經(jīng)死亡,子系統(tǒng)會幫助父進(jìn)程回收處理孤兒進(jìn)程。所以孤兒進(jìn)程實際上是不占用資源的,因為它最終是被系統(tǒng)回收了,不會像僵尸進(jìn)程那樣占用ID,損害運行系統(tǒng)。
1)僵尸進(jìn)程:一個進(jìn)程使用fork創(chuàng)建子進(jìn)程,如果子進(jìn)程退出,而父進(jìn)程沒有調(diào)用wait或者waitpid獲取子進(jìn)程的狀態(tài),那么子進(jìn)程的進(jìn)程描述符仍然保存在系統(tǒng)中,這種進(jìn)程稱為僵尸進(jìn)程。
2)孤兒進(jìn)程:一個父進(jìn)程退出,而他的一個或者多個子進(jìn)程還在運行,那么那些子進(jìn)程稱為孤兒進(jìn)程。孤兒進(jìn)程將被init(進(jìn)程號為1)收養(yǎng),并由init進(jìn)程對它們完成狀態(tài)收集的工作。子進(jìn)程的死亡需要父進(jìn)程來處理,所以正常的進(jìn)程應(yīng)該是子進(jìn)程先于父進(jìn)程死亡,當(dāng)父進(jìn)程先于子進(jìn)程死亡時,子進(jìn)程死亡沒有父進(jìn)程處理,這個死亡的子進(jìn)程就是孤兒進(jìn)程。簡單來說。
僵尸進(jìn)程:父進(jìn)程沒死,子進(jìn)程死了,但是父進(jìn)程不幫他收尸(通過wait,waitpid獲取其狀態(tài)),所以變成僵尸。
孤兒進(jìn)程:父進(jìn)程死了,子進(jìn)程沒死,子進(jìn)程成了孤兒,只能被孤兒院(init)收養(yǎng)。怎樣避免僵尸進(jìn)程呢?
單獨一個線程wait子進(jìn)程,或者,有兩個信號,一個SIGCHLD、一個SIGCLD,設(shè)置這兩個信號的處理方式為忽略,它們告訴內(nèi)核,不關(guān)心子進(jìn)程結(jié)束的狀態(tài)所以當(dāng)子進(jìn)程終止的時候直接釋放所有資源就行。它們的區(qū)別是SIGCLD在安裝完信號處理函數(shù)的時候還會檢查是否已經(jīng)存在結(jié)束的子進(jìn)程,如果有就調(diào)用信號處理函數(shù),而SIGCHLD不會,也就是可能會丟掉已經(jīng)有子進(jìn)程已經(jīng)結(jié)束這個事實












