Android系統啓動流程 -- linux kernel

 第二部分:linux啓動

 

一、zImage是怎樣煉成的?

    zImage是linux內核編譯之後產生的最終文件,它的生成過程比較複雜,這裏不談編譯過程,只聊聊編譯的最後階段:

    1.  arm-linux-gnu-ld用arch/arm/kernel/vmlinux.lds、arch/arm/kernel/head.o、

arch/arm/kernel/init_task.o、各子目錄下的built-in.o、lib/lib.a 、arch/arm/lib/lib.a生成頂層目錄下的vmlinux (根據arch/arm/kernel/vmlinux.lds來鏈接 0xc0008000)

 

    2. 生成system.map, 置於頂層目錄之下。

    3. arm-linux-gnu-objcopy,去掉頂層vmlinux兩個段-R .note -R .comment

的調試信息,減小映像文件的大小,此時大概3M多,生成arch/arm/boot/Image。

 

4. gzip -f -9 < arch/arm/boot/compressed/../Image > arch/arm/boot/compressed/piggy.gz,讀入arch/arm/boot/Image的內容,以最大壓縮比進行壓縮,生成arch/arm/boot/compressed/目錄下的piggy.gz。

 

5. arm-linux-gnu-gcc,在arch/arm/boot/compressed/piggy.S文件中是直接引入piggy.gz的內容(piggy.gz其實已經是二進制數據了),然後生成arch/arm/boot/compressed/piggy.o文件。下面是piggy.S的內容

其中所選擇的行就是加入了piggy.gz的內容,通過編譯生成piggy.o文件,以備後面接下來的ld鏈接。

 

6. arm-linux-gnu-ld,在arch/arm/boot/compressed/piggy.o的基礎上,加入重定位地址和參數地址的同時,加入解壓縮的代碼(arch/arm/boot/compressed/head.o、misc.o),最後生成arch/arm/boot/compressed目錄的vmlinux,此時在解壓縮代碼中還含有調試信息(根據arch/arm/boot/compressed/vmlinux.lds來鏈接 0x0)vmlinux.lds開始處。

注意到了27行的嗎?*(.piggydata)就表示需要將piggydata這個段放在這個位置,而piggydata這個段放的是什麼呢?往後翻翻,看看第五步的圖片,呵呵,其實就是將按最大壓縮比壓縮之後的Image,壓縮之後叫piggy.gz中的二進制數據。

 

    7. arm-linux-gnu-objcopy,去掉解壓縮代碼中的調試信息段,最後生成arch/arm/boot/目錄下的zImage。

   

8. /bin/sh

/home/farsight/Resources/kernel/linux-2.6.14/scripts/mkuboot.sh -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008000 -n 'Linux-2.6.14' -d arch/arm/boot/zImage arch/arm/boot/uImage

調用mkimage在arch/arm/boot/zImage的基礎上加入64字節的uImage頭,和入口地址,裝載地址, 最終生成arch/arm/boot/目錄下的uImage文件。

   

   

    實際上zImage是經過了高壓縮之後在和解壓縮程序合併在一起生成的。知道了這些之後,我們就可以給linux的啓動大致分成3段:zImage解壓縮、kernel的彙編啓動階段、kernel的c啓動階段。

    前兩個階段因爲都是彙編寫成的,代碼讀起來晦澀難懂,內存分佈複雜,涉及MMU、解壓縮等衆多知識。如果有對這部分感興趣的,可以自行分析,遇到問題可以上網查資料或者找我,這裏就不詳細分析了。下面是第二階段彙編啓動的主線,可以瞭解下:

1. 確定 processor type

    2. 確定 machine type

    3. 手動創建頁表 

    4. 調用平臺特定的cpu setup函數,設置中斷地址,刷新Cache,開啓Cache

                         (在struct proc_info_list中,in proc-arm920.S)

    5. 開啓mmu I、D cache ,設置cp15的控制寄存器,設置TTB寄存器爲0x30004000

    6. 切換數據(根據需要賦值數據段,清bss段,保存processor ID 和 machine type

        和 cp15的控制寄存器值)

7. 最終跳轉到start_kernel   

(在__switch_data的結束的時候,調用了 b start_kernel)

 

二、linux的c啓動階段

    經過解壓縮和彙編啓動兩個階段,將會進入init/Main.c中的start_kernel()函數去繼續執行。(2.6.1x、2.6.2x和2.6.3x之間的差異比較大,下面的分析基於2.6.14)

    1. printk(linux_banner)打印內核的一些信息,版本,作者,編譯器版本,日期等信

息。

 

    2. 接下來執行是一個及其重要的函數setup_arch(),主要做一些板級初始化,cpu初始

化,tag參數解析,u-boot傳遞的cmdline解析,建立mmu工作頁表(memtable_init),初始化內存佈局,調用mmap_io建立GPIO,IRQ,MEMCTRL,UART,及其他外設的靜態映射表,對時鐘,定時器,uart進行初始化, cpu_init():{打印一些關於cpu的信息,比如cpu id,cache 大小等。另外重要的是設置了IRQ、ABT、UND三種模式的stack空間,分別都是12個字節。最後將系統切換到svc模式}。

 

3. sched_init():初始化每個處理器的可運行隊列,設置系統初始化進程即0號進程。

 

4. 建立系統內存頁區(zone)鏈表  build_all_zonelists()。

 

5.printk(KERN_NOTICE "Kernel command line: %s\n", saved_command_line);打印出從uboot傳遞過來的command_line字符串,在setup_arch函數中獲得的。

 

6. parse_early_param(),這裏分析的是系統能夠辨別的一些早期參數(這個函數甚至可以去掉,__setup的形式的參數),而且在分析的時候並不是以setup_arch(&command_line)傳出來的command_line爲基礎,而是以最原生態的saved_command_line爲基礎的。

 

7. parse_args("Booting kernel", command_line, __start___param,

                __stop___param - __start___param,

                &unknown_bootoption);

    對於比較新的版本真正起作用的函數,與parse_early_param()相比,此處對解析列表的處理範圍加大了,解析列表中除了包括系統以setup定義的啓動參數,還包括模塊中定義的param參數以及系統不能辨別的參數。

    __start___param是param參數的起始地址,在System.map文件中能看到

    __stop___param - __start___param是參數個數

    unknown_bootoption是對應與啓動參數不是param的相應處理函數(查看parse_one()就知道怎麼回事)。

 

8. 在前面的setup_arch-àpaging_init-à memtable_init函數中爲系統創建頁表的時候,中斷向量表的虛地址init_maps,是用alloc_bootmem_low_pages分配的,ARM規定中斷向量表的地址只能是0或0xFFFF0000,所以該函數裏有部分代碼的作用就是映射一個物理頁到0或0xFFFF0000。

trap_init函數做了以下的工作:把放在.Lcvectors處的系統8個意外入口跳轉指令搬到高端中斷向量0xffff0000處,再將__stubs_start到__stubs_end之間的各種意外初始化代碼搬到0xffff0200處,等。

 

9. init_IRQ()

    初始化系統中所有的中斷描述結構數組:irq_desc[NR_IRQS]。接着執行init_arch_irq函數,該函數是在setup_arch函數最後初始化的一個全局函數指針,指向了smdk2410_init_irq函數(in mach-smdk2410.c),實際上是調用了s3c24xx_init_irq函數。在該函數中,首先清除所有的中斷未決標誌,之後就初始化中斷的觸發方式和屏蔽位,還有中斷句柄初始化,這裏不是最終用戶的中斷函數,而是do_level_IRQ或者do_edge_IRQ函數,在這兩個函數中都使用過__do_irq函數來找到真正最終驅動程序註冊在系統中的中斷處理函數。

 

10. softirq_init():內核的軟中斷機制初始化函數。

 

12.      console_init():

初始化系統的控制檯結構,該函數執行後調用printk函數將log_buf中所有符合打印級別的系統信息打印到控制檯上。

 

13. profile_init()函數

/* 對系統剖析做相關初始化, 系統剖析用於系統調用*/

//profile是用來對系統剖析的,在系統調試的時候有用

//需要打開內核選項,並且在bootargs中有profile這一項才能開啓這個功能/*

    profile只是內核的一個調試性能的工具,這個可以通過menuconfig中profiling support打開。

   

14.      vfs_caches_init()

該函數主要完成的是文件系統相關的初始化,cache、inode等高速緩存的建

立,在mnt_init()函數中有註冊並初始化sysfs、rootfs文件系統,這裏只是在內存中建立他們的架構,創建了超級塊,並沒有真正掛載上去。關於這個rootfs需要說明的是,這個文件系統生命期更加短暫的,爲什麼?之前說的ramdisk大家是否還記得,ramdisk即將在後面釋放到內存空間,來代替這裏的rootfs出現在根目錄之下,而這個rootfs則退居二線,隱藏在一個二級目錄中。本來在非android的系統上,這個ramdisk也是一個暫時的文件系統,之後也會被真正的yaffs2之類的文件系統替換。不過呢,在android上,這個ramdisk還是掛載在根目錄下的,只是將system、userdata等真實文件系統掛載了對應的二級目錄下。

       

        關於這部分ramdisk內容,有興趣的下來可以繼續探討。

       

15.      mem_init():

最後內存初始化,釋放前邊標誌爲保留的所有頁面,這個函數結束之後就不能再使

用alloc_bootmem(),alloc_bootmem_low(),alloc_bootmem_pages()等申請低端內存的函數來申請內存,也就不能申請大塊的連續物理內存了。

   

16.     中間還省略了很多內容,涉及到很多東西,這裏也沒有時間詳細討論,有興趣的自

己研究代碼吧!下面直接跳到start_kernel()函數的最後的一個重要函數:rest_init()。

   

17.     rest_init函數創建了兩個線程之後,自己調用cpu_idle()函數隱退了。

創建的第一個線程,習慣上我們將其叫做1號內核線程,第二個線程叫2號內核線程,因爲創建它們的父進程叫0號啓動進程。

說明一下:2.6.14的內核這裏只創建了一個內核線程叫init線程,而上面創建兩

個線程的內核版本至少都是2.6.2x了,所以爲了後面能和android的啓動接上,所以這裏開始linux轉到2.2.29去。

 

static noinline void __init_refok rest_init(void) __releases(kernel_lock)

{

int pid;

 

kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);

pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);

cpu_idle();

}

kthreadd這個線程之前的部門交流會上討論過,新版本的linux將線程創建這個艱鉅的工作專門交給了這個叫kthreadd的線程來完成。

接下來既然0號啓動進程idle了,那麼剩下的工作就都轉移到線程kernel_init中去了。

 

18.     kernel_init()

這個線程的任務還是比較艱鉅的,第一個重要任務就是調用函數

do_basic_setup(),先調用driver_init()來構建sysfs的目錄架構,然後調用do_initcalls()函數來一次執行linux編譯時設置的系統函數。

    這裏主要工作就是註冊系統設備的驅動程序,關於driver和device的註冊順序,是可以互相交換,例如通常的三星平臺都有一個struct machine_desc結構體來描述平臺相關的啓動代碼:

    MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch

                    * to SMDK2410 */

    /* Maintainer: Jonas Dietsche */

    .phys_io    = S3C2410_PA_UART,

    .io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,

    .boot_params    = S3C2410_SDRAM_PA + 0x100,

    .map_io     = smdk2410_map_io,

    .init_irq   = s3c24xx_init_irq,

    .init_machine   = smdk2410_init,

    .timer      = &s3c24xx_timer,

MACHINE_END

    所有devices的註冊都是在smdk2410_init()函數中調用函數:

    platform_add_devices(smdk2410_devices, ARRAY_SIZE(smdk2410_devices));

    來完成,所以drivers的註冊就放在後面了。不過這樣註冊是有一個壞處的,就是不能準確地控制driver代碼中probe的執行先後順序。

    現在mtk平臺上的devices和drivers註冊順序想法,也就是先註冊上drivers,然後再註冊devices,這樣的話,就可以控制probe函數的執行先後。

   

include/linux/init.h文件中有這些優先級的定義:

#define pure_initcall(fn)        __define_initcall("0",fn,0)

 

#define core_initcall(fn)        __define_initcall("1",fn,1)

#define core_initcall_sync(fn)       __define_initcall("1s",fn,1s)

#define postcore_initcall(fn)        __define_initcall("2",fn,2)

#define postcore_initcall_sync(fn)   __define_initcall("2s",fn,2s)

#define arch_initcall(fn)        __define_initcall("3",fn,3)

#define arch_initcall_sync(fn)       __define_initcall("3s",fn,3s)

#define subsys_initcall(fn)      __define_initcall("4",fn,4)

#define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)

#define fs_initcall(fn)          __define_initcall("5",fn,5)

#define fs_initcall_sync(fn)     __define_initcall("5s",fn,5s)

#define rootfs_initcall(fn)      __define_initcall("rootfs",fn,rootfs)

#define device_initcall(fn)      __define_initcall("6",fn,6)

#define device_initcall_sync(fn) __define_initcall("6s",fn,6s)

#define late_initcall(fn)        __define_initcall("7",fn,7)

#define late_initcall_sync(fn)       __define_initcall("7s",fn,7s)

當然函數的執行屬性從1~7,通常我們見到的設備都是6、7級的。另外系統中所有的initcalll函數都是可以從linux根目錄下的system.map中查看得到。

 

接下來的一段代碼就是來釋放前面提到的ramdisk.img的:

if (!ramdisk_execute_command)

     ramdisk_execute_command = "/init";

 

if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {

     ramdisk_execute_command = NULL;

     prepare_namespace();

}

釋放出來的ramdisk呈現出來的目錄就是android編譯出來之後,在out/…/root的目錄一樣了,這個目錄下有一個init可執行程序,下面就準備啓動它。

 

接着調用init_post()函數,來打開console設備,這個時候我們的控制檯就可以操作了,最後會執行以下代碼來尋找和啓動init程序:

if (execute_command) {

     run_init_process(execute_command);

     printk(KERN_WARNING "Failed to execute %s.  Attempting "

                 "defaults...\n", execute_command);

}

run_init_process("/sbin/init");

run_init_process("/etc/init");

run_init_process("/bin/init");

run_init_process("/bin/sh");

 

panic("No init found.  Try passing init= option to kernel.");

 

這裏執行的init程序需要我們在u-boot傳給kernel的cmdline中使用init=/init

來告知kernel,或者kernel啓動代碼中直接寫死。否則在上面的那些目錄中找不到init的話,系統就用panic機制將這個警告信息保存在nand的panic分區,在下次啓動的時候,會自動將這個分區的信息輸出。

 

init進程是linux起來之後啓動的第一個用戶進程,android系統也就是在這個進

程的基礎上啓動的。進程號是1。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章