Linux 内核启动分析

Linux 内核启动分析

1. 内核启动地址

1.1. 名词解释

ZTEXTADDR

解压代码运行的开始地址。没有物理地址和虚拟地址之分,因为此时MMU处于关闭状态。这个地址不一定时RAM的地址,可以是支持读写寻址的flash等存储中介。

Start address of decompressor. here's no point in talking about virtual or physical addresses here, since the MMU will be off at the time when you call the decompressor code. You normally call the kernel at this address to start it booting. This doesn't have to be located in RAM, it can be in flash or other read-only or read-write addressable medium.

ZRELADDR

内核启动在RAM中的地址。压缩的内核映像被解压到这个地址,然后执行。

This is the address where the decompressed kernel will be written, and eventually executed. The following constraint must be valid:

__virt_to_phys(TEXTADDR) == ZRELADDR

The initial part of the kernel is carefully coded to be position independent.

TEXTADDR

内核启动的虚拟地址,与ZRELADDR相对应。一般内核启动的虚拟地址为RAM的第一个bank地址加上0x8000。

TEXTADDR = PAGE_OFFSET + TEXTOFFST

Virtual start address of kernel, normally PAGE_OFFSET + 0x8000.This is where the kernel image ends up. With the latest kernels, it must be located at 32768 bytes into a 128MB region. Previous kernels placed a restriction of 256MB here.

TEXTOFFSET

内核偏移地址。在arch/arm/makefile中设定。

PHYS_OFFSET

RAM第一个bank的物理起始地址。

Physical start address of the first bank of RAM.

PAGE_OFFSET

RAM第一个bank的虚拟起始地址。

Virtual start address of the first bank of RAM. During the kernel

boot phase, virtual address PAGE_OFFSET will be mapped to physical

address PHYS_OFFSET, along with any other mappings you supply.

This should be the same value as TASK_SIZE.

1.2. 内核启动地址确定

内核启动引导地址由bootp.lds决定。 Bootp.lds : arch/arm/bootp

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS

{

. = 0;

.text : {

_stext = .;

*(.start)

*(.text)

initrd_size = initrd_end - initrd_start;

_etext = .;

}

}

由上 .= 0可以确定解压代码运行的开始地址在0x0的位置。ZTEXTADDR的值决定了这个值得选取。

Makefile : arch/arm/boot/compressed

如果设定内核从ROM中启动的话,可以在make menuconfig 的配置界面中设置解压代码的起始地址,否则解压代码的起始地址为0x0。实际上,默认从ROM启动时,解压代码的起始地址也是0x0。

feq ($(CONFIG_ZBOOT_ROM),y)

ZTEXTADDR := $(CONFIG_ZBOOT_ROM_TEXT)

ZBSSADDR := $(CONFIG_ZBOOT_ROM_BSS)

else

ZTEXTADDR :=0 ZBSSADDR := ALIGN(4)

endif

SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/BSS_START/$(ZBSSADDR)/

……

$(obj)/vmlinux.lds: $(obj)/vmlinux.lds.in arch/arm/mach-s3c2410/Makefile .config

@sed "$(SEDFLAGS)" < $< > $@

@sed "$(SEDFLAGS)" < $< > $@ 规则将TEXT_START设定为ZTEXTADDR。TEXT_START在arch/arm/boot/compressed/vmlinux.lds.in 中被用来设定解压代码的起始地址。

OUTPUT_ARCH(arm)

ENTRY(_start)

SECTIONS

{

. = TEXT_START;

_text = .;

.text : {

_start = .;

*(.start)

*(.text)

*(.text.*)

……

}

}

内核的编译依靠vmlinux.lds,vmlinux.lds由vmlinux.lds.s 生成。从下面代码可以看出内核启动的虚拟地址被设置为PAGE_OFFSET + TEXT_OFFSET,而内核启动的物理地址ZRELADDR在arch/arm/boot/Makefile中设定。

OUTPUT_ARCH(arm)

ENTRY(stext)

SECTIONS

{

#ifdef CONFIG_XIP_KERNEL

. = XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR);

#else

. = PAGE_OFFSET + TEXT_OFFSET;

#endif

.init : { /* Init code and data */

_stext = .;

_sinittext = .;

*(.init.text)

_einittext = .;

……

}

}

# arch/arm/boot/Makefile

# Note: the following conditions must always be true:

# ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)

# PARAMS_PHYS must be within 4MB of ZRELADDR

# INITRD_PHYS must be in RAM

ZRELADDR := $(zreladdr-y)

#---> zrealaddr-y is specified with 0x30008000 in arch/arm/boot/makefile.boot

PARAMS_PHYS := $(params_phys-y)

INITRD_PHYS := $(initrd_phys-y)

export ZRELADDR INITRD_PHYS PARAMS_PHYS

通过下面的命令编译内核映像,由参数-a, -e设置其入口地址为ZRELADDR,此值在上面ZRELADDR := $(zreladdr-y)指定。

quiet_cmd_uimage= UIMAGE $@

cmd_uimage = $(CONFIG_SHELL) $(MKIMAGE) -A arm -O linux -T kernel /

-C none -a $(ZRELADDR) -e $(ZRELADDR) /

-n 'Linux-$(KERNELRELEASE)' -d $< $@

1.3. 小结

从上面分析可知道,linux内核被bootloader拷贝到RAM后,解压代码从ZTEXTADDR开始运行(这段代码是与位置无关的PIC)。内核被解压缩到ZREALADDR处,也就是内核启动的物理地址处。相应地,内核启动的虚拟地址被设定为TEXTADDR,满足如下条件:

TEXTADDR = PAGE_OFFSET + TEXT_OFFSET

内核启动的物理地址和虚拟地址满足入下条件:

ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)= virt_to_phys(TEXTADDR)

假定开发板为smdk2410,则有:

内核启动的虚拟地址

TEXTADDR     = 0xC0008000

内核启动的物理地址

ZRELADDR     = 0x30008000

如果直接从flash中启动还需要设置ZTEXTADDR地址。

2. 内核启动过程分析

内核启动过程经过大体可以分为两个阶段:内核映像的自引导;linux内核子模块的初始化。

clip_image002

2.1. 内核映像的自引导

这阶段的主要工作是实现压缩内核的解压和进入内核代码的入口。

Bootloader完成系统引导后,内核映像被调入内存指定的物理地址ZTEXTADDR。典型的内核映像由自引导程序和压缩的VMlinux组成。因此在启动内核之前需要先把内核解压缩。内核映像的入口的第一条代码就是自引导程序。它在arch/arm/boot/compressed/head.S文件中。

Head.S文件主要功能是实现压缩内核的解压和跳转到内核vmlinux内核的入口。Decompress_kernel(): arch/arm/boot/compressed/misc.c 和call_kernel这两个函数实现了上述功能。在调用decompress_kernel()解压内核之前,需要确保解压后的内核代码不会覆盖掉原来的内核映像。以及设定内核代码的入口地址ZREALADDR。

.text

adr r0, LC0

ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}

.type LC0, #object

LC0: .word LC0 @ r1

.word __bss_start @ r2

.word _end @ r3

.word zreladdr @ r4

.word _start @ r5

.word _got_start @ r6

.word _got_end @ ip

.word user_stack+4096 @ sp

上面这段代码得到内核代码的入口地址,保存在r4中。

/*

* Check to see if we will overwrite ourselves.

* r4 = final kernel address

* r5 = start of this image

* r2 = end of malloc space (and therefore this image)

* We basically want:

* r4 >= r2 -> OK

* r4 + image length <= r5 -> OK

*/

cmp r4, r2

bhs wont_overwrite

add r0, r4, #4096*1024 @ 4MB largest kernel size

cmp r0, r5

bls wont_overwrite

mov r5, r2 @ decompress after malloc space

mov r0, r5

mov r3, r7

bl decompress_kernel

b call_kernel

上面代码判断解压后的内核代码会不会覆盖原来的内核映像,然后调用内核解压缩函数decompress_kernel()。

ulg

decompress_kernel(ulg output_start, ulg free_mem_ptr_p, ulg free_mem_ptr_end_p,

int arch_id)

{

output_data = (uch *)output_start; /* 指定内核执行地址,保存在r4中*/

free_mem_ptr = free_mem_ptr_p;

free_mem_ptr_end = free_mem_ptr_end_p;

__machine_arch_type = arch_id;

arch_decomp_setup(); /*解压缩前的初始化和设置,包括串口波特率设置等*/

makecrc(); /*CRC校验*/

putstr("Uncompressing Linux...");

gunzip(); /*调用解压缩函数*/

putstr(" done, booting the kernel./n");

return output_ptr;

}

把内核映像解压到ZERALADDR地址后,调用call_kernel函数进入内核代码的入口地址。

call_kernel: bl cache_clean_flush

bl cache_off

mov r0, #0 @ must be zero

mov r1, r7 @ restore architecture number

mov r2, r8 @ restore atags pointer

mov pc, r4 @ call kernel

我们知道r4寄存器内保存的是内核的执行地址,mov pc, r4使得程序指针指向了内核的执行地址,所以下面将进入内核代码执行阶段。

2.2. linux内核子模块的初始化
2.2.1. 预备工作

进入真正的内核代码,首先执行的也是一个叫做head.S(arch/arm/kernel/)的文件。同时head.S也包含了同目录下head-common.S(arch/arm/kernel/)。这两个文件联合起来主要负责下面几项工作:

clip_image001 判断CPU类型,查找运行的CPU ID值与Linux编译支持的ID值是否支持

clip_image001[1] 判断体系类型,查看R1寄存器的Architecture Type值是否支持

clip_image001[2] 创建页表

clip_image001[3] 开启MMU

clip_image001[4] 跳转到start_kernel()(内核子模块初始化程序)

注: 暂时不对各个子程序实现作细节性的分析。

2.2.2. 内核各子模块初始化

Start_kernel函数是Linux内核通用的初始化函数。无论对于什么体系结构的Linux,都要执行这个函数。Start_kernel()函数是内核初始化的基本过程。下面按照函数对内核模块初始化的先后顺序进行分析。

asmlinkage void __init start_kernel(void)

{

char * command_line;

extern struct kernel_param __start___param[], __stop___param[];

smp_setup_processor_id(); /*指定当前的cpu的逻辑号,这个函数对应于对称多处理器的设置,当系统中只有一个cpu的情况,此函数为空,什么也不做*/

lockdep_init(); /* 初始化lockdep hash 表 */

/* 初始化irq */

local_irq_disable();

early_boot_irqs_off();

early_init_irq_lock_class();

/* 锁定内核、设置cpu的状态为’present’,’online’等状态、初始化页表、打印内核版本号等信息、设置体系结构、为cpu分配启动内存空间 ,在此期间中断仍然处于关闭状态*/

lock_kernel();

boot_cpu_init();

page_address_init();

printk(KERN_NOTICE);

printk(linux_banner);

setup_arch(&command_line);

setup_per_cpu_areas();

smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */

/*在打开任何中断之前打开调度器 */

sched_init();

/*关闭任务抢占功能,因为早期的调度器功能比较脆弱,直到第一次调用cpu_idle()*/

preempt_disable();

/* 建立内存区域链表节点,对于单cpu节点数为1 */

build_all_zonelists();

/* 发通知给每个CPU,处理每个CPU的内存状态*/

page_alloc_init();

/* 分析早期没命令参数*/

printk(KERN_NOTICE "Kernel command line: %s/n", saved_command_line);

parse_early_param();

/* 分析命令参数 */

parse_args("Booting kernel", command_line, __start___param,

__stop___param - __start___param,

&unknown_bootoption);

/* 排序内核创建的异常表 */

sort_main_extable();

unwind_init();

/*设置陷阱门和中断门 */

trap_init();

/*初始化内核中的读-拷贝-更新(Read-Copy-Update RCU)子系统 */

rcu_init();

/*初始化IRQ */

init_IRQ();

/* 按照开发办上的物理内存初始化pid hash表 */

pidhash_init();

/*初始化计时器 */

init_timers();

/* 高解析度&高精度的计时器 (high resolution)初始化 */

hrtimers_init();

/*初始化软中断 */

softirq_init();

/* 初始化时钟资源和普通计时器的值 */

timekeeping_init();

/* 初始化系统时间*/

time_init();

/*为内核分配内存以存储收集的数据*/

profile_init();

/* 开中断 */

if (!irqs_disabled())

printk("start_kernel(): bug: interrupts were enabled early/n");

early_boot_irqs_on();

local_irq_enable();

/*

* HACK ALERT! This is early. We're enabling the console before

* we've done PCI setups etc, and console_init() must be aware of

* this. But we do want output early, in case something goes wrong.

*/

/* 初始化控制台,为了能够尽早地帮助调试,显示系统引导的信息*/

console_init();

if (panic_later)

panic(panic_later, panic_param);

/*如果定义了CONFIG_LOCKDEP宏,则打印锁依赖信息,否则什么也不做 */

lockdep_info();

/*

* Need to run this when irqs are enabled, because it wants

* to self-test [hard/soft]-irqs on/off lock inversion bugs

* too:

*/

/* 如果定义CONFIG_DEBUG_LOCKING_API_SELFTESTS宏,则locking_selftest()是一个空函数,否则执行锁自测*/

locking_selftest();

#ifdef CONFIG_BLK_DEV_INITRD

if (initrd_start && !initrd_below_start_ok &&

initrd_start < min_low_pfn << PAGE_SHIFT) {

printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "

"disabling it./n",initrd_start,min_low_pfn << PAGE_SHIFT);

initrd_start = 0;

}

#endif

/*

(1)dcache_init()创建SLAB缓存,该缓存保存目录项描述符。传存本身被称作dentry_cache。当进程访问文件或目录时所涉及的目录名有多个目录分量组成,目录项描述符就是针对每个分量而创建的。目录项各结构把文件或目录分量与其索引结点结合起来,因而可以通过该目录项可以更快地找到与其对应的索引结点

(2)inode_init()初始化哈希表索引结点和等待队列对头,该队头存放内核要锁存的哈希索引结点。

(3)file_init()确定给每个进程呢个zhogn的文件所分配的最大内存量

(4)mnt_init()创建了保存vfsmount对象且名为mnt_cache的缓存,VFS利用这协对吸纳给来挂载文件系统。该例程也创建mount_hashtable队列,该队列存放mnt_cache中引用的快速访问对象。然后该例程发出调用来初始化sysfs文件系统并挂载root文件系统。

*/

vfs_caches_init_early();

cpuset_init_early();

/* Mem_init()函数为mem_map中的自由区作标记并且打印出自由内存的大小。这个函数在系统的各个部分申请过内存后执行 */

mem_init();

/* 初始化cache相关的链表,函数在初始化页表分配器后smp_init()之前执行 */

kmem_cache_init();

/* 为逻辑号为0的cpu初始化页面。如果是smp情况下,只要cpu表现为online态,此函数就会执行 */

setup_per_cpu_pageset();

/* numa内存策略器初始化 */

numa_policy_init();

/* 内存初始化后调用 */

if (late_time_init)

late_time_init();

/*计算并打印许多著名的"BogoMips"的值,该值度量处理器在一个时钟节拍内可以反复执行多少个delay().对不同速度的处理器,cali_brate_delay()允许的延迟大约相同*/

calibrate_delay();

/* 初始化pidmap_array,分配pid=0给当前进程 */

pidmap_init();

/* 初始化页表高速缓存 */

pgtable_cache_init();

/*初始化优先级树index_bits_to_maxindex数组*/

prio_tree_init();

/*创建anon_vma结构对象slab缓存*/

anon_vma_init();

#ifdef CONFIG_X86

if (efi_enabled)

efi_enter_virtual_mode();

#endif

/*根据可用内存大小来建立用户缓冲区uid_cache,初始化最大线程数max_threads,为init_task配置RLIMIT_NPROC的值为max_threads/2 */

fork_init(num_physpages);

/*建立各种块缓冲区,比如VFS, VM等*/

proc_caches_init();

buffer_init();

unnamed_dev_init();

key_init();

security_init();

vfs_caches_init(num_physpages);

radix_tree_init();

signals_init();

/* rootfs populating might need page-writeback */

page_writeback_init();

#ifdef CONFIG_PROC_FS

proc_root_init();

#endif

cpuset_init();

taskstats_init_early();

delayacct_init();

/*检查错误,其实是调用check_writebuffer_bugs()函数检查是否有物理地址混淆的现象 */

check_bugs();

/* advanced configuration and power management interface */

acpi_early_init(); /* before LAPIC and SMP init */

/* Do the rest non-__init'ed, we're now alive */

/* 创建init进程,删除内核锁,启动idle线程 */

rest_init();

}

进入init进程后,将执行init()函数负责完成挂接根文件系统、初始化设备驱动和启动用户空间的init进程。(sunny 负责研究这部分)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值