1. 程式人生 > >《16.核心的啟動過程分析》

《16.核心的啟動過程分析》

《16.核心的啟動過程分析》

第一部分、章節目錄
2.16.1.做好核心分析的準備工作
2.16.2.head.S檔案分析1
2.16.3.核心啟動的彙編階段
2.16.4.核心啟動的C語言階段1
2.16.5.核心啟動的C語言階段2
2.16.6.核心啟動的C語言階段3
2.16.7.核心啟動的C語言階段4
2.16.8.核心啟動的C語言階段5
2.16.9.核心啟動的C語言階段6
2.16.10.init程序詳解1
2.16.11.init程序詳解2
2.16.12.cmdline常用引數
2.16.13.核心中架構相關程式碼簡介

第二部分、章節介紹
2.16.1.做好核心分析的準備工作
本節課開始建立工程、分析Makefile等,主要目的是為開始核心程式碼的分析掃清障礙,做好準備工作。

2.16.2.head.S檔案分析1
本節開始分析head.S檔案,主要分析了核心執行的虛擬地址與實體地址,核心真正入口以及啟動條件等。
2.16.3.核心啟動的彙編階段
本節講解核心啟動的彙編階段剩餘內容,主要是cpu的校驗、機器碼的校驗、傳參tag的校驗、頁表的建立、各種段的處理等。
2.16.4.核心啟動的C語言階段1
本節講述核心學習的學習思路、學習方法和主體線路。本節課程的學習目的是讓大家對核心的特點和不同的學習思路有個認識。
2.16.5.核心啟動的C語言階段2
本節開始按照程式碼執行路徑分析核心的C階段。本節課主要講了printk函式的工作原理和核心初始化打印出banner資訊的部分。
2.16.6.核心啟動的C語言階段3
本節講解setup_arch函式中的machine查詢的部分,初步分析了核心對機器碼的定義和儲存方式、比對方式、獲取方式。
2.16.7.核心啟動的C語言階段4
本節講解setup_arch函式中對cmdline的處理,重點講解了cmdline的傳遞方式、確認方式和其對核心的影響和意義。
2.16.8.核心啟動的C語言階段5
本節講解start_kernel函式中剩餘部分函式呼叫直到rest_init的部分,並無太多重點細節。
2.16.9.核心啟動的C語言階段6
本節重點講解了rest_init函式中建立三個核心執行緒以及對這三個核心執行緒的介紹,本節課聽完後就明白了作業系統最終的走向和歸宿。
2.16.10.init程序詳解1
本節課從巨集觀上講解了init程序的工作和主要作用,交代了作業系統在核心態和使用者態下的切換過程,init程序如何一步步發展成為平時看到的作業系統。
2.16.11.init程序詳解2
本節對init程序的程式碼進行分析對比,重點是一些細節和對上節講到的巨集觀理論的程式碼驗證和支援。
2.16.12.cmdline常用引數
本節補充講解uboot給核心傳參時常用的一些cmdline引數選項及其含義。
2.16.13.核心中架構相關程式碼簡介
本節補充講解核心原始碼中架構相關的程式碼部分,並非程式碼詳解而是從巨集觀上講述相關的資料夾和檔案位置、作用介紹等。

第三部分、隨堂記錄
2.16.1.做好核心分析的準備工作
2.16.1.1、刪除無用檔案
(1)官方版本的kernel中是支援各種硬體架構、各種開發板的,因此有很多資料夾和檔案和我們無關,在建立SI工程前應該刪掉這些傢伙。
(2)我們現在分析的是開發板廠商九鼎科技移植好的針對X210開發板的kernel,因此其中一些無用檔案已經被刪掉了。

2.16.1.2、建立SI工程並解析
(2)建立方法和uboot中當時講的是一樣的。

2.16.1.3、Makefile分析
(1)kernel的Makefile寫法和規則等和uboot的Makefile是一樣的,甚至Makefile中的很多內容都是一樣的。
(2)kernel的Makefile比uboot的Makefile要複雜,這裡我們並不會一行一行的詳細分析。
(3)Makefile中只有一些值得關注的我會強調一下,其他不強調的地方暫時可以不管。
(4)Makefile中剛開始定義了kernel的核心版本號。這個版本號挺重要(在模組化驅動安裝時會需要用到),要注意會查,會改。
(5)在make編譯核心時,也可以通過命令列給核心makefile傳參(跟uboot配置編譯時傳參一樣)。譬如make O=xxx可以指定不在原始碼目錄下編譯,而到另外一個單獨資料夾下編譯。
(6)kernel的頂層Makefile中定義了2個變數很重要,一個是ARCH,一個是CROSS_COMPILE。ARCH決定當前配置編譯的路徑,譬如ARCH = arm的時候,將來在原始碼目錄下去操作的arch/arm目錄。CROSS_COMPILE用來指定交叉編譯工具鏈的路徑和字首。
(7)CROSS_COMPILE = xxx和ARCH = xxx和O=xxx這些都可以在make時通過命令列傳參的方式傳給頂層Makefile。
所以有時候你會看到別人編譯核心時:make O=/tmp/mykernel ARCH=arm CROSS_COMPILE=/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-

2.16.1.4、連結指令碼分析
(1)分析連線指令碼的目的就是找到整個程式的entry
(2)kernel的連線指令碼並不是直接提供的,而是提供了一個彙編檔案vmlinux.lds.S,然後在編譯的時候再去編譯這個彙編檔案得到真正的連結指令碼vmlinux.lds。
(3)vmlinux.lds.S在arch/arm/kernel/目錄下。
(4)思考:為什麼linux kernel不直接提供vmlinux.lds而要提供一個vmlinux.lds.S然後在編譯時才去動態生成vmlinux.lds呢?
猜測:.lds檔案中只能寫死,不能用條件編譯。但是我們在kernel中連結指令碼確實有條件編譯的需求(但是lds格式又不支援),於是乎kernel工作者找了個投機取巧的方法,就是把vmlinux.lds寫成一個彙編格式,然後彙編器處理的時候順便條件編譯給處理了,得到一個不需要條件編譯的vmlinux.lds。
(5)入門在哪裡?從vmlinux.lds中ENTRY(stext)可以知道入口符號是stext,在SI中搜索這個符號,發現arch/arm/kernel/目錄下的head.S和head-nommu.S中都有。
(6)head.S是啟用了MMU情況下的kernel啟動檔案,相當於uboot中的start.S。head-nommu.S是未使用mmu情況下的kernel啟動檔案。

2.16.2.head.S檔案分析1
2.16.2.1、核心執行的實體地址與虛擬地址
(1)KERNEL_RAM_VADDR(VADDR就是virtual address),這個巨集定義了核心執行時的虛擬地址。值為0xC0008000
(2)KERNEL_RAM_PADDR(PADDR就是physical address),這個巨集定義核心執行時的實體地址。值為0x30008000
(3)總結:核心執行的實體地址是0x30008000,對應的虛擬地址是0xC0008000。

2.16.2.2、核心的真正入口
(1)核心的真正入口就是ENTRY(stext)處
(2)前面的__HEAD定義了後面的程式碼屬於段名為.head.text的段

2.16.2.3、核心執行的硬體條件
(1)核心的起始部分程式碼是被解壓程式碼呼叫的。回憶之前講zImage的時候,uboot啟動核心後實際呼叫執行的是zImage前面的那段未經壓縮的解壓程式碼,解壓程式碼執行時先將zImage後段的核心解壓開,然後再去呼叫執行真正的核心入口。
(2)核心啟動不是無條件的,而是有一定的先決條件,這個條件由啟動核心的bootloader(我們這裡就是uboot)來構建保證。
(3)ARM體系中,函式呼叫時實際是通過暫存器傳參的(函式呼叫時傳參有兩種設計:一種是暫存器傳參,另一種是棧記憶體傳參)。所以uboot中最後theKernel (0, machid, bd->bi_boot_params);執行核心時,執行時實際把0放入r0中,machid放入到了r1中,bd->bi_boot_params放入到了r2中。ARM的這種處理技巧剛好滿足了kernel啟動的條件和要求。
(4)kernel啟動時MMU是關閉的,因此硬體上需要的是實體地址。但是核心是一個整體(zImage)只能被連線到一個地址(不能分散載入),這個連線地址肯定是虛擬地址。因此核心執行時前段head.S中尚未開啟MMU之前的這段程式碼就很難受。所以這段程式碼必須是位置無關碼,而且其中涉及到操作硬體暫存器等時必須使用實體地址。
2.16.2.4、核心啟動要求的傳參方式

2.16.3.核心啟動的彙編階段
2.16.3.1、__lookup_processor_type
(1)我們從cp15協處理器的c0暫存器中讀取出硬體的CPU ID號,然後呼叫這個函式來進行合法性檢驗。如果合法則繼續啟動,如果不合法則停止啟動,轉向__error_p啟動失敗。
(2)該函式檢驗cpu id的合法性方法是:核心會維護一個本核心支援的CPU ID號碼的陣列,然後該函式所做的就是將從硬體中讀取的cpu id號碼和陣列中儲存的各個id號碼依次對比,如果沒有一個相等則不合法,如果有一個相等的則合法。
(3)核心啟動時設計這個校驗,也是為了核心啟動的安全性著想。
2.16.3.2、__lookup_machine_type
(1)該函式的設計理念和思路和上面校驗cpu id的函式一樣的。不同之處是本函式校驗的是機器碼。
2.16.3.3、__vet_atags
(1)該函式的設計理念和思路和上面2個一樣,不同之處是用來校驗uboot給核心的傳參ATAGS格式是否正確。這裡說的傳參指的是uboot通過tag給核心傳的引數(主要是板子的記憶體分佈memtag、uboot的bootargs)
(2)核心認為如果uboot給我的傳參格式不正確,那麼我就不啟動。
(3)uboot給核心傳參的部分如果不對,是會導致核心不啟動的。譬如uboot的bootargs設定不正確核心可能就會不啟動。
2.16.3.4、__create_page_tables
(1)顧名思義,這個函式用來建立頁表。
(2)linux核心本身被連線在虛擬地址處,因此kernel希望儘快建立頁表並且啟動MMU進入虛擬地址工作狀態。但是kernel本身工作起來後頁表體系是非常複雜的,建立起來也不是那麼容易的。kernel想了一個好辦法
(3)kernel建立頁表其實分為2步。第一步,kernel先建立了一個段式頁表(和uboot中之前建立的頁表一樣,頁表以1MB為單位來區分的),這裡的函式就是建立段式頁表的。段式頁表本身比較好建立(段式頁表1MB一個對映,4GB空間需要4096個頁表項,每個頁表項4位元組,因此一共需要16KB記憶體來做頁表),壞處是比較粗不能精細管理記憶體;第二步,再去建立一個細頁表(4kb為單位的細頁表),然後啟用新的細頁表廢除第一步建立的段式對映頁表。
(4)核心啟動的早期建立段式頁表,並在核心啟動前期使用;核心啟動後期就會再次建立細頁表並啟用。等核心工作起來之後就只有細頁表了。
2.6.3.5、__switch_data
(1)建立了段式頁表後進入了__switch_data部分,這東西是個函式指標陣列。
(2)分析得知下一步要執行__mmap_switched函式
(3)複製資料段、清除bss段(目的是構建C語言執行環境)
(4)儲存起來cpu id號、機器碼、tag傳參的首地址。
(5)b start_kernel跳轉到C語言執行階段。

總結:彙編階段其實也沒幹啥,主要原因是uboot幹了大部分活。彙編階段主要就是校驗啟動合法性、建立段式對映的頁表並開啟MMU以方便使用記憶體、跳入C階段。

2.16.4.核心啟動的C語言階段1
2.16.4.1、這一塊的學習思路
(1)抓大放小,不深究.
(2)感興趣可以就某個話題去網上搜索資料學習
(3)重點區域性深入分析
2.16.4.2、具體學習方法
(1)順著程式碼執行路徑抓全。這是我們的學習主線。
(2)對照核心啟動的列印資訊進行分析。
2.16.4.3、幾條學習線路
(1)分析uboot給kernel傳參的影響和實現
(2)硬體初始化與驅動載入
(3)核心啟動後的結局與歸宿

2.16.5.核心啟動的C語言階段2
2.16.5.1、雜碎
(1)smp。smp就是對稱多處理器(其實就是我們說的多核心CPU)
(2)lockdep。鎖定依賴,是一個核心除錯模組,處理核心自旋鎖死鎖問題相關的。
(3)cgroup。control group,核心提供的一種來處理程序組的技術。
2.16.5.2、列印核心版本資訊
(1)程式碼位於:kernel/init/main.c中的572行
(2)printk函式是核心中用來從console列印資訊的,類似於應用層程式設計中的printf。核心程式設計時不能使用標準庫函式,因此不能使用printf,其實printk就是核心自己實現的一個printf。
(3)printk函式的用法和printf幾乎一樣,不同之處在於可以在引數最前面用一個巨集來定義訊息輸出的級別。為什麼要有這種級別?主要原因是linux核心太大了,程式碼量太多,裡面的printk列印資訊太多了。如果所有的printk都能打印出來而不加任何限制,則最終核心啟動後得到海量的輸出資訊。
(4)為了解決列印資訊過多,無效資訊會淹沒有效資訊這個問題,linux核心的解決方案是給每一個printk新增一個列印級別。級別定義0-7(注意程式設計的時候要用相應的巨集定義,不要直接用數字)分別代表8種輸出的重要性級別,0表示最重要,7表示最不重要。我們在printk的時候自己根據自己的訊息的重要性去設定列印級別。
(5)linux的控制檯監測訊息的地方也有一個訊息過濾顯示機制,控制檯實際只會顯示級別比我的控制檯定義的級別高的訊息。譬如說控制檯的訊息顯示級別設定為4,那麼只有printk中訊息級別為0-3(也可能是0-4)的才可以顯示看見,其餘的被過濾掉了。
(6)linux_banner的內容解析。

2.16.6.核心啟動的C語言階段3
2.16.6.1、setup_arch函式簡介
(1)從名字看,這個函式是CPU架構相關的一些建立過程。
(2)實際上這個函式是用來確定我們當前核心的機器(arch、machine)的。我們的linux核心會支援一種CPU的執行,CPU+開發板就確定了一個硬體平臺,然後我們當前配置的核心就在這個平臺上可以執行。之前說過的機器碼就是給這個硬體平臺一個固定的編碼,以表徵這個平臺。
(3)當前核心支援的機器碼以及硬體平臺相關的一些定義都在這個函式中處理。

2.16.6.2、Machine查詢
(1)setup_processor函式用來查詢CPU資訊,可以結合串列埠列印的資訊來分析。
(2)setup_machine函式的傳參是機器碼編號,machine_arch_type符號在include/generated/mach-types.h的32039-32050行定義了。經過分析後確定這個傳參值就是2456.
(3)函式的作用是通過傳入的機器碼編號,找到對應這個機器碼的machine_desc描述符,並且返回這個描述符的指標。
(4)其實真正幹活的函式是lookup_machine_type,找這個函式發現在head-common.S中,真正幹活的函式是__lookup_machine_type
(5)__lookup_machine_type函式的工作原理:核心在建立的時候就把各種CPU架構的資訊組織成一個一個的machine_desc結構體例項,然後都給一個段屬性.arch.info.init,連結的時候會保證這些描述符會被連線在一起。__lookup_machine_type就去那個那些描述符所在處依次挨個遍歷各個描述符,比對看機器碼哪個相同。

2.16.7.核心啟動的C語言階段4
2.16.6.3、setup_arch函式進行了基本的cmdline處理
(1)這裡說的cmdline就是指的uboot給kernel傳參時傳遞的命令列啟動引數,也就是uboot的bootargs。
(2)有幾個相關的變數需要注意:
default_command_line:看名字是預設的命令列引數,實際是一個全域性變數字元陣列,這個字元陣列可以用來存東西。
CONFIG_CMDLINE:在.config檔案中定義的(可以在make menuconfig中去更改設定),這個表示核心的一個預設的命令列引數。
(3)核心對cmdline的處理思路是:核心中自己維護了一個預設的cmdline(就是.config中配置的這一個),然後uboot還可以通過tag給kernel再傳遞一個cmdline。如果uboot給核心傳cmdline成功則核心會優先使用uboot傳遞的這一個;如果uboot沒有給核心傳cmdline或者傳參失敗,則核心會使用自己預設的這個cmdline。以上說的這個處理思路就是在setup_arch函式中實現的。

2.6.6.4、實驗驗證核心的cmdline確定
(1)驗證思路:首先給核心配置時配置一個基本的cmdline,然後在uboot啟動核心時給uboot設定一個bootargs,然後啟動核心看打印出來的cmdline和uboot傳參時是否一樣。
(2)在uboot中去掉bootargs,然後再次啟動核心看打印出來的cmdline是否和核心中設定的預設的cmdline一樣。

注意:uboot給核心傳遞的cmdline非常重要,會影響核心的執行,所以要謹慎。有時候核心啟動有問題,可以分析下是不是uboot的bootargs設定不對。

注意:這個傳參在這裡確定出來之後,還沒完。後面還會對這個傳參進行解析。解析之後cmdline中的每一個設定項都會對核心啟動有影響。
思考:核心為什麼要這樣設計?

2.16.8.核心啟動的C語言階段5
2.16.8.1、setup_command_line
(1)也是在處理和命令列引數cmdline有關的任務。
2.16.8.2、parse_early_param&parse_args
(1)解析cmdline傳參和其他傳參
(2)這裡的解析意思是把cmdline的細節設定資訊給解析出來。譬如cmdline:console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3,則解析出的內容就是就是一個字串陣列,陣列中依次存放了一個設定專案資訊。
console=ttySAC2,115200 一個
root=/dev/mmcblk0p2 rw 一個
init=/linuxrc 一個
rootfstype=ext3 一個
(3)這裡只是進行了解析,並沒有去處理。也就是說只是把長字串解析成了短字串,最多和核心裡控制這個相應功能的變數掛鉤了,但是並沒有去執行。執行的程式碼在各自模組初始化的程式碼部分。

2.16.8.3、雜碎
(1)trap_init 設定異常向量表
(2)mm_init 記憶體管理模組初始化
(3)sched_init 核心排程系統初始化
(4)early_irq_init&init_IRQ 中斷初始化
(5)console_init 控制檯初始化
總結:start_kernel函式中呼叫了很多的xx_init函式,全都是核心工作需要的模組的初始化函式。這些初始化之後核心就具有了一個基本的可以工作的條件了。
如果把核心比喻成一個複雜機器,那麼start_kernel函式就是把這個機器的眾多零部件組裝在一起形成這個機器,讓他具有可以工作的基本條件。

2.16.8.4、rest_init
(1)這個函式之前核心的基本組裝已經完成。
(2)剩下的一些工作就比較重要了,放在了一個單獨的函式中,叫rest_init。

總結:start_kernel函式做的主要工作:列印了一些資訊、核心工作需要的模組的初始化被依次呼叫(譬如記憶體管理、排程系統、異常處理···)、我們需要重點了解的就是setup_arch中做的2件事情:機器碼架構的查詢並且執行架構相關的硬體的初始化、uboot給核心的傳參cmdline。

2.16.9.核心啟動的C語言階段6
2.16.9.1、作業系統去哪了
(1)rest_init中呼叫kernel_thread函式啟動了2個核心執行緒,分別是:kernel_init和kthreadd
(2)呼叫schedule函式開啟了核心的排程系統,從此linux系統開始轉起來了。
(3)rest_init最終呼叫cpu_idle函式結束了整個核心的啟動。也就是說linux核心最終結束了一個函式cpu_idle。這個函式裡面肯定是死迴圈。
(4)簡單來說,linux核心最終的狀態是:有事幹的時候去執行有意義的工作(執行各個程序任務),實在沒活幹的時候就去死迴圈(實際上死迴圈也可以看成是一個任務)。
(5)之前已經啟動了核心排程系統,排程系統會負責考評系統中所有的程序,這些程序裡面只有有哪個需要被執行,排程系統就會終止cpu_idle死迴圈程序(空閒程序)轉而去執行有意義的幹活的程序。這樣作業系統就轉起來了。

2.16.9.2、什麼是核心執行緒
(1)程序和執行緒。簡單來理解,一個執行的程式就是一個程序。所以程序就是任務、程序就是一個獨立的程式。獨立的意思就是這個程式和別的程式是分開的,這個程式可以被核心單獨呼叫執行或者暫停。
(2)在linux系統中,執行緒和程序非常相似,幾乎可以看成是一樣的。實際上我們當前講課用到的程序和執行緒的概念就是一樣的。
(3)程序/執行緒就是一個獨立的程式。應用層執行一個程式就構成一個使用者程序/執行緒,那麼核心中執行一個函式(函式其實就是一個程式)就構成了一個核心程序/執行緒。
(4)所以我們kernel_thead函式執行一個函式,其實就是把這個函式變成了一個核心執行緒去執行起來,然後他可以被核心排程系統去排程。說白了就是去排程器註冊了一下,以後人家排程的時候會考慮你。

2.16.9.3、程序0、程序1、程序2
(1)截至目前為止,我們一共涉及到3個核心程序/執行緒。
(2)作業系統是用一個數字來表示/記錄一個程序/執行緒的,這個數字就被稱為這個程序的程序號。這個號碼是從0開始分配的。因此這裡涉及到的三個程序分別是linux系統的程序0、程序1、程序2.
(3)在linux命令列下,使用ps命令可以檢視當前linux系統中執行的程序情況。
(4)我們在ubuntu下ps -aux可以看到當前系統執行的所有程序,可以看出程序號是從1開始的。為什麼不從0開始,因為程序0不是一個使用者程序,而屬於核心程序。
(5)三個程序
程序0:程序0其實就是剛才講過的idle程序,叫空閒程序,也就是死迴圈。
程序1:kernel_init函式就是程序1,這個程序被稱為init程序。
程序2:kthreadd函式就是程序2,這個程序是linux核心的守護程序。這個程序是用來保證linux核心自己本身能正常工作的。

總結1:本節課的重點在於理解linux核心啟動後達到的一個穩定狀態。注意去對比核心啟動後的穩定狀態和uboot啟動後的穩定狀態的區別。
總結2:本節課的第二個重點就是初步理解程序/執行緒的概念。
總結3:你得明白每個程序有個程序號,程序號從0開始依次分配的。明白程序0是idle程序(idle程序是幹嘛的);程序2是ktheadd程序(基本明白乾嘛的就行)
總結4:分析到此,發現後續的料都在程序1.所以後面課程會重點從程序1出發,分析之後發生的事情。

2.16.10.init程序詳解1
2.16.10.1、init程序完成了從核心態向用戶態的轉變
(1)一個程序2種狀態。init程序剛開始執行的時候是核心態,它屬於一個核心執行緒,然後他自己運行了一個使用者態下面的程式後把自己強行轉成了使用者態。因為init程序自身完成了從核心態到使用者態的過度,因此後續的其他程序都可以工作在使用者態下面了。
(2)核心態下做了什麼?重點就做了一件事情,就是掛載根檔案系統並試圖找到使用者態下的那個init程式。init程序要把自己轉成使用者態就必須執行一個使用者態的應用程式(這個應用程式名字一般也叫init),要執行這個應用程式就必須得找到這個應用程式,要找到它就必須得掛載根檔案系統,因為所有的應用程式都在檔案系統中。
核心原始碼中的所有函式都是核心態下面的,執行任何一個都不能脫離核心態。應用程式必須不屬於核心原始碼,這樣才能保證自己是使用者態。也就是說我們這裡執行的這個init程式和核心不在一起,他是另外提供的。提供這個init程式的那個人就是根檔案系統。

(3)使用者態下做了什麼?init程序大部分有意義的工作都是在使用者態下進行的。init程序對我們作業系統的意義在於:其他所有的使用者程序都直接或者間接派生自init程序。

(4)如何從核心態跳躍到使用者態?還能回來不?
init程序在核心態下面時,通過一個函式kernel_execve來執行一個使用者空間編譯連線的應用程式就跳躍到使用者態了。注意這個跳躍過程中程序號是沒有改變的,所以一直是程序1.這個跳躍過程是單向的,也就是說一旦執行了init程式轉到了使用者態下整個作業系統就算真正的運轉起來了,以後只能在使用者態下工作了,使用者態下想要進入核心態只有走API這一條路了。

2.16.10.2、init程序構建了使用者互動介面
(1)init程序是其他使用者程序的老祖宗。linux系統中一個程序的建立是通過其父程序創建出來的。根據這個理論只要有一個父程序就能生出一堆子孫程序了。
(2)init啟動了login程序、命令列程序、shell程序
(3)shell程序啟動了其他使用者程序。命令列和shell一旦工作了,使用者就可以在命令列下通過./xx的方式來執行其他應用程式,每一個應用程式的執行就是一個程序。

總結:本節的主要目的是讓大家認識到init程序如何一步步發展成為我們平時看到的那種作業系統的樣子。

2.16.11.init程序詳解2
2.16.11.1、開啟控制檯
(1)linux系統中每個程序都有自己的一個檔案描述符表,表中儲存的是本程序開啟的檔案。
(2)linux系統中有一個設計理念:一切屆是檔案。所以裝置也是以檔案的方式來訪問的。我們要訪問一個裝置,就要去開啟這個裝置對應的檔案描述符。譬如/dev/fb0這個裝置檔案就代表LCD顯示器裝置,/dev/buzzer代表蜂鳴器裝置,/dev/console代表控制檯裝置。
(3)這裡我們打開了/dev/console檔案,並且複製了2次檔案描述符,一共得到了3個檔案描述符。這三個檔案描述符分別是0、1、2.這三個檔案描述符就是所謂的:標準輸入、標準輸出、標準錯誤。
(4)程序1打開了三個標準輸出輸出錯誤檔案,因此後續的程序1衍生出來的所有的程序預設都具有這3個三件描述符。

2.16.11.2、掛載根檔案系統
(1)prepare_namespace函式中掛載根檔案系統
(2)根檔案系統在哪裡?根檔案系統的檔案系統型別是什麼? uboot通過傳參來告訴核心這些資訊。
uboot傳參中的root=/dev/mmcblk0p2 rw 這一句就是告訴核心根檔案系統在哪裡
uboot傳參中的rootfstype=ext3這一句就是告訴核心rootfs的型別。
(3)如果核心掛載根檔案系統成功,則會打印出:VFS: Mounted root (ext3 filesystem) on device 179:2.
如果掛載根檔案系統失敗,則會列印:No filesystem could mount root, tried: yaffs2
(4)如果核心啟動時掛載rootfs失敗,則後面肯定沒法執行了,肯定會死。核心中設定了啟動失敗休息5s自動重啟的機制,因此這裡會自動重啟,所以有時候大家會看到反覆重啟的情況。
(5)如果掛載rootfs失敗,可能的原因有:
最常見的錯誤就是uboot的bootargs設定不對。
rootfs燒錄失敗(fastboot燒錄不容易出錯,以前是手工燒錄很容易出錯)
rootfs本身製作失敗的。(尤其是自己做的rootfs,或者別人給的第一次用)

2.16.11.3、執行使用者態下的程序1程式
(1)上面一旦掛載rootfs成功,則進入rootfs中尋找應用程式的init程式,這個程式就是使用者空間的程序1.找到後用run_init_process去執行他
(2)我們如果確定init程式是誰?方法是:
先從uboot傳參cmdline中看有沒有指定,如果有指定先執行cmdline中指定的程式。cmdline中的init=/linuxrc這個就是指定rootfs中哪個程式是init程式。這裡的指定方式就表示我們rootfs的根目錄下面有個名字叫linuxrc的程式,這個程式就是init程式。
如果uboot傳參cmdline中沒有init=xx或者cmdline中指定的這個xx執行失敗,還有備用方案。第一備用:/sbin/init,第二備用:/etc/init,第三備用:/bin/init,第四備用:/bin/sh。
如果以上都不成功,則認命了,死了。

2.16.12.cmdline常用引數
2.16.12.1、格式簡介
(1)格式就是由很多個專案用空格隔開依次排列,每個專案中都是專案名=專案值
(2)整個cmdline會被核心啟動時解析,解析成一個一個的專案名=專案值的字串。這些字串又會被再次解析從而影響啟動過程。
2.16.12.2、root=
(1)這個是用來指定根檔案系統在哪裡的
(2)一般格式是root=/dev/xxx(一般如果是nandflash上則/dev/mtdblock2,如果是inand/sd的話則/dev/mmcblk0p2)
(3)如果是nfs的rootfs,則root=/dev/nfs。

2.16.12.3、rootfstype=
(1)根檔案系統的檔案系統型別,一般是jffs2、yaffs2、ext3、ubi
2.16.12.4、console=
(1)控制檯資訊宣告,譬如console=/dev/ttySAC0,115200表示控制檯使用串列埠0,波特率是115200.
(2)正常情況下,核心啟動的時候會根據console=這個專案來初始化硬體,並且重定位console到具體的一個串列埠上,所以這裡的傳參會影響後續是否能從串列埠終端上接收到核心的資訊。

2.16.12.5、mem=
(1)mem=用來告訴核心當前系統的記憶體有多少

2.16.12.6、init=
(1)init=用來指定程序1的程式pathname,一般都是init=/linuxrc

2.16.12.7、常見cmdline介紹
(1)console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3
第一種這種方式對應rootfs在SD/iNand/Nand/Nor等物理儲存器上。這種對應產品正式出貨工作時的情況。

(2)root=/dev/nfs nfsroot=192.168.1.141:/root/s3c2440/build_rootfs/aston_rootfs ip=192.168.1.10:192.168.1.141:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC0,115200
第二種這種方式對應rootfs在nfs上,這種對應我們實驗室開發產品做除錯的時候。

2.16.13.核心中架構相關程式碼簡介
2.16.13.1、核心程式碼基本分為3塊
(1)arch。 本目錄下全是cpu架構有關的程式碼
(2)drivers 本目錄下全是硬體的驅動
(3)其他 相同點是這些程式碼都和硬體無關,因此係統移植和驅動開發的時候這些程式碼幾乎都是不用關注的。

2.16.13.2、架構相關的常用目錄名及含義
(1)mach。(mach就是machine architecture)。arch/arm目錄下的一個mach-xx目錄就表示一類machine的定義,這類machine的共同點是都用xx這個cpu來做主晶片。(譬如mach-s5pv210這個資料夾裡面都是s5pv210這個主晶片的開發板machine);mach-xx目錄裡面的一個mach-yy.c檔案中定義了一個開發板(一個開發板對應一個機器碼),這個是可以被擴充套件的。
(2)plat(plat是platform的縮寫,含義是平臺)plat在這裡可以理解為SoC,也就是說這個plat目錄下都是SoC裡面的一些硬體(內部外設)相關的一些程式碼。
在核心中把SoC內部外設相關的硬體操作程式碼就叫做平臺裝置驅動。
(3)include。這個include目錄中的所有程式碼都是架構相關的標頭檔案。(linux核心通用的標頭檔案在核心原始碼樹根目錄下的include目錄裡)

2.16.13.3、補充
(1)核心中的檔案結構很龐大、很凌亂(不同版本的核心可能一個檔案存放的位置是不同的),會給我們初學者帶來一定的困擾。
(2)標頭檔案目錄include有好幾個,譬如:
kernel/include 核心通用標頭檔案
kernel/arch/arm/include 架構相關的標頭檔案
kernel/arch/arm/include/asm
kernel\arch\arm\include\asm\mach
kernel\arch\arm\mach-s5pv210\include\mach
kernel\arch\arm\plat-s5p\include\plat
(3)核心中包含標頭檔案時有一些格式

#include <linux/kernel.h> kernel/include/linux/kernel.h
#include <asm/mach/arch.h> kernel/arch/arm/include/asm/mach/arch.h
#include <asm/setup.h> kernel\arch\arm\include\asm/setup.h
#include <plat/s5pv210.h> kernel\arch\arm\plat-s5p\include\plat/s5pv210.h

(4)有些同名的標頭檔案是有包含關係的,有時候我們需要包含某個標頭檔案時可能並不是直接包含他,而是包含一個包含了他的標頭檔案。