内核启动分析

来源:互联网 发布:淘宝网店转让合法吗 编辑:程序博客网 时间:2024/06/04 19:03

内核启动分析

顶层Makefile分析uboot的Makefile写法、规则等是一样的。 在编译内核时,也可以通过命令行给内核Makefile传参。(make o=XXX,指定不在源代码目录下编译,而是到另一个单独文件夹下编译)

两个重要参数:ARCH和CROSS_COMPILE。ARCH决定当前配置编译的路径,如ARCH = arm的时候,将来在源码目录下去操作的arch/arm目录。CROSS_COMPILE用来指定交叉编译工具链的路径和前缀。

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-

(编译生成的zImage在arch/arm/boot/目录下)

 

链接脚本分析

为何找链接脚本?就是为了找到程序的entry。内核没有像uboot那样直接提供.lds脚本文件,而是提供一个汇编文件vmlinux.lds.S,然后编译的时候再去编译这个汇编文件得到真正的链接脚本vmlinux.lds。在arch/arm/kernel目录下。从vmlinux.lds中ENTRY(stext)可以知道入口符号是stext,就在head.S文件中。

 

head.S文件分析

内核运行的物理地址与虚拟地址:

 

#define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)

#define KERNEL_RAM_PADDR (PHYS_OFFSET + TEXT_OFFSET)

 

#define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)

CONFIG_PAGE_OFFSET 在.config中可以查到。

CONFIG_PAGE_OFFSET = 0xC0000000

 

TEXT_OFFSET = 0x00008000

#define PHYS_OFFSET UL(0x30000000)

 

(1) KERNEL_RAM_VADDR:这个宏定义了内核运行时的虚拟地址。值为0xC0008000

(2) KERNEL_RAM_PADDR,这个宏定义内核运行时的物理地址。值为0x30008000

(3)总结:内核运行的物理地址是0x30008000,对应的虚拟地址是0xC0008000。

 

内核的真正入口是ENTRY(stext),内核的起始部分代码是被解压代码调用的,其启动的条件是由bootloader(uboot)来构建保证的。ARM体系中,函数调用时实际是通过寄存器传参的(函数调用时传参有两种设计:一种是寄存器传参,另一种是栈内存传参)。所以uboot中最后调用函数

theKernel (0, machid, bd->bi_boot_params) 执行内核时,运行时实际把0放入r0中,machid(机器码)放入到了r1中,bd->bi_boot_params(boot_argc)放入到了r2中。

kernel启动时MMU是关闭的,因此硬件上需要的是物理地址。但是内核是一个整体(zImage)只能被连接到一个地址(不能分散加载),这个连接地址肯定是虚拟地址。因此内核运行时前段head.S中尚未开启MMU之前的这段代码必须是位置无关码,而且其中涉及到操作硬件寄存器等时必须使用物理地址。

 

内核启动的汇编阶段

1、__lookup_processor_type

cp15协处理器的c0寄存器中读取出硬件的CPU ID号,然后调用这个函数来进行合法性检验。如果失败就会转向__error_p函数。

2、__lookup_machine_type和__lookup_processor_type一样对机器码进行校 验。

3、__vet_atags 检验bootargs传的参数是否正确。

4、__create_page_tables 建立页表。linux内核本身被连接在虚拟地址处,因 kernel希望尽快建立页表并且启动MMU进入虚拟地址工作状态。

 

kernel建立页表其实分为2步。第一步,kernel先建立了一个段式页表(和uboot中之前建立的页表一样,页表以1MB为单位来区分的),这里的函数就是建立段式页表的。段式页表本身比较好建立(段式页表1MB一个映射,4GB空间需要4096个页表项,每个页表项4字节,因此一共需要16KB内存来做页表),坏处是比较粗不能精细管理内存;第二步,再去建立一个细页表(4kb为单位的细页表),然后启用新的细页表废除第一步建立的段式映射页表。内核启动的早期建立段式页表,并在内核启动前期使用;内核启动后期就会再次建立细页表并启用。等内核工作起来之后就只有细页表了。

5、__switch_data

执行的功能:复制数据段、清除bss段(目的是构建C语言运行环境);保存起来cpu id号、机器码、tag传参的首地址。b start_kernel跳转到C语言运行阶段。

 

C语言启动阶段

lockdep_init   锁定依赖,是一个内核调试模块,相关的处理内核自旋锁死锁问题。

cgroup_init_early  内核提供的一种来处理进程组的技术。

 

打印内核版本信息

printk(KERN_NOTICE "%s", linux_banner);

setup_arch  确定当前平台

setup_processor 查找CPU信息,可通过串口打印分析。

setup_machine 传参是机器码编号,machine_arch_type符号在include/generated/mach-types.h的32039-32050行定义了。X210传参值就是2456。函数的作用是通过传入的机器码编号,找到对应这个机器码的machine_desc描述符,并且返回这个描述符的指针。

__lookup_machine_type函数的工作原理:内核在建立的时候就把各种CPU架构的信息组织成一个一个的machine_desc结构体实例,然后都给一个段属性.arch.info.init,链接的时候会保证这些描述符会被连接在一起。__lookup_machine_type就去那个那些描述符所在处依次挨个遍历各个描述符,比对看机器码哪个相同。

 

setup_arch函数进行了基本的cmdline处理。内核对cmdline的处理思路是:内核中自己维护了一个默认的cmdline(就是.config中配置的这一个),然后uboot还可以通过tag给kernel再传递一个cmdline。如果uboot给内核传cmdline成功则内核会优先使用uboot传递的这一个;如果uboot没有给内核传cmdline或者传参失败,则内核会使用自己默认的这个cmdline。以上说的这个处理思路就是在setup_arch函数中实现的。

 

setup_command_line

parse_early_param & parse_args 解析cmdline传参和其他传参

trap_init 设置异常向量表

mm_init 内存管理模块初始化

sched_init 内核调度系统初始化

early_irq_init&init_IRQ 中断初始化

console_init 控制台初始化

 

rest_init   

rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd,调用schedule函数开启了内核的调度系统。到了cpu_idle()函数,系统就起来了。

 

三个进程

进程0:进程0就是idle进程,叫空闲进程。

进程1:kernel_init函数就是进程1,这个进程被称为init进程。

进程2:kthreadd函数就是进程2,这个进程是linux内核的守护进程。这个进程是用来保证linux内核自己本身能正常工作的。

 

如何从内核态跳跃到用户态?

init进程在内核态下面时,通过一个函数kernel_execve来执行一个用户空间编译连接的应用程序就跳跃到用户态了,这个跳跃过程是单向的。

 

prepare_namespace函数中挂载根文件系统

uboot通过传参来告诉内核这些信息。

uboot传参中的root=/dev/mmcblk0p2 rw 这一句就是告诉内核根文件系统在哪里

uboot传参中的rootfstype=ext3这一句就是告诉内核rootfs的类型。

如果内核挂载根文件系统成功,则会打印出:VFS: Mounted root (ext3 filesystem) on device 179:2.

如果挂载根文件系统失败,则会打印:No filesystem could mount root, tried:  yaffs2

如果内核启动时挂载rootfs失败,则后面肯定没法执行了。

 

如何确定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。

 

cmdline常用参数

root=

(1)这个是用来指定根文件系统在哪里的。

(2)一般格式是root=/dev/xxx(一般如果是nandflash上则/dev/mtdblock2, 如果是inand/sd的话则/dev/mmcblk0p2)

(3)如果是nfs的rootfs,则root=/dev/nfs。

 

rootfstype=

根文件系统的文件系统类型,一般是jffs2、yaffs2、ext3、ubi

 

console=

控制台信息声明,譬如console=/dev/ttySAC0,115200表示控制台使用串口0,波特率是115200.

 

mem=

用来告诉内核当前系统的内存有多少

 

init=

用来指定进程1的程序pathname,一般都是init=/linuxrc

 

常见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上

 

 

 

内核代码基本分为3块

(1)arch。 本目录下全是cpu架构有关的代码

(2)drivers 本目录下全是硬件的驱动

(3)其他 相同点是这些代码都和硬件无关,因此系统移植和驱动开发的时候这些代码几乎都是不用关注的。

 

架构相关的常用目录名及含义

 

(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)头文件目录include有好几个,譬如:

kernel/include 内核通用头文件

kernel/arch/arm/include 架构相关的头文件

Kernel/arch/arm/include/asm

Kernel/arch/arm/include/asm/mach

Kernel/arch/arm/arch-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)有些同名的头文件是有包含关系的,有时候我们需要包含某个头文件时可能并不是直接包含他,而是包含一个包含了他的头文件。