移植的最后步骤:内核的启动过程分析

此前的博文均是以uboot为分析的目标,而此篇则是对内核的代码进行分析,抓大放小,分析linux系统的启动过程。由于牛油自己能力有限,欢迎大家交流指正!

分析的开始:从链接脚本开始找到程序的入口:

链接脚本:vmlinux.lds:内核提供一个arch/arm/kernel/vmlinux.lds.S文件,然后在编译时再去动态生成vmlinux.lds链接脚本。这样动态加载的原因:将vmlinux.lds.S这个条件编译的文件经过一个预处理,最终得到一个不用条件编译的vmlinux.lds文件来指导编译过程的进行。

通过对链接脚本的分析得知内核文件的入口程序:

ENTRY(stext)

追踪stext得知这个入口程序就在head.s汇编文件中。

	__HEAD
ENTRY(stext)
。。。。。。。。。

 

下面对head.S文件进行分析,分析启动的汇编阶段的原理:

1.定义了运行的虚拟地址和物理地址:

#define KERNEL_RAM_VADDR	(PAGE_OFFSET + TEXT_OFFSET)
#define KERNEL_RAM_PADDR	(PHYS_OFFSET + TEXT_OFFSET)

进过分析得知PAGE_OFFSET = 0xc0000000   TEXT_OFFSET=0x00008000。

则虚拟地址为:0xc0008000;物理地址为:0x30008000。 这也验证了uboot中传参与内核中设置的位置相同

2.内核的真正入口:

ENTRY(stext)

3.校验CPU的id号,机器码,内核传参:

校验校验CPU的ID号:

mrc	p15, 0, r9, c0, c0		@ get processor id

调用__lookup_processor_type()函数检验cpu的合法性,如果合法则继续启动:

bl	__lookup_processor_type

在内核中会维护一份支持的CPU的ID号,这里获取的ID号和维护的那份挨个进行比对查找,从而来保证内核启动的安全性。

校验机器码的真确性,合法则继续启动:

bl	__lookup_machine_type		@ r5=machinfo

 校验uboot通过tag给内核传参格式是否正确:

	bl	__vet_atags

4.页表的建立:

bl	__create_page_tables

这里尽快的建立页表,进入虚拟地址进行操作。  

而kernel建立页表实际分为两步:前期kernel先建立一个以1MB为单位的段式页表(在这里进行建立),4G内存需要4096个页表,每个页表4字节,页表会占用16KB的内存;后期内核启动之后再次建立一个更细的页表。 

5.跳转执行一个类似于函数指针数组内的函数:

ldr	r13, __switch_data		@ address to jump to after
__switch_data:
	.long	__mmap_switched
	.long	__data_loc			@ r4
	.long	_data				@ r5
	.long	__bss_start			@ r6
	.long	_end				@ r7
	.long	processor_id			@ r4
	.long	__machine_arch_type		@ r5
	.long	__atags_pointer			@ r6
	.long	cr_alignment			@ r7
	.long	init_thread_union + THREAD_START_SP @ sp

这个数组内先将前面校验的传参进行了保存。

其中最重要的是跳转至 __mmap_switched 函数指针指向的地址执行一些很重要的操作:

大致可总结为:拷贝代码段,清除BSS段,为C语言的执行提供软件环境:

    ldmia	r3!, {r4, r5, r6, r7}
	cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6
	ldrne	fp, [r4], #4
	strne	fp, [r5], #4
	bne	1b

	mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b

保存传递的参数:

 ARM(	ldmia	r3, {r4, r5, r6, r7, sp})
 THUMB(	ldmia	r3, {r4, r5, r6, r7}	)
 THUMB(	ldr	sp, [r3, #16]		)
	str	r9, [r4]			@ Save processor ID
	str	r1, [r5]			@ Save machine type
	str	r2, [r6]			@ Save atags pointer

跳转执行C语言部分的启动代码:

b	start_kernel

此后进入C语言的部分进行系统的启动。索引得知这个函数在Main.c中。

 

C语言阶段的分析:Main.c中的start_kernel()函数:

1.一些比较杂的初始化:

(1)多核性的对称多处理器的初始化:

smp_setup_processor_id();

(2)内核的调试模块,与内核的自旋锁,死锁有关:

lockdep_init();

(3)处理进程组的技术:

cgroup_init_early();

2.内核信息的打印:

	printk(KERN_NOTICE "%s", linux_banner);

 printk是内核中负责从console打印信息的函数,内核编程时不能使用标准库函数,与printf的功能基本相同,只不过printk可以选定消息输出的级别(共计0~7八个级别,控制台的消息显示级别为4)。

linux_banner经过分析为:

const char linux_banner[] ="Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";

而linux_banner中的很多信息的是在编译之后从一些生成的文件中动态加载过来的

3.确定当前内核运行的机器:

setup_arch(&command_line);

这个函数内记录着很多的硬件信息,打印出CPU的信息,将机器码进行比对。

而传入的那个参数就是uboot给kernel传入的启动参数bootagrs。而关于这个参数condline的设置,内核中自己设置了一份基本的comdilne,然后在启动时uboot如果不进行传参则会使用这份基本的comdline,否则会使用传参的comdline。

4.很多的内核工作模块的初始化部分:

(1)处理和命令行相关的东西:

setup_command_line(command_line);

(2)解析cmdline传参和其他的传参:

page_alloc_init();

(3)设置异常向量表:

trap_init()	

(4)内存管理模块初始化 :

mm_init()

(5)内核调度系统的初始化:

sched_init();

(6)中断初始化:

early_irq_init&init_IRQ        

 (7)控制台初始化:

console_init

5.函数中最后一个也是很重要的一个函数rest_init():

	/* Do the rest non-__init'ed, we're now alive */
	rest_init();

函数中启动了2个内核线程:kernel_init和kthreadd

kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
		numa_default_policy();
		pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

调用schedule()函数开启内核的调度系统,至此linux系统开始运行

schedule();

最后linux系统进入死循环,也是一个空闲进程

cpu_idle();
	void cpu_idle(void){
			local_fiq_enable();
			wile(1){
				......
			}
		}

这个死循环内如果需要可以进行其他进程的调度。

到这里,linux系统的三个进程0,1,2就开启不停的运行,他们分别是:
进程0:idle空闲进程;
进程1:kernel_init()函数,常被称为init进程;
进程2:kthreadd()函数,常被称为守护进程。

到这里Main.c中的start_kernel()函数执行完毕,内核成功启动,下面将接着对进程1这个init重要的进程做详细的分析

 

进程1:init进程的功能介绍:

1.完成由内核态到用户态的转换:内核刚开始运行时是内核态,然后通过运行用户态下面的程序将自己强行转换成用户态,此后后续的其他进程都可以工作在用户态下。跳跃的实现是通过一个函数kernel_execve()完成的。从此只能工作在用户态下,要使用内核态只能使用API函数。

2.内核态下的工作:挂载根文件系统;试图找到用户态下的那个init()函数完成内核态到用户态的转换,而其他的所有用户进程都派生自init进程。(内核态下的进程:内核源代码中的所有函数的执行都属于内核态下的任务)

3.用户态下的进程:应用程序,程序不属于内核源码,脱离了内核态,这就需要根文件系统,之后的程序都运行在用户态下。

4.构建了用户交互界面:启动了login进程,使得用户可以进行登录;启动了命令行进程(shell进程),命令行进程又启动了其他用户进程。

总结:挂载了根文件系统,找到了第一个用户空间下的应用程序(init进程),并执行了这个应用程序,从此进入了用户态下的空间。

kernel_init()函数的分析:

1.打开控制台设备,构建文件描述符:

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);

复制后就得到了三个文件描述符:0 1 2 标准输入、输出、错误文件描述符。

2.挂载根文件系统:

prepare_namespace();

通过prepare_namespace()函数挂载根文件系统。

而挂载前uboot已经将需要的参数传递给了kernel,例如:

const char *cmdline= "console=ttySAC0,115200 init=/linuxrc root = /dev/nfs nfsroot=192.168.1.163:work/nfs_root/ws  rootfstypes=ext3";

规定了控制台信息打印的串口是串口0  规定了串口0的波特率;
规定了init进程是执行的是根文件系统下/linuxrc程序; 
规定了根文件系统存放的位置(这里使用的是nhs进行根文件系统的挂载)。

3.根文件系统挂载的重启机制:

init_post();

若在传参的地址无法找到根文件系统的位置,内核会调用init_post()函数在其他的文件内尝试找到根文件系统进而启动。

4.挂载完根文件系统后运行用户态的init进程:

(1)用户态的init进程一般会在cmdline中指定,通常是根文件系统下的/linux可执行文件;

(2)如果cmdline中位指定,则在init_post()函数中有其他的预设方案:

run_init_process("/sbin/init");
			run_init_process("/etc/init");
			run_init_process("/bin/init");
			run_init_process("/bin/sh");

会接着去找正四个路径对应的可执行文件进而执行。

(3)如果都不成功,内核也不能正常的启动。

End。。。。。。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值