內核編譯與進程管理
內核與操作系統
由於商業操作系統設計上日益龐雜,「操作系統」的概念對很多人而言變得含糊不清。在進一步討論Linux內核的話題前,我們先區分「內核」與「操作系統」這兩個概念。
操作系統:指在整個系統中完成最基本功能和系統管理的部分,包括內核、設備驅動、文件管理工具、系統管理工具、shell命令行或其他用戶界面(gnome/KDE等)
內核:是操作系統的核心,完成進程管理、cpu調度、內存管理、中斷處理等功能
一般我們編寫的應用程序,跑在操作系統上,完成文字編輯、音樂播放、網頁遊覽等特定功能。
內核編譯
內核源碼一般放在/usr/src目錄下,我們也可以從網上獲取所需內核版本的源碼包。編譯內核的第一步是配置內核功能,例如配置是否支持對稱多處理器(SMP),可通過設置CONFIG_SMP的值。
通常我們使用"make menuconfig"命令進行配置,其提供了友好的配置界面:
保存配置後,源碼目錄下將生成.config配置文件,打開該文件,可以看到其內容為各種選項設置:CONFIG_X86_64=ynCONFIG_64BIT=ynCONFIG_X86=ynCONFIG_SEMAPHORE_SLEEPERS=ynCONFIG_MMU=yn……n
我們也可以使用當前的內核配置,使用以下命令快速地生成.config文件:
zcat /proc/config.gz > .config n
之後根據.config配置,對源碼進行編譯:
make -j4n
以上使用-j選項,指定並行編譯工作任務數目,在多核環境下,減少了編譯時間。
編譯完成後生成內核壓縮鏡像:
make bzImagen
生成的內核壓縮鏡像文件位於 arch/x86/boot目錄下:
linux-2.6.32.59 # ll arch/x86/boot/bzImagen-rw-r--r-- 1 root root 2814112 07-02 22:27 arch/x86/boot/bzImagen
接著安裝內核模塊:
make modules_installn
新的模塊會被放置在/lib/modules目錄下:
/lib/modules # lln總計 8ndrwxr-xr-x 4 root root 4096 03-08 23:53 2.6.32.12-0.7-defaultndrwxr-xr-x 3 root root 4096 07-02 23:31 2.6.32.59-0.7-defaultn
最後執行make install安裝內核,在/boot目錄下將生成System.map、vmlinuz和initrd文件:
linux-2.6.32.59 # make installnsh /home/lx/kernel/linux-2.6.32.59/arch/x86/boot/install.sh n2.6.32.59-0.7-default arch/x86/boot/bzImagenSystem.map "/boot"nKernel image: /boot/vmlinuz-2.6.32.59-0.7-defaultnInitrd image: /boot/initrd-2.6.32.59-0.7-defaultn……n
完成安裝後,在/boot/grub/menu.lst文件中增加了新內核相應的啟動項,我們可以修改該文件,指定系統啟動後使用新編譯的內核。
進程與線程
Linux下,進程與線程的最大不同是進程擁有獨立的內存地址空間,而線程與其他線程共享內存地址空間。除此之外,進程與線程的實現基本相同,都有task_struct結構,都被分配PID。
內核線程沒有獨立的地址空間,它們完成特定工作並接受內核的調度,不同於一般用戶進程,它們不接收kill命令發送的信號:
F S UID PID PPID C PRI NI ADDR SZ WCHAN STIME TTY TIME CMDn1 S root 2 1 0 -40 - - 0 migrat Jul01 ? 00:00:00 [migration/0]n1 S root 3 1 0 94 19 - 0 ksofti Jul01 ? 00:00:00 [ksoftirqd/0]n5 S root 18 1 0 70 -5 - 0 worker Jul01 ? 00:00:00 [events/0]n……n
task_struct
task_struct結構包含進程使用的虛擬內存、打開的文件、進程狀態、進程pid等信息,佔用的內存由slab分配,在文件中定義。thread_info結構的第一個欄位為task_struct類型的指針,當進程創建時,thread_info存放在進程內核棧的頂部:
current全局變數指向當前運行進程的task_struct結構,由於thread_info存放的位置固定,這樣我們通過以下彙編指令就能很容易地計算出current的值:
movl $-8192, %eaxnandl $esp, %eaxn
進程狀態
進程可處於以下幾種狀態:
RUNNING nINTERRUPTABLE nUNINTERRUPTABLEnSTOP nZOMBIEn
這些進程狀態作為宏,在sched.h文件中被定義。
- RUNNING狀態表示進程是可執行的,或正在執行或在運行隊列中,這些進程佔用或等待cpu資源。
- 進程調用退出函數exit後,進程中止,進入ZOMBIE狀態。在ZOMBIE狀態下進程不會佔用cpu,但因其task_struct結構尚未釋放,仍佔用一點內存,直到父進程調用wait函數接受子進程遺願,假如父進程先於子進程退出,則由init進程接受子進程遺願。如果一個進程長期處於ZOMBIE狀態,則是父進程中未調用wait,為程序編碼問題。
- UNINTERRUPTABLE狀態表示進程不可中斷,處於此狀態的進程處於內核態,並且不接收任何信號。
設置進程狀態的函數為set_task_state函數,在文件中定義。
進程間關係
進程間關係與目錄結構一樣,為樹狀結構,目錄結構以/為根,而進程關係以init為根。我們可以使用pstree查看進程間關係:
linux-14:~ # echo $$n10939nlinux-14:~ # pstree -G -p 10939nbash(10939)─┬─pstree(12806)n └─sh(12796)───sleep(12801)n
內核代碼中提供了一條雙向閉環鏈表,自init進程始,鏈表連接了所有進程的task_struct結構,可以通過for_each_process宏遍歷系統的所有進程:
#define for_each_process(p) n for (p = &init_task ; (p = next_task(p)) != &init_task ; )n
進程創建
Linux kernel將進程創建的步驟分成兩步:fork和exec。fork生成子進程的pid,將父進程執行上下文、打開的文件描述符等內容複製一份給子進程;exec將子進程自己的執行上下文載入進內存地址空間。有以下fork例子,問執行該程序將輸出多少個1?
#include <stdio.h>n#include <unistd.h>nint main()n{n int i;n for(i=0 ; i < 10; i++)n {n fork();n }n printf("%dn", 1);n return 0;n}n
fork拷貝父進程的內容到子進程,開銷較大,假若調用fork之後馬上調用exec,子進程載入自己的執行文件,則拷貝的動作就是多餘的。寫時拷貝(copy-on-write,COW)解決了拷貝帶來無謂開銷的問題,在子進程寫父進程地址空間時,才觸發拷貝的動作。不做多餘事情、非到不得已的時候才完成工作,這也是Linux kernel高效的原因之一。
內核中do_fork函數完成fork調用的工作,do_fork調用copy_process。copy_process函數中主要完成以下工作:
- 調用dup_task_struct函數申請新進程的task_struct、thread_info結構
- 根據clone_flags標誌,調用copy_files、copy_fs、copy_mm等函數完成文件、文件系統、內存等信息的拷貝
- 調用alloc_pid申請新進程pid
fork返回兩次,在do_fork函數中實現。
進程中止
最終進程會調用exit函數中止,exit系統調用最終會調用內核中do_exit函數,do_exit在中定義,其完成以下工作:
- 調用exit_signals設置進程flags標誌為PF_EXITING
- 調用exit_mm、exit_files、exit_fs等函數釋放進程內存、文件、文件系統等結構
- 設置進程的exit_code
- 調用exit_notify,向當前進程的父進程、子進程發送信號,告知當前進程將要中止,並設置當前進程退出狀態exit_state為EXIT_DEAD或EXIT_ZOMBIE
- 調用schedule,切換到另一個進程,從do_exit函數不會返回到調用它的函數
Reference: Chapter 1 to chapter 3, Linux kernel development.3rd.Edition
推薦閱讀:
※基於Linux開發的操作系統有哪些?安卓是基於哪個Linux版本開發的呢?
※計算機行業領先的公司大多在美國,Linux為什麼卻是芬蘭人發明的?
※為何linux作為伺服器端十年不重啟都不卡而安卓用半年就十分卡?
※為什麼微內核系統在PC不如宏內核普及?
※多個socket同時發送數據,網卡是輪流發送每個socket數據包嗎?每個包多大?