Linux 的初始化与启动过程
我们运行程序只需要点击应用程序的图标就可以了,但在这之前,我们必须启动我们的系统。在一切之前,我们必须有某些程序去引导我们系统的内核,这些程序就是内核引导程序了,例如LILO、GRUB、U-Boot、RedBoot。而这些引导程序同样需要被其他程序加载和运行,这样说下去,茫茫人生何处才是尽头啊?想必大家可以想到的----硬件!这么长的过程复杂、崎岖!正所谓万事开头难,但不怕,我们来一起走过去吧!
X86的引导过程如图:
cpu自身的初始化:这是引导的第一步,如果在多处理器系统上,那么每个cpu都要自身初始化。cpu初始化后,cpu从某个固定的位置(应该是0Xfffffff0)取指,这条指令是跳转指令,目的地是BIOS的首部代码,但是cpu并不在乎BIOS是否存在,它仅仅只是执行这个地址的指令而已!
BIOS:BIOS是只读存储器(ROM),被固化于主板上。其工作主要有两个,就是上图的加电自检即是POST(post on self test)与加载内核引导程序。
那么他们是具体完成什么工作的呢?
1) 加电自检:完成系统的硬件检测,其中包括内存检测、系统总线检测等工作。
2) 加载内核引导程序:在POST完成后,就要加载内核引导程序了,那它保存在哪里呢?磁盘里!哈哈,BIOS会读取0磁头,0磁道,一扇区的512个字节,这个扇区有叫做MBR(主引导记录),MBR中保存了内核引导程序的开始部分,BIOS将其装入内存执行。512个字节的MBR有些什么呢?这里有必要说说MBR!MBR分区表以80为起始,以55AA为结束,共64个字节。具体的MBR知识自己百度!
MBR:1) 446个字节的引导程序代码
2) 64个字节的分区表,有多少个分区呢。。?这还真不知道!分为4个分区表,一个可启动分区和三个不可启动分区。
3) 2个字节的0XAA55,用于检查MBR是否有效。
需要注意的是,内核引导程序被加载完后,POST部分的代码会被从内存中清理,只留部分在内存中留给目标操作系统使用。
内核引导程序:内核引导程序分两部分:主、次引导程序。主引导程序的主要工作就是收索,寻找活动的分区,将活动的分区引导记录中的次引导程序加载到内存中并且执行。而这个次引导程序就是负责加载内核的并且将控制权交给内核。上面提过内核引导程序有LILO、GRUB、U-Boot、RedBoot。其中前面两个为pc中的,而后面两个是嵌入式的。
内核:内核以压缩的形式存在,不是一个可执行的内核!所以内核阶段首先要做的是自解压内核映像。这里说说编译内核后形成的内核压缩的映像vmlinuz。编译生成vmlinux后,一般会对其进行压缩为vmlinuz,使其成为zImage--小于512KB的小内核,或者成为bzImage--大于512KB的大内核。
vmlinuz结构如图:
做了这么多工作终于把linux的内核给引导出来了。!!下面我们来初始化这个千呼万唤始出来的linux内核!
内核初始化:内核会调用一系列的初始化函数去对所有的内核组件进行初始化,由start_kernel()---.....---> rest_init() ----..----> kernel_init() ----....--> init_post() ------到---> 第一个用户init进程结束。
start_kernel():其完成大部分内核初始化的工作。相关的代码去查阅内核的源代码吧!www.kernel.org
rest_init():start_kernel() 调用rest_init() 进行后面的初始化工作。
kernel_init():此函数主要完成设备驱动程序的初始化,并且调用 init_post() 启动用户空间的init进程。
init_post():初始化的尾声,第一个用户空间的init 横空出世!其PID始终为1。
init: 内核会在过去曾使用过init的几个地方查找它,它的正确位置(对Linux系统来说)是/sbin/init。如果内核找不到init,它就会试着运行/bin/sh,如果运行失败,系统的启动也会失败。找到/sbin/init 后init会根据/etc/inittab (网上的资料都这样说的,我在Ubuntu3.8的内核里找不到,但在Fedora中可以找到!是发行版本不同吧?(求指教)这里附上图片!)文件完成其他一些工作,例如:getty进程接受用户的登录,设置网络等。这里详细说说吧。
fedora19的etc有inittab文件:
系统中所有的进程形成树型结构,而这棵树的根就是在内核态形成的,系统自动构造的0号进程,它是所有的进程的祖先。大致是在vmlinux的入口 startup_32(head.S)中为pid号为0的原始进程设置了执行环境,然后原是进程开始执行start_kernel()完成Linux内核的初始化工作。包括初始化页表,初始化中断向量表,初始化系统时间等。继而调用 fork(),创建第内核init进程:
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); // 参数CLONE_FS | CLONE_SIGHAND表示0号线程和1号线程分别共享文件系统(CLONE_FS)、打开的文件(CLONE_FILES)和信号处理程序 (CLONE_SIGHAND)。
这个进程就是著名的pid为1的init进程(内核态的),它会继续完成剩下的初始化工作比且创建若干个用于高速缓存和虚拟主存管理的内核线程,如kswapd和bdflush等,然后execve(/sbin/init)(生成用户态的init进程pid=1,因为没有调用fork(),所以pid还是1!), 成为系统中的其他所有进程的祖先。回过头来看pid=0的进程,在创建了init进程后(内核态的),pid=0的进程调用 cpu_idle()在主cpu中演变成了idle进程。而内核态pid=1的init进程同样会在各个从cpu上生成idel进程。 init在演变成/sbin/init之前,会执行一部分初始化工作,其中一个就是smp_prepare_cpus(),初始化SMP处理器,在这过程中会在处理每个从处理器时调用
task =copy_process(CLONE_VM, 0, idle_regs(®s), 0, NULL, NULL, 0); init_idle(task, cpu);
即从init中复制出一个进程,并把它初始化为idle进程(pid仍然为0)。从处理器上的idle进程会进行一Activate工作,然后执行cpu_idle()。
执行/sbin/init,这样从内核太过度到用户态,按照配置文件/etc/inittab 要求完成启动的工作,并且创建若干个不编号为1,2,3...号的终端注册进程getty,其作用就是设置其进程组的标识号,监视配置到系统终端的接口电路,当有信号来到的时候,getty会执行execve()生成注册进程login,用户可以注册登录了,如果登录成功,则login会演化为shell进程,若login不成功则关闭打开的终端线路,用户1号进程会创建新的getty。到这里init的流程基本完成了。奉上大图一张!
init的过程:
init 的流程讲完了,这里粗略说说init还做了什么事。先来认识下运行级别。
运行级别: linux可以在不同的场合启动不同的开机启动程序,这就叫做运行级别。根据不同的运行级别启动不同的程序。例如在用作服务器的时候要开启Apache,而桌面就不需要。
linux预先设置了7种运行级别(0--6)。ubuntu有8种(0--6、S),这里主要以ubuntu来说。0:关闭系统,1:系统进入单用户模式,S:单用户恢复模式,文本登录界面,只运行少数的系统服务。2:多用户模式(系统默认的级别),图形登录界面,运行所有预定的系统服务。3--5:多用户模式,图形登录界面,运行所有预定的系统服务(对于系统定制而言,运行级别2-5的作用等同),6:重启系统。
对于每个运行级别,在/etc/都有对应的子录目---/etc/rcN.d 用来指定要加载的程序。
细心看的话可以发现,除README外其他的文件都是“S开头+两位数字+程序名”的形式。这代表什么呢?S代表Start启动。如果是K的话则代表关闭kill,如果从其他的运行级别切换过来的话则要关闭程序。之后的数字为处理的顺序,越小则越早执行,如果数字相同,按字母的顺序启动。
上图可以看出这里的文件都是链接文件,为什么呢?上面说过各种运行级别有各自的一个录目用来存放各自的开机程序,如果有多个运行级别要启动同一个程序,那么这个程序的脚本会被拷贝到每一个录目里,这样做的话,如果要修改启动脚本就要修改每一个录目,这样不科学啊!!!!所以这些文件都是链接文件指向/etc/init.d。启动时就是运行这些脚本的。
子系统的初始化:
内核选项:linux 允许用户传递内核配置选项给内核,内核在初始化的过程中调用parse_args()函数对这些选项进行解析,之后调用相应的函数进行处理。对于parse_args()函数,其能够解析形如“变量名=值”的字符串,在模块加载的时候也会被调用来解析模块的参数。
子系统的初始化:在完成内核选项的解析后,就进入初始化的函数调用。在kernel_init()函数中调用do_basic_setup()函数再去调用do_initcalls()函数来完成。各个函数具体实现请查阅源代码!
登录:登录有三种方式。
1) 命令行登录
init创建getty,等用户输入用户名和密码,输入完成后调用login程序进行核对密码,如果正确就读取/etc/passwd文件,读取这个用户的指定的shell并启动它。
2) ssh登录
系统调用sshd程序,取代getty和login,之后启动shell。
3) 图形界面登录
init进程调用显示管理器,Gnome图形界面对应为gdm,然后输入用户名、密码,如果密码正确就启动用户会话。
到这里系统就启动起来了!说了这么多,现在我们来玩点好玩的!!!!!!简单熟悉一下linux的启动!!!!!!!!!!!首先要准备一个ubuntu系统最好13.04或者12.04都得,可以是虚拟机(最好是在虚拟机上操作!因为我因为这个小实验而重装了一遍真机系统!当时不懂啊!惨。。。。。。。。。)。
1:进入系统,在主文件夹(方便)新建一个c语言文件并且命名为init.c,输入---->最简单的c语言程序helloworld,不这是最伟大的c语言程序!
main(){
printf("helloworld!\n");
}
之后不用说就是编译啦。打开终端(ctrl + alt + t)执行这条指令:gcc --static -o init init.c 这样init文件就准备好了!猜到我想做什么了吗。。?哈哈我们继续!!
2:将上文提到过的/sbin/init 文件备份执行这条指令:sudo cp /sbin/init /sbin/init.bak 备份成init.bak 文件
将原来的init 文件删除,执行指令:sudo rm /sbin/init
好了,下一步就将我们的helloworld init复制到/sbin/录目下!执行指令: sudo mv init /sbin/ 这条指令之前要注意你终端当前的路径与init的路径是不是相同,要不不能成功的!!
3:这样就做好了!!!我们果断重启!这样在启动中我们看到了我们的 helloworld!唉。。它停哪里了!!那是肯定的,上文介绍了init的作用,你换了,不能启动是正常的啦。。思考下,到这里内核处于什么状态。?这里其实内核基本已经初始化完成了!就剩下进程的生成与子系统的初始化了。在init_post()函数的最尾会查找init的路径,如果找不到就崩溃。而且会试图建立一个交互式的shell(/bin/sh)来代替找不到的init,让用户可以修复这种错误、重新启动。
4:现在我们来恢复我们的系统!刚才在 2 中的步骤不会让你白做的!重新启动系统,并在一开始就按着左Shift (我以前的系统是不用的,不知道以前对grub.cfg做了什么坏事!!),系统会自动检测到这个信号的,这样会出现下面图中的界面。(建议去看看grub2的特性!):
按下e,进行编辑,按键盘右下角 向下的按键,在linux 这一行的最尾(quiet)前面加入----> init=/sbin/init.bak ,按下 ctrl + x 启动系统。如图:
如果一切没有错,那么你肯定能成功地重新启动系统!如果看到登录界面,那么恭喜你,你已经在漫漫人生路上走了一趟了!
好了,到这里本文要讲的知识已经讲完了。下面我们一起来思考一下我在这其中遇到的没有解决的问题,请大神们指教!
问题: 首先我的实验是在虚拟机上做的,原本我以为init是跟内核的压缩文件有关,所以用3.8.0-19 内核进去系统将init删了,这样3.8.0-19内核肯定启动不了内核。我用3.8.0-27内核进去系统,同样不能进去。。还是出现伟大的helloworld!这样看 init跟vmlinux等 在boot里的文件无关了!那么init关机后会被保存在哪里!?还有一个最重要的!我重新装了一遍系统(虚拟机的),这是没有3.8.0-27内核的,我将我真机的3.8.0-27的内核文件(/boot 里的vmlinuz文件等)放到虚拟机的/boot/文件夹里,重新启动系统,发现分辨率变了,光标不灵活(可能是分辨率的关系!),屏幕大小感觉是原来的两倍。这是为什么呢?
ps:本文挺长!如果你有疑问请提出来,交流交流,学习学习!如果你知道上面的问题的原因,那么请大神留几句话共小弟学习学习!