Linux内核引导简析

FutureChen

FutureChen的日记

  • FutureChen的主页
  • 广播
  • 相册
  • 推荐
  • 活动
  • 发豆邮

bootsect.S、setup.S、head.S分析 收藏

2010-01-14 13:36:34
bootsect.S,系统引导程序,一般不超过512字节。
在PC系统结构中,线性地址0xA0000以上,即640K以上用于图形接口卡和BIOS自身,640K以下为系统的基本内存。如果配置更多的内存,则0x100000,即1MB处开始称为高内存。当BIOS引导一个系统时,总是把引导扇区读入到基本内存地址为0x7c00的地方,然后跳转到此执行引导扇区的代码。这段代码将自身搬运到0x90000处,并跳转到那继续执行,然后通过BIOS提供的读磁盘调用“int 0x13”从磁盘上读入setup和内核映像。其中setup的映像读入到0x90200处,然后跳转到setup的代码中。
从0x90000到0xA0000一共64K,bootsect仅占512字节,所以setup大小理论上可到63.5KB。
在Linux2.4版本以前,在最前面的512字节里保护了一个mini “boot loader”,只要拷贝启动代码运行就可从软盘启动;但在2.6版本中不再保护这样的”boot loader”,所以必须在第一个磁盘分区上存储一个合适的boot loader才能从软盘启动,软盘、硬盘和光驱启动都是一样的过程。
setup进行映像的解压缩,从BIOS收集一些数据,在控制台显示一些信息。
基本内存中开头一部分空间是保留给BIOS自己用的,另一方面对于Linux内核的引导也需要保留一些运行空间,一共保存了64K。基本内存中用于内核映像的就是8*64K=512K,其中顶端留4K用于引导命令行及从BIOS获取需要传递给内核的数据。内核映像一般都经过压缩,压缩后的映像和引导扇区及辅助引导程序的映像拼接在一起,成为内核的引导映像。大小不超过508K的映像称为小映像zImage,早期版本放在0x10000位置处,否则称为大内核bzImage,放在0x100000位置处。
CPU在bootsect时处于16位实地址模式,然后在setup的执行过程中转入32位保护模式。
Setup从BIOS中读取系统数据(内存大小、显卡模式、磁盘等参数),将数据保存在0x90000-0x901FF,覆盖了bootsect的内容。设置32位运行方式:加载中断描述表寄存器IDTR、全局描述表寄存器GDTR;临时设置IDT表和GDT表,并在GDT表中设置内核代码段和数据段的描述符,在Head.S中会根据内核的需要重新设置这些描述符表;开启A20地址线;重新设置两个中断控制器8259A,将硬件中断号重新设置为0x20和0x2f;最后设置CPU的控制寄存器CR0(机器状态字)的保护模式比特(PE)位,从而进入32位保护模式运行;然后跳转到head.S中的startup_32执行。
对于小内核映像放在0x10000处,Setup会把system从0x10000移到0x0000开始处。对于大内核映像,vmlinux中普通内核代码被编译成以PAGE_OFFSET+1MB为起始地址,在Head.S中初始化代码把虚拟地址减去PAGE_OFFSET就能得到以1MB为起始位置的物理地址,这也正是内核映像在物理内存中的存放位置。
Head.S中的startup_32主要用于开启页面单元。初始化工作在编译过程中开始进行,它先定义一个称为swapper_pg_dir的数组,使用链接器指示在地址0x00101000。然后分别为两个页面pg0和pg1创建页表项。第一组指向pg0和pg1的指针放在能覆盖1~9MB内存的位置,第二组指针放在PAGE_OFFSET+1MB的位置。一旦开始页机制,在上述页表和页表项指针建立后可以保证,在内核映像中不论是采用物理地址还是虚拟地址,都可以进行正确的页面映射。内核其他部分的页表初始化在paging_init()中完成。映射建立后,通过设置cr0寄存器中的某位开启页面映射,然后通过一个跳转指令保证指令指针的正确性。
 
1.Bootsect启动过程:
假设用LILO启动,启动时用户可以选择启动哪个操作系统。LILO将boot loader分为两部分,一部分放到启动分区的第一个扇区;
1)        BIOS将MBR或启动分区的第一个扇区的启动部分加载到地址0x00007c00处;
2)        该程序将自身移到0x00096a00,建立实模式栈(从0x00098000到0x000969ff),将LILO的第二部分加载到0x00096c00处,然后跳转到此执行;
3)        然后第二部分程序从磁盘读取一个可启动的操作系统列表让用户选择,最后用户选择每个OS后,boot loader可以拷贝不启动分区或者之间拷贝内核映像到RAM中去;
4)        加载Linux内核映像时,LILO boot loader首先调用BIOS例程显示”Loading …”信息;
5)        调用BIOS例程加载内核映像的初始化部分到RAM上,内核映像的前512字节放在0x00090000位置,setup()函数代码放在0x00090200位置;
6)        接着调用BIOS例程装载内核映像的其余部分,映像可能放在低地址0x00010000(使用make zImage编译的小内核映像)或者高地址0x00100000(使用make bzImage编译的大内核映像)。
7)        然后跳至刚刚setup部分。
 
2.Setup.S分析
setup()汇编函数被连接器放在内核映像文件中的0x200偏移处。Setup函数必须初始化计算机中的硬件设备并为内核程序的执行建立环境。
1)        在ACPI兼容的系统中,调用BIOS例程建立描述系统物理内存布局的表。在早期系统中,它调用BIOS例程返回系统可以的RAM大小;
2)        设置键盘的重复延迟和速率;
3)        初始化显卡;
4)        检测IBM MCA总线、PS/2鼠标设备、APM BIOS支持等;
5)        如果BIOS支持Enhanced Disk Drive Services (EDD),将调用正确的BIOS例程建立描述系统可用硬盘的表;
6)        如果内核加载在低RAM地址0x00010000,则把它移动到0x00001000处;如果映像加载在高内存1M位置,则不动;
7)        启动位于8042键盘控制器的A20 pin。
8)        建立一个中断描述表IDT和全局描述表GDT表;
9)        如果有的话,重启FPU单元;
10)    对可编程中断控制器进行重新编程,屏蔽所以中断,级连PIC的IRQ2不需要;
11)    设置CR0状态寄存器的PE位使CPU从实模式切换到保护模式,PG位清0,禁止分页功能;
12)    跳转到startup_32()汇编函数, jmpi 0x100000, __BOOT_CS,终于进入内核Head.S;
 
3.Head.S分析
有两个不同的startup_32()函数,一个在arch/i386/boot/compressed/head.S文件中,setup结束后,该函数被放在0x00001000或者0x00100000位置,该函数主要操作:
1)        首先初始化段寄存器和临时堆栈;
2)        清除eflags寄存器的所有位;
3)        将_edata和_end区间的所有内核未初始化区填充0;
4)        调用decompress_kernel( )函数解压内核映像。首先显示"Uncompressing Linux..."信息,解压完成后显示 "OK, booting the kernel."。内核解压后,如果时低地址载入,则放在0x00100000位置;否则解压后的映像先放在压缩映像后的临时缓存里,最后解压后的映像被放置到物理位置0x00100000处;
5)        跳转到0x00100000物理内存处执行;
   
    解压后的映像开始于arch/i386/kernel/head.S 文件中的startup_32()函数,因为通过物理地址的跳转执行该函数的,所以相同的函数名并没有什么问题。该函数未Linux第一个进程建立执行环境,操作如下:
1)         初始化ds,es,fs,gs段寄存器的最终值;
2)        用0填充内核bss段;
3)        初始化swapper_pg_dir数组和pg0包含的临时内核页表:
l          将swapper_pg_dir(0x1000)和pg0(0x2000)清空,swapper_pg_dir作为整个系统的页目录;
l          将pg0作为第一个页表,将其地址赋到swapper_pg_dir的第一个32位字中。
l          同时将该页表项也赋给swapper_pg_dir的第3072个入口,表示虚拟地址0xc0000000也指向pg0。
l          将pg0这个页表填满指向内存前4M。
l          在cr3寄存器中存放PGD的地址,并设置cr0寄存器中的PG位,启用分页支持。
4)        建立进程0idle进程的内核模式的堆栈;
5)        再次清除eflags寄存器的所有位;
6)        调用setup_idt()用非空的中断处理函数填充IDT表;
7)        将从BIOS获取的系统参数传递到操作系统的第一个页面帧;
8)        识别处理器的模式;
9)        将GDT和IDT表的地址加载到gdtr和idtr寄存器中;
10) 跳转到start_kernel函数,这个函数是第一个C编制的函数,内核又有了一个新的开始。
 
4.start_kernel()分析:
1)        调度器初始化,调用sched_init();
2)        调用build_all_zonelists函数初始化内存区;
3)        调用page_alloc_init()和mem_init()初始化伙伴系统分配器;
4)        调用trap_init()和init_IRQ()对中断控制表IDT进行最后的初始化;
5)        调用softirq_init() 初始化TASKLET_SOFTIRQ和HI_SOFTIRQ;
6)        Time_init()对系统日期和时间进行初始化;
7)        调用kmem_cache_init()初始化slab分配器;
8)        调用calibrate_delay()计算CPU时钟频率;
通过调用kernel_thread()启动进程1init进程的内核线程,然后该线程再创建其他的内核线程执行/sbin/init程序。  

2010-01-14 13:41:32: FutureChen (Life With Youtube And Twitter)

  从以上几讲我们知道,Linux简化了分段机制,使得虚拟地址与线性地址总是一致,因此,Linux的虚拟地址空间也为0~4G。Linux内核将这4G字节的空间分为两部分。将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为“内核空间”。而将较低的 3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为“用户空间)。因为每个进程可以通过系统调用进入内核,因此,Linux内核由系统内的所有进程共享。于是,从具体进程的角度来看,每个进程可以拥有4G字节的虚拟空间。
   Linux使用两级保护机制:0级供内核使用,3级供用户程序使用。从图中可以看出(这里无法表示图),每个进程有各自的私有用户空间(0~3G),这个空间对系统中的其他进程是不可见的。最高的1GB字节虚拟内核空间则为所有进程以及内核所共享。
  1.虚拟内核空间到物理空间的映射
   内核空间中存放的是内核代码和数据,而进程的用户空间中存放的是用户程序的代码和数据。不管是内核空间还是用户空间,它们都处于虚拟空间中。读者会问,系统启动时,内核的代码和数据不是被装入到物理内存吗?它们为什么也处于虚拟内存中呢?这和编译程序有关,后面我们通过具体讨论就会明白这一点。
  虽然内核空间占据了每个虚拟空间中的最高1GB字节,但映射到物理内存却总是从最低地址(0x00000000)开始。对内核空间来说,其地址映射是很简单的线性映射,0xC0000000就是物理地址与线性地址之间的位移量,在Linux代码中就叫做PAGE_OFFSET。
  
  
  我们来看一下在include/asm/i386/page.h中对内核空间中地址映射的说明及定义:
  /*
  * This handles the memory map.. We could make this a config
  * option, but too many people screw it up, and too few need
  * it.
  *
  * A __PAGE_OFFSET of 0xC0000000 means that the kernel has
  * a virtual address space of one gigabyte, which limits the
  * amount of physical memory you can use to about 950MB.
  *
  * If you want more physical memory than this then see the CONFIG_HIGHMEM4G
  * and CONFIG_HIGHMEM64G options in the kernel configuration.
  */
  
  #define __PAGE_OFFSET (0xC0000000)
  ……
  #define PAGE_OFFSET ((unsigned long)__PAGE_OFFSET)
  #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
  #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
  源代码的注释中说明,如果你的物理内存大于950MB,那么在编译内核时就需要加CONFIG_HIGHMEM4G和CONFIG_HIGHMEM64G 选项,这种情况我们暂不考虑。如果物理内存小于950MB,则对于内核空间而言,给定一个虚地址x,其物理地址为“x- PAGE_OFFSET”,给定一个物理地址x,其虚地址为“x+ PAGE_OFFSET”。
  这里再次说明,宏__pa()仅仅把一个内核空间的虚地址映射到物理地址,而决不适用于用户空间,用户空间的地址映射要复杂得多。
  2.内核映像
   在下面的描述中,我们把内核的代码和数据就叫内核映像(kernel image)。当系统启动时,Linux内核映像被安装在物理地址0x00100000开始的地方,即1MB开始的区间(第1M留作它用)。然而,在正常运行时,整个内核映像应该在虚拟内核空间中,因此,连接程序在连接内核映像时,在所有的符号地址上加一个偏移量PAGE_OFFSET,这样,内核映像在内核空间的起始地址就为0xC0100000。
  例如,进程的页目录PGD(属于内核数据结构)就处于内核空间中。在进程切换时,要将寄存器CR3设置成指向新进程的页目录PGD,而该目录的起始地址在内核空间中是虚地址,但CR3所需要的是物理地址,这时候就要用__pa()进行地址转换。在mm_context.h中就有这么一行语句:
  asm volatile(“movl %0,%%cr3”: :”r” (__pa(next->pgd));
  这是一行嵌入式汇编代码,其含义是将下一个进程的页目录起始地址next_pgd,通过__pa()转换成物理地址,存放在某个寄存器中,然后用mov指令将其写入CR3寄存器中。经过这行语句的处理,CR3就指向新进程next的页目录表PGD了。

 

Linux内核引导简析

 

  2009-03-01 16:47
   以前学计算机的时候就很好奇,为什么电源一打开,操作系统就会在最后神奇般的出现?这中间到底发生了些什么事情?我试着用这篇小文来解释,但水平有限,难免有错误和不足。因为引导过程与体系结构有关,这里就只以Intel X86体系结构32位机为例来进行说明。
  
  一,PC机物理编址
  
   PC机最早是由IBM生产,使用的是Intel 8088处理器。这个处理器只有20根地址线,可以寻址1M的空间。这1M空间大概有如下的结构:
   +------------------+ <- 0x00100000 (1MB)
   | BIOS ROM |
   +------------------+ <- 0x000F0000 (960KB)
   | 16-bit devices, |
   | expansion ROMs |
   +------------------+ <-0x000C0000 (768KB)
   | VGA Display |
   +------------------+ <-0x000A0000 (640KB)
   | Low Memory |
   +------------------+ <- 0x00000000
  其中可以自由使用的空间是最低的640K(0x0000_0000 ~ 0x000F_FFFF),称为Low Memory。余下的384K有特殊的用途,最突出的是最后的64K,那是BIOS的代码。
   最后Intel终于打破了1MB的屏障,80286,80386处理器分别支持16MB和4GB内存。然而,为了向后兼容,最初的1M内存空间仍然保留了下来。因此现代的PC机物理内存中存在着0x000A_0000到0x0010_0000的“空洞“,它把RAM分成了两个部分,一是最低的640K,称为”传统内存“,一是”扩展内存“(它的地址空间不固定)。另外,位于32位物理地址空间的最顶端的部分,高于任何物理RAM,被BIOS保留了下来,用于32位PCI设备。目前,物理内存可以超过4G,被保留的32位PCI设备地址空间又会形成新的“空洞”。
  
  二,BIOS
  
   当打开PC机的电源时,处理器处于实模式,CS:IP = 0xF000:0xFFFF。这个逻辑地址的虚拟地址是0xFFFFF0,是将CS寄存器的值左移4个二进制位,再加上IP寄存器的值得到的。这种方法隐含了一个信息,就是在实模式中,也是有分段的,只不过段是固定的,每个段的大小都是64K(2^16),段寄存器中保存的就是段的编号。那么这个初始地址是哪里的指令了?在PC机的物理编址一节提到,BIOS的位于1M的最后64K,也即0x000F0000~0x000FFFFF,所以第一条指令是 BIOS的代码。BIOS,也即基本输入输出系统,它主要分为两个部分,POST(加电自检)以及Runtime Routines。实际上在0xFFFFF0这个地址上保存了一条跳转指令,跳转到BIOS的POST的第一代指令。POST主要进行一些硬件的检测操作,这时可以在屏幕上看到很多输出。当检测完毕后,BIOS根据CMOS里的设置,查找引导设备,并从主引导分区中读取第1个扇区,并加载到0x7C00 的位置,BIOS会在最后跳转到这个地址。POST的代码会在结束后从内存中移除,而Runtime Routeines的代码不会。
  
  三,Boot Loader
  
   主引导记录位于一个扇区里,有512字节,分为三个部分。前446字节是引导代码部分,随后64字节是分区表,最后的2个字节是魔数0xAA55。分区表里含有4个表项,每个使用16字节描述,这里不详细说明。魔数起到一个标志的作用。操作系统是通过称为Boot Loader的程序加载到内存中,主引导记录的代码就与Boot Loader有关。在早期的操作系统中(包括Linux),Boot Loader是做为内核的一部分,和内核同时编译链接的。现在, Boot Loader和操作系统进行了分离,比如Grub就是一个Boot Loader,它即可以引导Linux,也可以引导Windows,而Linux还可以被LILO引导。
   引导操作系统的过程就好像如何把大象从冰箱里拿出来一样(可怜的大象!),第一步,把冰箱门打开,第二步,把大象拿出来。目前的Boot Loader,比如Grub,也是一个两阶段的过程。第一阶段的代码就是位于MBR记录里的,它负责加载第二阶段的代码。第二阶段加载内核到内存中,并为其准备引导参数。GRUB(Grand Unified Bootloader)实际上是一个2.5阶段的Boot Loader,多出的第1.5阶段是为了支持多文件系统。为了实现操作系统与Boot Loader的分离,操作系统映像的第一个8K必须含有一个multiboot header,并以0x1BADB002结束。
  
  四,Linux 2.6 内核加载过程
  
   GRUB将Linux内核映像的前两个扇区(init扇区以及setup扇区)加载到物理内存的0x00090000地址处。这两个扇区的代码是体系结构相关的,位于arch/x86/boot/header.S中。init扇区最初是用做软盘MBR的引导代码的,现在的Linux不支持软盘引导,所以这个扇区没有什么意义,只是输出一些提示信息"Direct booting form floppy is no longer supported. Please use a boot loader program instead.",(用bochs虚拟机去执行内核的压缩映像bzImage,可以看到这些信息)。setup扇区是一些代码和引导参数,它被加载到 0x00090200处。代码部分的主要工作是调用引导阶段的main函数,比较重要的引导参数是进入保护模式后的32位代码的入口点。参数说明当内核是大内核时,内核映像会被加载到0x0010000的位置,否则,就被加载到0x1000处。
   code32_start:
   #ifndef __BIG_KERNEL__
   .long 0x1000 # 0x1000 = default for zImage
   #else
   .long 0x100000 # 0x100000 = default for big kernel
   引导阶段的main函数位于arch/x86/boot/main.c中,它首先会复制引导参数,然后初始堆,检测物理内存布局,最重要是进入保护模式,跳转到32位代码的入口点。进入保护模式是通过位于arch/x86/boot/pm.c中的go_to_protected_mode()函数来实现的,它会开启A20地址线,设置boot阶段的IDT,GDT,(内核代码段0x10,数据段0x18),最后,执行 protected_mode_jump(boot_params.hdr.code32_start, (u32)&boot_params + (ds() << 4)),跳转到引导参数定义的入口点,如果是big kernel,则是0x00100000(1M)。
   位于0x00100000之后的代码也可分为两部分,一是解压内核的代码,一是被压缩过的32位代码。解压缩的代码位于arch/x86/boot /compressed/head_32.S,值得注意的是,解压的最终位置会在计算后,保存在ebp寄存器中,实际上还是0x00100000。解压后,位于1M位置的就是位于arch/x86/kernel/head_32.S中的入口点了,这也是真实意义的内核入口点。这段代码会设置页目录,页表,内核的虚地址空间被设成0xC0000000~0xFFFFFFFF,也就是最后1G,并使用宏__PAGE_OFFSET表示起始地址 0xC00000000。经过一系列基本的与硬件有关的初始化工作后,执行流跳转到(*initial_code),也就是 i386_start_kernel函数。i386_start_kernel()位于arch/x86/kernel/head32.c中,如果需要,它首先初始化与ramdisk相关的数据。ramdisk会在引导过程中做为临时的根文件系统,它包含一些可执行程序,脚本,可以用来加载内核模块等。最后调用start_kernel()。
  
  五,start_kernel()函数
  
   start_kernel()函数位于init/main.c中,是引导过程中最重要的一个函数,就像它的名字一样,它初始化了内核所有的功能。
   1,调用lock_kernel(),防止内核被意外抢断,定义在lib/kernel_lock.c中。在SMP或者抢断式调度环境中,内核可以被抢断。内核初始化时,功能还不完善,为防止此种情况发生,使用称为Big Kernel Lock的spinlock。spinlock是一种忙等待锁,如果等待周期不是很长,它比信号有效,因为信号会造成进程调度。Big Kernel Lock只在内核初始化时使用,当初始化结束后,该锁被释放。
   2,page_address_init()函数初始化页管理,创建了页管理所需的数据结构,定义在mm/highmem.c中。
   3,输出内核版本信息,执行了两个内核输出语句printk(KERN_NOTICE)和printk(linux_banner)。因为此时还没有初始化控制台,所以这些信息不能输出到屏幕上或者输出到串口上,而是输出到一个buffer中。printk()函数定义在kernel/printk.c 中,KERN_NOTICE宏定义在include/linux/kernel.h中,值为"<5>"。linux_banner定义在 init/version.c中,在我的实验环境中是这样的一个字符串:Linux version 2.6.28 (zctan@dbgkrnl) (gcc version 4.3.0 20080428 (Red Hat 4.3.0-8) (GCC) ) #1 SMP Sun Feb 8 20:56:17 CST 2009。
   4,setup_arch(),位于arch/x86/kernel/setup.c,初始化了许多体系结构相关的子系统。
   5,setup_per_cpu_area(),定义在arch/x86/kernel/setup_percpu.c中,如果是SMP环境,则为每个CPU创建数据结构,分配初始工作内存。
   6,smp_prepare_boot_cpu(),定义在include/asm-x86/smp.h。如果是SMP环境,则设置boot CPU的一些数据。在引导过程中使用的CPU称为boot CPU。
   7,sched_init(),定义在kernel/sched.c。初始化每个CPU的运行队列和超时队列。Linux使用多优先级队列的调度方法,就绪进程位于运行队列中。
   8,build_all_zonelists(),定义在mm/page_alloc.c中,建立内存区域链表。Linux将所有物理内存分为三个区,ZONE_DMA, ZONE_NORMAM, ZONE_HIGHMEM。
   9,trap_init(),定义在arch/x86/kernel/traps_32.c中,初始化IDT, 如除0错,缺页中断等。
   10,rcu_init(),定义在kernel/rcupdate.c中,初始化Read-Copy-Update子系统。当使用spinlock会造成效率低下时,RCU被用来实现临界区的互斥。
   11,init_IRQ(),定义在arch/x86/kernel/paravirt.c中,初始化中断控制器。
   12,pidhash_init(),定义在kernel/pid.c中,Linux的进程描述符称为PID, 使用名称空间以及hash表来管理。
   13,init_timers(),定义在kernel/timer.c中,初始化定时器。
   14,softirq_init(),定义在kernel/softirq.c中,初始化中断子系统,如softirq, tasklet。
   15,time_init(),定义在arch/x86/kernel/time_32.c中,初始化系统时间。
   16,profile_init(),定义在kernel/profile.c中,为profiling data分配存储空间。Profiling data这个术语描述在程序运行过程中采集到的一些数据,用于性能的分析。
   17,local_irq_enable(),定义在include/linux/irqflags.h中,开启引导CPU的中断。
   18,console_init(),定义在drivers/char/tty_io.c中,初始化控制台,可以是显示器也可以是串口。此时屏幕上才会有输出,前面printk输出到buffer中的内容会在这里全部输出。
   19,initrd检测。如果定义了Init Ram Disk,则检测其是否有效。
   20,mem_init(),定义在arch/x86/mm/init_32.c,检测所有可用物理页。
   21,pgtable_cache_init(),定义在include/asm-x86/pgtable_32.h,在slab存储管理子系统中创建页目录页表的cache。
   22,fork_init(),定义在kernel/fork.c中,初始化多进程环境。此时,执行start_kernel的进程就是所谓的进程0。
   23,buffer_init(),定义在fs/buffer.c中,初始化文件系统的缓冲区。
   24,vfs_cache_init(),定义在fs/dcache.c中,创建虚拟文件系统的Slab Cache。
   25,radix_tree_init(),定义在lib/radix-tree.c。Linux使用radix树来管理位于文件系统缓冲区中的磁盘块,radix树是trie树的一种。
   26,signals_init(),定义在kernel/signal.c中,初始化信号队列。
   27,page_writeback_init(),定义在mm/page-writeback.c中,初始化将脏页页同步到磁盘上的控制信息。
   28,proc_root_init(),定义在fs/proc/root.c, 初始化proc文件系统
   29,rest_init(),定义在init/main.c中,创建init内核线程(也就是进程1)。init进程创建成功后,进程0释放Big Kernel Lock,重新调度(因为现在只有两个进程,所以调度的是init进程)。进程0,就变成了idle进程,只负责调度。
   注:start_kernel函数涉及到很多内容和硬件知识,比如SMP等,有很多是我不知道的,所以只能简要的从功能上说明一下,有些可能理解错了,也会略过一些函数,请见谅。
  
  六,init进程
  
   init进程执行定义在init/main.c中的kernel_init()函数,完成余下的初始化工作。
   1,lock_kernel(),加上Big Kernel Lock。
   2,初始化SMP环境。
   3,do_basic_setup()。调用driver_init(),加载设备驱动程序。执行do_initcalls(),调用内建模块的初始化函数,比如kgdb。
   4,init_post()函数会打开/dev/console做为标准输入文件,并复制出标准输出和标准错误输出。最后,按下列顺序偿试执行init程序,位于ramdisk的/init,以及磁盘上的/sbin/init, /etc/init, /bin/init和/bin/sh, 只要有一个能执行就可以。init进程会使用类exec()去调用其它进程,因而不会返回。
  
  七,结语
  
   以上简要说明了从打开电源到Linux内核引导成功之间发生的一些操作,可以使用bochs虚拟机安装一个Linux进行调试验证。各个部分之间的切换我觉得是没有什么大问题的,其余的就不好说了(^_^!)。

你可能感兴趣的:(Linux内核引导简析)