這年頭,Linux成了一個時髦詞。自詡對電腦玩的精通的學生和IT人士們,沒有哪個不在自己的電腦上安裝一、兩個Linux,並自覺趕上了時髦。然而,在Ubuntu或SUSE的論壇中,經常有這樣的對話:
“你學Linux學了這麼久,都學到了什嗎?”
“哦,我現在Linux的安裝、升級、案頭美化都很熟練!你看我這是最新版的Ubuntu,案頭很漂亮吧!”
“……”
Linux社區中有一句名言:如果你進入你的作業系統不知道該做什麼,那最好還是關掉電腦,一定有更重要的事等著你去做。說真的,如果對Linux命令不熟練,真的不能算是學過Linux。然而另一方面,Linux核心雖然是一般使用者可學可不學的內容,但可以說卻是Linux作業系統中最好玩的部分。尤其對於開發人員而言,Linux核心開發絕對是最理想的磨練場所。51CTO編輯一直認為,國外之所以IT技術大拿林立,和他們從小接觸類UNIX系統、把玩核心開發是脫不了關係的。
下面是Linux核心開發人員Robert Love寫的一篇入門文章,號稱“包教會”,推薦對Linux核心開發感興趣的學生、Linux愛好者、開發人員以及系統管理員們一定不要錯過。當然,雖然標題說是包教會,你可能需要一定的Linux命令以及C語言的基礎。
以下是本文內容:
Linux核心一直都被視為學習Linux最難的一塊,相信大家也一定看過不少關於核心的文章,但捫心自問,你現在究竟掌握了多少?本文將從零開始介紹被視為高深的Linux核心,內容涉及核心原始碼的下載,編譯,安裝,以及核心開發相關的內容。
如何擷取Linux核心原始碼
下載Linux核心當然要去官方網站了,網站提供了兩種檔案下載,一種是完整的Linux核心,另一種是核心增量補丁,它們都是tar歸檔壓縮包。除非你有特別的原因需要使用舊版本的Linux核心,否則你應該總是升級到最新版本。
使用Git
由Linus領頭的核心開發隊伍從幾年前就開始使用Git版本控制系統管理Linux核心了(參考閱讀:什麼是Git?),而Git項目本身也是由Linus建立的,它和傳統的CVS不一樣,Git是分布式的,因此它的用法和工作流程很多開發人員可能會感到很陌生,但我強烈建議使用Git下載和管理Linux核心原始碼。
你可以使用下面的Git命令擷取Linus核心代碼樹的最新“推送”版本:
$ git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
然後使用下面的命令將你的代碼樹與Linus的代碼樹最新狀態同步:
$ git pull
安裝核心原始碼
核心包有GNU zip(gzip)和bzip2格式。Bzip2是預設和首選格式,因為它的壓縮比通常比gzip更好,bzip2格式的Linux核心包一般採用linux-x.y.z.tar.bz2形式的檔案名稱,這裡的x.y.z是核心原始碼的具體版本號碼,下載到原始碼包後,解壓和抽取就很簡單了,如果你下載的是bzip2包,運行:
$ tar xvjf linux-x.y.z.tar.bz2
如果你下載的是gzip包,則運行:
$ tar xvzf linux-x.y.z.tar.gz
無論執行上面哪一個命令,最後都會將原始碼解壓和抽取到linux-x.y.z目錄下,如果你使用Git下載和管理核心原始碼,你不需要下載tar包,只需要運行git clone命令,它就會自動下載和解壓。
核心原始碼通常都會安裝到/usr/src/linux下,但在開發的時候最好不要使用這個原始碼樹,因為針對你的C庫編譯的核心版本通常也連結到這裡的。
應用補丁
Linux核心開發人員會將自己的修改做成補丁與其它人員分享,而且補丁是增量的,增量補丁是從一個核心樹移動到另一個核心樹的有效方法,不用下載完整的核心包就可以升級核心,不僅可節省頻寬,也節省了核心升級時間,應用補丁之前先進入核心原始碼樹所在目錄,然後運行:
$ patch –p1 < ../patch-x.y.z
注意,補丁包也有明確的版本號碼,這裡的版本號碼與Linux核心原始碼的版本號碼要一致,核心和補丁版本號碼不一致時,強制應用補丁會引起意想不到的後果。
核心原始碼樹介紹
核心原始碼樹分為許多目錄,它們下面又包含許多子目錄,原始碼樹的頂級目錄及其描述參見下表。
| 目錄 |
描述 |
| arch |
特定架構的原始碼 |
| block |
塊I/O層 |
| crypto |
加密API |
| Documentation |
核心原始碼文檔 |
| drivers |
裝置驅動 |
| firmware |
使用某個驅動需要的裝置韌體 |
| fs |
VFS和獨立檔案系統 |
| include |
核心頭 |
| init |
核心啟動和初始化 |
| ipc |
處理序間通訊 |
| kernel |
核心子系統,如調度器 |
| lib |
助手例行程式 |
| mm |
記憶體管理子系統和VM |
| net |
網路子系統 |
| samples |
樣本,示範代碼 |
| scripts |
用於產生核心的指令碼 |
| security |
Linux安全模組 |
| sound |
聲音子系統 |
| usr |
早期的使用者空間代碼(叫做initramfs) |
| tools |
輔助Linux開發的工具 |
| virt |
虛擬化基礎設施 |
在原始碼樹的根目錄下還有很多檔案需要說明,COPYING是核心許可描述檔案(即GNU GPL v2),CREDITS是參與Linux核心的開發人員名單,MAINTAINERS列出了維護各個子系統和驅動的個人,Makefile是核心Makefile的基礎。
產生核心
產生核心其實很簡單,甚至比編譯和安裝其它系統級組件,如glibc還要簡單,從2.6版本開始,Linux核心引入了一個新的配置和產生系統,它使生產核心的操作變得更加簡單了。
配置核心
既然已經拿到核心原始碼,那我們在開始編譯前就可以根據需要自行配置和定製,可以編譯你指定的功能和想要的驅動,配置核心是產生核心必須的一步,因為核心提供了大量的功能,支援各種不同的硬體,有很多都需要配置,核心配置是由配置選項控制的,配置選項都有CONFIG首碼,例如,對稱式多處理(SMP)是由CONFIG_SMP配置選項配置的,如果設定了這個選項,SMP就被啟用了,反之則被禁用,配置選項可以確定會產生哪個檔案,也可以通過預先處理指令操控代碼。
配置選項可以控制產生過程要麼是布爾型,要麼是三態型,布爾型就是“是”或“否”,大部分核心配置選項都屬於布爾型,如CONFIG_PREEMPT,而三態型則在“是”和“否”的基礎上,又增加一個“模組”選項,模組選項表示配置選項被設定了,但最後會編譯成模組,而不是直接編譯進核心,模組可以理解為可獨立動態載入的對象,一般來說,驅動配置通常都是三態型。
配置選項也可以是字串或整數,這樣的選項不會控制產生過程,指定的值由核心原始碼訪問預先處理宏時使用,例如,可以為某個配置選項指定靜態分配數組的大小。
Linux廠商也會隨發行版提供先行編譯的核心,如Canonical為Ubuntu,或Red Hat為Fedora提供的核心,這樣的核心通常只啟用了需要的核心功能,幾乎所有驅動都被編譯成模組了,這樣的核心提供了一個良好的基礎核心和廣泛的硬體模組支援,無論如何,想要成為核心高手,你應該編譯自己的核心。
值得慶幸的是,核心提供了很多工具簡化配置 ,最簡單的工具是基於文本命令列的公用程式,如:
$ make config
這個工具會一個選項一個選項地配置,但使用者需要參與,如指定“是(y)”,“否(m)”還是“模組(m)”,整個配置過程需要很長的時間,因此,除非是有人按小時計費請你升級核心,實在找不出別的理由用這種最原始的方法配置核心了,相反,有現成的基於ncurses的圖形化工具可以代替。
$ make menuconfig
或是基於gtk+的圖形化工具
$ make gconfig
上述三個工具都將配置選項分成多個類別,如“處理器類型和特徵”,你可以在這些類別上來回移動,查看核心選項,當然也可以修改它們的設定了。
下面這個命令會根據你的架構建立一個預設的配置基礎。
$ make defconfig
雖然預設配置有些武斷(在i386上,預設配置是由Linus配置的),但如果你從未配置過核心,它提供了一個良好的開端。
配置選項儲存在原始碼樹根目錄下一個名叫.config的檔案中,你可以開啟這個檔案手工編輯其中的配置選項,修改後或要在新的核心原始碼樹上應用現有設定檔,你可以使用下面的命令驗證和更新配置:
$ make oldconfig
在產生核心之前必須運行這個命令。
配置選項CONFIG_IKCONFIG_PROC指定了完整的核心設定檔壓縮包位置,預設是/proc/config.gz,這樣在產生新核心時要複製現有的配置就變得非常簡單了。如果你當前的核心開啟了這個選項,你可以從/proc拷貝該設定檔,然後在此基礎上產生新的核心:
$ zcat /proc/config.gz > .config$ make oldconfig
核心配置好後,使用下面的命令進行產生:
$ make
和2.6以前的核心不一樣,在產生核心前不再需要執行make dep命令了,依賴樹會自動維護,也不需要再指定特定的組建類型,如bzImage,或獨立產生模組,預設Makefile規則會自動處理好一切。
將幹擾資訊最小化
在產生過程中會遭到警告和錯誤的幹擾。最小化幹擾資訊的一個訣竅是重新導向make的輸出,但仍然會看到一些警告和錯誤:
$ make > ../detritus
如果你想查看產生輸出,你可以事後閱讀這個檔案,如果你完全不想看到任何輸出,那麼就重新導向到/dev/null:
$ make > /dev/null
同時執行多個產生作業
Make命令提供了一個功能可以將產生過程拆分成多個平行的作業,這些作業可以獨立運行,也可以並行運行,在多處理器系統上可以極大地提高產生速度,也提高了處理器利用率,因為產生大型原始碼樹會出現大量的I/O等待時間。
預設情況下,make只能拆分成一個作業,因為Makefiles常常會包含不正確的依賴資訊,如果真是這樣,多個並存執行的作業將會引起混亂,最終會導致產生過程失敗,如果Makefiles中的依賴資訊無誤,那麼完全可以拆分成多個作業執行,如:
$ make –jn
這裡的n表示拆分的作業數量,通常按每個處理器拆分成1-2個作業,例如,在一個16核心的機器上 ,你可以運行:
$ make -j32 > /dev/null
使用distcc或ccache等優秀的工具也可以大大提高產生速度。
安裝新核心
核心產生好之後,你需要安裝它,如何安裝於系統架構和引導載入程式有關,我們以x86架構,grub引導載入程式為例進行說明。
首先將arch/i386/boot/bzImage拷貝到/boot,重新命名為vmlinuz- version,這裡的version也是版本號碼,然後編輯/boot/grub/grub.conf,為新核心添加相應的項目,如果是使用LILO引導裝載程式,則修改/etc/lilo.conf檔案,然後運行lilo。
模組的安裝與系統架構無關,都是自動完成的,以root使用者運行:
% make modules_install
這個命令會將所有編譯好的模組安裝到/lib/modules下對應的子目錄中。
產生過程會在原始碼樹根目錄下建立一個System.map檔案,它包含一個符號尋找表,映射核心符號到它們的起始地址,在調試期間可以用它將記憶體位址轉換成函數和變數名。
可能會遇到的問題
與普通使用者空間的應用程式相比,Linux核心有多個特殊的屬性,下面是我認為最重要的一些不同:
◆核心既不訪問C庫也不訪問標準C頭;
◆核心是用GNU C編碼的;
◆核心缺少使用者空間提供的記憶體保護;
◆核心不能容易地執行浮點運算;
◆核心有一個小型的固定大小的進程堆棧;
◆由於核心支援非同步中斷和SMP,因此同步和並發是核心主要擔心的問題;
◆可移植性也很重要。
下面我們就逐個來瞭解一下這些問題,所有核心開發人員都必須記住它們。
無libc或標準頭
和使用者空間應用程式不一樣,核心並沒有連結到標準的C庫,也沒有連結到任何其它的庫,這樣設計的原因有很多,包括如先有雞還是先有蛋的問題,但主要原因還是速度和核心大小,不要說完整的C庫,就是它的一個子集也夠大,核心太大隻會導致效率低下。
不要擔心,許多常用的libc函數都在核心中實現了,例如,常見的字串操作函數就位於lib/string.c中,只需要包括它的標頭檔<linux/string.h >就可以了。
這裡的標頭檔指的是核心原始碼樹中的標頭檔,核心也只能使用樹內的標頭檔,基礎檔案位於原始碼根目錄的include/目錄下,例如,<linux/inotify.h>標頭檔就位於include/linux/inotify.h。
與架構相關的標頭檔則位於arch/<architecture>/include/asm,例如,如果在x86架構下編譯,與你架構相關的檔案就是arch/x86/include/asm,只需要在引用這些頭的地方加上asm/首碼即可,如<asm/ioctl.h>。
漏掉的大部分都是類似printf()這樣的函數,核心不會使用printf(),但它提供了printk()函數,其表現絕不比printf()差,printk()會拷貝格式化的字串到核心日誌緩衝區,syslog程式就是從這裡讀取資訊的,其用法也和printf()類似:
printk("Hello world! A string '%s' and an integer '%d'/n", str, i);
printf()和printk()之間最大的不同是,printk()允許你指定一個優先順序標記,syslogd使用這個標記確定在哪裡顯示核心訊息,下面是一個使用優先順序標記的樣本:
printk(KERN_ERR "this is an error!/n");
注意在KERN_ERR和列印的訊息之間沒有逗號,這是故意這麼設計的,優先順序使用一個預定義的字元定義,在編譯期間它與列印的資訊是串聯的。
GNU C
和許多Unix核心類似,Linux核心也是用C編寫的,但也許會讓人很意外,核心不是用嚴謹的ANSI C編寫的,核心開發人員用的卻是gcc(GNU編譯器集,包含了編譯核心和Linux C程式的C編譯器)中的各種語言擴充。
核心開發人員同時使用了C語言的ISO C99和GNU C擴充,這些變化讓Linux核心與gcc結合得更緊密,但最近又出現了一個編譯器 – 英特爾的C編譯器 – 也對gcc的功能支援得相當好,因此也可以用它來編譯Linux核心。最低支援的gcc版本是3.2,建議採用gcc 4.4或更高的版本編譯。使用ISO C99擴充也是可以的,因為C99是C語言的官方版本。
內嵌函式
C99和GNU C都支援內嵌函式,內嵌函式是直接插入到每個函數調用的位置的,消除了函數調用和返回的開銷,允許進一步最佳化,因為編譯器可以同時最佳化調用者和被調用函數,但它也有缺點,代碼大小會增加,因為函數的內容被直接複製到所調用者內部了,因此也會增加記憶體消耗和指令緩衝空間。核心開發人員一般在小型時間很關鍵的函數中才會使用內嵌函式。
定義函數時,使用static和inline關鍵字聲明內嵌函式,例如:
static inline void wolf(unsigned long tail_size)
函數必須先聲明後使用,否則編譯器就不能使函數內聯,一般做法是將內嵌函式放在標頭檔中,因為它們被標記為static,不會建立輸出函數,如果內嵌函式僅在一個檔案中使用,可以放在該檔案的頂部。
在核心中,與複雜的宏相比,出於安全和可讀性方面考慮,內嵌函式是首選。
內聯彙編
Gcc C編譯器允許在C函數中嵌入彙編指令,asm()編譯器指令用於內聯彙編代碼,例如,這個內聯彙編指令執行x86處理器的rdtsc指令,返回時間戳記寄存器(tsc)的值:
unsigned int low, high;
asm volatile("rdtsc" : "=a" (low), "=d" (high));
/* low and high now contain the lower and upper 32-bits of the 64-bit tsc */
Linux核心是用C和組合語言混合編寫的,與底層硬體相關的代碼很多都是用組合語言寫的,剩下的大部分核心代碼都是直接用C編寫的。
分支註解
Gcc C編譯器內建了一個指令最佳化條件分支,核心將這個打包成便於使用的宏 - 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)) { /* ... */}
當分支指令已經知道一個優先順序,或你想在一種情況下最佳化另一種情況時應該使用上述指令,最重要的是,當分支正確標記時,這些指令會提升效能,但如果分支標記錯誤則會降低效能,在核心代碼中,unlikely()要使用得更多,因為if語句傾向於表示一種特殊情況。
無記憶體保護
當使用者空間的應用程式嘗試一個非法的記憶體訪問時,核心可以捕捉到錯誤,發送SIGSEGV訊號,殺掉進程,如果核心嘗試一個非法的記憶體訪問時,結果就不受控制了,因為誰也無法去控制核心,這也是核心最主要的失誤。
此外,核心記憶體也是不可分頁的,因此你消耗的每個記憶體位元組都比實體記憶體的一個位元組要少。
不能(容易)使用浮點數
當使用者空間進程使用浮點指令時,核心要負責處理從整型到浮點模式的轉換。
與使用者空間不一樣,核心不能無縫支援浮點數,因為它自己不能輕易地捕捉到自己,在核心中使用浮點數需要手動儲存和恢複浮點數寄存器,因此除非卻有必要,否則盡量不要在核心中做浮點運算。
小型,固定大小的堆棧
使用者空間可以靜態分配許多不同的堆棧,包括巨型結構和千元數組,這個行為是合法的,因為使用者空間有很大的堆棧,並可以動態增長。
核心堆棧不大也不是動態,相反,它很小且是固定的,核心堆棧的精確大小根據架構有所不同,在x86上,堆棧大小是在編譯時間確定的,一般是4KB或8KB,曆史上,核心堆棧有2頁,通常表示它處於32位架構上,大小是8KB,如果是16KB就表示是64位架構,總之大小是固定的,每個進程接收它自己的堆棧。
同步和並發
核心最容易受競爭條件影響,和一個單線程的使用者空間應用程式不一樣,有許多核心特性允許同時訪問共用資源,因此需要同步以防止競爭,特別是:
◆Linux是一種搶佔式多任務作業系統,進程是由核心的進程調度器隨意調度和再次調度的,核心必須在這些任務之間同步;
◆Linux支援對稱式多處理(SMP),因此,如果沒有適當的保護,在兩個或多個處理器上同時執行的核心代碼可能會同時訪問相同的資源;
◆中斷是非同步發生的,因此,如果沒有適當的保護,在訪問資源期間也可能發生中斷,中斷處理常式可能就會訪問到相同的資源;
◆Linux是有優先權的,因此,如果沒有適當的保護,核心代碼可能會優先執行,訪問其它代碼正在使用的資源。
解決這些問題的一般方法是自旋鎖和訊號量。
可移植性的重要性
雖然使用者空間應用程式一般不會太重視可移植性,但Linux的確是一個可移植性作業系統,應該保持一致,這意味著與架構無關的C代碼必須在大量的系統上正確地編譯和運行,與架構相關的代碼必須在核心原始碼樹中使用特定的目錄分隔開。
總結
可以肯定,核心有它獨特的性質,它有它自己的一些原則,不過,核心的複雜性和障礙與其它大型軟體項目相比,並沒有什麼大的不同,Linux開發道路上最重要的一步是認識到核心並不可怕,不熟悉?當然!不可逾越?當然不是!
原文地址:http://www.informit.com/articles/article.aspx?p=1610334