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


    李俊锋 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

    一.实验原理

     

    1.1课堂笔记

     

     

    三个法宝:存储程序计算机,函数调用堆栈,中断
    两把宝剑:中断上下文的切换(保存现场,回复现场),进程上下文的切换


    linux内核源码的目录结构:
    arch:支持不同cpu的代码,我们比较关心x86文件夹
    init文件夹,main.c start_kernel
    ipc:进程通信

    start_kernel代码:
    init_task:即手工创建的PCB,0号进程即最终的idle进程。
    trap_init:设置中断向量
    rest_init:创建1号进程,它是一直存在的0号进程。


    老师口误纠正:init是第一个用户态进程,是1号进程

     

    1.2 计算机的启动过程概述

     

    • x86 CPU启动的第一个动作CS:EIP=FFFF:0000H(换算为物理地址为000FFFF0H,因为16位CPU有20根地址线),即BIOS程序的位置。http://wenku.baidu.com/view/4e5c49eb172ded630b1cb699.html

    • BIOS例行程序检测完硬件并完成相应的初始化之后就会寻找可引导介质,找到后把引导程序加载到指定内存区域后,就把控制权交给了引导程序。这里一般是把硬盘的第一个扇区MBR和活动分区的引导程序加载到内存(即加载BootLoader),加载完整后把控制权交给BootLoader。

    • 引导程序BootLoader开始负责操作系统初始化,然后起动操作系统。启动操作系统时一般会指定kernel、initrd和root所在的分区和目录,比如root (hd0,0),kernel (hd0,0)/bzImage root=/dev/ram init=/bin/ash,initrd (hd0,0)/myinitrd4M.img

    • 内核启动过程包括start_kernel之前和之后,之前全部是做初始化的汇编指令,之后开始C代码的操作系统初始化,最后执行第一个用户态进程init。

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

     

    1.3 linux目录结构

     

    arch:arch是architecture的缩写。内核所支持的每种CPU体系,在该目录下都有对应的子目录。每个CPU的子目录,又进一步分解为 boot,mm,kernel等子目录,分别包含控制系统引导,内存管理,系统调用等。

    block:部分块设备驱动程序。

    crypto:加密、压缩、CRC校验算法。

    documentation:内核的文档。

    drivers:设备驱动程序。

    fs:存放各种文件系统的实现代码。每个子目录对应一种文件系统的实现,公用的源程序用于实现虚拟文件系统vfs。

    include:内核所需要的头文件。与平台无关的头文件在include/linux 子目录下,与平台相关的头文件则放在相应的子目录中。

     

    init:内核初始化代码。

    ipc:进程间通信的实验代码。

    kernel:Linux大多数关键的核心功能都是在这个目录实现。(调度程序,进程控制,模块化)

    lib:库文件代码。

    mm:mm目录中的文件用于实现内存管理中与体系结构无关的部分。

    net:网络协议的实现代码。

    samples:一些内核编程的范例。

    scripts:配置内核的脚本。

    security:SELinux的模块。

    sound:音频设备的驱动程序。

    usr:cpio命令实现。

    virt:内核虚拟机。

     

    1.4 start_kernel函数(linux-3.18.6)详细解析,了解Linux内核的启动过程,即从start_kernel到init进程启动。

     (由于函数中的代码很多,下面只列举除了几个具有代表性的)

    char * command_line; 命令行,用来存放bootloader传递过来的参数

    lockdep_init();  建立一个哈希表(hash tables),就是一个前后指向的指针结构体数组。 【函数的主要作用是初始化锁的状态跟踪模块。由于内核大量使用锁来进行多进程多处理器的同步操作,死锁就会在代码不合理的时候出现,但是要定位哪个锁比较困难,用哈希表可以跟踪锁的使用状态。死锁情况:一个进程递归加锁同一把锁;同一把锁在两次中断中加锁;几把锁形成闭环死锁】

    smp_setup_processor_id(); 针对SMP处理器,用于获取当前CPU的硬件ID,如果不是多核,函数为空 【判断是否定义了CONFIG_SMP,如果定义了调用read_cpuid_mpidr读取寄存器CPUID_MPIDR的值,就是当前正在执行初始化的CPU ID,为了在初始化时做个区分,初始化完成后,所有处理器都是平等的,没有主从】

    debug_objects_early_init();  初始化哈希桶(hash buckets)并将static object和pool object放入poll列表,这样堆栈就可以完全操作了 【这个函数的主要作用就是对调试对象进行早期的初始化,就是HASH锁和静态对象池进行初始化,执行完后,object tracker已经开始完全运作了】

    boot_init_stack_canary(); 初始化堆栈保护的加纳利值,防止栈溢出攻击的堆栈保护关键字 

    cgroup_init_early();  在系统启动时初始化cgroups,同时初始化需要early_init的子系统 【这个函数作用是控制组(control groups)早期的初始化,控制组就是定义一组进程具有相同资源的占有程度,比如,可以指定一组进程使用CPU为30%,磁盘IO为40%,网络带宽为50%。目的就是为了把所有进程分配不同的资源】

    local_irq_disable(); 关闭当前CPU的所有中断响应,操作CPSR寄存器。对应后面的

    early_boot_irqs_disabled= true;系统中断关闭标志,当early_init完毕后,会恢复中断设置标志为false。

    boot_cpu_init(); 设置当前引导系统的CPU在物理上存在,在逻辑上可以使用,并且初始化准备好,即激活当前CPU 【在多CPU的系统里,内核需要管理多个CPU,那么就需要知道系统有多少个CPU,在内核里使用cpu_present_map位图表达有多少个CPU,每一位表示一个CPU的存在。如果是单个CPU,就是第0位设置为1。虽然系统里有多个CPU存在,但是每个CPU不一定可以使用,或者没有初始化,在内核使用cpu_online_map位图来表示那些CPU可以运行内核代码和接受中断处理。随着移动系统的节能需求,需要对CPU进行节能处理,比如有多个CPU运行时可以提高性能,但花费太多电能,导致电池不耐用,需要减少运行的CPU个数,或者只需要一个CPU运行。这样内核又引入了一个cpu_possible_map位图,表示最多可以使用多少个CPU。在本函数里就是依次设置这三个位图的标志,让引导的CPU物理上存在,已经初始化好,最少需要运行的CPU。

    page_address_init();  初始化高端内存的映射表

    setup_arch(&command_line);内核架构相关初始化函数,是非常重要的一个初始化步骤。其中包含了处理器相关参数的初始化、内核启动参数(tagged list)的获取和前期处理、内存子系统的早期初始化(bootmem分配器)

    trap_init(); 对内核陷阱异常进行初始化,在ARM系统里是空函数,没有任何的初始化

    mm_init(); 标记哪些内存可以使用,并且告诉系统有多少内存可以使用,当然是除了内核使用的内存以外 

    sched_init();  对进程调度器的数据结构进行初始化,创建运行队列,设置当前任务的空线程,当前任务的调度策略为CFS调度器

    rest_init():  创建1号进程,它是一直存在的0号进程。

    二.实验过程

    2.1启动内核

    (1)使用实验楼的实验环境,输入如下命令:

    cd LinuxKernel/
    qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

    (2)可以看到内核成功启动,如下图所示:

     

     (3)目前它只支持三种命令:

     1.help

     2.version

    3.quit

    2.2 使用gdb调试内核

    (1)输入如下命令开启内核,并使内核处于冻结状态:

    qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
    # -S freeze CPU at startup (use ’c’ to start execution)
    # -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项

    (2)开启gdb调试,并连接到内核上

    gdb
    (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
    (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
    (gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后

    (3)在start_kernel设置断点之后,运行到该函数处,并列出函数代码:

    (4)在rest_init函数处下断点,并列出函数代码:

    四.实验总结

    Linux系统启动过程:

    在linux系统内核启动时会首先调用start_kernel函数,在该函数中会对cpu驱动等硬件驱动,文件系统等进行初始化。在初始化结束后,就会调用rest_init创建0号进程,

    在0号进程中会调用1号进程,之后会调用进程的调度函数。idle进程就是0号进程。

     系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个linux系统的所有进程也是一个树形结构。树根是系统自动构造的,

    即在内核态下执行的0号进程,它是所有进程的祖先。由0号进程创建1号进程(内核态),1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主

    存管理的内核线程。随后,1号进程调用execve()运行可执行程序init,并演变成用户态1号进程,即init进程。它按照配置文件/etc/initab的要求,完成系统启动工作,创建编号

    为1号、2号...的若干终端注册进程getty。

    每个getty进程设置其进程组标识号,并监视配置到系统终端的接口线路。当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可输入

    注册名和密码进入登录过程,如果成功,由login程序再通过函数execv()执行shell,该shell进程接收getty进程的pid,取代原来的getty进程。再由shell直接或间接地产生其他进程。

    本次实验虽然不需要写代码,但主要是对代码的理解,这次实验让我对linux内核的启动有了一定的了解,这次实验让我深深的感受到了linux内核的代码量的庞大,希望自己能坚持不懈

    的把这门课上好。

  • 相关阅读:
    实习第十天
    实习第九天
    实习第八天
    武汉第七天
    武汉第六天
    实习第五天
    实习第四天
    NSArray
    NSString
    NSObject
  • 原文地址:https://www.cnblogs.com/crowpurple/p/5272747.html
Copyright © 2020-2023  润新知