主Makefile分析:
- 刚开始定义了kernel的版本号
- make 编译内核时,也可以通过make O=…传参(和uboot一样)指定编译目录
- 两个重要的变量 ARCH,CROSS_COMPILE决定了架构和编译工具链路径,这两个参数也可以通过make时传参
例:
make O=/tmp/mykernel ARCH=arm CROSS_COMPILE=/usr/local/arm/arm-2009q3/bin/arm-none-linux-gnueabi-
链接脚本分析:
- 找到整个程序的入口
ENTRY(stext)
stext 在 arch/arm/kernel/目录下的head.S中
- kernel的连接脚本并不是直接提供的,而是提供了一个汇编文件vmlinux.lds.S,然后在编译的时候再去编译这个汇编文件得到真正的链接脚本vmlinux.lds,因为链接脚本不能使用条件编译,所以使用汇编文件进行条件编译,然后生成链接脚本
head.S文件:
1. 内核运行的物理地址和虚拟地址
#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET) #0xC0008000
#define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET) #0x30008000
这里内核运行的物理地址是0x30008000,对应的虚拟地址是0xC0008000
2. 内核的真正入口
__HEAD
ENTRY(stext)
__HEAD定义了后面的代码属于段名为.head.text段
3. 内核运行的条件
(1)内核的起始部分代码是被解压代码调用的。,解压代码运行时先将zImage后段的内核解压开,然后再去调用运行真正的内核入口。
(2)内核启动不是无条件的,而是有一定的先决条件,这个条件由启动内核的bootloader来构建保证。
(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之前的这段代码必须是位置无关码,而且其中涉及到操作硬件寄存器等时必须使用物理地址。
内核启动的汇编阶段:
从 ENTRY(stext) 到 b start_kernel
- 从cp15协处理器的c0寄存器中读取出硬件的CPU ID号,然后调用这个函数来进行合法性检验
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
- 函数校验的是机器码
bl __lookup_machine_type @ r5=machinfo
- 校验uboot给内核的传参ATAGS格式是否正确(主要是板子的内存分布memtag、uboot的bootargs)
bl __vet_atags
- 建立页表
bl __create_page_tables
(1)linux内核本身被连接在虚拟地址处,因此kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态。但是kernel本身工作起来后页表体系是非常复杂的,建立起来也不是那么容易的。kernel使用下面的方法
(2)kernel建立页表其实分为2步。第一步,kernel先建立了一个段式页表(和uboot中之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表的。段式页表本身比较好建立(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。
(3)内核启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会再次建立细页表并启用。等内核工作起来之后就只有细页表了。
5. __switch_data
ldr r13, __switch_data @ address to jump to after
(1)建立了段式页表后进入了__switch_data部分,这是个函数指针数组。
(2)下一步要执行__mmap_switched函数
在__mmap_switched函数中做了下面的事情:
1.复制数据段、清除bss段(目的是构建C语言运行环境)
2.保存起来cpu id号、机器码、tag传参的首地址。
3. b start_kernel跳转到C语言运行阶段。
总结:汇编阶段主要就是校验启动合法性、建立段式映射的页表并开启MMU以方便使用内存、跳入C阶段
C语言阶段:
打印内核版本信息
处理传参信息
初始化各种内核需要的工作模块(内存管理,调度系统,异常处理)
rest_init函数:
这个函数之前内核基本组装完成, 剩下的一些重要工作在这里做
1.启动两个内核线程(应该是进程?)
kernel_init:init进程,1号进程
kthreadd:linux内核的守护进程,2号进程,用来保证内核自身能够正常工作
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
2.开启调度系统
从此linux系统开始运转
schedule();
3.内核的最终归宿
最终rest_init函数调用cpu_idle函数结束了整个内核的启动
cpu_idle();
这里面是idle进程,即0号进程,空闲进程,CPU没有被其他进程占用时就执行这个进程(死循环)
init进程:
init进程完成了从内核态到用户态的转变:
应用层运行一个程序就构成一个用户进程/线程,内核中运行一个函数(函数其实就是一个程序)就构成了一个内核进程/线程
init进程具有两种状态,init进程刚开始运行时是内核态,它属于一个内核进程,然后他自己运行了一个用户态下面的程序后把自己强行转成了用户态。因为init进程自身完成了从内核态到用户态的过度,因此后续的其他进程都可以工作在用户态下面了
(1)内核态下:重点就做了一件事情,挂载根文件系统并试图找到用户态下的那个init程序。init进程要把自己转成用户态就必须运行一个用户态的应用程序(这个应用程序名字一般也叫init),要运行这个应用程序就必须得找到这个应用程序,提供这个init程序的就是根文件系统,所以要找到它就必须得挂载根文件系统,因为所有的应用程序都在文件系统中。
(2)用户态下:init进程大部分有意义的工作都是在用户态下进行的。
如何转变:
init进程通过一个函数kernel_execve来执行一个用户空间编译连接的应用程序就跳跃到用户态了,这个跳跃过程中进程号是没有改变的,PID一直是1
init进程对我们操作系统的意义在于:
- init进程构建了用户交互界面
- init启动了login进程、命令行进程、shell进程
- shell进程启动了其他用户进程。命令行和shell一旦工作了,用户就可以在命令行下通过./xx的方式来执行其他应用程序,每一个应用程序的运行就是一个进程,所以所有其他应用进程都是init进程的派生进程
kernel_init函数概览:
1. 打开控制台
/* Open the /dev/console on the rootfs, this should never fail */
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
- 这里我们打开了/dev/console文件,并且复制了2次文件描述符,一共得到了3个文件描述符。这三个文件描述符分别是0、1、2.这三个文件描述符就是所谓的:标准输入、标准输出、标准错误。
- 进程1打开了三个标准输出输出错误文件,因此后续的进程1衍生出来的所有的进程默认都具有这3个三件描述符。
2. 挂载根文件系统
prepare_namespace函数中挂载根文件系统
prepare_namespace();
(1)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: xxx
(4)如果内核启动时挂载rootfs失败,会自动重启
(5)如果挂载rootfs失败,可能的原因有:
- uboot的bootargs设置不对
- rootfs烧录失败(fastboot烧录不容易出错)
- rootfs本身制作失败的
3.执行用户态下的进程1程序
挂载rootfs成功后就进入rootfs中寻找init程序,这个程序就是用户态下的进程1,找到后用run_init_process去执行他
- 首先查看传参中的指定程序,cmdline中的init=/linuxrc这个就是指定rootfs中哪个程序是init程序
- cmdline中没有init=xx或者cmdline中指定的这个xx执行失败,还有备用方案。第一备用:/sbin/init,第二备用:/etc/init,第三备用:/bin/init,第四备用:/bin/sh
cmdline常用参数:
root=/dev/xxx:
指定根文件系统位置,
举例:
如果是nandflash上则/dev/mtdblock2(mtd设备,block2)
如果是inand/sd的话则/dev/mmcblk0p2(mmc设备,block0,扇区2)
如果是nfs的rootfs,则root=/dev/nfs
rootfstype=:
根文件系统类型,一般有jffs2、yaffs2、ext3、ubi
console=:
控制台指定
举例:
console=/dev/ttySAC0,115200 表示控制台使用串口0,波特率是115200
mem=:
用来告诉内核当前系统内存有多少
init=:
指定init进程的pathname,一般都是init=/linuxrc