linux 启动协议分析--由boot.txt翻译而来

在x86平台上,linux内核用了一个比较复杂的启动协议规范,主要由于历史原因,早期想把内核做成可启动映像,复杂的计算机内存模型和由于实模式DOS成为主流操作系统而影响的.

现在有下列启动协议存在:

旧版内核:只支持zImage和Image.

2.00:增加bzImage和initrd支持,也拥有了一种正规化的方法来实现启动装载器(* boot loader)和内核间的通信。setup.S建造了一块可移动,但是仍旧可写的传统的安装程序加载区域。

2.01:增加了一些溢出警告

2.02:这是新的命令行协议。它降低了常规的内存使用上限(* 见以下MEMORY LAYOUT内存布局介绍)。没有覆盖传统的安装程序区域,因此对于那些使用了来自SMM或者32位BIOS入口地址的EBDA(* Extended BIOS Data Area,拓展BIOS数据域)的系统,这样做可以使得启动更加安全。

2.03:明确了高端的initrd地址可被 bootloader 使用.

2.04:扩展syssize域到四个字节

2.05:使得保护模式下的 内核可被重定位,增加 relocatable_kernel 和kernel_alignment 域.

2.06:增加一个域存放 boot command line 的大小.

2.07:增加半虚拟化启动协议,在load_flags中 增加hardware_subarch 和 hardware_subarch_data 和 KEEP_SEGMENTS 标志.
2.08:增加CRC32校验和ELF格式的有效载荷。增加 payload_offset 和payload_length 域 用于定位有效载荷(payload)

2.09:增加一个64位的物理指针指向setup_data结构体的单链表.

2.10:增加了init_size 和  pref_address 域.

 

传统的用于早期的Image和bImage的Memory的映射图:(可以参考下Linux-0.11的内存布局)

100000

       图形接口卡信息和BIOS自身

0A0000 +------------------------+ 从这一下的640K称为基本内存
       | Reserved for BIOS      | 未使用,被BIOS EBDA保留
09A000 +------------------------+
       | Command line             |
       | Stack/heap                 | 被实模式下的内核代码所使用
098000 +------------------------+
       | kernel setup                | 实模式下的内核代码 31.5k
090200 +------------------------+
       | kernel boot sector        | 历史遗留下来的内核启动扇区 0.5k
090000 +------------------------+
       | Protected-mode kernel  | 内核镜像的主要部分 512k

010000 +------------------------+
       | Boot loader                  | - 启动扇区的入口位置 0000:7c00
001000 +------------------------+从这以下的4k 全是不可以直接用的
       | Reserved for MBR/BIOS  |
000800 +------------------------+
       | Typically used by MBR  |
000600 +------------------------+
       | BIOS use only             |
000000 +------------------------+

When using bzImage, the protected-mode kernel was relocated to 0x100000 ("high memory"), and the kernel real-mode block (boot sector,
setup, and stack/heap) was made relocatable to any address between 0x10000 and end of low memory. Unfortunately, in protocols 2.00 and
2.01 the 0x90000+ memory range is still used internally by the kernel; the 2.02 protocol resolves that problem.

It is desirable to keep the "memory ceiling" -- the highest point in low memory touched by the boot loader -- as low as possible, since
some newer BIOSes have begun to allocate some rather large amounts of memory, called the Extended BIOS Data Area, near the top of low
memory.     The boot loader should use the "INT 12h" BIOS call to verify how much low memory is available.

当使用大内核的时候,保护模式下的内核被重定位到100000高地址,并且实模式块(boot sector ,setuo ,stack/heap)被设计位可重定位在10000到100000间的任何位置. 但由于EDBA越来越大,所以 boot loader 能够触及到的内存上限越低越好.通常 boot loader会使用 12号中断 判断还有多少低内存(10000~~100000)可用.

Unfortunately, if INT 12h reports that the amount of memory is too low, there is usually nothing the boot loader can do but to report an
error to the user.  The boot loader should therefore be designed to take up as little space in low memory as it reasonably can.  For
zImage or old bzImage kernels, which need data written into the 0x90000 segment, the boot loader should make sure not to use memory
above the 0x9A000 point; too many BIOSes will break above that point.

现在的bzImage内核的内存映像建议为如下方式:
        | Protected-mode kernel  |
100000  +------------------------+
        | I/O memory hole        |
0A0000  +------------------------+
        | Reserved for BIOS      | 尽可能预留更多空闲空间 0x9A000之上
        ~                        ~
        | Command line           | (也可置于 X+10000 标记下)
X+10000 +------------------------+
        | Stack/heap             | 被实模式下的内核代码所使用
X+08000 +------------------------+
        | Kernel setup           | 实模式下的内核代码
        | Kernel boot sector     | 历史遗留下来的内核启动扇区
X       +------------------------+   这里的X应尽可能地低
        | Boot loader            | - 启动扇区的入口位置 0000:7c00
001000  +------------------------+
        | Reserved for MBR/BIOS  |
000800  +------------------------+
        | Typically used by MBR  |
000600  +------------------------+
        | BIOS use only          |
000000  +------------------------+

... X位置应和boot loader设计允许的上限一样低。

实模式下的内核首部:

加载内核首先加载实模式代码(boot sector 和setup),然后在0x01f1处检查随后的首部,实模式代码可达32K。尽管bloader只加载1K.首部如下:

01F1/1 ALL(1 setup_sects The size of the setup in sectors           #boot sector+setup部分所占的扇区数,即占多少个512  ,read
01F2/2 ALL root_flags If set, the root is mounted readonly          #不推荐使用,在命令行使用ro 和rw 代替  ,modify
01F4/4 2.04+(2 syssize  The size of the 32-bit code in 16-byte paras     #2.04版本以前的用低16位
01F8/2 ALL ram_size DO NOT USE - for bootsect.S use only                 #obsolete 
01FA/2 ALL vid_mode Video mode control                                           #视频模式控制,参见特殊命令行选项 ,modify
01FC/2 ALL root_dev Default root device number   #obsolete 默认的根设备号  ,被命令行中的 root= 代替
01FE/2 ALL boot_flag 0xAA55 magic number          #魔数AA55 ,read
0200/2 2.00+ jump  Jump instruction                     #这个地方有一个Jump指令和一个地址
0202/4 2.00+ header  Magic signature "HdrS"        #魔数 HdrS =0x53726448 ,read
0206/2 2.00+ version  Boot protocol version supported             #启动协议号 read
0208/4 2.00+ realmode_swtch Boot loader hook (see below)     #参见高级 bootloader HOOKS modify
020C/2 2.00+ start_sys_seg The load-low segment (0x1000) (obsolete) #0x1000
020E/2 2.00+ kernel_version Pointer to kernel version string   #内核版本 ,read
0210/1 2.00+ type_of_loader Boot loader identifier                #加载器类型GRUB or LILO等
0211/1 2.00+ loadflags Boot protocol option flags                   #启动协议的选项标志,其实是位掩码,modify
                  Bit 0 LOADED_HIGH         :0:保护模式代码加载到10000    1:保护模式代码加载到100000
                  Bit 5 QUIET_FLAG             :0:打印早期信息                      1:不打印早期信息
                  Bit 6 KEEP_SEGMENTS     :0:在进入保护模式的时候重新加载段寄存器 1:不重新加载
                  Bit 7 CAN_USE_HEAP       :0:                                          1:heap_end_ptr是有效的
0212/2 2.00+ setup_move_size Move to high memory size (used with hooks)
0214/4 2.00+ code32_start Boot loader hook (see below)   #保护模式的入口地址.由两个原因会被修改 1:做一个bootloader HOOKS   2:... modify
0218/4 2.00+ ramdisk_image initrd load address (set by boot loader) #initrd加载位置,由 bootloader给定
021C/4 2.00+ ramdisk_size initrd size (set by boot loader)    #initrd大小,由bootloader给定
0220/4 2.00+ bootsect_kludge DO NOT USE - for bootsect.S use only
0224/2 2.01+ heap_end_ptr Free memory after setup end    #安装setup后的空闲位置 ,stack/heap地址减去实模式代码开始位置【就是堆的底部相对实模式代码开始地方的便宜,因为堆是向下延伸的】,write
0226/1 2.02+(3 ext_loader_ver Extended boot loader version  #扩展bootloader版本
0227/1 2.02+(3 ext_loader_type Extended boot loader ID    #扩展bootloader 的ID
0228/4 2.02+ cmd_line_ptr 32-bit pointer to the kernel command line  #指向内核命令行的32位的指针,如果不设置会被认为不支持2.02+协议,write
022C/4 2.03+ ramdisk_max Highest legal initrd address   #限定的合法的最高initrd地址,2.10协议以后可以修改,
0230/4 2.05+ kernel_alignment Physical addr alignment required for kernel  #内核需要的物理地址对齐值
0234/1 2.05+ relocatable_kernel Whether kernel is relocatable or not  #显示内核是否可以重定位
0235/1 2.10+ min_alignment Minimum alignment, as a power of two    
0236/2 N/A pad3  Unused
0238/4 2.06+ cmdline_size Maximum size of the kernel command line  #命令行的最大尺寸
023C/4 2.07+ hardware_subarch Hardware subarchitecture 
0240/8 2.07+ hardware_subarch_data Subarchitecture-specific data
0248/4 2.08+ payload_offset Offset of kernel payload
024C/4 2.08+ payload_length Length of kernel payload
0250/8 2.09+ setup_data 64-bit physical pointer to linked list   of struct setup_data
0258/8 2.10+ pref_address Preferred loading address  #代表内核的加载地址,一个可重定位bootloader应该试图在这个地址去加载,一个不可重定位内核会无条件的把自己移动到这个地方并从这个地方开始运行.
0260/4 2.10+ init_size Linear memory required during initialization #初始化阶段要求的线性地址大小.
如果在偏移0x202的位置无法找到魔术数字“HdrS”(0x53726448),那么这个启动协议版本可被归结至“旧版内核”。装载一个旧版本内核,就需要假设以下参数:Image type = zImage  , 不支持initrd  ,实模式部分的内核将被装载到0x90000 。否则version域就会包含启动协议版本。

首部域的作用有三:

1:bootloader从内核中获取信息‘read’ ;内核编译过程中写入的,用于向BIOS传递信息。

2:被bootloader设值 ‘write’ ;BIOS写入内存的,用于向内核传递参数。

3:被bootloader读取并且被修改 ‘modify’

All general purpose boot loaders should write the fields marked (obligatory [əˈbligətəri]强制性的).  Boot loaders who want to load the kernel at a
nonstandard address should fill in the fields marked (reloc); other boot loaders can ignore those fields.

内核的运行开始地址计算方法如下:

if (relocatable_kernel)
    runtime_start = align_up(load_address, kernel_alignment)
else
    runtime_start = pref_address

内核命令行:

   内核命令行是bootloader和内核通信的重要方式.有些也只是bootloader自己用的.最大长度由cmdline_size限定.地址由cmd_line_ptr限定,可以在Stack/heap和A0000之间的任何地址

 

实模式下的内存布局:

    实模式下要求建一个stack/heap 和 为cmd 分配内存,这些需要在最低1M空间分配,并且由于现在的EDBA比较可观 和 一些老的内核要求将使用90000以上的内存‘即使使用也要避免使用到9a000’,所以尽可能地为heap和cmdline分配较少内存。

     协议高于2.02的时候cmd_line可以不使用实模式的64k(X~~~X+10000) 内存,这就可以给stack/heap配足64k,然后在起上面的内存给cmd_line分配内存.


启动配置的例子:

当加载位置低于90000的时候,偏移地址如下:

 0x0000-0x7fff Real mode kernel
 0x8000-0xdfff Stack and heap
 0xe000-0xffff Kernel command line    // 最终不超过 A00000   

当加载在90000或者协议版本低于 2.01的时候,偏移地址如下:

 0x0000-0x7fff Real mode kernel
 0x8000-0x97ff Stack and heap
 0x9800-0x9fff Kernel command line   //不超过9a000

如此的bootloader可象下面这样填写首部域:

unsigned long base_ptr; /* base address for real-mode segment */

 if ( setup_sects == 0 ) {
  setup_sects = 4;               #根据协议规定
 }

 if ( protocol >= 0x0200 ) {
  type_of_loader = <type code>;
  if ( loading_initrd ) {    #loading_initrd  从哪来的
   ramdisk_image = <initrd_address>;
   ramdisk_size = <initrd_size>;
  }

  if ( protocol >= 0x0202 && loadflags & 0x01 )
   heap_end = 0xe000;
  else
   heap_end = 0x9800;

  if ( protocol >= 0x0201 ) {
   heap_end_ptr = heap_end - 0x200;      #为什么减去200 ? 堆的大小是200,剩下的都是栈?
   loadflags |= 0x80; /* CAN_USE_HEAP */   #设置可用HEAP
  }

  if ( protocol >= 0x0202 ) {
   cmd_line_ptr = base_ptr + heap_end; 
   strcpy(cmd_line_ptr, cmdline);
  } else {
   cmd_line_magic = 0xA33F;
   cmd_line_offset = heap_end;
   setup_move_size = heap_end + strlen(cmdline)+1;
   strcpy(base_ptr+cmd_line_offset, cmdline);
  }
 } else {
  /* Very old kernel */

  heap_end = 0x9800;

  cmd_line_magic = 0xA33F;
  cmd_line_offset = heap_end;

  /* A very old kernel MUST have its real-mode code
     loaded at 0x90000 */

  if ( base_ptr != 0x90000 ) {
   /* Copy the real-mode kernel */
   memcpy(0x90000, base_ptr, (setup_sects+1)*512);
   base_ptr = 0x90000;   /* Relocated */
  }

  strcpy(0x90000+cmd_line_offset, cmdline);

  /* It is recommended to clear memory up to the 32K mark */
  memset(0x90000 + (setup_sects+1)*512, 0,
         (64-(setup_sects+1))*512);
 }

 

 

加载内核的剩下部分:

  32位的内核开始于内核文件的偏移为(setup_sects+1)*512‘当setup_sects=0时,其实为4.’ 处.

  is_bzImage = (protocol >= 0x0200) && (loadflags & 0x01);
  load_address = is_bzImage ? 0x100000 : 0x10000;

 

特殊命令行选项:参见Documentation/kernel-parameters.txt 

vga=<mode></mode>
这里的<mode>不是一个整数(在C语言表示法中,应是十进制,八进制或者十六进制其中之一),就是“normal” (0xFFFF),“ext”(0xFFFE),“ask”(0xFFFD)中的一个。这个值应被填入vid_mode域,因为他会在命令行被解析前被内核使用。

mem=<size></size>
<size> 是用C语言表示法定义的整形,后面可以追加(大小写不敏感的)K,M,G,T,P或者E(代表<< 10, << 20, << 30, << 40, << 50或者 << 60)。这就指明了内核在内存中的末尾。它影响了initrd可能存放的位置,因为initrd应该被放置在内核末尾的附近。要注意:这个选项同时作用于内核和启动装载器!

initrd=<file></file>
指定装载的initrd,<file>显然是和启动装载器是独立的文件,而且一些启动装载器(比如LILO)甚至不需要这个选项。

另外,有些启动装载器在额定的命令行上添加了以下参数:
BOOT_IMAGE=<file></file>
要加载的启动镜像。同样,<file>也是和bootloader独立的。
auto
内核不需要用户外界干预自行启动。
如果这些参数被启动装载器添加,强烈建议将它们放置在用户指定的或者设置项指定的命令行之前。否则,“init=/bin/sh”跟上auto会让人产生歧义。

运行内核: 

 内核的入口点在实模式内核段地址的偏移处为20地方.在入口点,ds=es=ss且都指向内核代码的段起始位置(如果加载在90000则为9000).sp指向heap的top位置,同时关中断, 建议将fs=gs=ds=es=ss,如下所示:

/* Note: in the case of the "old" kernel protocol, base_ptr must
    be == 0x90000 at this point; see the previous sample code */

 seg = base_ptr >> 4;  #取得段地址

 cli(); /* Enter with interrupts disabled! */ #关中断

 /* Set up the real-mode kernel stack */
 _SS = seg;             #设置实模式下的段寄存器
 _SP = heap_end;

 _DS = _ES = _FS = _GS = seg;
 jmp_far(seg+0x20, 0); /* Run the kernel */    #跳到内核入口处

 

高级Bootloader HOOK

 有的时候bootloader运行在敌对环境,无法获得需要的标准内存.这个时候就需要 HOOK了.这个就是最后的手段.

 注意:所有的HOOKS在调用的时候需要保存 esp ebp  esi  edi 寄存器的值.

realmode_swtch

  在进入保护模式前的16位实模式下的远子程序调用.默认的程序会禁用NMI‘NMI (Non Maskable Interrupt)——不可屏蔽中断(即CPU不能屏蔽) ’。

code32_start

  进入保护模式后立即跳转到32位平坦模式‘也被称为 unreal mode,Big real mode ,和实模式一样PE=0,’ ,但是在内核解压以前,除了cs没有其他段保证被设置好. 故应该自己将他们设置为BOOT_DS(0x18).

当设置完成HOOK后,应该在bootloader覆盖之前跳转到这个地址.

 

32位启动协议

 对一些新的BIOS,例如EFI,linuxBIOS ,16位实模式的setup代码将不能用,所以定义了32位的启动协议.

 在32位启动协议中,加载内核的第一步简历boot parameters (struct boot_params,就是所谓的 ’ZERO PAGE‘),这个内存应该被分配并全部设置为0,然后地址01f1处的 setup 首部将会被填入 boot_params .

 除了像16位实模式下设置boot_params一样,32位下还需要设置 zero_page.txt 中定义的域.

设置完首部后,剩下的就和16位的一样加载内核. 然后跳到内核的 入口点.

在入口处,CPU必须处于页禁止 的保护模式下,描述段选择子_BOOT_CS(0x10) and __BOOT_DS(0x18的GDT 必须被加载,__BOOT_CS必须有执行和读权限,__BOOT_DS必须有读写权限.cs存放的是__BOOT_CS,ds,es,ss必须存放的是__BOOT_DS,关中断,esi存放boot_params的地址,ebp,edi,ebx 必须为0.


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
内容: 一. Bootloader 二.Kernel引导入口 三.核心数据结构初始化--内核引导第一部分 四.外设初始化--内核引导第二部分 五.init进程和inittab引导指令 六.rc启动脚本 七.getty和login 八.bash 附:XDM方式登录 本文以Redhat 6.0 Linux 2.2.19 for Alpha/AXP为平台,描述了从开机到登录的 Linux 启动全过程。该文对i386平台同样适用。 一. Bootloader 在Alpha/AXP 平台上引导Linux通常有两种方法,一种是由MILO及其他类似的引导程序引导,另一种是由Firmware直接引导。MILO功能与i386平台的LILO相近,但内置有基本的磁盘驱动程序(如IDE、SCSI等),以及常见的文件系统驱动程序(如ext2,iso9660等), firmware有ARC、SRM两种形式,ARC具有类BIOS界面,甚至还有多重引导的设置;而SRM则具有功能强大的命令行界面,用户可以在控制台上使用boot等命令引导系统。ARC有分区(Partition)的概念,因此可以访问到分区的首扇区;而SRM只能将控制转给磁盘的首扇区。两种firmware都可以通过引导MILO来引导Linux,也可以直接引导Linux的引导代码。 “arch/alpha/boot” 下就是制作Linux Bootloader的文件。“head.S”文件提供了对 OSF PAL/1的调用入口,它将被编译后置于引导扇区(ARC的分区首扇区或SRM的磁盘0扇区),得到控制后初始化一些数据结构,再将控制转给“main.c”中的start_kernel(), start_kernel()向控制台输出一些提示,调用pal_init()初始化PAL代码,调用openboot() 打开引导设备(通过读取Firmware环境),调用load()将核心代码加载到START_ADDR(见 “include/asm-alpha/system.h”),再将Firmware中的核心引导参数加载到ZERO_PAGE(0) 中,最后调用runkernel()将控制转给0x100000的kernel,bootloader部分结束。 “arch/alpha/boot/bootp.c”以“main.c”为基础,可代替“main.c”与“head.S” 生成用于BOOTP协议网络引导的Bootloader。 Bootloader中使用的所有“srm_”函数在“arch/alpha/lib/”中定义。 以上这种Boot方式是一种最简单的方式,即不需其他工具就能引导Kernel,前提是按照 Makefile的指导,生成bootimage文件,内含以上提到的bootloader以及vmlinux,然后将 bootimage写入自磁盘引导扇区始的位置中。 当采用MILO这样的引导程序来引导Linux时,不需要上面所说的Bootloader,而只需要 vmlinux或vmlinux.gz,引导程序会主动解压加载内核到0x1000(小内核)或0x100000(大内核),并直接进入内核引导部分,即本文的第二节。 对于I386平台 i386系统中一般都有BIOS做最初的引导工作,那就是将四个主分区表中的第一个可引导 分区的第一个扇区加载到实模式地址0x7c00上,然后将控制转交给它。 在“arch/i386/boot” 目录下,bootsect.S是生成引导扇区的汇编源码,它首先将自己拷贝到0x90000上,然后将紧接其后的setup部分(第二扇区)拷贝到0x90200,将真正的内核代码拷贝到0x100000。以上这些拷贝动作都是以bootsect.S、setup.S以及vmlinux在磁盘上连续存放为前提的,也就是说,我们的bzImage文件或者zImage文件是按照bootsect,setup, vmlinux这样的顺序组织,并存放于始于引导分区的首扇区的连续磁盘扇区之中。 bootsect.S完成加载动作后,就直接跳转到0x90200,这里正是setup.S的程序入口。 setup.S的主要功能就是将系统参数(包括内存、磁盘等,由BIOS返回)拷贝到 0x90000-0x901FF内存中,这个地方正是bootsect.S存放的地方,这时它将被系统参数覆盖。以后这些参数将由保护模式下的代码来读取。 除此之外,setup.S还将video.S中的代码包含进来,检测和设置显示器和显示模式。最 后,setup.S将系统转换到保护模式,并跳转到0x100000(对于bzImage格式的大内核是 0x100000,对于zImage格式的是0x1000)的内核引导代码,Bootloader过程结束。 对于2.4.x版内核 没有什么变化。 二.Kernel引导入口 在arch/alpha/vmlinux.lds 的链接脚本控制下,链接程序将vmlinux的入口置于 "arch/alpha/kernel/head.S"中的__start上,因此当Bootloader跳转到0x100000时, __start处的代码开始执行。__start的代码很简单,只需要设置一下全局变量,然后就跳转到start_kernel去了。start_kernel()是"init/main.c"中的asmlinkage函数,至此,启动过程转入体系结构无关的通用C代码中。 对于I386平台 在i386体系结构中,因为i386本身的问题,在 "arch/alpha/kernel/head.S"中需要更多的设置,但最终也是通过call SYMBOL_NAME(start_kernel)转到start_kernel()这个体系结构无关的函数中去执行了。 所不同的是,在i386系统中,当内核以bzImage的形式压缩,即大内核方式(__BIG_KERNEL__)压缩时就需要预先处理bootsect.S和setup.S,按照大核模式使用$(CPP) 处理生成bbootsect.S和bsetup.S,然后再编译生成相应的.o文件,并使用 "arch/i386/boot/compressed/build.c"生成的build工具,将实际的内核(未压缩的,含 kernel中的head.S代码)与"arch/i386/boot/compressed"下的head.S和misc.c合成到一起,其中的 head.S代替了"arch/i386/kernel/head.S"的位置,由Bootloader引导执行(startup_32入口),然后它调用misc.c中定义的decompress_kernel()函数,使用 "lib/inflate.c"中定义的gunzip()将内核解压到0x100000,再转到其上执行 "arch/i386/kernel/head.S"中的startup_32代码。 对于2.4.x版内核 没有变化。 三.核心数据结构初始化--内核引导第一部分 start_kernel()中调用了一系列初始化函数,以完成kernel本身的设置。 这些动作有的是公共的,有的则是需要配置的才会执行的。 在start_kernel()函数中, 输出Linux版本信息(printk(linux_banner)) 设置与体系结构相关的环境(setup_arch()) 页表结构初始化(paging_init()) 使用"arch/alpha/kernel/entry.S"中的入口点设置系统自陷入口(trap_init()) 使用alpha_mv结构和entry.S入口初始化系统IRQ(init_IRQ()) 核心进程调度器初始化(包括初始化几个缺省的Bottom-half,sched_init()) 时间、定时器初始化(包括读取CMOS时钟、估测主频、初始化定时器中断等,time_init()) 提取并分析核心启动参数(从环境变量中读取参数,设置相应标志位等待处理,(parse_options()) 控制台初始化(为输出信息而先于PCI初始化,console_init()) 剖析器数据结构初始化(prof_buffer和prof_len变量) 核心Cache初始化(描述Cache信息的Cache,kmem_cache_init()) 延迟校准(获得时钟jiffies与CPU主频ticks的延迟,calibrate_delay()) 内存初始化(设置内存上下界和页表项初始值,mem_init()) 创建和设置内部及通用cache("slab_cache",kmem_cache_sizes_init()) 创建uid taskcount SLAB cache("uid_cache",uidcache_init()) 创建文件cache("files_cache",filescache_init()) 创建目录cache("dentry_cache",dcache_init()) 创建与虚存相关的cache("vm_area_struct","mm_struct",vma_init()) 块设备读写缓冲区初始化(同时创建"buffer_head"cache用户加速访问,buffer_init()) 创建页cache(内存页hash表初始化,page_cache_init()) 创建信号队列cache("signal_queue",signals_init()) 初始化内存inode表(inode_init()) 创建内存文件描述符表("filp_cache",file_table_init()) 检查体系结构漏洞(对于alpha,此函数为空,check_bugs()) SMP机器其余CPU(除当前引导CPU)初始化(对于没有配置SMP的内核,此函数为空,smp_init()) 启动init过程(创建第一个核心线程,调用init()函数,原执行序列调用cpu_idle() 等待调度,init()) 至此start_kernel()结束,基本的核心环境已经建立起来了。 对于I386平台 i386平台上的内核启动过程与此基本相同,所不同的主要是实现方式。 对于2.4.x版内核 2.4.x中变化比较大,但基本过程没变,变动的是各个数据结构的具体实现,比如Cache。 四.外设初始化--内核引导第二部分 init()函数作为核心线程,首先锁定内核(仅对SMP机器有效),然后调用 do_basic_setup()完成外设及其驱动程序的加载和初始化。过程如下: 总线初始化(比如pci_init()) 网络初始化(初始化网络数据结构,包括sk_init()、skb_init()和proto_init()三部分,在proto_init()中,将调用protocols结构中包含的所有协议的初始化过程,sock_init()) 创建bdflush核心线程(bdflush()过程常驻核心空间,由核心唤醒来清理被写过的内存缓冲区,当bdflush()由kernel_thread()启动后,它将自己命名为kflushd) 创建kupdate核心线程(kupdate()过程常驻核心空间,由核心按时调度执行,将内存缓冲区中的信息更新到磁盘中,更新的内容包括超级块和inode表) 设置并启动核心调页线程kswapd(为了防止kswapd启动时将版本信息输出到其他信息中间,核心线调用kswapd_setup()设置kswapd运行所要求的环境,然后再创建 kswapd核心线程) 创建事件管理核心线程(start_context_thread()函数启动context_thread()过程,并重命名为keventd) 设备初始化(包括并口parport_init()、字符设备chr_dev_init()、块设备 blk_dev_init()、SCSI设备scsi_dev_init()、网络设备net_dev_init()、磁盘初始化及分区检查等等, device_setup()) 执行文件格式设置(binfmt_setup()) 启动任何使用__initcall标识的函数(方便核心开发者添加启动函数,do_initcalls()) 文件系统初始化(filesystem_setup()) 安装root文件系统(mount_root()) 至此do_basic_setup()函数返回init(),在释放启动内存段(free_initmem())并给内核解锁以后,init()打开 /dev/console设备,重定向stdin、stdout和stderr到控制台,最后,搜索文件系统中的init程序(或者由init=命令行参数指定的程序),并使用 execve()系统调用加载执行init程序。 init()函数到此结束,内核的引导部分也到此结束了,这个由start_kernel()创建的第一个线程已经成为一个用户模式下的进程了。此时系统中存在着六个运行实体: start_kernel()本身所在的执行体,这其实是一个"手工"创建的线程,它在创建了init()线程以后就进入cpu_idle()循环了,它不会在进程(线程)列表中出现 init线程,由start_kernel()创建,当前处于用户态,加载了init程序 kflushd核心线程,由init线程创建,在核心态运行bdflush()函数 kupdate核心线程,由init线程创建,在核心态运行kupdate()函数 kswapd核心线程,由init线程创建,在核心态运行kswapd()函数 keventd核心线程,由init线程创建,在核心态运行context_thread()函数 对于I386平台 基本相同。 对于2.4.x版内核 这一部分的启动过程在2.4.x内核中简化了不少,缺省的独立初始化过程只剩下网络 (sock_init())和创建事件管理核心线程,而其他所需要的初始化都使用__initcall()宏 包含在do_initcalls()函数中启动执行。 五.init进程和inittab引导指令 init进程是系统所有进程的起点,内核在完成核内引导以后,即在本线程(进程)空 间内加载init程序,它的进程号是1。 init程序需要读取/etc/inittab文件作为其行为指针,inittab是以行为单位的描述性(非执行性)文本,每一个指令行都具有以下格式: id:runlevel:action:process其中id为入口标识符,runlevel为运行级别,action为动作代号,process为具体的执行程序。 id一般要求4个字符以内,对于getty或其他login程序项,要求id与tty的编号相同,否则getty程序将不能正常工作。 runlevel 是init所处于的运行级别的标识,一般使用0-6以及S或s。0、1、6运行级别被系统保留,0作为shutdown动作,1作为重启至单用户模式,6 为重启;S和s意义相同,表示单用户模式,且无需inittab文件,因此也不在inittab中出现,实际上,进入单用户模式时,init直接在控制台(/dev/console)上运行/sbin/sulogin。 在一般的系统实现中,都使用了2、3、4、5几个级别,在 Redhat系统中,2表示无NFS支持的多用户模式,3表示完全多用户模式(也是最常用的级别),4保留给用户自定义,5表示XDM图形登录方式。7- 9级别也是可以使用的,传统的Unix系统没有定义这几个级别。runlevel可以是并列的多个值,以匹配多个运行级别,对大多数action来说,仅当runlevel与当前运行级别匹配成功才会执行。 initdefault是一个特殊的action值,用于标识缺省的启动级别;当init由核心激活 以后,它将读取inittab中的initdefault项,取得其中的runlevel,并作为当前的运行级别。如果没有inittab文件,或者其中没有initdefault项,init将在控制台上请求输入 runlevel。 sysinit、 bootbootwait等action将在系统启动时无条件运行,而忽略其中的runlevel,其余的action(不含initdefault)都与某个runlevel相关。各个action的定义在inittab的man手册中有详细的描述。 在Redhat系统中,一般情况下inittab都会有如下几项: id:3:initdefault: #表示当前缺省运行级别为3--完全多任务模式; si::sysinit:/etc/rc.d/rc.sysinit #启动时自动执行/etc/rc.d/rc.sysinit脚本 l3:3:wait:/etc/rc.d/rc 3 #当运行级别为3时,以3为参数运行/etc/rc.d/rc脚本,init将等待其返回 0:12345:respawn:/sbin/mingetty tty0 #在1-5各个级别上以tty0为参数执行/sbin/mingetty程序,打开tty0终端用于 #用户登录,如果进程退出则再次运行mingetty程序 x:5:respawn:/usr/bin/X11/xdm -nodaemon #在5级别上运行xdm程序,提供xdm图形方式登录界面,并在退出时重新执行. 六.rc启动脚本 上一节已经提到init进程将启动运行rc脚本,这一节将介绍rc脚本具体的工作。 一般情况下,rc启动脚本都位于/etc/rc.d目录下,rc.sysinit中最常见的动作就是激活交换分区,检查磁盘,加载硬件模块,这些动作无论哪个运行级别都是需要优先执行的。仅当rc.sysinit执行完以后init才会执行其他的bootbootwait动作。 如果没有其他bootbootwait动作,在运行级别3下,/etc/rc.d/rc将会得到执行,命令行参数为3,即执行 /etc/rc.d/rc3.d/目录下的所有文件。rc3.d下的文件都是指向/etc/rc.d/init.d/目录下各个Shell脚本的符号连接,而这些脚本一般能接受start、stop、restart、status等参数。rc脚本以start参数启动所有以S开头的脚本,在此之前,如果相应的脚本也存在K打头的链接,而且已经处于运行态了(以/var/lock/subsys/下的文件作为标志),则将首先启动K开头的脚本,以stop 作为参数停止这些已经启动了的服务,然后再重新运行。显然,这样做的直接目的就是当init改变运行级别时,所有相关的服务都将重启,即使是同一个级别。 rc程序执行完毕后,系统环境已经设置好了,下面就该用户登录系统了。 七.getty和login 在rc返回后,init将得到控制,并启动mingetty(见第五节)。mingetty是getty的简化,不能处理串口操作。getty的功能一般包括: 打开终端线,并设置模式 输出登录界面及提示,接受用户名的输入 以该用户名作为login的参数,加载login程序 缺省的登录提示记录在/etc/issue文件中,但每次启动,一般都会由rc.local脚本根据系统环境重新生成。 注:用于远程登录的提示信息位于/etc/issue.net中。 login程序在getty的同一个进程空间中运行,接受getty传来的用户名参数作为登录的用户名。 如果用户名不是root,且存在/etc/nologin文件,login将输出nologin文件的内容,然后退出。这通常用来系统维护时防止非root用户登录。 只有/etc/securetty中登记了的终端才允许root用户登录,如果不存在这个文件,则root可以在任何终端上登录。/etc/usertty文件用于对用户作出附加访问限制,如果不存在这个文件,则没有其他限制。 当用户登录通过了这些检查后,login将搜索/etc/passwd文件(必要时搜索 /etc/shadow文件)用于匹配密码、设置主目录和加载shell。如果没有指定主目录,将默认为根目录;如果没有指定shell,将默认为 /bin/sh。在将控制转交给shell以前, getty将输出/var/log/lastlog中记录的上次登录系统的信息,然后检查用户是否有新邮件(/usr/spool/mail/ {username})。在设置好shell的uid、gid,以及TERM,PATH 等环境变量以后,进程加载shell,login的任务也就完成了。 八.bash 运行级别3下的用户login以后,将启动一个用户指定的shell,以下以/bin/bash为例继续我们的启动过程。 bash 是Bourne Shell的GNU扩展,除了继承了sh的所有特点以外,还增加了很多特性和功能。由login启动的bash是作为一个登录shell启动的,它继承了 getty设置的TERM、PATH等环境变量,其中PATH对于普通用户为"/bin:/usr/bin:/usr/local/bin",对于 root 为"/sbin:/bin:/usr/sbin:/usr/bin"。作为登录shell,它将首先寻找/etc/profile 脚本文件,并执行它;然后如果存在~/.bash_profile,则执行它,否则执行 ~/.bash_login,如果该文件也不存在,则执行~/.profile文件。然后bash将作为一个交互式shell执行~/.bashrc文件(如果存在的话),很多系统中,~/.bashrc都将启动 /etc/bashrc作为系统范围内的配置文件。 当显示出命令行提示符的时候,整个启动过程就结束了。此时的系统,运行着内核,运行着几个核心线程,运行着init进程,运行着一批由rc启动脚本激活的守护进程(如 inetd等),运行着一个bash作为用户的命令解释器。 附:XDM方式登录 如果缺省运行级别设为5,则系统中不光有1-6个getty监听着文本终端,还有启动了一个XDM的图形登录窗口。登录过程和文本方式差不多,也需要提供用户名和口令,XDM 的配置文件缺省为/usr/X11R6/lib/X11/xdm/xdm-config文件,其中指定了 /usr/X11R6/lib/X11/xdm/xsession作为XDM的会话描述脚本。登录成功后,XDM将执行这个脚本以运行一个会话管理器,比如gnome-session等。 除了XDM以外,不同的窗口管理系统(如KDE和GNOME)都提供了一个XDM的替代品,如gdm和kdm,这些程序的功能和XDM都差不多。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值