生成內(nèi)核 生成內(nèi)核其實很簡單,甚至比編譯和安裝其它系統(tǒng)級組件,如glibc還要簡單,從2.6版本開始,Linux內(nèi)核引入了一個新的配置和生成系統(tǒng),它使生產(chǎn)內(nèi)核的操作變得更加簡單了。 配置內(nèi)核 既然已經(jīng)拿到內(nèi)核源代碼,那我們在開始編譯前就可以根據(jù)需要自行配置和定制,可以編譯你指定的功能和想要的驅(qū)動,配置內(nèi)核是生成內(nèi)核必須的一步,因為內(nèi)核提供了大量的功能,支持各種不同的硬件,有很多都需要配置,內(nèi)核配置是由配置選項控制的,配置選項都有CONFIG前綴,例如,對稱多處理(SMP)是由CONFIG_SMP配置選項配置的,如果設(shè)置了這個選項,SMP就被啟用了,反之則被禁用,配置選項可以確定會生成哪個文件,也可以通過預處理指令操控代碼。 配置選項可以控制生成過程要么是布爾型,要么是三態(tài)型,布爾型就是“是”或“否”,大部分內(nèi)核配置選項都屬于布爾型,如CONFIG_PREEMPT,而三態(tài)型則在“是”和“否”的基礎(chǔ)上,又增加一個“模塊”選項,模塊選項表示配置選項被設(shè)置了,但最后會編譯成模塊,而不是直接編譯進內(nèi)核,模塊可以理解為可獨立動態(tài)載入的對象,一般來說,驅(qū)動配置通常都是三態(tài)型。 配置選項也可以是字符串或整數(shù),這樣的選項不會控制生成過程,指定的值由內(nèi)核源代碼訪問預處理宏時使用,例如,可以為某個配置選項指定靜態(tài)分配數(shù)組的大小。 Linux廠商也會隨發(fā)行版提供預編譯的內(nèi)核,如Canonical為Ubuntu,或Red Hat為Fedora提供的內(nèi)核,這樣的內(nèi)核通常只啟用了需要的內(nèi)核功能,幾乎所有驅(qū)動都被編譯成模塊了,這樣的內(nèi)核提供了一個良好的基礎(chǔ)內(nèi)核和廣泛的硬件模塊支持,無論如何,想要成為內(nèi)核高手,你應該編譯自己的內(nèi)核。 值得慶幸的是,內(nèi)核提供了很多工具簡化配置 ,最簡單的工具是基于文本命令行的實用程序,如: $ make config 這個工具會一個選項一個選項地配置,但用戶需要參與,如指定“是(y)”,“否(m)”還是“模塊(m)”,整個配置過程需要很長的時間,因此,除非是有人按小時計費請你升級內(nèi)核,實在找不出別的理由用這種最原始的方法配置內(nèi)核了,相反,有現(xiàn)成的基于ncurses的圖形化工具可以代替。 $ make menuconfig 或是基于gtk+的圖形化工具 $ make gconfig 上述三個工具都將配置選項分成多個類別,如“處理器類型和特征”,你可以在這些類別上來回移動,查看內(nèi)核選項,當然也可以修改它們的設(shè)置了。 下面這個命令會根據(jù)你的架構(gòu)創(chuàng)建一個默認的配置基礎(chǔ)。 $ make defconfig 雖然默認配置有些武斷(在i386上,默認配置是由Linus配置的),但如果你從未配置過內(nèi)核,它提供了一個良好的開端。 配置選項存儲在源代碼樹根目錄下一個名叫.config的文件中,你可以打開這個文件手工編輯其中的配置選項,修改后或要在新的內(nèi)核源代碼樹上應用現(xiàn)有配置文件,你可以使用下面的命令驗證和更新配置: $ make oldconfig 在生成內(nèi)核之前必須運行這個命令。 配置選項CONFIG_IKCONFIG_PROC指定了完整的內(nèi)核配置文件壓縮包位置,默認是/proc/config.gz,這樣在生成新內(nèi)核時要克隆現(xiàn)有的配置就變得非常簡單了。如果你當前的內(nèi)核開啟了這個選項,你可以從/proc拷貝該配置文件,然后在此基礎(chǔ)上生成新的內(nèi)核: $ zcat /proc/config.gz > .config $ make oldconfig 內(nèi)核配置好后,使用下面的命令進行生成: $ make 和2.6以前的內(nèi)核不一樣,在生成內(nèi)核前不再需要執(zhí)行make dep命令了,依賴樹會自動維護,也不需要再指定特定的生成類型,如bzImage,或獨立生成模塊,默認Makefile規(guī)則會自動處理好一切。 將干擾信息最小化 在生成過程中會遭到警告和錯誤的干擾。最小化干擾信息的一個訣竅是重定向make的輸出,但仍然會看到一些警告和錯誤: $ make > ../detritus 如果你想查看生成輸出,你可以事后閱讀這個文件,如果你完全不想看到任何輸出,那么就重定向到/dev/null: $ make > /dev/null 同時執(zhí)行多個生成作業(yè) Make命令提供了一個功能可以將生成過程拆分成多個平行的作業(yè),這些作業(yè)可以獨立運行,也可以并行運行,在多處理器系統(tǒng)上可以極大地提高生成速度,也提高了處理器利用率,因為生成大型源代碼樹會出現(xiàn)大量的I/O等待時間。 默認情況下,make只能拆分成一個作業(yè),因為Makefiles常常會包含不正確的依賴信息,如果真是這樣,多個并行執(zhí)行的作業(yè)將會引起混亂,最終會導致生成過程失敗,如果Makefiles中的依賴信息無誤,那么完全可以拆分成多個作業(yè)執(zhí)行,如: $ make –jn 這里的n表示拆分的作業(yè)數(shù)量,通常按每個處理器拆分成1-2個作業(yè),例如,在一個16核心的機器上 ,你可以運行: $ make -j32 > /dev/null 使用distcc或ccache等優(yōu)秀的工具也可以大大提高生成速度。 安裝新內(nèi)核 內(nèi)核生成好之后,你需要安裝它,如何安裝于系統(tǒng)架構(gòu)和引導加載程序有關(guān),我們以x86架構(gòu),grub引導加載程序為例進行說明。 首先將arch/i386/boot/bzImage拷貝到/boot,重命名為vmlinuz- version,這里的version也是版本號,然后編輯/boot/grub/grub.conf,為新內(nèi)核添加相應的項目,如果是使用LILO引導裝載程序,則修改/etc/lilo.conf文件,然后運行l(wèi)ilo。 模塊的安裝與系統(tǒng)架構(gòu)無關(guān),都是自動完成的,以root用戶運行: % make modules_install 這個命令會將所有編譯好的模塊安裝到/lib/modules下對應的子目錄中。 生成過程會在源代碼樹根目錄下創(chuàng)建一個System.map文件,它包含一個符號查找表,映射內(nèi)核符號到它們的起始地址,在調(diào)試期間可以用它將內(nèi)存地址轉(zhuǎn)換成函數(shù)和變量名。 可能會遇到的問題 與普通用戶空間的應用程序相比,Linux內(nèi)核有多個特殊的屬性,下面是我認為最重要的一些不同: ◆內(nèi)核既不訪問C庫也不訪問標準C頭; ◆內(nèi)核是用GNU C編碼的; ◆內(nèi)核缺少用戶空間提供的內(nèi)存保護; ◆內(nèi)核不能容易地執(zhí)行浮點運算; ◆內(nèi)核有一個小型的固定大小的進程堆棧; ◆由于內(nèi)核支持異步中斷和SMP,因此同步和并發(fā)是內(nèi)核主要擔心的問題; ◆可移植性也很重要。 下面我們就逐個來了解一下這些問題,所有內(nèi)核開發(fā)人員都必須記住它們。
無libc或標準頭 和用戶空間應用程序不一樣,內(nèi)核并沒有鏈接到標準的C庫,也沒有鏈接到任何其它的庫,這樣設(shè)計的原因有很多,包括如先有雞還是先有蛋的問題,但主要原因還是速度和內(nèi)核大小,不要說完整的C庫,就是它的一個子集也夠大,內(nèi)核太大只會導致效率低下。 不要擔心,許多常用的libc函數(shù)都在內(nèi)核中實現(xiàn)了,例如,常見的字符串操作函數(shù)就位于lib/string.c中,只需要包括它的頭文件<linux/string.h >就可以了。 這里的頭文件指的是內(nèi)核源代碼樹中的頭文件,內(nèi)核也只能使用樹內(nèi)的頭文件,基礎(chǔ)文件位于源代碼根目錄的include/目錄下,例如,<linux/inotify.h>頭文件就位于include/linux/inotify.h。 與架構(gòu)相關(guān)的頭文件則位于arch/<architecture>/include/asm,例如,如果在x86架構(gòu)下編譯,與你架構(gòu)相關(guān)的文件就是arch/x86/include/asm,只需要在引用這些頭的地方加上asm/前綴即可,如<asm/ioctl.h>。 漏掉的大部分都是類似printf()這樣的函數(shù),內(nèi)核不會使用printf(),但它提供了printk()函數(shù),其表現(xiàn)絕不比printf()差,printk()會拷貝格式化的字符串到內(nèi)核日志緩沖區(qū),syslog程序就是從這里讀取信息的,其用法也和printf()類似: printk("Hello world! A string '%s' and an integer '%d'\n", str, i); printf()和printk()之間最大的不同是,printk()允許你指定一個優(yōu)先級標記,syslogd使用這個標記確定在哪里顯示內(nèi)核消息,下面是一個使用優(yōu)先級標記的示例: printk(KERN_ERR "this is an error!\n"); 注意在KERN_ERR和打印的消息之間沒有逗號,這是故意這么設(shè)計的,優(yōu)先級使用一個預定義的字符定義,在編譯期間它與打印的信息是串聯(lián)的。 GNU C 和許多Unix內(nèi)核類似,Linux內(nèi)核也是用C編寫的,但也許會讓人很意外,內(nèi)核不是用嚴謹?shù)腁NSI C編寫的,內(nèi)核開發(fā)人員用的卻是gcc(GNU編譯器集,包含了編譯內(nèi)核和Linux C程序的C編譯器)中的各種語言擴展。 內(nèi)核開發(fā)人員同時使用了C語言的ISO C99和GNU C擴展,這些變化讓Linux內(nèi)核與gcc結(jié)合得更緊密,但最近又出現(xiàn)了一個編譯器 – 英特爾的C編譯器 – 也對gcc的功能支持得相當好,因此也可以用它來編譯Linux內(nèi)核。最低支持的gcc版本是3.2,建議采用gcc 4.4或更高的版本編譯。使用ISO C99擴展也是可以的,因為C99是C語言的官方版本。 內(nèi)聯(lián)函數(shù) C99和GNU C都支持內(nèi)聯(lián)函數(shù),內(nèi)聯(lián)函數(shù)是直接插入到每個函數(shù)調(diào)用的位置的,消除了函數(shù)調(diào)用和返回的開銷,允許進一步優(yōu)化,因為編譯器可以同時優(yōu)化調(diào)用者和被調(diào)用函數(shù),但它也有缺點,代碼大小會增加,因為函數(shù)的內(nèi)容被直接復制到所調(diào)用者內(nèi)部了,因此也會增加內(nèi)存消耗和指令緩存空間。內(nèi)核開發(fā)人員一般在小型時間很關(guān)鍵的函數(shù)中才會使用內(nèi)聯(lián)函數(shù)。 定義函數(shù)時,使用static和inline關(guān)鍵字聲明內(nèi)聯(lián)函數(shù),例如: static inline void wolf(unsigned long tail_size) 函數(shù)必須先聲明后使用,否則編譯器就不能使函數(shù)內(nèi)聯(lián),一般做法是將內(nèi)聯(lián)函數(shù)放在頭文件中,因為它們被標記為static,不會創(chuàng)建輸出函數(shù),如果內(nèi)聯(lián)函數(shù)僅在一個文件中使用,可以放在該文件的頂部。 在內(nèi)核中,與復雜的宏相比,出于安全和可讀性方面考慮,內(nèi)聯(lián)函數(shù)是首選。 內(nèi)聯(lián)匯編 Gcc C編譯器允許在C函數(shù)中嵌入?yún)R編指令,asm()編譯器指令用于內(nèi)聯(lián)匯編代碼,例如,這個內(nèi)聯(lián)匯編指令執(zhí)行x86處理器的rdtsc指令,返回時間戳寄存器(tsc)的值: unsigned int low, high; Linux內(nèi)核是用C和匯編語言混合編寫的,與底層硬件相關(guān)的代碼很多都是用匯編語言寫的,剩下的大部分內(nèi)核代碼都是直接用C編寫的。 分支注解 Gcc C編譯器內(nèi)置了一個指令優(yōu)化條件分支,內(nèi)核將這個打包成易于使用的宏 - likely()和unlikely()。 先看下面這樣的if語句: if (error) { /* ... */ } 將這個分支標記為非常不可能采用 /* we predict 'error' is nearly always zero ... */ if (unlikely(error)) { /* ... */ } 相反,將這個分支標記為非常可能采用 /* we predict 'success' is nearly always nonzero ... */ if (likely(success)) { /* ... */ } 當分支指令已經(jīng)知道一個優(yōu)先級,或你想在一種情況下優(yōu)化另一種情況時應該使用上述指令,最重要的是,當分支正確標記時,這些指令會提升性能,但如果分支標記錯誤則會降低性能,在內(nèi)核代碼中,unlikely()要使用得更多,因為if語句傾向于表示一種特殊情況。 無內(nèi)存保護 當用戶空間的應用程序嘗試一個非法的內(nèi)存訪問時,內(nèi)核可以捕捉到錯誤,發(fā)送SIGSEGV信號,殺掉進程,如果內(nèi)核嘗試一個非法的內(nèi)存訪問時,結(jié)果就不受控制了,因為誰也無法去控制內(nèi)核,這也是內(nèi)核最主要的失誤。 此外,內(nèi)核內(nèi)存也是不可分頁的,因此你消耗的每個內(nèi)存字節(jié)都比物理內(nèi)存的一個字節(jié)要少。 不能(容易)使用浮點數(shù) 當用戶空間進程使用浮點指令時,內(nèi)核要負責處理從整型到浮點模式的轉(zhuǎn)換。 與用戶空間不一樣,內(nèi)核不能無縫支持浮點數(shù),因為它自己不能輕易地捕捉到自己,在內(nèi)核中使用浮點數(shù)需要手動保存和恢復浮點數(shù)寄存器,因此除非卻有必要,否則盡量不要在內(nèi)核中做浮點運算。 小型,固定大小的堆棧 用戶空間可以靜態(tài)分配許多不同的堆棧,包括巨型結(jié)構(gòu)和千元數(shù)組,這個行為是合法的,因為用戶空間有很大的堆棧,并可以動態(tài)增長。 內(nèi)核堆棧不大也不是動態(tài)的,相反,它很小且是固定的,內(nèi)核堆棧的精確大小根據(jù)架構(gòu)有所不同,在x86上,堆棧大小是在編譯時確定的,一般是4KB或8KB,歷史上,內(nèi)核堆棧有2頁,通常表示它處于32位架構(gòu)上,大小是8KB,如果是16KB就表示是64位架構(gòu),總之大小是固定的,每個進程接收它自己的堆棧。 同步和并發(fā) 內(nèi)核最容易受競爭條件影響,和一個單線程的用戶空間應用程序不一樣,有許多內(nèi)核特性允許同時訪問共享資源,因此需要同步以防止競爭,特別是: ◆Linux是一種搶占式多任務操作系統(tǒng),進程是由內(nèi)核的進程調(diào)度器隨意調(diào)度和再次調(diào)度的,內(nèi)核必須在這些任務之間同步; ◆Linux支持對稱多處理(SMP),因此,如果沒有適當?shù)谋Wo,在兩個或多個處理器上同時執(zhí)行的內(nèi)核代碼可能會同時訪問相同的資源; ◆中斷是異步發(fā)生的,因此,如果沒有適當?shù)谋Wo,在訪問資源期間也可能發(fā)生中斷,中斷處理程序可能就會訪問到相同的資源; ◆Linux是有優(yōu)先權(quán)的,因此,如果沒有適當?shù)谋Wo,內(nèi)核代碼可能會優(yōu)先執(zhí)行,訪問其它代碼正在使用的資源。 解決這些問題的一般方法是自旋鎖和信號量。 可移植性的重要性 雖然用戶空間應用程序一般不會太重視可移植性,但Linux的確是一個可移植性操作系統(tǒng),應該保持一致,這意味著與架構(gòu)無關(guān)的C代碼必須在大量的系統(tǒng)上正確地編譯和運行,與架構(gòu)相關(guān)的代碼必須在內(nèi)核源代碼樹中使用特定的目錄分隔開。 總結(jié) 可以肯定,內(nèi)核有它獨特的性質(zhì),它有它自己的一些原則,不過,內(nèi)核的復雜性和障礙與其它大型軟件項目相比,并沒有什么大的不同,Linux開發(fā)道路上最重要的一步是認識到內(nèi)核并不可怕,不熟悉?當然!不可逾越?當然不是! |
|