本文主要解决了以下几个问题:
1、当你按下电源键以后都发生了什么?
2、Linux kernel初始化都在初始化什么?
3、init进程何时创建?
注:本文所有的的内核代码都是来自于kernel3.14.54,读者可以未经作者允许随意转载,但请保证文章的完整性。
第一部分:当你按下电源的时候,你的计算机都做了什么。
当你按下电源的时候,你的内存,寄存器都是空的(材质都是易失性存储,断电后数据就消失了)。这时候计算机去ROM中装入BIOS系统(处于0xfffffff0位置),BIOS系统执行加电自检,初始化GDT、LDT ,初始化硬件设备,找操作系统(注意这个时候是实模式)。
找到操作系统以后进行操作系统的载入任务:由GRUB引导程序载入内核映像,由setup函数执行硬件的初始化(即使BIOS已经初始化过了大部分硬件设备,但是Linux内核还是会在函数setup里边再次初始化),跳转到startup_32函数为0号进程执行建立环境,最后调用start_kernel函数初始化内核。
以上就是开机全部过程的简单介绍,至于setup函数和start_kernel函数都在初始化什么内容,这就到了本文的第二部分内容。
第二部分,linux kernel初始化都在初始化什么。
initrd:是initial RAM disk的简写。grub引导在加载内核的时候会把相关的initrd也一起加载进来,内核在挂载根文件之前会执行initrd中的程序,执行完了以后才会把cpu交给kernel的initial程序来完成kernel初始化。
1、start_kernel函数
setup函数和startup_32函数都是汇编语言函数,执行大部分硬件和寄存器的初始化操作。真正的Linux的初始化操作是在start_kernel函数中执行的,函数定义如下:
void start_kernel(void){
srm_printk("Linux/AXP bootp loader for Linux " UTS_RELEASE "\n");
if (INIT_HWRPB->pagesize != 8192) {
srm_printk("Expected 8kB pages, got %ldkB\n",INIT_HWRPB->pagesize >> 10);
return ;
}
pal_init();
initrd_start = ((START_ADDR + 5*KERNEL_SIZE + PAGE_SIZE) |(PAGE_SIZE-1)) + 1;
srm_printk("Initrd positioned at %#lx\n", initrd_start);
move_stack(initrd_start - PAGE_SIZE);
nbytes = callback_getenv(ENV_BOOTED_OSFLAGS, envval, sizeof(envval));
}
首先屏幕上会显示你的内核版本号(UTS_REKEASE),如果pagesize不是8k,屏幕打印提示信息然后退出初始化函数。图形化界面初始化(为图形化界面准备函数初始化一个临时的PCB然后进行XXXX操作,没看懂),准备ininrd的起始地址并打印出该地址。移动栈防止栈被内核镜像重写,得到环境变量以及环境变量字符串的长度。
srm_printk("Loading the kernel...'%s'\n", envval);
load(initrd_start, KERNEL_ORIGIN+KERNEL_SIZE, INITRD_IMAGE_SIZE);
load(START_ADDR+(4*KERNEL_SIZE), KERNEL_ORIGIN, KERNEL_SIZE);
load(START_ADDR, START_ADDR+(4*KERNEL_SIZE), KERNEL_SIZE);
载入initrd镜像(load函数参数分别是:源地址、目的地址和拷贝字节数;执行的操作是:从initrd-_start所指向的地址开始拷贝INITRD_IMAGE_SIZE大小到KERNEL_ORIGIN+KERNEL_SIZE处,之后的load函数类似);
内核在START_ADDR地址处,需要载入到START_ADDR+(4*KERNEL_SIZE)地址处,在加载内核之前先要把START_ADDR+(4*KERNEL_SIZE)地址处KERNEL_SIZE大小空间里边的内容复制到KERNEL_ORIGIN处(KERNEL_SIZE、KERNEL_ORIGIN这两个宏定义我没找到)
memset((char*)ZERO_PGE, 0, PAGE_SIZE);
strcpy((char*)ZERO_PGE, envval);
((long *)(ZERO_PGE+256))[0] = initrd_start;
((long *)(ZERO_PGE+256))[1] = INITRD_IMAGE_SIZE;
runkernel();
清空第零页内容(实际上是清空ZERO_PGE往下的4096字节内容,我们暂且把这一块内容称为第零页),把环境变量写入零页。
把initrd的起始地址写入(ZERO_PGE+256))[0]单元内,把initrd镜像的大小写入(ZERO_PGE+256))[1]单元处,调用函数runkernel。到这里start_kernel函数的执行就结束了。
通过前面的分析我们能看出,start_kernel函数很大一部分在进行对内核空间的初始化,为分页做准备。这部分都是一些对地址的直接操作,比较乱,我们来画张图整理以下。
上图描述了start_kernel用到的一些宏定义以及定义的内容。
2、runkernel函数
runkernel函数只有一行代码,是一行内联汇编,下面我们来一句一句分析这样汇编代码。
__asm__ __volatile__(
"bis %0,%0,$27\n\t"
"jmp ($27)"
: /* no outputs: it doesn't even return */
: "r" (START_ADDR));
内联汇编的格式是:
asm ( “statements” : output_registers : input_registers : clobbered_registers);
那么对于我们上边的内联汇编,汇编语句是bis %0,%0,$27 jmp ($27)
,没有输出部分,输入部分是"r" (START_ADDR)
,没有要修改的寄存器。
这两行汇编还不是很懂的样子,等我在复习复习汇编再来完善。
到这里差不多start_kernel函数就看完了。就觉得奇怪,为什么我没有看到初始化调度函数初始化页面那些东西,可能被封装在了哪个函数里了吧,目前没办法写第三部分了。
说两点:
1、我想像中的这个博文是从实模式开始描述的,现在这个样子是一个雏形,后来慢慢看的多了会继续补充(主要是汇编这个短板)。
2、 能开这个文,我是欣喜的。
3、众所周知,对于软件开发人员硬件一直是努力回避的问题,至少之前的我是这么想的也是这么做的。回避着回避着,突然有一天觉得这样做是不对的,是不均衡的。不了解硬件怎么写出牛逼的软件。