技术的乐趣在于分享,欢迎多多交流,多多沟通。
全部博文(877)
分类: LINUX
2014-03-21 16:25:55
原文地址:Linux 启动过程分析 作者:doujiao
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内核子模块的初始化。
start
Decompress_kernel()
Call_kernel
Stext:
Prepare_namespace
Do_basic_setup
init
Rest_init
Setup_arch
……
Start_kernel
_enable_mmu
Execve(“/sbin/init”))
内
核启动流程图
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/)。这两个文件联合起来主要负责下面几项工作:
判断CPU类型,查找运行的CPU ID值与Linux编译支持的ID值是否支持
判断体系类型,查看R1寄存器的Architecture Type值是否支持
创建页表
开启MMU
跳转到start_kernel()(内核子模块初始化程序)
注: 暂时不对各个子程序实现作细节性的分析。
2.2.2. 内核各子模块初始化
Start_kernel函数是Linux内核通用的初始化函数。无论对于什么体系结构的Linux,都
要执行这个函数。Start_kernel()函数是内核初始化的基本过程。下面按照函数对内核模块初始化的先后顺序进行分析。
start_kernel
函数位于init/main.c
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 负责研究这部分)