• linux内核启动分析


    -----以下内容为从网络上整理所得------

    嵌入式Linux启动后,会先运行系统的bootloader,一般是用开源的U-boot;

    Broadcom芯片的bootloader则是自己公司的CFE。

     

    bootloader前面文章有大体介绍了一下,以下主要分析Linux内核的启动过程,以MIPS芯片为例。

    内核版本:2.6.31

     

    Bootloader将Linux内核映像拷贝到RAM中某个空闲地址处,然后一般有个内存移动操作,目的地址在

    arch/mips/Makefile内指定:

    load-$(CONFIG_MIPS_PB1550) += 0xFFFFFFFF80100000,
    则最终bootloader定会将内核移到物理地址   0x00100000  处。

    上面Makefile 里指定的的 load 地址,最后会被编译系统写入到 arch/mips/kernel/vmlinux.lds 中:

    OUTPUT_ARCH(mips)
    ENTRY(kernel_entry)
    jiffies = jiffies_64;
    SECTIONS
    {
    . = 0xFFFFFFFF80100000;
    /* read-only */
    _text = .; /* Text and read-only data */
    .text : {
        *(.text)
    ...

    这个文件最终会以参数 -Xlinker --script -Xlinker vmlinux.lds 的形式传给 gcc,并最终传给链接器 ld 来控制其行为。
    ld 会将 .text 节的地址链接到 0xFFFFFFFF80100000 处。

    关于内核 ELF 文件的入口地址(Entry point),即 bootloader 移动完内核后,直接跳转到的地址,由ld 写入 ELF的头中,

    其会依次用下面的方法尝试设置入口点,当遇到成功时则停止:
    a. 命令行选项 -e entry
    b. 脚本中的 ENTRY(symbol)
    c. 如果有定义 start 符号,则使用start符号(symbol)
    d. 如果存在 .text 节,则使用第一个字节的地址。
    e. 地址0

    注意到上面的 ld script 中,用 ENTRY 宏设置了内核的 entry point 是 kernel_entry,
    因此内核取得控制权后执行的第一条指令是在 kernel_entry 处。

     

    linux  内核启动的第一个阶段是从  /arch/mips/kernel/head.s文件开始的。
    而此处正是内核入口函数kernel_entry(),该函数定义在 /arch/mips/kernel/head.s文件里。

    kernel_entry()函数是体系结构相关的汇编语言,它首先初始化内核堆栈段,来为创建系统中的第一个进程进行准备,
    接着用一段循环将内核映像的未初始化数据段(bss段,在_edata和_end之间)清零,
    最后跳转到  /init/main.c 中的 start_kernel()初始化硬件平台相关的代码。

    /*asmlinkage: This is a #define for some gcc magic that tells the 
     *compiler that the function should not expect to find any of its arguments 
     *in registers (a common optimization), but only on the CPU's stack.意思是如
     *果函数定义前加宏asmlinkage ,表示这些函数通过堆栈而不是通过寄存器传递参数。 
     *init: 宏定义__init,用于告诉编译器相关函数或变量的仅用于初始化。
     *编译器将标有—init的所有代码存在初始化段中,初始化结束后就释放这段内存
     */
    asmlinkage void __init start_kernel(void)
    {
        char * command_line;
        extern struct kernel_param __start___param[], __stop___param[];
        //定义了核的参数数据结构
    
        lockdep_init();
        //初始化核依赖关系哈希表,classhash_table 和 chainhash_table
    
        smp_setup_processor_id();
        //获取当前正在执行初始化的处理器ID,如果kernel是单处理器,则此函数是空的,不作任何处理
    
        debug_objects_early_init();
      //这个函数主要作用是对调试对象进行早期的初始化,其实就是HASH锁和静态对象池进行初始化。
       
      boot_init_stack_canary();
      //stack_canary的是带防止栈溢出攻击保护的堆栈
    
      cgroup_init_early();
      //控制组进行早期的初始化
    
      local_irq_disable();
        //关闭当前CPU所有中断响应
    
      early_boot_irqs_off();
      /*标记内核还在早期初始化代码阶段,并且中断在关闭状态,如果有任何中断打开或请求中断的事情出现,都是会提出警告,以便跟踪代码错误情况。
       *早期代码初始化结束之后,就会调用函数early_boot_irqs_on来设置这个标志为真。
      **/
      
      early_init_irq_lock_class();
      //每一个中断都有一个IRQ 描述符(struct irq_desc)来进行描述,这个函数的主要作用是设置所有的IRQ 描述符(struct irq_desc)的锁是统一的锁,
      //还是每一个IRQ 描述符(struct irq_desc)都有一个小锁。
    
      lock_kernel();
      //这个函数主要作用是初始化大内核锁。在对称多处理器的系统里,每一个CPU都可以运行内核的代码,但有时需要只能一个CPU运行内核代码,那么怎么办呢?
      //要解决这个问题,就需要给内核配备一把锁  //只要拥有这把锁的CPU才可以运行内核的代码,并且同一个CPU可以递归地运行内核。
    
      tick_init();
      //这个函数主要作用是初始化时钟事件管理器的回调函数,比如当时钟设备添加时处理
    
      boot_cpu_init();
      //这个函数主要作用是设置当前引导系统的CPU在物理上存在,在逻辑上可以使用,并且初始化准备好。
    
      page_address_init();
      //这个函数主要作用是初始化高端内存的映射表。在32位系统里,内核为了访问超过1G的物理内存空间,需要使用高端内存映射表。
      //比如当内核需要读取1G的缓存数据时,就需要分配高端内存来使用,这样才可以管理起来。  
      //使用高端内存之后,32位的系统也可以访问达到64G内存。在移动操作系统里,目前还没有这个必要,最多才1G多内存。
    
      printk(KERN_NOTICE "%s", linux_banner);
      //输出终端上显示版本信息、编译的电脑用户名称、编译器版本、编译时间
    
      setup_arch(&command_line);
      //每种体系结构都有setup_arch函数,主要是再次获取CPU类型和系统架构,分析引导程序传入的命令行参数,
      //进行页面内存初始化,处理器初始化,中断早期初始化等等。
    
      mm_init_owner(&init_mm, &init_task);
      //这个函数主要作用是设置最开始的初始化任务属于init_mm内存
    
      setup_command_line(command_line);
      //保存命令行
    
      setup_nr_cpu_ids();
      //设置最多有多少个nr_cpu_ids结构
    
      setup_per_cpu_areas();
      //设置SMP体系每个CPU使用的内存空间,同时拷贝初始化段里数据
    
      smp_prepare_boot_cpu();
      //为SMP系统里引导CPU进行准备工作
    
      build_all_zonelists();
      //初始化所有内存管理节点列表,以便后面进行内存管理初始化
    
      page_alloc_init();
      //设置内存页分配通知器
    
      printk(KERN_NOTICE "Kernel command line: %s
    ", boot_command_line);
      //输出命令参数到显示终端
    
      parse_early_param();
      //分析命令行最早使用的参数
    
      parse_args("Bootingkernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption);
      //对传入内核参数进行解释,如果不能识别的命令就调用最后参数的函数
    
      pidhash_init();
      //进程ID的HASH表初始化,这样可以提供通PID进行高效访问进程结构的信息。LINUX里共有四种类型的PID,因此就有四种HASH表相对应。
    
      vfs_caches_init_early();
      //虚拟文件系统的早期初始化
    
      sort_main_extable();
      //对内核内部的异常表进行堆排序,以便加速访问
    
      trap_init();
      //对异常进行初始化
    
      mm_init();
      //设置内核内存分配器
    
      sched_init();
      //这个函数主要作用是对进程调度器进行初始化,比如分配调度器占用的内存,初始化任务队列,设置当前任务的空线程,当前任务的调度策略为CFS调度器。
    
      preempt_disable();
      //这个函数主要作用是关闭优先级调度。由于每个进程任务都有优先级,目前系统还没有完全初始化,还不能打开优先级调度。
      
      if(!irqs_disabled() {
        printk(KERN_WARNING "start_kernel(): bug: interrupts were "
                  "enabled *very* early, fixing it
    ");
        local_irq_disable();
      }
      //判断是否过早打开中断,如果是这样,就会提示,并把中断关闭。
    
      rcu_init();
      //初始化直接读拷贝更新的锁机制。RCU主要提供在读取数据机会比较多,但更新比较的少的场合,这样减少读取数据锁的性能低下的问题。
    
      early_irq_init();
      //用于管理中断的irq_desc[NR_IRQS]数组的每个元素的部分字段设置为确定的状态,它设置每一个成员的中断号
    
      init_IRQ();
      //是一个与特定体系结构相关的函数,用于管理中断的irq_desc[NR_IRQS]结构数组各成员字段设置为IRQ——NOREQUEST | IRQ——NOPROBE,
      //也就是未请求和未探测状态,然后调用特定平台的中断初始化init_arch_irq()函数
      
      prio_tree_init();
      //优先搜索树的初始化,主要用在内在反向搜索方面
    
      init_timers();
      //初始化引导CPU的时钟相关的体系结构,注册时钟的回调函数,当时钟到达时可以回调时钟处理函数,最后初始化时钟软件中断处理。
    
      hrtimers_init();
      //初始化高精度的定时器,并设置回调函数
    
      softirq_init();
      //这个函数是初始化软件中断,软件中断与硬件中断区别就是中断发生时,软件中断是使用线程来监视中断信号,而硬件中断是使用CPU硬件来监视中断。
    
      timekeeping_init();
      //初始化系统时钟计时,并且初始化内核里与时钟计时相关的变量。
    
      time_init();
      //初始化系统时钟
    
      sched_clock_init();
      //系统进程调度时钟初始化
    
      profile_init();
      //分配内核性能统计保存的内存,以便统计的性能变量可以保存到这里
    
      if (!irqs_disabled())
        printk(KERN_CRIT "start_kernel(): bug: interrupts were "
                "enabled early
    ");
    
      early_boot_irqs_on();
      //设置内核还在早期初始化阶段的标志,以便用来调试时输出信息
    
      local_irq_enable();
      //打开本CPU的中断,也即允许本CPU处理中断事件,在这里打开引CPU的中断处理。如果有多核心,别的CPU还没有打开中断处理。
    
      set_gfp_allowed_mask(__GFP_BITS_MASK);
      //Interrupts are enabled now so all GFP allocations are safe.
    
      kmem_cache_init_late();
      //slab分配器的缓存机制
      
      console_init();
      //初始化控制台,从这个函数之后就可以输出内容到控制台了
      //在这个函数初化之前,都没有办法输出内容,就是输出,也是写到输出缓冲区里,缓存起来,等到这个函数调用之后,就立即输出内容。
    
      if (panic_later)
        panic(panic_later, panic_param);
      //判断分析输入的参数是否出错,如果有出错,就启动控制台输出之后,立即打印出错的参数,以便用户立即看到出错的地方。
    
      lockdep_info();
      //打印锁的依赖信息,用来调试锁。
    
      locking_selftest();
      //用来测试锁的API是否使用正常,进行自我测试。比如测试自旋锁、读写锁、一般信号量和读写信号量。
    
    #ifdef CONFIG_BLK_DEV_INITRD
      if (initrd_start && !initrd_below_start_ok &&
        page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
          printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
            "disabling it.
    ",
            page_to_pfn(virt_to_page((void *)initrd_start)),
            min_low_pfn);
          initrd_start = 0;
      }
    #endif
      //这段代码是要支持初始RAM 磁盘,内核必须要使用CONFIG_BLK_DEV_RAM 和CONFIG_BLK_DEV_INITRD 选项进行编译。
    
      page_cgroup_init();
      //容器组的页面内存分配。
    
      enable_debug_pagealloc();
      //设置内存分配是否需要输出调试信息,如果调用这个函数,当分配内存时,不会输出一些相关的信息。
    
      kmemtrace_init();
      //
    
      kmemleak_init();
      //memory lead侦测初始化
    
      debug_objects_mem_init();
      //在kmem_caches之后表示建立一个高速缓冲池,建立起SLAB_DEBUG_OBJECTS标志。
    
      idr_init_cache();
      //创建IDR机制的内存缓存对象。所谓的IDR就是整数标识管理机制(integerIDmanagement)。
      //引入的主要原因是管理整数的ID与对象的指针的关系,
      //由于这个ID可以达到32位,也就是说,如果使用线性数组来管理,那么分配的内存太大了;
      //如果使用线性表来管理,又效率太低了,所以就引用IDR管理机制来实现这个需求。
    
      setup_per_cpu_pageset();
      //创建每个CPU的高速缓存集合数组。因为每个CPU都不定时需要使用一些页面内存和释放页面内存,
      //为了提高效率,就预先创建一些内存页面作为每个CPU的页面集合。
    
      numa_policy_init();
      //初始化NUMA的内存访问策略。所谓NUMA,它是NonUniform Memory AccessAchitecture的缩写,
      //主要用来提高多个CPU访问内存的速度。因为多个CPU访问同一个节点的内存速度远远比访问多个节点的速度来得快。
    
      if(late_time_init)
        late_time_init();
      //主要运行时钟相关后期的初始化功能。
    
      calibrate_delay();
      //这个函数是主要计算CPU需要校准的时间,这里说的时间是CPU执行时间。如果是引导CPU,
      //这个函数计算出来的校准时间是不需要使用的,主要使用在非引导CPU上,因为非引导CPU执行的频率不一样,导致时间计算不准确。
      
      pidmap_init();
      //进程位图初始化,一般情况下使用一页来表示所有进程占用情况
    
      anon_vma_init();
      //这个函数是初始化反向映射的匿名内存,提供反向查找内存的结构指针位置,快速地回收内存。
    
    #ifdef CONFIG_X86
      if (efi_enabled)
        efi_enter_virtual_mode();
    #endif
      //这段代码是初始化EFI的接口,并进入虚拟模式。EFI是ExtensibleFirmware Interface的缩写,就是INTEL公司新开发的BIOS接口。
    
      thread_info_cache_init();
      //这个函数是线程信息的缓存初始化。
    
      cred_init();
      //证书初始化
    
      fork_init(num_physpages);
      //根据当前物理内存计算出来可以创建进程(线程)的数量,并进行进程环境初始化
    
      proc_caches_init();
      //进程缓存初始化。
    
      buffer_init();
      //初始化文件系统的缓冲区,并计算最大可以使用的文件缓存。
    
      key_init();
      //初始化安全键管理列表和结构。
    
      security_init();
      //初始化安全管理框架,以便提供访问文件/登录等权限。
    
      vfs_caches_init(num_physpages);
      //虚拟文件系统进行缓存初始化,提高虚拟文件系统的访问速度。
    
      radix_treee_init();
      //初始化radix树,radix树基于二进制键值的查找树。
    
      signals_init();
      //初始化信号队列缓存。
    
      page_writeback_init();
      //计算当前系统vm-radio等,设置是否需要回写操作
    
    #ifdef CONFIG_PROC_FS
      proc_root_init();
    #endif
      //初始化系统进程文件系统,主要提供内核与用户进行交互的平台,方便用户实时查看进程的信息。
    
      cgroup_init();
      //初始化进程控制组,主要用来为进程和其子程提供性能控制。比如限定这组进程的CPU使用率为20%。
    
      cpuset_init();
      //初始化CPUSET,CPUSET主要为控制组提供CPU和内存节点的管理的结构。
    
      taskstats_init_early();
      //初始化任务状态相关的缓存、队列和信号量。任务状态主要向用户提供任务的状态信息。
    
      delayacct_init();
      //初始化每个任务延时计数。当一个任务等CPU运行,或者等IO同步时,都需要计算等待时间。
    
      check_bugs();
      //用来检查CPU配置、FPU等是否非法使用不具备的功能。
    
      acpi_early_init();
      //这个函数是初始化ACPI电源管理。高级配置与能源接口(ACPI)ACPI规范介绍ACPI能使软、硬件、操作系统(OS),
      //主机板和外围设备,依照一定的方式管理用电情况,系统硬件产生的Hot-Plug事件,让操作系统从用户的角度上直接支配即插即用设备,
      //不同于以往直接通过基于BIOS 的方式的管理。
    
      ftrace_init();
      //初始化内核跟踪模块,ftrace的作用是帮助开发人员了解Linux 内核的运行时行为,以便进行故障调试或性能分析。
    
      /* Do the rest non-__init'ed, we're now alive */
      rest_init();
      //这个函数是后继初始化,主要是创建内核线程init,并运行。
    }

    在上面已经对基本的硬件、系统的结构初始化完成,接着下来系统要做的工作,就是创建进 程,对进程进行管理,才可以让系统生生不息,处理各种各样的任务。虽然大部份的初始化工作已经完成,但还需要更进一步初始化,因此创建一个内核初始化线程 来继续初始化。为了有一个干净,又可以拷贝,又方便创建线程的方法,就是创建一个特别的内核线程kthreadd,这样所有以后需要创建的线程都是由这个线程创建出来的,可以说这个线程为其余内核线程之母。创建完这两个线程之后,初始化进程还需要对线程调度器的状态进行运行一次,以便初始化到合适的值。最后,这个初始化进程就退化为一个IDLE空闲进程了,完成了引导系统所有的工作,进入系统正常运行的状态。

    staticvoid noinline __init_refok rest_init(void)
            __releases(kernel_lock)
    {
        int pid;
    
        kernel_thread(kernel_init,NULL, CLONE_FS | CLONE_SIGHAND);
        //这行代码是调用创建内核线程函数kernel_thread来创建内核第二阶段的初始化线程。
    
        numa_default_policy();
        //这行代码是设置当前进程使用缺省的内存管理策略。
    
        pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
        //这行代码是创建一个干净内核线程,以便以后其它所有内核线程全部拷贝它,并由它来创建,这样达到更方便创建线程,不用设置太多参数。
    
        kthreadd_task= find_task_by_pid_ns(pid, &init_pid_ns);
        //这行代码是通过进程ID查找内核线程的任务地址,并保存起来,方便访问。
    
        unlock_kernel();
        //这行代码是释放大内核锁,以便多CPU可以访问内核代码。
    
      /*
       * The boot idle thread must execute schedule()
       * at least once to get things moving:
       */
    
      init_idle_bootup_task(current);
      //这行代码是初始化空闲进程的调度器,以便让空闲进程知道怎么样调度任务列表里的进程。current是指向当前IDLE任务的结构。
    
      rcu_scheduler_starting();
      //内核RCU锁机制调度启动
    
      preempt_enable_no_resched();
      //这行代码是减少内核抢先计数,并且不进行调度操作。这样运行后,以便IDLE进程可以让别的高优先级的进程运行。
    
      schedule();
      //这行代码是调用进程调度函数schedule,主要初始化调度器可以切换回到空闲任务。
    
      preempt_disable();
      //这行代码是增加内核抢先计数。
    
      /*Call into cpu_idle with preempt disabled */
      cpu_idle();
      //这行代码是调用CPU空闲任务进程运行,不再返回来。
    }

    系统已经完成了整个初始化过程,那么这个初始化进程最好的归宿是那里呢?显然它就是进化为一个空闲进程,当系统没有其它任务处理时,就会通过进程管理器选择这个优先级最低,没有什么事情做的任务,以便整个CPU还有事情可做。也许你也会问,为什么一定要一个空闲进程,不要这个进程不行吗?肯定回答是不行的,整个系统里的CPU资源总需要使用的,如果不使用CPU资源,那么这个CPU就意味着不再执行指令了,CPU就已经停机了,当有新的任务到来时就没有办法切换过去。如果选择CPU停机的方式,那么这个CPU需要再运行任务时,就需要唤醒。而唤醒的方法,需要外界施加条件才可以,一般都是物理条件,就是触发CPU的中断信号。

    空闲进程主要做的工作,就是不断查找是否有新的任务可以运行,如果没有新的任务运行,就继续执行空闲任务处理的事情。下面就来仔细地分析这个函数的具体工作,代码如下:

    void cpu_idle(void)
    {
      local_fiq_enable();
      //这行代码是打开ARM系统的快速中断,所谓的FIQ是相对于普通的IRQ来说的,FIQ是可以打断普通的IRQ中断,反之不行。
    
      //下面就进入无限循环的空闲进程处理:
      /* endless idle loopwith no priority at all */
      while (1) {
        void (*idle)(void)= pm_idle;
        //这行代码是调用定制的空闲处理函数。
    
    #ifdefCONFIG_HOTPLUG_CPU
        if(cpu_is_offline(smp_processor_id())) {
          leds_event(led_idle_start);
          cpu_die();
        }
    #endif
        //这段代码是处理热插拔的CPU机制,当允许当前这个CPU进入睡眠状态,就可以进入。
    
        if (!idle)
          idle =default_idle;
        //这段代码是当没有用户定义的空闲处理函数时,就调用缺省的空闲处理函数。缺省的空闲处理函数,就会调用系统架构的空闲处理函数。
    
        leds_event(led_idle_start);
        //这行代码是打开LED显示空闲运行状态。
    
        tick_nohz_stop_sched_tick(1);
        //这行代码是停止进程调度计数。
    
        while(!need_resched())
          idle();
        //这段代码是当需要调度标志为空时,就不断调用空闲处理函数进行运行。
    
        leds_event(led_idle_end);
        //这行代码是当需要调度其它进行运行了,LED结束显示空闲运行状态。
      
        tick_nohz_restart_sched_tick();
        //这行代码是重新开始计算调度运行计数。
    
        preempt_enable_no_resched();
        //这行代码是减少抢占计数,不需要重新调度,下面马上就开始调度。
    
        schedule();
        //这行代码是对进程任务进行调度,以便立即运行正在等待的任务。
    
        preempt_disable();
        //这行代码是增加抢占计数,禁止调度发生。
      }
    }
  • 相关阅读:
    C# 异步编程 (12)
    C# 动态语言扩展(11)
    C# LINQ(10)
    代码整洁之道(1)
    C# 集合(9) 持续更新
    C# 字符串和正则表达式(8) 持续更新
    C# 委托、lambda表达式和事件 (7) 持续更新
    C# 运算符和类型强制转换(6) 持续更新
    C++_将图二维矩阵形式转为邻接表结构
    .NET(C#)连接各类数据库-集锦
  • 原文地址:https://www.cnblogs.com/morphling/p/3595287.html
Copyright © 2020-2023  润新知