Android系统启动流程 -- linux kernel

时间:2022-10-31 04:35:23
  第二部分: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的内容

Android系统启动流程 --  linux kernel

其中所选择的行就是加入了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开始处。

Android系统启动流程 --  linux kernel

注意到了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():内核的软中断机制初始化函数。

 Android系统启动流程 --  linux kernel

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。