您的位置:首页 > 运维架构 > Linux

Linux内核分析3:跟踪分析Linux内核的启动过程

2016-03-10 21:56 295 查看
席金玉+ 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”

一、计算机的启动过程

1、加电自检:完成系统的硬件检测,其中包括内存检测、系统总线检测等工作,之后进行各项程序的加载。

2、加载内核引导程序:在POST完成后,就要加载内核引导程序了,那它保存在哪里呢?磁盘里!哈哈,BIOS会读取0磁头,0磁道,一扇区的512个字节,这个扇区有叫做MBR(主引导记录),MBR中保存了内核引导程序的开始部分,BIOS将其装入内存执行。512个字节的MBR有些什么呢?这里有必要说说MBR!MBR分区表以80为起始,以55AA为结束,共64个字节。x86
CPU启动的第一个动作CS:EIP=FFFF:0000H(换算为物理地址为000FFFF0H,因为16位CPU有20根地址线),即BIOS程序的位置。BIOS例行程序检测完硬件并完成相应的初始化之后就会寻找可引导介质,找到后把引导程序加载到指定内存区域后,就把控制权交给了引导程序。这里一般是把硬盘的第一个扇区MBR和活动分区的引导程序加载到内存(即加载BootLoader),加载完整后把控制权交给BootLoader。

3、内核引导程序:内核引导程序分两部分:主、次引导程序。主引导程序的主要工作就是收索,寻找活动的分区,将活动的分区引导记录中的次引导程序加载到内存中并且执行。而这个次引导程序就是负责加载内核的并且将控制权交给内核。上面提过内核引导程序有LILO、GRUB、U-Boot、RedBoot。其中前面两个为pc中的,而后面两个是嵌入式的。引导程序BootLoader开始负责操作系统初始化,然后起动操作系统。启动操作系统时一般会指定kernel、initrd和root所在的分区和目录。

4、内核:内核以压缩的形式存在,不是一个可执行的内核!所以内核阶段首先要做的是自解压内核映像。这里说说编译内核后形成的内核压缩的映像vmlinuz。编译生成vmlinux后,一般会对其进行压缩为vmlinuz,使其成为zImage--小于512KB的小内核,或者成为bzImage--大于512KB的大内核。内核启动过程包括start_kernel之前和之后,之前全部是做初始化的汇编指令,之后开始C代码的操作系统初始化,最后执行第一个用户态进程init。

5、一般分两阶段启动,先是利用initrd的内存文件系统,然后切换到硬盘文件系统继续启动。initrd文件的功能主要有两个:1、提供开机必需的但kernel文件(即vmlinuz)没有提供的驱动模块(modules) 2、负责加载硬盘上的根文件系统并执行其中的/sbin/init程序进而将开机过程持续下去。

MBR:

(1).446个字节的引导程序代码

(2).64个字节的分区表,有多少个分区呢。。?这还真不知道!分为4个分区表,一个可启动分区和三个不可启动分区。

(3).2个字节的0XAA55,用于检查MBR是否有效。

需要注意的是,内核引导程序被加载完后,POST部分的代码会被从内存中清理,只留部分在内存中留给目标操作系统使用。(本段内容参考百度百科,参考比例:40%)

二、实验过程

(1)首先打开实验楼虚拟机,进入终端模式,依次输入以下两个命令:



说明:第一个命令是进入LinuxKernel文件夹下;第二个命令是:执行内核程序。

(2)内核程序开始执行了,如下图所示:



说明:输入help命令查看帮助信息,可见有三个命令:help,version,quit。依次输入help和version命令,最后输入quit命令退出内核的执行状态。如下图所示:



(3)进入调试阶段

输入命令:qemu -kernel-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S,后运行窗口处于暂停状态。

qemu参数:

-s : 在1234接受gdb调试连接

-S : 虚拟机启动后立即暂停,等侍gdb连接

这时,我们右键点击,选择“水平分割”划分出另外一个窗口,输入:gdb,进入gdb调试状态。依次键入:





设置好start_kernel断点后,输入:c命令便可执行内核程序,如下图:



输入list:将会显示start_kernel代码:如图所示:



我们还可以设置更多的断点:如break rest_init,然后按:c,如下图所示:



我们发现系统执行到rest_init,如下图所示:



再键入:list,将会显示rest_init代码:



说明:rest_init()是Linux 内核初始化的尾声。

三、简单分析内核:内核启动过程

先找到内核启动的起点start_kernel,在init文件夹下的main.c文件中找到start_kernel;如下图



分析:init_task即手工创建的PCB,0号进程即最终的idle进程。

不管分析内核的那一部分代码,都需要设计到start_kernel代码,因为初始化的时候,都需要调用start_kernel进行其他部分的初始化。

在561行的trap_init()函数是用来初始化一些中断向量的。

当系统没有进程需要执行时,就调度到idle进程。rest_init()从start-kernel()执行时就会一直存在,即0号进程。

(1)Linux3.18.6内核部分的源码,我们主要关注的是arch目录下面的x86目录,至于arch目录下其他的文件夹我们可以直接删除。

函数start_kernel就好像一般可执行程序中的主函数main,系统进入这个函数之前已经进行了一些最低限度的初始化,再往前研究就涉及很多硬件相关及编程语言了,这里是较高层次的初始化,基本是C代码,一直想搞清楚内核的初始化流程,好对整个linux内核有更深理解。分析程序习惯性的找main函数,那么就从这个start_kernel看看。这个函数在init/main.c。

(2)init启动过程:

init process 是 Linux 系统的第一个用户态进程,既然是第一个进程,所以他是Linux内核运行过程中唯一一个没有父进程的进程。它是由 Linux 内核直接启动的。start_kernel()函数是init文件夹下main.c文件中的一个关键性函数,它是整个内核开始工作的起点。在该函数被调用之前,内核是没有一条代码在执行的。

(3)init 1的启动:

进程init 1 是由内核启动的,不带任何命令行参数,即直接执行/sbin/init。

四、Linux内核启动的一些其他函数

1、系统调用 printk() 函数在屏幕上打印 Linux 内核版本号和编译内核所使用的 gcc 编译器版本号、启用时间等,如果这个过程失败,将显示一个参考信息给用户。

2、调用 arch/i386/kernel/setup.c 中的 setup_arch() 函数,初始化系统主板上各个集成电路控制器,最后在 command line、memory_start 和 memory_end 中返回结果。

3、调用 arch/i386/init.c 中的 paging_init() 函数初始化内核页表。

4、调用 arch/i386/kernel/trap.c 中的 trap_init() 函数中对中断描述符表IDT进行初始化。为了使用异常处理,trap_init() 函数将处理异常的函数地址的选择符写入 IDT的陷阱门描述符中。这些门的设置时由 set_trap_gate() 和set_system_gate() 函数来完成的。

5、调用 arch/i386/kernel/irq.c 中的 init_IRQ() 函数。设置基准时钟和中断门。

6、调用 kernel/sched.c 中的 sched() ,为进程调度程序的执行做准备。通过 init_bh() 函数设置用于内核例程处理程序的数组 bh_base[]。即分别把定时器队列 TIMER_BH 、设备队列TQUEVE_BH 和即时队列 IMMEDIATE_BH 填加到数组 bh_base[]中。

7、调用 arch/i386/kernel/time.c 中的 time_init() 函数,初始化系统时钟和日期。这个过程是从 CMOS中读取实时时间,并在屏幕上显示,然后调用 stup_x86_irq() 函数把时间中断服务程序的偏移量(中断号:0×20)装入相应的中断门描述符中。

8、调用 init/main.c 中的 parse_options() 函数,对命令行选项进行分析。这些选项是在命令启动时,由内核引导程序(主要是 arch/i386/boot/bootseet.c)装入,并存放在empty_zero_page 页的后 2K 字节中。它首先检查命令行参数,并设置相应的变量。然后把环境变量送入数组 envp_init[] ,把参数送入数组
argv_init[] 中。

9、调用 drivers/char/tty_io.c 中的 console_init() 函数初始化控制台。调用drivers/char/console.c 中的 con_init() 初始化显示器。

10、调用 kernel/module.c 中的 init_modules() 函数,初始化内核模块 kernel_module 。

11、 通过 if 语句来判断 prof_shift ,如果该值等于 0 ,说明 prof_buffer 表中没有空间,因此不执行x86_do_profile() 函数对 profile_buffer 执行初始化,否则就对 profile_buffer 执行初始化。

12、调用 mm/slab.c 中的 keme_cache_init() 函数,初始化高速通用缓存及 slab 分配器。

13、 调用 sti() 开中断。激活硬件中断系统,以便接收时钟中断。紧接着调用 calibrate_delay()测试机器的 BogoMIPS 值(这个值的含义是:内核发出读取设备信号后,需要等待多少时间才能得到从设备返回的请求信息或者说是 CPU 在一秒钟内执行一个短延时循环的近似次数)。

14、调用 arch/i386/mm/init.c 中的 mem_init() 函数,初始化页描述符,系统中所有的页框描述符存放在 mem_map[] 数组中,页框描述符的初始化是由 free_area_init() 函数完成的。 mem_init() 函数确定系统现有的页框总数,并计算出保留给硬件、内核代码和内核数据的页框数,以及内核初始化期间使用过,但随后又被释放掉的页框数。最
后,mem_init() 在结构数组 mem_map[]中标记已被占用的页框,计数没有使用的页框。该函数返回时,变量 nr_free_pages 中包含了动态内存中没有使用的页框总数。

15、调用 mm/slab.c 中的 kmem_cache_sizes_init() 函数,初始化通用高速缓存的大小。

16、调用 /fs/proc/root.c 中的 proc_root_init()函数 ,初始化根文件系统的各种数据结构。

17、调用 /kernel/fork.c 中的 uidcache_init()函数 ,创建用户进程标志符缓存。

18、调用 /kernel/fork.c 中的 filescache_init()函数,创建文件缓存。

19、调用 fs/dcache.c 中的 dcache_init() 函数,创建目录项高速缓存 dentry_cache 。

20、调用 mm/mmap.c 中的 vma_init() 函数,创建 vm_area_struct 结构和 mm_struct 结构。

21、调用 fs/buffer.c 中的 buffer_init() 函数,创建 buffer_head SLAB 缓存。

22、调用 kernel/signal.c 中的 signals_init() 函数,创建信号队列。

23、调用 fs/inode.c 中的 inode_init() 函数,初始化 inode 节点,即:将数组 hash_table[]全部清零,在把指向第一个 i 节点的全局变量置为空。

24、调用 fs/file_table.c 中的 file_table_init() 函数,创建 filp SLAB结构。

25、调用 ipc/util.c 中的 ipc_init() ,初始化信号量、消息和共享内存。

26、调用 fs/dquot.c 中的 dquot_init_hash()函数,创建磁盘配额缓存。

27、调用 include/i386/bugs.h 中的 check_bugs() 函数,测试 CPU的各种属性,检查协处理器状态。

28、调用 printk() 函数,在屏幕上打印出:“POSIX conformance testing by UNIFIX \n”。

29、调用 init/main.c 中的 smp_init() 函数,激活对称多处理方式中的其他的 CPU。

30、 调用 arch/i386/kernel/process.c 中的 kernel_thread() 函数,创建一个内核态线程,以便执行 init函数,这里应该注意,kernel_thread()是由汇编语言写成的,它使用了 int 0×80 系统调用创建一个新的内核态线程。该系统调用返回后通过比较 ESP 和 ESI 两个寄存器的值来判断父、子进程,如果是复进程则执行
0 号进程,如果是子进程则执行 init() 函数。

31、设置 idle 进程的 need_resched 标志位,调用 schedule 使 CPU 处理更多的进程。

五、总结

本次实验主要是熟练start_kernel的原理过程,掌握Linux内核的工作原理。掌握内核进程的init()过程中是怎么调用的。

实验分析了start_kernel进程启动的过程。通过对start_kernel的源代码分析,体会进程在切换过程中操作系统对进程运行环境的管理。事实上有时候我们可以把它叫做运行框架,也可以说是上下文,现场环境等等。实际上这些东西指的都是程序运行时候的堆栈,运行内存空间的地址,寄存器值的集合。中断上下文和进程上下文的切换是操作系统的“两把剑”,熟练掌握和理解中断上下文和进程上下文,我们才能充分理解Linux内核源码中进程调度的原理,才能为后续学习Linux操作系统做好基础准备。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: