本文分成两部分:智能手机的硬件系统架构和linux内核的启动两部分,前者作为后者的基础。
硬件系统主要分为应用处理器模块AP(Application Processor),电源管理模块(PMU),存储器模块SDRAM(Synchronous Dynamic Random Access Memory)和NAND Flash,LCD 显示模块 Camera 模块,Bluetooth和FM 模块,WiFi 模块,GPS 模块。AP 模块搭配存储单元(NAND+DDR)以及LCD(Liquid Crystal Display)、cmera、Bluetooth、WiFi、GPS(Global Position System)等外设模块,实现丰富的多媒体和短距离无线业务。PMU 一方面为整个系统的各个模块单元提供供电,另一方面提供Audio Codec、USB PHY、HKADC、Clock 等功能。
下面主要说一下存储器模块,存储器单元主要提供程序存储和运行空间,以及资料数据的存储空间,这些功能由SDRAM ( Synchronous Dynamic Random Access Memory)和NAND Flash实现。 系统内核保存在Nand Flash之上,断电后仍然存在,而运行后程序是装入SDRAM或Mobile DDR之类的内存设备运行。一般是就用ROM来指Nand Flash,RAM来指SDRAM之类设备。
CPU上电或复位将先执行BootLoader程序。BootLoader是在操作系统运行之前运行的一段程序,它可以将系统的软硬件环境带到一个合适状态,为运行操作系统做好准备。它比较像电脑上的BIOS,它就是要把OS拉起来运行。Bootloader做的事情主要有:初始化CPU时钟,内存,串口等;设置Linux启动参数;加载Linux内核镜像。BootLoader可以分为两个阶段。在阶段一,做了一些初始化,在阶段二,如果发现按键有特殊的组合,就进入相应的模式。Bootloader有三种模式如下:
a:开机后按组合键启动到fastboot模式,即命令或SD卡烧写模式,不加载内核及文件系统,可以通过数据线与电脑连接,然后在电脑上执行一些命令,如刷系统镜像到手机上。fastboot可以理解为实现了一个简单的通信协议,接收命令并更新镜像文件,其他的基本都干不了。
b:开机后按组合键启动到recovery模式,加载recovery.img,recovery.img包含内核,部分文件系统,可以读sdcard中的update.zip进行刷机,也可以清除cache和用户数据。这就是一个小型操作系统,和正常启动进入的系统的kernel差不多,只是init及之后干的事情不同。
c:开机按Power,正常启动系统,加载boot.img,boot.img包含内核,基本文件系统,用于正常启动手机。
以下只分析正常启动的情况。
不同的硬件设备BootLoader不同,最常用的Bootloader还是U-boot,可以引导多种操作系统,支持多种架构的CPU。
Bootloader的上电引导程序把NandFlash上的前4K代码搬移到SDRAM,然后系统从起始地址是0x0000_0000的SDRAM启动,在这4K代码中我们必须完成CPU的核心配置,把NandFlash上的代码全部拷贝到SDRAM中去,然后跳转到SDRAM中去继续跑Bootloader。其实这就是加载各种各样的内核镜像到SDRAM中的过程。
内核镜像被加载到内存,首先进行自解压。Linux内核有多种格式的镜像,常见的zImage、bzImage、uImage等。zImage和bzImage都是用gzip压缩的。它们不仅是一个压缩文件,而且在这两个文件的开头部分内嵌有gzip解压缩代码。所以你不能用gunzip或gzip –dc解包。用于个微型的gzip用于解压缩内核并引导它。两者的不同之处在于,老的zImage解压缩内核到低端内存(第一个 640K),bzImage解压缩内核到高端内存(1M以上)。如果内核比较小,那么可以采用zImage或bzImage之一,两种方式引导的系统运行时是相同的。大的内核采用bzImage,不能采用zImage。uImage是u-boot使用bootm命令引导的Linux压缩内核映像文件格式,是使用工具mkimage对普通的压缩内核映像文件(zImage)加工而得。它是U-boot专用的映像文件,它是在zImage之前加上一个长度为 64字节的“头”,说明这个内核的版本、加载位置、生成时间、大小等信息;其0x40之后与zImage没区别。由于Bootloader一般要占用0X0地址,所以,uImage相比zImage的好处就是可以和bootloader共存。其实就是一个自动跟手动的区别,有了uImage头部的描述,u-boot就知道对应Image的信息,如果没有头部则需要自己手动去搞那些参数。
将内核解压到内存的指定位置,就要开始运行内核。
内核入口函数是init/main.c中的start_kernel(void),Bootloader加载完内核映像后跳到Linux执行的第一个c语言程序,该程序完成的功能主要体现在其start_kernel(void)函数中,完成初始化Linux系统的进程管理,内存管理,文件系统等工作。
start_kernel(void)
asmlinkage void __init start_kernel(void)
{
...
//输出Linux版本信息
printk(KERN_NOTICE "%s", linux_banner);
//设置与体系结构相关的环境
setup_arch(&command_line);
...
//页表结构化
page_alloc_init();
printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
//内核选项的解析,内核解析完后,各个子系统的初始化就可通过kernel_init()=>do_basic_setup()=>do_initcalls()来完成。
parse_early_param();
parse_args("Booting kernel", static_command_line, __start___param,
__stop___param - __start___param,
0, 0, &unknown_bootoption);
...
//使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口
trap_init();
//内存初始化
mm_init();
...
//时间,定时器初始化
time_init();
...
//控制台初始化
console_init();
...
/* Do the rest non-__init'ed, we're now alive */
rest_init();
}
rest_init(void)
开启内核线程等。
static noinline void __init_refok rest_init(void)
{
...
//尽管init程将会挂起来等待创建kthreads进程complete,然而我们必须先创建init内核进程,这样它的pid为1。
//启动一个内核线程,这里的kernel_init是要执行的函数的指针,NULL表示传递给该函数的参数为空,CLONE_FS |CLONE_SIGHAND为do_fork产生线程时的标志,表示进程间的fs信息共享,信号处理和块信号共享。
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
...
//创建kthreadd内核线程
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
...
//获取kthreadd的线程信息,获取完成说明kthreadd已经 创建成功。并通过一个complete变量(kthreadd_done)来通知kernel_init线程。
complete(&kthreadd_done);
//为让系统运作起来,boot idle线程必须至少执行一次schedule()。
init_idle_bootup_task(current);//设置当前进程为idle(闲置)进程类。
schedule_preempt_disabled();//进程调度完成,回到这里,禁用抢占。
...
//在抢占禁用时调用cpu_idle,此时内核本体进入了idle状态,用循环消耗空闲的CPU时间片,该函数从不返回。在有其他进程需要工作的时候,该函数就会被抢占。
cpu_idle();
}
kernel_thread
创建一个内核线程,又称守护进程。源码在kernel/Process.c
/*
* Create a kernel thread.
*/
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
struct pt_regs regs;
memset(®s, 0, sizeof(regs));
regs.ARM_r4 = (unsigned long)arg;
regs.ARM_r5 = (unsigned long)fn;
regs.ARM_r6 = (unsigned long)kernel_thread_exit;
regs.ARM_r7 = SVC_MODE | PSR_ENDSTATE | PSR_ISETSTATE;
regs.ARM_pc = (unsigned long)kernel_thread_helper;
regs.ARM_cpsr = regs.ARM_r7 | PSR_I_BIT;
//do_fork来产生一个新的线程,共享父进程地址空间,并且不允许调试子进程。源码在kernel/Fork.c
return do_fork(flags|CLONE_VM|CLONE_UNTRACED, 0, ®s, 0, NULL, NULL);
}
kernel_init(void * unused)
此时与体系相关的部分已经初始化完成,开始初始化设备,完成外设及驱动程序(直接编译进内核的模块)的加载和初始化。
static int __init kernel_init(void * unused)
{
...
//此时与体系相关的部分已经初始化完成,do_basic_setup()开始初始化设备,完成外设及驱动程序(直接编译进内核的模块)的加载和初始化。
do_basic_setup();
...
/*
* Ok, we have completed the initial bootup, and
* we're essentially up and running. Get rid of the
* initmem segments and start the user-mode stuff..
*/
//开启init用户进程,所谓Android的启动流程从这里才刚刚开始。
init_post();
return 0;
}
do_basic_setup(void)
初始化设备
static void __init do_basic_setup(void)
{
...
do_initcalls();
}
kthreadd(void *unused)
内核线程函数,源码在kernel/kthread.c
int kthreadd(void *unused)
{
...
for (;;) {
set_current_state(TASK_INTERRUPTIBLE);
//如果发现kthread_create_list是一空链表,则调用schedule调度函数,会使当前进程进入睡眠
if (list_empty(&kthread_create_list))
schedule();
__set_current_state(TASK_RUNNING);
spin_lock(&kthread_create_lock);
while (!list_empty(&kthread_create_list)) {
struct kthread_create_info *create;
//会删除create对应的列表entry
create = list_entry(kthread_create_list.next,
struct kthread_create_info, list);
list_del_init(&create->list);
spin_unlock(&kthread_create_lock);
//在create_kthread()函数中,会调用kernel_thread来生成一个新的进程,该进程的内核函数为kthread,调用参数为create,kernel_thread(kthread, create, CLONE_FS | CLONE_FILES | SIGCHLD);
//kthread将会使其所在的进程进入休眠状态,直到被别的进程唤醒。如果被唤醒,将会调用create->threadfn(create->data);
create_kthread(create);
spin_lock(&kthread_create_lock);
}
spin_unlock(&kthread_create_lock);
}
return 0;
}
总之到目前为止,android的kernel创建了两个重要的进程,ps一下如下:
root@android:/ # ps
ps
USER PID PPID VSIZE RSS WCHAN PC NAME
root 1 0 624 476 c0123e84 0000872c S /init
root 2 0 0 0 c00b6600 00000000 S kthreadd
init进程父进程为0号进程,执行根目录底下的init可执行程序,是用户空间进程。
kthreadd父进程为0号进程,是内核进程,其他内核进程都是直接或者间接以它为父进程