24小时学通Linux内核之电源开和关时都发生了什么

    bootloader引导装入程序将内核映像加载到内存并处理控制权传送到内核后在内核引导时每个子系统都必须要初始化,我们根据实际执行的线性顺序跟踪内核的初始化过程,下图说明了从系统加电到断电这一过程中所有事情发生的顺序,这个图不多加解释了,看图就知道其线性执行顺序,中间过程也是很简单的步骤啦。

    我们首先讨论的是BIOS和Open和Fireware,它们分别是x86和PPC系统加电后在只读内存的某一地址(一般是Flash ROM)最先运行的代码,这些代码负责激活系统中相应的部分,以便处理内核的加载,对于x86而言,这就是系统BIOS的驻留之处,基本的输入输出是一块引导系统并与硬件相关的系统初始化代码,这里不多说了,对于PowerPC而言,初始化代码的类型与PowerPC体系结构的出现时间有关,详情就参见Open Fireware 的主页www.openfireware.org


    引导装入程序Bootloaders大家应该早就有所了解了,Boot Loaders是驻留在计算机引导设备的程序,第一个引导设备往往是系统中的第一个硬盘,完成足够的系统初始化工作后,BIOS或固件调用引导装入程序,一旦成功加载进来,内核就初始化并配置操作系统,对x86系统而言,BIOS允许用户为其系统设置引导设备的顺序,这里提一下GRUB,Grand Unified Bootloader 是基于x86的引导装入程序,用来加载Linux。GRUB2在设计之初就考虑了移植到PPC系统的问题,哎具体的就www.gnu.org/software/grub上有丰富的文档,并且百度上也有相关的例程之类的,我觉得这里我就不再阐释了。

    

    Linux装入程序即LILO这个我还是得说说,和GRUB相类似但是LILO仅仅使用配置文件,并且没有命令行接口。LILO运行时的第一阶段步骤如下:

第一阶段:

  • 开始执行并且显示“L.";

  • 检测磁盘几何信息并且显示“I.";

  • 加载第二阶段的代码。

第二阶段:

  • 开始执行并且显示“L.";

  • 确定引导数据和操作系统的位置,并且显示”O.";


    确定启动哪个操作系统并且跳转到该操作系统,LILO配置文件中的一段代码如下(代码在etc/lilo.conf上可以查看):

 image = /boot/bzimage-2.6.7-mytestkernel  //image指明内核所在地 label = Kernel 2.6.7, my test kernel  //label描述配置的字符串 root = /dev/hda6          //root指明根文件系统驻留的分区 read-only              //表明根分区在引导时候不可被修改

GRUB和LILO的主要区别

  • LILO将配置信息存储在主引导记录中,若有任何改动,必须运行/sbin/lilo来更新主引导记录

  • LILO没有交互式的命令行接口

  • LILO不能读取不同的文件系统


    最后说一下Yaboot,yaboot是另一个引导程序(比如说,grub和lilo是比较出名的引导程序),用于Macintosh。主页:http://yaboot.ozlabs.org/ Yaboot引导时的步骤如下:

  • OF调用Yaboot

  • 找到引导设备和引导路径并打开引导分区

  • 打开/etc/yaboot.conf或命令解释器

  • 加载映像或内核以及initrd

  • 执行映像

这里需要在Ubuntu上自己操作,当然对Ubuntu的基本命令操作是首先要了解的,然后才能根据步骤一步一步执行下去。


    x86和PowerPC体系结构的硬件初始化,由于内存管理的初始化与硬件息息相关,要理解其初始化过程就必须了解硬件的规格,那么都只能去看看资料才能了解了,这里我多讲不了。现在的PowerPC和x86的代码都集中在init/main.c的start_kernel()中,该例程位于体系结构无关的代码段,它调用特定体系结构的例程来完成内存初始化。下面我们来探究一下start_kernel()函数。

  

    跳转到start_kernel()时候,执行进程0,也就是平时说的超级用户进程,进程0孕育了进程1,也就是init进程,然后进程0就变成CPU的空闲进程,调/sbin/init时,仅有这两个进程在运行:

asmlinkage void __init start_kernel(void)  {  char * command_line;  extern struct kernel_param __start___param[], __stop___param[];  //来设置smp process id,当然目前看到的代码里面这里是空的  smp_setup_processor_id();  //lockdep是linux内核的一个调试模块,用来检查内核互斥机制尤其是自旋锁潜在的死锁问题。//自旋锁由于是查询方式等待,不释放处理器,比一般的互斥机制更容易死锁,  //故引入lockdep检查以下几种情况可能的死锁(lockdep将有专门的文章详细介绍,在此只是简单列举)://  //·同一个进程递归地加锁同一把锁;//  //·一把锁既在中断(或中断下半部)使能的情况下执行过加锁操作,  // 又在中断(或中断下半部)里执行过加锁操作。这样该锁有可能在锁定时由于中断发生又试图在同一处理器上加锁;//  //·加锁后导致依赖图产生成闭环,这是典型的死锁现象。lockdep_init();  debug_objects_early_init();  //初始化stack_canary栈3  //stack_canary的是带防止栈溢出攻击保护的堆栈。//  当user space的程序通过int 0x80进入内核空间的时候,CPU自动完成一次堆栈切换,   //从user space的stack切换到kernel space的stack。//   在这个进程exit之前所发生的所有系统调用所使用的kernel stack都是同一个。//kernel stack的大小一般为4096/8192,  //内核堆栈示意图帮助大家理解://  //   内存低址                                                              内存高址  //                      |                 |<-----------------------------esp|     //   +-----------------------------------4096-------------------------------+  //   |        72        |     4           |       x < 4016    |     4       |  //   +------------------+-----------------+---------------------------------+  //   |thread_info  |    | STACK_END_MAGIC |   var/call chain  |stack_canary |  //   +------------------+-----------------+---------------------------------+  //   |     28      | 44 |                 |                                 |  //                 V    |                                                   |  //             restart_block                                                V  //  //esp+0x0                                                                   +0x40  //    +---------------------------------------------------------------------------+  //    |ebx|ecx|edx|esi|edi|ebp|eax|ds|es|fs|gs|orig_eax|eip|cs|eflags|oldesp|oldss|  //    +---------------------------------------------------------------------------+  //    |             kernel完成                          |         cpu自动完成       |  boot_init_stack_canary();   //  cgroup: 它的全称为control group.即一组进程的行为控制.  //  比如,我们限制进程/bin/sh的CPU使用为20%.我们就可以建一个cpu占用为20%的cgroup.  //  然后将/bin/sh进程添加到这个cgroup中.当然,一个cgroup可以有多个进程.  
cgroup_init_early(); //更新kernel中的所有的立即数值,但是包括哪些需要再看?core_imv_update(); //关闭当前CUP中断 local_irq_disable(); //修改标记early_boot_irqs_enabled; //通过一个静态全局变量 early_boot_irqs_enabled来帮助我们调试代码, //通过这个标记可以帮助我们知道是否在”early bootup code”,也可以通过这个标志警告是有无效的终端打开 early_boot_irqs_off(); //每一个中断都有一个IRQ描述符(struct irq_desc)来进行描述。//这个函数的主要作用是设置所有的 IRQ描述符(struct irq_desc)的锁是统一的锁, //还是每一个IRQ描述符(struct irq_desc)都有一个小锁。early_init_irq_lock_class();
// 大内核锁(BKL--Big Kernel Lock) //大内核锁本质上也是自旋锁,但是它又不同于自旋锁,自旋锁是不可以递归获得锁的,因为那样会导致死锁。//但大内核锁可以递归获得锁。大内核锁用于保护整个内核,而自旋锁用于保护非常特定的某一共享资源。//进程保持大内核锁时可以发生调度,具体实现是://在执行schedule时,schedule将检查进程是否拥有大内核锁,如果有,它将被释放,以致于其它的进程能够获得该锁, //而当轮到该进程运行时,再让它重新获得大内核锁。注意在保持自旋锁期间是不运行发生调度的。//需要特别指出,整个内核只有一个大内核锁,其实不难理解,内核只有一个,而大内核锁是保护整个内核的,当然有且只有一个就足够了。//还需要特别指出的是,大内核锁是历史遗留,内核中用的非常少,一般保持该锁的时间较长,因此不提倡使用它。//从2.6.11内核起,大内核锁可以通过配置内核使其变得可抢占(自旋锁是不可抢占的),这时它实质上是一个互斥锁,使用信号量实现。//大内核锁的API包括:// //void lock_kernel(void); // //该函数用于得到大内核锁。它可以递归调用而不会导致死锁。// //void unlock_kernel(void); // //该函数用于释放大内核锁。当然必须与lock_kernel配对使用,调用了多少次lock_kernel,就需要调用多少次unlock_kernel。//大内核锁的API使用非常简单,按照以下方式使用就可以了://lock_kernel(); //对被保护的共享资源的访问 … unlock_kernel();lock_kernel(); //初始化time ticket,时钟 tick_init(); //函数 tick_init() 很简单,调用 clockevents_register_notifier 函数向 clockevents_chain 通知链注册元素:// tick_notifier。这个元素的回调函数指明了当时钟事件设备信息发生变化(例如新加入一个时钟事件设备等等)时, //应该执行的操作,该回调函数为 tick_notify boot_cpu_init(); //初始化页地址,当然对于arm这里是个空函数 page_address_init(); printk(KERN_NOTICE "%s", linux_banner); //系结构相关的内核初始化过程 setup_arch(&command_line); //初始化内存管理 mm_init_owner(&init_mm, &init_task); //处理启动命令,这里就是设置的cmd_line setup_command_line(command_line); //这个在定义了SMP的时候有作用,现在这里为空函数;对于smp的使用,后面在看。。。setup_nr_cpu_ids(); //如果没有定义CONFIG_SMP宏,则这个函数为空函数。//如果定义了CONFIG_SMP宏,则这个setup_per_cpu_areas()函数给每个CPU分配内存, //并拷贝.data.percpu段的数据。为系统中的每个CPU的per_cpu变量申请空间。setup_per_cpu_areas(); //定义在include/asm-x86/smp.h。//如果是SMP环境,则设置boot CPU的一些数据。在引导过程中使用的CPU称为boot CPU smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */ //设置node 和 zone 数据结构 //内存管理的讲解:build_all_zonelists(NULL); //初始化page allocation相关结构 page_alloc_init(); printk(KERN_NOTICE "Kernel command line: %s/n", boot_command_line); //解析内核参数 parse_early_param(); parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption);
//初始化hash表,以便于从进程的PID获得对应的进程描述指针,按照实际的物理内存初始化pid hash表 //这里涉及到进程管理pidhash_init(); //初始化VFS的两个重要数据结构dcache和inode的缓存。vfs_caches_init_early(); //把编译期间,kbuild设置的异常表,也就是__start___ex_table和__stop___ex_table之中的所有元素进行排序 sort_main_extable(); //初始化中断向量表 trap_init(); //memory map初始化 mm_init(); //核心进程调度器初始化,调度器的初始化的优先级要高于任何中断的建立, //并且初始化进程0,即idle进程,但是并没有设置idle进程的NEED_RESCHED标志, //所以还会继续完成内核初始化剩下的事情。//这里仅仅为进程调度程序的执行做准备。//它所做的具体工作是调用init_bh函数(kernel/softirq.c)把timer,tqueue,immediate三个人物队列加入下半部分的数组 sched_init(); //抢占计数器加1 preempt_disable(); //检查中断是否打开 if (!irqs_disabled()) { printk(KERN_WARNING "start_kernel(): bug: interrupts were " "enabled *very* early, fixing it/n"); local_irq_disable(); } //Read-Copy-Update的初始化 //RCU机制是Linux2.6之后提供的一种数据一致性访问的机制, //从RCU(read-copy-update)的名称上看,我们就能对他的实现机制有一个大概的了解, //在修改数据的时候,首先需要读取数据,然后生成一个副本,对副本进行修改, //修改完成之后再将老数据update成新的数据,此所谓RCU。rcu_init(); //定义在lib/radix-tree.c。//Linux使用radix树来管理位于文件系统缓冲区中的磁盘块, //radix树是trie树的一种 radix_tree_init(); /* init some links before init_ISA_irqs() */ //early_irq_init 则对数组中每个成员结构进行初始化, //例如, 初始每个中断源的中断号.其他的函数基本为空. early_irq_init(); //初始化IRQ中断和终端描述符。//初始化系统中支持的最大可能的中断描述结构struct irqdesc变量数组irq_desc[NR_IRQS], //把每个结构变量irq_desc[n]都初始化为预先定义好的坏中断描述结构变量bad_irq_desc, //并初始化该中断的链表表头成员结构变量pend init_IRQ(); //prio-tree是一棵查找树,管理的是什么?//http://blog.csdn.net/dog250/archive/2010/06/28/5700317.aspx prio_tree_init(); //初始化定时器Timer相关的数据结构 init_timers(); //对高精度时钟进行初始化 hrtimers_init(); //软中断初始化 softirq_init(); //初始化时钟源 timekeeping_init(); //初始化系统时间, //检查系统定时器描述结构struct sys_timer全局变量system_timer是否为空, //如果为空将其指向dummy_gettimeoffset()函数。time_init(); //profile只是内核的一个调试性能的工具, //这个可以通过menuconfig中的Instrumentation Support->profile打开。profile_init(); if (!irqs_disabled()) printk(KERN_CRIT "start_kernel(): bug: interrupts were " "enabled early/n"); //与开始的early_boot_irqs_off相对应 early_boot_irqs_on(); //与local_irq_disbale相对应,开中断 local_irq_enable(); gfp_allowed_mask = __GFP_BITS_MASK; //memory cache的初始化 kmem_cache_init_late(); //初始化控制台以显示printk的内容,在此之前调用的printk,只是把数据存到缓冲区里, //只有在这个函数调用后,才会在控制台打印出内容 //该函数执行后可调用printk()函数将log_buf中符合打印级别要求的系统信息打印到控制台上。console_init(); if (panic_later) panic(panic_later, panic_param); //如果定义了CONFIG_LOCKDEP宏,那么就打印锁依赖信息,否则什么也不做 lockdep_info();
//如果定义CONFIG_DEBUG_LOCKING_API_SELFTESTS宏 //则locking_selftest()是一个空函数,否则执行锁自测 locking_selftest(); #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./n", page_to_pfn(virt_to_page((void *)initrd_start)), min_low_pfn); initrd_start = 0; } #endif //页面初始化,可以参考上面的cgroup机制 page_cgroup_init(); //页面分配debug启用 enable_debug_pagealloc(); //此处函数为空 kmemtrace_init(); //memory lead侦测初始化,如何侦测???kmemleak_init();
//在kmem_caches之后表示建立一个高速缓冲池,建立起SLAB_DEBUG_OBJECTS标志。???debug_objects_mem_init(); //idr在linux内核中指的就是整数ID管理机制, //从本质上来说,这就是一种将整数ID号和特定指针关联在一起的机制 //idr机制适用在那些需要把某个整数和特定指针关联在一起的地方。idr_init_cache(); //是否是对SMP的支持,单核是否需要??这个要分析 setup_per_cpu_pageset(); //NUMA (Non Uniform Memory Access) policy //具体是什么不懂 numa_policy_init(); if (late_time_init) late_time_init(); //初始化调度时钟 sched_clock_init(); //calibrate_delay()函数可以计算出cpu在一秒钟内执行了多少次一个极短的循环, //计算出来的值经过处理后得到BogoMIPS 值, //Bogo是Bogus(伪)的意思,MIPS是millions of instructions per second(百万条指令每秒)的缩写。//这样我们就知道了其实这个函数是linux内核中一个cpu性能测试函数。calibrate_delay(); //PID是process id的缩写 pidmap_init(); //来自mm/rmap.c //分配一个anon_vma_cachep作为anon_vma的slab缓存。//这个技术是PFRA(页框回收算法)技术中的组成部分。//这个技术为定位而生——快速的定位指向同一页框的所有页表项。anon_vma_init(); #ifdef CONFIG_X86 if (efi_enabled) efi_enter_virtual_mode(); #endif //创建thread_info缓存 thread_info_cache_init(); //申请了一个slab来存放credentials??????如何理解?cred_init(); //根据物理内存大小计算允许创建进程的数量 fork_init(totalram_pages); //给进程的各种资源管理结构分配了相应的对象缓存区 proc_caches_init(); //创建 buffer_head SLAB 缓存 buffer_init(); //初始化key的management stuff key_init(); //关于系统安全的初始化,主要是访问控制 security_init(); //与debug kernel相关 dbg_late_init(); //调用kmem_cache_create()函数来为VFS创建各种SLAB分配器缓存 //包括:names_cachep、filp_cachep、dquot_cachep和bh_cachep等四个SLAB分配器缓存 vfs_caches_init(totalram_pages); //创建信号队列 signals_init(); //回写相关的初始化 page_writeback_init(); #ifdef CONFIG_PROC_FS proc_root_init(); #endif //它将剩余的subsys初始化.然后将init_css_set添加进哈希数组css_set_table[ ]中. //在上面的代码中css_set_hash()是css_set_table的哈希函数. //它是css_set->subsys为哈希键值,到css_set_table[ ]中找到对应项.然后调用hlist_add_head()将init_css_set添加到冲突项中. //然后,注册了cgroup文件系统.这个文件系统也是我们在用户空间使用cgroup时必须挂载的. //最后,在proc的根目录下创建了一个名为cgroups的文件.用来从用户空间观察cgroup的状态. cgroup_init(); cpuset_init(); ////进程状态初始化,实际上就是分配了一个存储线程状态的高速缓存 taskstats_init_early(); delayacct_init(); //此处为一空函数 imv_init_complete(); //测试CPU的各种缺陷,记录检测到的缺陷,以便于内核的其他部分以后可以使用他们工作。check_bugs(); //电源相关的初始化 acpi_early_init(); /* before LAPIC and SMP init */ // sfi_init_late(); ftrace_init(); //创建1号进程,详细分析之 rest_init(); }

  

    下面将一些内核引导的函数归类,以后查询就可以使这个样子的,,,这些都是我百度到的,总结的如下,如果想了解每个函数的含义,可以参考一个文档,我把链接附在下面,http://wenku.baidu.com/link?url=BW4g5AS9KHtvkHorS90lbFOAac2HZFgErLaqeiQr-fejBuRWu8bF28LxKKKxGwGuaQI0ALSUsJeY_dj6m_jgkxX9ozZiE17U0i__sZk_YYa


CPU初始化

smp_setup_processor_id()

boot_cpu_init()

setup_arch(&command_line);

setup_nr_cpu_ids()

setup_per_cpu_areas()

smp_prepare_boot_cpu()

setup_per_cpu_pageset(); 

calibrate_delay();

cpuset_init();


内存管理初始化

boot_init_stack_canary()

page_address_init();

mm_init_owner();

page_alloc_init(); 

mm_init();

rcu_init();

kmem_cache_init_late();

page_cgroup_init(); 

kmemleak_init(); 

numa_policy_init();

anon_vma_init();

page_writeback_init();


进程管理

pidhash_init();

sched_init(); 

sched_clock_init()

pidmap_init();

fork_init(totalram_pages); 

taskstats_init_early();


文件系统

vfs_caches_init_early(); 

thread_info_cache_init();

vfs_caches_init(totalram_pages);


中断

early_irq_init();

init_IRQ(); 

softirq_init();


同步互斥

lockdep_init();

lockdep_info();

locking_selftest(); 


时钟

tick_init(); 

init_timers(); 

hrtimers_init();

timekeeping_init();

time_init();


调试

debug_objects_early_init();

console_init();

enable_debug_pagealloc(); 

debug_objects_mem_init();

dbg_late_init();


其他

sort_main_extable();

trap_init(); 

efi_enter_virtual_mode();

cred_init(); 

proc_caches_init(); 

buffer_init();

key_init(); 

security_init();

signals_init();

proc_root_init();

delayacct_init();

check_bugs();

acpi_early_init();

sfi_init_late(); 


未知

cgroup_init_early();
build_all_zonelists(NULL);preempt_disable();radix_tree_init();prio_tree_init();profile_init();idr_init_cache();cgroup_init();ftrace_init();


最后来总结一下Linux内核的初始化过程:

  • 启动和锁住内核

  • 为Linux的内存管理初始化页高速缓存和页面地址

  • 为多CPU做好准备

  • 显示内核标志

  • 初始化Linux调度程序

  • 分析传到Linux内核的参数

  • 初始化中断处理程序,定时器处理程序和信号处理程序

  • 挂载初始文件系统

  • 完成系统的初始化,并且将控制权从init交回系统

 

小结

    今天主要是描述了系统加电和断电时候的内核引导期间发生了什么事情,先讨论了BIOS和Firmware以及它们是如何与内核引导装入程序交互的,也讨论了装入程序LILO,GRUB和Yaboot,,最后着重分析了start_kernel()函数的代码,这个是我看网上代码的,主要是别人分析的太好了,所以借鉴了一下,只要能懂就行,最后列出了一系列的函数,只要看了链接上的 都能懂的,,我也努力在看,,共同进步吧大家~

 

特别声明:

    版权所有,转载请注明转载地址:        
http://www.cnblogs.com/lihuidashen/p/4250095.html

    我们不再是我们,我们仍然是我们......



推荐阅读

(点击标题可跳转阅读)

24小时学通Linux内核--内核探索工具类

24小时学通Linux内核之进程

24小时学通Linux内核之内存管理方式

24小时学通Linux内核之如何处理输入输出操作

24小时学通Linux内核之有关Linux文件系统实现的问题

24小时学通Linux内核之调度和内核同步


关注公众号【技术让梦想更伟大】,获取更多Linux/C/C++/Python/FPGA等原创技术文章。后台免费获取经典电子书籍和视频资源,实时更新,原创不易,请多支持,谢谢!