初识Linux初始化Init系统
2016-03-28 13:55:12 阿炯
init(为英语:initialization的简写)是 Unix 和类Unix 系统中用来产生其它所有进程的程序。它以守护进程的方式存在,其进程号为1。
Unix 系列中(如 System III 和 System V)init的作用,和研究中的 Unix 和 BSD 派生版本相比,发生了一些变化。大多数Linux发行版是和 System V 相兼容的,但是一些发行版如 Slackware 采用的是BSD风格,其它的如 Gentoo 是自己定制的。Ubuntu和其它一些发行版现在开始采用Upstart来代替传统的 init 进程(现也转向Systemd)。
BSD风格
BSD init 运行存放于'/etc/rc'的初始化 shell 脚本,然后启动基于文本模式的终端(getty)或者基于图形界面的终端(窗口系统,如 X)。 这里没有运行模式的问题,因为文件 'rc' 决定了 init 如何执行。
优点:简单且易于手动编辑。
缺点:如果第三方软件需要在启动过程执行它自身的初始化脚本,它必须修改已经存在的启动脚本,一旦这种过程中有一个小错误,都将导致系统无法正常启动。
值得注意的是,现代的 BSD 派生系统一直支持使用 'rc.local' 文件的方式,它将在正常启动过程接近最后的时间以子脚本的方式来执行。这样做减少了整个系统无法启动的风险。然后,第三方软件包可以将它们独立的 start/stop 脚本安装到一个本地的 'rc.d' 目录中(通常这是由 ports collection/pkgsrc 完成的)。FreeBSD 和 NetBSD 现在默认使用 rc.d ,该目录中所有的用户启动脚本,都被分成更小的子脚本,这点和 SysV 类似。rcorder 通常根据在 rc.d目录中脚本之间的依赖关系来决定脚本的执行顺序。
SysV风格
System V init 检查 '/etc/inittab' 文件中是否含有 'initdefault' 项。 这告诉 init 系统是否有一个默认运行模式。如果没有默认的运行模式,那么用户将进入系统控制台,手动决定进入何种运行模式。
优点:灵活性强
缺陷:比较复杂
运行模式
System V中运行模式描述了系统各种可能的状态。通常会有 8 种运行模式,即运行模式 0 到 6 和 S 或者 s。其中运行模式 3 为"保留的"运行模式:
0. 关机
1. 单用户模式
6. 重启
除了模式 0, 1,6外, 每种 Unix 和 Unix-like 系统对运行模式的定义不太一样。通常在 /etc/inittab 文件中定义了各种运行模式的工作范围。
大多数操作系统的用户可以用下面的命令来得到当前的运行模式:
$ runlevel
$ who -r
在 root 权限下,运行 telinit 或者 init 命令可以改变当前的运行模式。/etc/inittab 文件中设置的默认的运行模式在 :initdefault: 项中。另见新增一些功能 pidof 或者 killall5, 从 System V 开始在很多发行版中使用的另一种程序,但兼容于System V。
如何跳过 init
Linux系统中,现代的bootloader(如 LILO 或者 GRUB),用户可以在初始化过程中最后启动的进程来取代默认的 /sbin/init。通常是在 bootloader 环境中通过执行 init=/foo/bar 命令。例如,如果执行 init=/bin/bash,启动单用户 root 的 shell 环境,无需用户密码。
BSD的变种,大多数平台, bootstrap 程序是可以被打断的,然后执行 boot -s 命令进入单用户模式。
单用户模式并不没有跳过 init,它仍然可以执行 /sbin/init,但是它将使 init 询问 exec() 将要执行的命令 (默认为 /bin/sh) 的路径,而不是采用正常的多用户启动顺序。如果内核启动时在 /etc/ttys 文件中被标注为 "不安全" (在某些系统中,当前的"安全模式" 可能会有些变化), 在允许这种情况(或者回退到单用户模式,如果用户执行 CTRL+D),init 将首先询问 root 用户的密码。 如果该程序退出,内核将在多用户模式下重新执行 init。如果系统从多用户模式切换到单用户模式,还将碰到上述的情况。
如果内核加载后, init 不能被正常启动, 这将导致 panic 错误,此时系统将不可使用。想要通过 init 自身来改变 init 的路径,不同的版本情况不太一样(NetBSD中可执行 boot -a ; FreeBSD中利用 init_path 命令装载变量)。
其他风格
很多人一直努力地从某些方面改进传统的 init 守护进程,使它变得更完善。下面列出的是一些改进(排序不分先后):
SystemStarter, 用来替代 launchd — Apple Mac OS X开启进程
Initng, 完全代替 init,可以异步开启进程
Upstart, 完全代替 init,可以异步开启进程 曾由Ubuntu使用
Service Management Facility, 完全代替/重新设计 Solaris 启动 Solaris 10
runit, 跨平台的完全代替 init 可以并行启动服务
BootScripts, GoboLinux
Mudur, 用 Python 写成的 init 替代品,可以异步开启进程,Pardus Linux 发行版
systemd, 完全替代init,可并行启动服务,并能减少在shell上的系统开销,被认为是最邪恶的项目,已被大多数Linux发行版所使用
下面列出的项目还没有大范围的使用:
eINIT, 完全代替 init ,可以异步开启进程,但是完成这个过程可以不使用 shell 脚本
svscan 来自 daemontools 被用作 1 号进程 - 似乎将被 runit 替代
twsinit, 部分用 x86 汇编写成, 只是用来证明一种概念
OpenRC,Gentoo默认使用
关于 Linux 系统的运行级别(runlevel)
Linux 系统有 7 个运行级别,Linux 系统任何时候都运行在一个指定的运行级别上,不同的运行级别所运行的程序和服务不尽相同,所要完成的工作和要达到的目的也不相同:
运行级别 0 系统停机(halt)状态,系统的默认运行级别不能设为 0,否则不能正常启动
运行级别 1 单用户工作(single user)状态,root 权限,用于系统维护,禁止远程登陆
运行级别 2 多用户(multiuser)状态 (没有 NFS)
运行级别 3 完全的多用户(multiuser)状态 (有 NFS),登陆后进入控制台命令行模式
运行级别 4 系统未使用,保留
运行级别 5 X11 控制台 (xdm、gdm、kdm),登陆后进入图形 GUI 模式
运行级别 6 系统正常关闭并重启(reboot),默认运行级别不能设为 6,否则不能正常启动
在一些新的发行版本中这个规则已经变的无效了或不再被遵守了,在CentOS 7中这种定义已经不再有效了,从/etc/inittab中的内容已经变更为如下:
# inittab is no longer used when using systemd.
# ADDING CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM.
# Ctrl-Alt-Delete is handled by /usr/lib/systemd/system/ctrl-alt-del.target
# systemd uses 'targets' instead of runlevels. By default, there are two main targets:
# multi-user.target: analogous to runlevel 3
# graphical.target: analogous to runlevel 5
#
# To view current default target, run:
# systemctl get-default
#
# To set a default target, run:
# systemctl set-default TARGET.target
解读Linux启动过程
1. 概述
本文解读一下从CPU加电自检到启动init进程的过程,先通过下面这张图大致看一下Linux启动的整个过程,分析环境是GRUB 0.97 + Linux 2.6.18。
2. BIOS
CPU加电后首先工作在实模式并初始化CS:IP=FFFF:FFF0,BIOS的入口代码必须从该地址开始。BIOS完成相应的硬件检查并提供一系列中断服务例程,这些中断服务提供给系统软件访问硬件资源(比如磁盘、显示器等),最后选择一个启动盘加载第一个扇区(即:MBR,共512字节)数据到内存0x7C00处,并从这里开始执行指令(CS:IP=0000:7C00),对于笔者的电脑来说这就是GRUB的Stage1部分。
3. GRUB
GRUB的作用是bootloader,用来引导各种OS的。
3.1. Stage1
Stage1就是MBR,由BIOS把它从磁盘的0扇区加载到0x7c00处,大小固定位512字节,此时的CPU上下文如下:
eax=0011aa55 ebx=00000080 ecx=00000000 edx=00000080 esi=0000f4a0 edi=0000fff0
eip=00007c00 esp=00007800 ebp=00000000 iopl=0 nv up ei pl zr na po nc
cs=0000 ds=0000 es=0000 fs=0000 gs=0000 ss=0000 eflags=00000246
// 注: dl=启动磁盘号, 00H~7FH是软盘, 80H~FFH是硬盘。
因为只能是512字节,大小受限,它就干一件事,把Stage2的第一个512字节读取到0x8000,然后jmp到0x8000继续执行。
3.1.1. 读磁盘
磁盘扇区寻址有两种方式:
CHS方式:传统的方式,使用三元组(10位Cylinder, 8位Head, 6位Sector)来寻找扇区,最大只能找到(2^10) * (2^8) * (2^6) * 512 = 8GB的硬盘容量,现在的硬盘明显不够用了。
LBA方式:现在的方式,使用48位线性地址来寻找扇区,最大支持(2^48) * 512 = 128PB的硬盘空间。虽然机械上还是CHS的结构,不过磁盘的固件会自动完成LBA到CHS的转换。
因为CHS明显不适合现在的硬盘,所以LBA模式寻址是现在的PC的标配了吧!万一磁盘不支持LBA或者是软盘,需要我们手工转换成CHS模式。转换公式如下(就是三维空间定位一个点的问题):
磁道号C = LBA / 每磁道的扇区数SPT / 盘面总HPC
磁头号H = (LBA / 每磁道的扇区数SPT) mod HPC
扇区号S = (LBA mod SPT) + 1
判断是否支持LBA模式
/* check if LBA is supported */
movb $0x41, %ah
movw $0x55aa, %bx
int $0x13
如果返回成功(CF=1)并且BX值是0xAA55表示支持LBA寻址(用Extensions方法)。
注意:3.5英寸软盘需要使用CHS方式寻址,它的CHS参数是80个柱面、2个磁头、每个磁道18个扇区,每扇区512字节,共1.44MB容量。
LBA模式读的功能号是AH=42h,DL参数是磁盘号,DS:SI参数是Disk Address Packet(DAP)结构体的内存地址,定义如下:
struct DAP {
uint8_t sz; // 结构体大小
uint8_t unused;
uint16_t sector_cnt; // 需要都的扇区总数
struct dst_addr { // 内存地址,读到这里
uint16_t offset;
uint16_t segment;
};
uint64_t lba_addr; // 磁盘的LBA地址
};
参考:
https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
https://en.wikipedia.org/wiki/INT_13H
3.2. Stage2
Stage2就是GRUB剩下的全部的代码了,包括BIOS中断服务的封装给C代码使用、键盘驱动、文件系统驱动、串口、网络驱动等等,它提供了一个小型的命令行环境,可以解析用户输入命令并执行对OS的启动。
3.2.1. start.S
首先Stage2的头512字节(start.S)被加载到0x8000,并在这里开始执行,此时的CPU上下文如下:
eax=00000000 ebx=00007000 ecx=00646165 edx=00000080 esi=00007c05 edi=0000fff0
eip=00008000 esp=00001ffe ebp=00000000 iopl=0 nv up ei pl zr na po nc
cs=0000 ds=0000 es=0800 fs=0000 gs=0000 ss=0000 eflags=00000246
start.S的工作是把Stage2的后续部分全部加载到内存中(从0x8200开始),有103KB大小。
3.2.2. asm.S
asm.S是0x8200处的代码,先看一下CPU上下文环境:
eax=00000e00 ebx=00000001 ecx=00646165 edx=00000080 esi=00008116 edi=000081e8
eip=00008200 esp=00001ffe ebp=000062d8 iopl=0 nv up ei pl zr na po nc
cs=0000 ds=0000 es=1ae0 fs=0000 gs=0000 ss=0000 eflags=00000246
3.2.2.1. 最开始的代码应该设置好段寄存器和栈
cli
/* set up %ds, %ss, and %es */
/* cs=0000 ds=0000 es=0000 fs=0000 gs=0000 ss=0000 */
xorw %ax, %ax
movw %ax, %ds
movw %ax, %ss
movw %ax, %es
/* set up the real mode/BIOS stack */
movl $STACKOFF, %ebp
movl %ebp, %esp
sti
此时:
cs=0000 ds=0000 es=0000 ss=0000 esp=00001ff0 ebp=00001ff0。
3.2.2.2. 保护模式和实模式
因为GRUB没有实现自己的中断服务,所以访问硬件资源还是使用BIOS的中断服务例程(实模式)。GRUB的命令行环境是工作在保护模式下的,所以当GRUB需要访问BIOS中断的时候需要切换回实模式,于是在GRUB执行过程中会有频繁的实模式和保护模式的互相切换操作,当切换回实模式后别忘了保存保护模式下的栈指针。
1 实模式进入保护模式
/* transition to protected mode */
DATA32 call EXT_C(real_to_prot)
/* The ".code32" directive takes GAS out of 16-bit mode. */
.code32
下图是实模式到保护模式的切换步骤:
GRUB没有设置分页机制和新的中断,所以GRUB的保护模式访问的是物理内存且是不能使用INT指令,不过对于bootloader来说够用了;因为需要切换到保护模式栈,原来的返回地址要放到新的栈上,以保证能够正常ret:
ENTRY(real_to_prot)
...
/* put the return address in a known safe location */
movl (%esp), %eax
movl %eax, STACKOFF ; 把返回地址保存起来备用
/* get protected mode stack */
movl protstack, %eax
movl %eax, %esp
movl %eax, %ebp ; 设置保护模式的栈
/* get return address onto the right stack */
movl STACKOFF, %eax
movl %eax, (%esp) ; 把返回地址重新放到栈上
/* zero %eax */
xorl %eax, %eax
/* return on the old (or initialized) stack! */
ret ; 正常返回
(2) 保护模式切换回实模式
/* enter real mode */
call EXT_C(prot_to_real)
.code16
下图说明了保护模式切换回实模式的步骤:
保护模式的栈需要保存起来以便恢复现场,让C代码正确运行,实模式的栈每次都重置为STACKOFF即可,和(1)一样,也要设置好返回地址:
ENTRY(prot_to_real)
...
/* save the protected mode stack */
movl %esp, %eax
movl %eax, protstack ; 把栈保存起来
/* get the return address */
movl (%esp), %eax
movl %eax, STACKOFF ; 返回地址放到实模式栈里
/* set up new stack */
movl $STACKOFF, %eax ; 设置实模式的栈
movl %eax, %esp
movl %eax, %ebp
...
3.2.2.3. 创建C运行时环境
C的运行环境主要包括栈、bss数据区、代码区。随着切换到保护模式,栈已经设置好了;随着Stage2从磁盘加载到内存,代码区和bss区都已经在内存了,最后还需要把bss区给初始化一下(清0),接下来即可愉快的执行C代码了。
3.2.2.4. 执行cmain()
先执行一个init_bios_info()获取BIOS的信息,比如被BIOS使用的内存空间(影响我们Linux映像加载的位置)、磁盘信息、ROM信息、APM信息,最后调用cmain()。cmain()函数在stage2.c文件中,其中最主要的函数run_menu()是启动一个死循环来提供命令行解析执行环境。
3.2.2.5. load_image()
如果grub.cfg或者用户执行kenrel命令,会调用load_image()函数来将内核加载到内存中。至于如何加载linux镜像在Documentation的boot.txt和zero-page.txt有详细说明。
load_image()是一个非常长的函数,它要处理支持的各种内核镜像格式。Linux镜像vmlinuz文件头是struct linux_kernel_header结构体,该结构体里头说明了这个镜像使用的boot协议版本、实模式大小、加载标记位和需要GRUB填写的一些参数(比如:内核启动参数地址)。
实模式部分:始终被加载到0x90000位置,并从0x90200开始执行(linux 0.11就这样做了)。
保护模式部分:我们现在使用的内核比较大(大于512KB),叫做bzImage,加载到0x100000(高位地址,1MB)开始的位置,可以任意大小了。否则小内核zImage放在0x10000到mbi.mem_lower * 1024(一般是0x90000)区域。
3.2.2.6. linux_boot()
我们正常的启动过程调用的是big_linux_boot()函数,把实模式部分copy到0x90000后,设置其他段寄存器值位0x9000,设置CS:IP=9020:0000开始执行(使用far jmp)。
至此GRUB的工作完成,接下来执行权交给Linux了。
4. setup.S
该文件在arch/i386/boot/setup.S,主要作用是收集硬件信息并进入保护模式head.S。初始的CPU上下文如下:
eax=00000000 ebx=00009000 ecx=00000000 edx=00000003 esi=002d8b54 edi=0009a000
eip=00000000 esp=00009000 ebp=00001ff0 iopl=0 nv up di pl zr na po nc
cs=9020 ds=9000 es=9000 fs=9000 gs=9000 ss=9000 eflags=00000046
4.1. 自身检查
先检查自己setup.S是否合法,主要是检查末尾的两个magic是否一致
# Setup signature -- must be last
setup_sig1: .word SIG1
setup_sig2: .word SIG2
4.2. 收集硬件信息
主要是通过BIOS中断来收集硬件信息。收集的信息包括内存大小、键盘、鼠标、显卡、硬盘、APM等等。收集的硬件信息保存在0x9000处:
# 设置ds = 0x9000,用来保存硬件信息
movw %cs, %ax # aka SETUPSEG
subw $DELTA_INITSEG, %ax # aka INITSEG
movw %ax, %ds
这里看一下如何获取内存大小,这样OS才能进行内存管理。这里用三种方法获取内存信息:
e820h:请求中断INT 15H,AX=E820H时返回可用的物理内存信息,e820由此得名,参考此处。由于内存的使用是不连续的,通过连续调用INT 15H得到所有可用的内存区域,每次查询得到的结果ES:DI是个struct address_range_descriptor结构体,返回的结果都是64位的,完全能够满足目前PC的需求了。
struct address_range_descriptor {
uint32_t base_addr_low; // 起始物理地址
uint32_t base_addr_high;
uint32_t length_low; // 长度
uint32_t length_high;
uint8_t type; // 1=OS可用的, 2=保留的,OS不可用
};
e801h:通过请求中断INT15h,AX=e801H返回结果,最高只能得到4GB内存结果。
88h:古老的办法,通过请求中断INT15h,AH=88H返回结果。最高只能得到16MB或者64MB的内存,现在的电脑不适用了。
扩展阅读:Detecting_Memory_(x86)#E820h
4.3. 启用A20
让CPU访问1MB以上的扩展内存,否则访问的是X mod 1MB的地址。下面列举三种开启A20的方法:
使用I/0端口92H,AL的将1-bit置1
inb $0x92, %al # Configuration Port A
orb $0x02, %al # "fast A20" version
andb $0xFE, %al # don't accidentally reset
outb %al, $0x92
使用BIOS中断INT 0x15, AX=0x2401
movw $0x2401, %ax
pushfl # Be paranoid about flags
int $0x15
popfl
使用键盘控制器
movb $0xD1, %al # command write
outb %al, $0x64
call empty_8042
movb $0xDF, %al # A20 on
outb %al, $0x60
call empty_8042
4.4. 进入保护模式
4.4.1. 临时的GDT和IDT
这里的IDT全部是0;Linux目前使用的GDT如下:
gdt:
.fill GDT_ENTRY_BOOT_CS,8,0
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9A00 # code read/exec
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9200 # data read/write
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
gdt_end:
这里只定义了两个DPL为0的代码段和数据段,只给内核使用的。
4.4.2. 设置CR0.PE
这里使用lmsw指令,它和mov cr0, X是等价的
movw $1, %ax # protected mode (PE) bit
lmsw %ax # This is it!
jmp flush_instr
4.5. 调转到head.S(CS:EIP=0x10:100000)
至此硬件信息就收集完成,这些收集到的硬件信息都保存在0x90000处,后续OS可以使用这些硬件信息来管理了。
5. head.S
该文件位于arch/i386/kernel/head.S,这个是内核保护模式的代码的起点,笔者电脑的位置在0x100000,此时CPU上下文是:
eax=00000001 ebx=00000000 ecx=0000ff03 edx=47530081 esi=00090000 edi=00090000
eip=00100000 esp=00008ffe ebp=00001ff0 iopl=0 nv up di pl nz na pe nc
cs=0010 ds=0018 es=0018 fs=0018 gs=0018 ss=0018 eflags=00000002
注:已经进入保护模式,CS的值是GDT表项的索引。
它的作用就是设置真正的分段机制和分页机制、启动多处理器、设置C运行环境,最后执行start_kernel()函数。
5.1. startup_32
5.1.1. 加载临时的分段机制
boot_gdt_table就是临时的GDT,其实和start.S的一样:
lgdt boot_gdt_descr - __PAGE_OFFSET
movl $(__BOOT_DS),%eax
movl %eax,%ds
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
ENTRY(boot_gdt_table)
.fill GDT_ENTRY_BOOT_CS,8,0
.quad 0x00cf9a000000ffff /* kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* kernel 4GB data at 0x00000000 */
5.1.2. 初始化内核bss区和内核启动参数
为了让C代码正常运行,bss区全部清0,启动参数需要移动到boot_params位置。
5.1.3. 启动临时分页机制
临时的页表,只要能够满足内核使用就行。页目录表是swapper_pg_dir,它是一个4096大小的内存区域,默认全是0。一般__PAGE_OFFSET=0xC0000000(3GB),这是要把物理地址0x00000000映射到0xc0000000的地址空间(内核地址空间)。下面是页目录表和页表的初始化代码:
page_pde_offset = (__PAGE_OFFSET >> 20); // 3072,页目录的偏移
// 页目录表存放在pg0位置,arch/i386/kernel/vmlinux.lds中定义
movl $(pg0 - __PAGE_OFFSET), %edi
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx // edx是页目录表的地址
movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */
10:
// 创建一个页目录项
leal 0x007(%edi),%ecx /* Create PDE entry */
movl %ecx,(%edx) /* Store identity PDE entry */
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
addl $4,%edx // 指向swapper_pg_dir的下一个项
movl $1024, %ecx // 每个页表1024个项目
11:
stosl // eax -> [edi]; edi = edi + 4
addl $0x1000,%eax // 每次循环,下一个页目录项
loop 11b
/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp // 页表覆盖到这里就终止
cmpl %ebp,%eax
jb 10b
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
下面是对上面代码的翻译(这样更有利于理解):
extern uint32_t *pg0; // 初始值全0
extern uint32_t *swapper_pg_dir; // 初始值全0
void init_page_tables(){
uint32_t PAGE_FLAGS = 0x007; // PRESENT+RW+USER
uint32_t page_pde_offset = (_PAGE_OFFSET >> 20); // 3072
uint32_t addr = 0 | PAGE_FLAGS; // 内存地址+页表属性
uint32_t *pg_dir_ptr = swapper_pg_dir; // 页目录表项指针
uint32_t *pg0_ptr = pg0; // 页表项指针
for (;;) {
// 设置页目录项,同时映射两个地址,让物理地址和虚拟地址都能访问,
*pg_dir_ptr = pg0 | PAGE_FLAGS; // 0, 1
*(uint32_t *)((char *)pg_dir_ptr + page_pde_offset) = pg0 | PAGE_FLAGS; // 768, 769
pg_dir_ptr++;
// 设置页表项目
for (int i = 0; i < 1024; i++) {
*pg0++ = addr;
addr += 0x1000;
}
// 退出条件,实际上只映射了两个页目录就退出了(0,1,768, 769)
if (pg0[INIT_MAP_BEYOND_END] | PAGE_FLAGS) >= addr) {
init_pg_tables_end = pg0_ptr;
return;
}
}
};
5.1.4. 设置栈
/* Set up the stack pointer */
lss stack_start,%esp
ENTRY(stack_start)
.long init_thread_union+THREAD_SIZE
.long __BOOT_DS
/* arch/i386/kernel/init_task.c
* Initial thread structure.
*
* We need to make sure that this is THREAD_SIZE aligned due to the
* way process stacks are handled. This is done by having a special
* "init_task" linker map entry..
*/
union thread_union init_thread_union
__attribute__((__section__(".data.init_task"))) =
{ INIT_THREAD_INFO(init_task) };
内核最初使用的栈是init_task进程的,也就是0号进程的栈,这个进程是系统唯一一个静态定义而不是通过fork()产生的进程。
5.1.5. 设置真正的IDT和GDT
lgdt cpu_gdt_descr // 真正的GDT
lidt idt_descr //真正的IDT
ljmp $(__KERNEL_CS),$1f // 重置CS
1: movl $(__KERNEL_DS),%eax # reload all the segment registers
movl %eax,%ss # after changing gdt. // 重置SS
movl $(__USER_DS),%eax # DS/ES contains default USER segment
movl %eax,%ds
movl %eax,%es
xorl %eax,%eax # Clear FS/GS and LDT
movl %eax,%fs
movl %eax,%gs
lldt %ax
cld # gcc2 wants the direction flag cleared at all times
// push一个假的返回地址以满足 start_kernel()函数return的要求
pushl %eax # fake return address
对于IDT先全部初始化成ignore_int例程:
setup_idt:
lea ignore_int,%edx
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea idt_table,%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
ret
ignore_int例程就干一件事,打印一个错误信息"Unknown interrupt or fault at EIP %p %p %p\n"。
对于GDT我们最关心的__KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS这4个段描述符:
.quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */
至此分段机制、分页机制、栈都设置好了,接下去可以开心的jmp start_kernel了。
6. start_kernel
该函数在linux/init/main.c文件里。我们可以认为start_kernel是0号进程init_task的入口函数,0号进程代表整个linux内核且每个CPU有一个。这个函数开始做一系列的内核功能初始化,我们重点看rest_init()函数。
6.1.rest_init
这是start_kernel的最后一行,它启动一个内核线程运行init函数后就什么事情也不做了(死循环,始终交出CPU使用权)。
static void noinline rest_init(void){
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); // 启动init
……
/* Call into cpu_idle with preempt disabled */
cpu_idle(); // 0号进程什么事也不做
}
6.2. init()
该函数的末尾fork了”/bin/init”进程。这样1号进程init就启动了,接下去就交给init进程去做应用层该做的事情了!
// 以下进程启动后父进程都是0号进程
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
附录1. 启动多核CPU
以上解读的内容只在0号CPU上执行,如果是多CPU的环境,还要初始化其他的CPU。多CPU启动的起点是start_kernel()->rest_init()->init()->smp_init()。而smp_init()函数给每个CPU上调用cpu_up()和do_boot_cpu()函数,每个CPU都要再走一遍head.S的流程,然后启动自己的idle进程(内核态0号进程)。
附录2. x64的不同
i386和x64的启动代码主要区别在head.S中。
页表格式不同,i386使用两级页表,x64使用4级页表。
多了兼容32位的代码段和数据段__USER32_CS、__USER32_DS和__KERNEL32_CS。
x64段寄存器用法和i386的不同:x64下面CS、DS、ES、SS不用了,始终为0。而FS、GS寄存器的用法倒像是实模式下的,主要考虑是保留两个作为基地址好让线性地址计算方便。FS:XX = MSR_FS_BASE + XX, GS:XX = MSR_GS_BASE + XX, 不是段描述符索引了(像实模式的分段)。
Linux系统构建之启动
1、Linux启动
Linux操作系统不仅仅有Linux的内核,更需要具备各种服务才能成为一个完整的操作系统。了解Linux系统的启动过程,对理解Linux系统是非常有益的.但是Linux系统涉及的服务众多, 不同的发行版又有差异,所以这里只描述一个轮廓,日后逐渐充实。
BIOS
按下电源的时候,CPU执行的第一条指令是BIOS(基本输入输出系统)。BIOS执行的最后将启动磁盘的第一个扇区(MBR)加载到内存中固定的位置(0000:7C00),然后CPU开始执行0000:7C00处的指令,也就是MBR中存储的指令。
MBR(Master Boot Record, 主引导记录),是磁盘的第一个扇区,前446字节存放引导程序,中间64字节存放磁盘分区表,最后两个字节固定为0x55、0xAA(0x550xAA是识别MBR的标记)。内存中0000:7C00之前的位置用来存放BIOS中断表等BIOS程序加载进去的内容。
引导程序
跳转到0000:7C00后,开始执行的就是引导程序。因为MBR只有512字节,所以引导程序不完成太多任务,主要是继续加载更多的内容。Linux系统的引导程序通常是grub。
grub2.0中,执行的第一条指令(加载到0000:7C00处的指令)在grub-2.00.tar\grub-2.00\grub-2.00\grub-core\boot\i386\pc\boot.S中.
.globl _start, start;
_start:
start:
/*
* _start is loaded at 0x7c00 and is jumped to with CS:IP 0:0x7c00
*/
/*
* Beginning of the sector is compatible with the FAT/HPFS BIOS
* parameter block.
*/
jmp LOCAL(after_BPB)
nop /* do I care about this ??? */
可以从boot.S开始梳理Grub的引导过程。
引导程序通过BIOS中断完成对硬件的操作。
grub的安装方式非常简单,假设我们要在磁盘sda中安装grub,并且将grub需要的文件存放在第一个分区sda1中。
mount /dev/sda1 /mnt
grub-install --root-directory=/mnt --no-floppy sda
执行上述命令后,sda的MBR就被写入了grub的引导程序,在/mnt目录下则多出一个boot目录,里面存放着grub需要的文件。这时候从sda启动就可以看到grub的执行(如果要看到完整的引导,需要在boot目录下进行grub的引导配置,指定内核和initrd)。
grub引导的具体过程涉及的内容比较复杂。其将内核加载,并加载了一个位于内存的根文件系统,作为内核看到的第一个根文件系统。
initrd
initrd是内核看到的第一个根文件系统,这个根文件系统中包含必要的驱动和设备文件,在这里加载驱动后,才能读取到最终的文件系统,这时切换到最终的根文件系统。可以将initrd解压,查看包含的内容和完成的操作。
CentOS系列的发行版可以在/boot下找到img文件,例如initrd-2.6.18-308.el5.img,这个就是initrd。在grub的grub.conf可以看到系统启动时使用的时那个img。
initrd-2.6.18-308.el5.img是一个gzip压缩文件,首先需要修改后缀,然后用gunzip解压, 再用cpio读取。
cp initrd-2.6.18-308.el5.img /tmp/initrd-2.6.18-308.el5.img.gz
cd /tmp
gunzip initrd-2.6.18-308.el5.img.gz
mkdir cpio;
cd cpio;
cpio -i -d < ../initrd-2.6.18-308.el5.img
initrd中包含一个init程序, 这是内核执行的第一个程序,CentOS中init是一个nash脚本。
CentOS中init的内容:
(挂载虚拟文件系统)
echo Creating /dev
mount -o mode=0755 -t tmpfs /dev /dev #dev使用虚拟文件系统tmpfs(内存)中
mkdir /dev/pts #/dev/pts存放ssh登陆时生成的虚拟终端设备文件
mount -t devpts -o gid=5,mode=620 /dev/pts /dev/pts #/dev/pts的文件系统类型devpts, 虚拟终端文件设备
mkdir /dev/shm #/dev/shm 只建立目录
mkdir /dev/mapper #/dev/mapper 只建立目录
(创建必要的设备文件)
echo Creating initial device nodes
mknod /dev/null c 1 3
mknod /dev/zero c 1 5
mknod /dev/urandom c 1 9
mknod /dev/systty c 4 0
...
...
mknod /dev/ttyS3 c 4 67
echo Setting up hotplug.
hotplug
echo Creating block device nodes.
mkblkdevs
(加载必要的驱动)
echo "Loading ehci-hcd.ko module"
insmod /lib/ehci-hcd.ko
echo "Loading ohci-hcd.ko module"
insmod /lib/ohci-hcd.ko
echo "Loading uhci-hcd.ko module"
insmod /lib/uhci-hcd.ko
mount -t usbfs /proc/bus/usb /proc/bus/usb
echo "Loading jbd.ko module"
insmod /lib/jbd.ko
echo "Loading vmxnet3.ko module"
...
...
insmod /lib/vmxnet3.ko
echo "Loading pvscsi.ko module"
insmod /lib/pvscsi.ko
echo "Loading vmxnet.ko module"
insmod /lib/vmxnet.ko
echo Waiting for driver initialization.
stabilized --hash --interval 1000 /proc/scsi/scsi
mkblkdevs
echo Scanning and configuring dmraid supported devices
resume LABEL=SWAP-sda2
(切换到最终的根文件系统)
echo Creating root device.
mkrootdev -t ext3 -o defaults,ro /dev/sda3 #定义root路径
echo Mounting root filesystem.
mount /sysroot #将实体根目录挂载到sysroot
echo Setting up other filesystems.
setuproot #将通过initrd的init建立的/proc /sys /dev目录中的资料转移到/sysroot
echo Switching to new root and running init.
switchroot #切入实体根目录,将原先系统的所有内容清空
initrd最后切换的最终的根文件系统,位于内存中的临时的根文件系统随之被清空。
系统的初始化
切换到最终的根文件系统后,开始了Linux操作系统的初始化;这时候就看到发行版之间的差异了,不同的发行版有不同的处理方式,初始化的内容也不尽相同。
初始化程序是一系列的shell脚本,繁多复杂,牵扯到太多的服务,如果能够全部了解,那么对Linux系统将会非常熟悉。
初始化脚本的组织方式主要分为System V(CentOS)和event-base(Ubuntu)两种风格。
在System V风格中,init通过/etc/inittab文件的内容得知初始化脚本、运行级别、每个级别的初始化脚本。
在event-base风格中,init遍历/etc/init目录中的事件文件,执行需要执行的脚本。
CentOS 5.8版本中使用的System V风格,6.4版本中换用了event-base风格。
sysvinit - System V风格
System V风格使用sysinit机制,所有init要执行的事项都在/etc/inittab文件中指明。
/etc/inittab是init的配置文件,指定了需要运行的脚本。可以查看对应的linux手册:man 5 inittab。/etc/inittab中的每一行都使用下面的格式:
id:runlevels:action:process
id是由1-4个字符组成,用来唯一标记每一行。
runlevels指定该行使用的运行级别.
action指定init应该采取的动作
respawn: 如果process运行结束,init立即重新调用process
wait: process执行一次,init等待process运行结束
once: process执行一次
boot: 在系统启动时运行process,忽略runlevel
bootwait: 在系统启动时运行process,init等待process运行结束
off: 什么都不做
ondemand:
initdefault: 默认的运行级别
sysinit: 系统初始化执行,在boot和bootwait之前执行
powerwait:
powerfail:
powerokwait:
powerfailnow:
ctrlaltdel: init收到ctl_alt_del组合键信号时执行
kbrequest:
process是要执行的命令.
下面是CentOS 5.8的/etc/inittab文件.
默认运行级别是5
init进程依次执行/etc/rc.d/rc.sysinit
对应运行级别的process(/etc/rd.c/rc 0)
重复运行/sbin/mingetty ttyXX.
id:5:initdefault:
# System initialization.
si::sysinit:/etc/rc.d/rc.sysinit
l0:0:wait:/etc/rc.d/rc 0
l1:1:wait:/etc/rc.d/rc 1
l2:2:wait:/etc/rc.d/rc 2
l3:3:wait:/etc/rc.d/rc 3
l4:4:wait:/etc/rc.d/rc 4
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6
# Trap CTRL-ALT-DELETE
ca::ctrlaltdel:/sbin/shutdown -t3 -r now
# When our UPS tells us power has failed, assume we have a few minutes
# of power left. Schedule a shutdown for 2 minutes from now.
# This does, of course, assume you have powerd installed and your
# UPS connected and working correctly.
pf::powerfail:/sbin/shutdown -f -h +2 "Power Failure; System Shutting Down"
# If power was restored before the shutdown kicked in, cancel it.
pr:12345:powerokwait:/sbin/shutdown -c "Power Restored; Shutdown Cancelled"
# Run gettys in standard runlevels
1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6
upstart - event-base风格
event-base风格的初始化机制,称为upstart机制.初始化过程被拆分成一个个事件,每个事件都各自的事件文件,事件文件中指明事件执行的时机和要执行的操作。
事件文件存放在/etc/init目录下,启动时init进程到/etc/init目录扫描执行需要执行的事件文件。
可以指定事件直接的依赖关系,例如B事件必须在A事件完成后开始。
init进程发出第一个事件startup,然后执行在startup时执行的事件,这批事件执行结束后各自发出相应的事件(事件文件中的emits命令),从而又启动一批事件,直至完成。
下面是CentOS 6.4中的事件文件/erc/init/rcS.conf
指定了事件执行的时机(start on startup)
停止的时机(stop on runlevel)
事件类型(task)
信息打印输出的位置(console output)
事件执行前进行的操作(pre-start script .... end script)
事件(exec /etc/rc.d/rc.sysinit)
事件结束后执行的操作(post-stop script ... end script)
# rcS - runlevel compatibility
#
# This task runs the old sysv-rc startup scripts.
start on startup
stop on runlevel
task
# Note: there can be no previous runlevel here, if we have one it's bad
# information (we enter rc1 not rcS for maintenance). Run /etc/rc.d/rc
# without information so that it defaults to previous=N runlevel=S.
console output
pre-start script
for t in $(cat /proc/cmdline); do
case $t in
emergency)
start rcS-emergency
break
;;
esac
done
end script
exec /etc/rc.d/rc.sysinit
post-stop script
if [ "$UPSTART_EVENTS" = "startup" ]; then
[ -f /etc/inittab ] && runlevel=$(/bin/awk -F ':' '$3 == "initdefault" && $1 !~ "^#" { print $2 }' /etc/inittab)
[ -z "$runlevel" ] && runlevel="3"
for t in $(cat /proc/cmdline); do
case $t in
-s|single|S|s) runlevel="S" ;;
[1-9]) runlevel="$t" ;;
esac
done
exec telinit $runlevel
fi
end script
从上面的事件文件中可以看到,CentOS6.4将原先在inittab中指定的rc.sysinit定位为一个rcS.conf事件,并且从inittab中获取运行级别,通过这种方式兼容了以往的System V风格的初始化过程。
Ubuntu中事件文件使用同样的格式
查看事件文件的linux帮助手册:man 5 init
第一批事件
以下几个事件要在startup时执行。
freeoa@ubuntu:/etc/init$ grep startup *
hostname.conf:start on startup
module-init-tools.conf:start on (startup
mountall.conf:start on startup
udev-fallback-graphics.conf:start on (startup and
udev-finish.conf:start on (startup
udevmonitor.conf:start on (startup
udevtrigger.conf:start on (startup
过滤掉同时依赖别的事件的事件后,得到最早执行的两个事件:
hostname.conf
mountall.conf
通过下面分析可以知道,第一批事件完成了两个工作,设置主机名,加载文件系统(磁盘)。
hostname事件只是简单的设置系统的主机名。
# hostname - set system hostname
#
# This task is run on startup to set the system hostname from /etc/hostname,
# falling back to "localhost" if that file is not readable or is empty and
# no hostname has yet been set.
description "set system hostname"
start on startup
task
exec hostname -b -F /etc/hostname
mountall发出的多个事件,带动其他事件的执行。
# mountall - Mount filesystems on boot
#
# This helper mounts filesystems in the correct order as the devices
# and mountpoints become available.
description "Mount filesystems on boot"
start on startup
stop on starting rcS
expect daemon
task
emits virtual-filesystems
emits local-filesystems
emits remote-filesystems
emits all-swaps
emits filesystem
emits mounting
emits mounted
# temporary, until we have progress indication
# and output capture (next week :p)
console output
script
. /etc/default/rcS
[ -f /forcefsck ] && force_fsck="--force-fsck" # 环境变量指示是否需要修复文件系统
[ "$FSCKFIX" = "yes" ] && fsck_fix="--fsck-fix"
# set $LANG so that messages appearing in plymouth are translated
if [ -r /etc/default/locale ]; then
. /etc/default/locale
export LANG LANGUAGE LC_MESSAGES LC_ALL # 语言环境
fi
exec mountall --daemon $force_fsck $fsck_fix # mountall命令,加载fstab中指定的挂载
end script
post-stop script # 事件结束
rm -f /forcefsck 2>dev/null || true
end script
systemd
systemd supports SysV and LSB init scripts and works as a replacement for sysvinit
systemd
redhat intro
Rethinking PID 1
2、CentOS initrd分析
解压initrd
在/boot下找到initrd-2.6.18-308.el5.img,复制到另一个目录下(防止操作将原文件覆盖),并修改后缀名为.gz,如下:
cp initrd-2.6.18-308.el5.img /root/initrd/initrd-2.6.18-308.el5.img.gz
initrd-2.6.18-308.el5.img是一个gzip压缩文件,修改后缀后,使gunzip可以识别解压:
gunzip initrd-2.6.18-308.el5.img.gz
解压后得到.img文件,使用cpio解压.imgd到一个新目录中:
mkdir cpio;
cd cpio;
cpio -i -d < ../initrd-2.6.18-308.el5.img
在cpio目录下得到initrd中所有文件:
bin dev etc init lib proc sbin sys sysroot
cpio中的文件就是内核在initrd环境中运行时使用的根文件系统。其中的init时执行的第一个进程。CentOS 5.8的initrd中的cpio/init是一个nash脚本,完成到磁盘上的根文件系统的切换。
nash是专门用于inird的类似shell的解析程序,内置了一些必用的命令。
cpio/init完成以下工作:
挂载虚拟文件系统
echo Creating /dev
mount -o mode=0755 -t tmpfs /dev /dev #dev使用虚拟文件系统tmpfs(内存)中
mkdir /dev/pts #/dev/pts存放ssh登陆时生成的虚拟终端设备文件
mount -t devpts -o gid=5,mode=620 /dev/pts /dev/pts #/dev/pts的文件系统类型devpts, 虚拟终端文件设备
mkdir /dev/shm #/dev/shm 只建立目录
mkdir /dev/mapper #/dev/mapper 只建立目录
最初需要的设备文件
echo Creating initial device nodes
mknod /dev/null c 1 3
mknod /dev/zero c 1 5
mknod /dev/urandom c 1 9
mknod /dev/systty c 4 0
mknod /dev/tty c 5 0
mknod /dev/console c 5 1
mknod /dev/ptmx c 5 2
mknod /dev/rtc c 10 135
mknod /dev/tty0 c 4 0
mknod /dev/tty1 c 4 1
mknod /dev/tty2 c 4 2
mknod /dev/tty3 c 4 3
mknod /dev/tty4 c 4 4
mknod /dev/tty5 c 4 5
mknod /dev/tty6 c 4 6
mknod /dev/tty7 c 4 7
mknod /dev/tty8 c 4 8
mknod /dev/tty9 c 4 9
mknod /dev/tty10 c 4 10
mknod /dev/tty11 c 4 11
mknod /dev/tty12 c 4 12
mknod /dev/ttyS0 c 4 64
mknod /dev/ttyS1 c 4 65
mknod /dev/ttyS2 c 4 66
mknod /dev/ttyS3 c 4 67
echo Setting up hotplug.
hotplug
echo Creating block device nodes.
mkblkdevs
加载相关模块
echo "Loading ehci-hcd.ko module"
insmod /lib/ehci-hcd.ko
echo "Loading ohci-hcd.ko module"
insmod /lib/ohci-hcd.ko
echo "Loading uhci-hcd.ko module"
insmod /lib/uhci-hcd.ko
mount -t usbfs /proc/bus/usb /proc/bus/usb
echo "Loading jbd.ko module"
insmod /lib/jbd.ko
echo "Loading ext3.ko module"
insmod /lib/ext3.ko
echo "Loading scsi_mod.ko module"
insmod /lib/scsi_mod.ko
echo "Loading sd_mod.ko module"
insmod /lib/sd_mod.ko
echo "Loading scsi_transport_spi.ko module"
insmod /lib/scsi_transport_spi.ko
echo "Loading mptbase.ko module"
insmod /lib/mptbase.ko
echo "Loading mptscsih.ko module"
insmod /lib/mptscsih.ko
echo "Loading mptspi.ko module"
insmod /lib/mptspi.ko
echo "Loading libata.ko module"
insmod /lib/libata.ko
echo "Loading ata_piix.ko module"
insmod /lib/ata_piix.ko
echo "Loading dm-mem-cache.ko module"
insmod /lib/dm-mem-cache.ko
echo "Loading dm-mod.ko module"
insmod /lib/dm-mod.ko
echo "Loading dm-log.ko module"
insmod /lib/dm-log.ko
echo "Loading dm-region_hash.ko module"
insmod /lib/dm-region_hash.ko
echo "Loading dm-message.ko module"
insmod /lib/dm-message.ko
echo "Loading dm-raid45.ko module"
insmod /lib/dm-raid45.ko
echo "Loading vmxnet3.ko module"
insmod /lib/vmxnet3.ko
echo "Loading pvscsi.ko module"
insmod /lib/pvscsi.ko
echo "Loading vmxnet.ko module"
insmod /lib/vmxnet.ko
echo Waiting for driver initialization.
stabilized --hash --interval 1000 /proc/scsi/scsi
mkblkdevs
echo Scanning and configuring dmraid supported devices
resume LABEL=SWAP-sda2
切入实体系统
echo Creating root device.
mkrootdev -t ext3 -o defaults,ro /dev/sda3 #定义root路径
echo Mounting root filesystem.
mount /sysroot #将实体根目录挂载到sysroot
echo Setting up other filesystems.
setuproot #将通过initrd的init建立的/proc /sys /dev目录中的资料转移到/sysroot
echo Switching to new root and running init.
switchroot #切入实体根目录,将原先系统的所有内容清空
切换完成
切换完成后,首先执行的是实体系统中的init进程。
实验
重新打包成cpio
find . | cpio -o -H newc > ../initrd_new.img #将当前目录下所有文件打包成cpo
cd ..; gzip initrd_new.img
将得到的initrd_new.img.gz复制到/boot目录下。 在/boot/grub/grub.conf中增加新的启动项:
title initrd_new (2.6.18-308.el5)
root (hd0,0)
kernel /vmlinuz-2.6.18-308.el5 ro root=LABEL=/
initrd /initrd_new.img.gz
将busybox集成到initrd
解压,编译,安装
make menconfig #需要安装curses-devel,最好选用编译成static,不然需要添加依赖的动态库
make #如果内核版本还没直至ubi, 在配置中将ubi相关的选项关闭(.config中),ubi是新的flash文件系统
make CONFIG_PREFIX=/path/from/root install #安装到的目录
3、Linux系统定制
自己动手定制一个linux,可以解除很多的疑惑,破除种种神秘。首先要有一台Linux机器:分为安装grub、编译内核、制作initrd三步。
安装grub
假设目标磁盘的设备文件是sdb,首先sdb进行分区、格式化;假设分成sdb1、sdb2,然后将sdb1挂载到/mnt
执行grub安装
grub-install --root-directory=/mnt --no-floppy sdb
执行上述命令后, 在/mnt目录下会多出一个boot目录。将来内核、initrd都会存放在boot目录下。
在/mnt/boot/grub下配置grub.conf。
可以参照原有系统的grub配置文件(/boot/grub/grub.conf), 这时候从sdb启动时就会进入到grub。
制作initrd
新建一个目录initrd
mkdir initrd
将需要的东西复制到initrd目录中
例如将busybox安装到initrd中、创建必要的几个设备文件、init程序
可以查看当前使用的系统initrd,进行参照
解压方式: cpio -idmv < filename.img
生成img
find . | cpio -o -H newc |gzip -9 >initrd.img
特别注意这一步必须在initrd目录下进行,这样才能保证解压后的内容。
丰富initrd
当可以启动进入到initrd的时候, 就说明已经制作成功。接下来需要做的就是在其中添加各种新的程序。
4、参考
《深度探索Linux操作系统–系统构建和原理解析》 王柏生
《深入探索Linux操作系统的奥秘》
Unix 系列中(如 System III 和 System V)init的作用,和研究中的 Unix 和 BSD 派生版本相比,发生了一些变化。大多数Linux发行版是和 System V 相兼容的,但是一些发行版如 Slackware 采用的是BSD风格,其它的如 Gentoo 是自己定制的。Ubuntu和其它一些发行版现在开始采用Upstart来代替传统的 init 进程(现也转向Systemd)。
BSD风格
BSD init 运行存放于'/etc/rc'的初始化 shell 脚本,然后启动基于文本模式的终端(getty)或者基于图形界面的终端(窗口系统,如 X)。 这里没有运行模式的问题,因为文件 'rc' 决定了 init 如何执行。
优点:简单且易于手动编辑。
缺点:如果第三方软件需要在启动过程执行它自身的初始化脚本,它必须修改已经存在的启动脚本,一旦这种过程中有一个小错误,都将导致系统无法正常启动。
值得注意的是,现代的 BSD 派生系统一直支持使用 'rc.local' 文件的方式,它将在正常启动过程接近最后的时间以子脚本的方式来执行。这样做减少了整个系统无法启动的风险。然后,第三方软件包可以将它们独立的 start/stop 脚本安装到一个本地的 'rc.d' 目录中(通常这是由 ports collection/pkgsrc 完成的)。FreeBSD 和 NetBSD 现在默认使用 rc.d ,该目录中所有的用户启动脚本,都被分成更小的子脚本,这点和 SysV 类似。rcorder 通常根据在 rc.d目录中脚本之间的依赖关系来决定脚本的执行顺序。
SysV风格
System V init 检查 '/etc/inittab' 文件中是否含有 'initdefault' 项。 这告诉 init 系统是否有一个默认运行模式。如果没有默认的运行模式,那么用户将进入系统控制台,手动决定进入何种运行模式。
优点:灵活性强
缺陷:比较复杂
运行模式
System V中运行模式描述了系统各种可能的状态。通常会有 8 种运行模式,即运行模式 0 到 6 和 S 或者 s。其中运行模式 3 为"保留的"运行模式:
0. 关机
1. 单用户模式
6. 重启
除了模式 0, 1,6外, 每种 Unix 和 Unix-like 系统对运行模式的定义不太一样。通常在 /etc/inittab 文件中定义了各种运行模式的工作范围。
大多数操作系统的用户可以用下面的命令来得到当前的运行模式:
$ runlevel
$ who -r
在 root 权限下,运行 telinit 或者 init 命令可以改变当前的运行模式。/etc/inittab 文件中设置的默认的运行模式在 :initdefault: 项中。另见新增一些功能 pidof 或者 killall5, 从 System V 开始在很多发行版中使用的另一种程序,但兼容于System V。
如何跳过 init
Linux系统中,现代的bootloader(如 LILO 或者 GRUB),用户可以在初始化过程中最后启动的进程来取代默认的 /sbin/init。通常是在 bootloader 环境中通过执行 init=/foo/bar 命令。例如,如果执行 init=/bin/bash,启动单用户 root 的 shell 环境,无需用户密码。
BSD的变种,大多数平台, bootstrap 程序是可以被打断的,然后执行 boot -s 命令进入单用户模式。
单用户模式并不没有跳过 init,它仍然可以执行 /sbin/init,但是它将使 init 询问 exec() 将要执行的命令 (默认为 /bin/sh) 的路径,而不是采用正常的多用户启动顺序。如果内核启动时在 /etc/ttys 文件中被标注为 "不安全" (在某些系统中,当前的"安全模式" 可能会有些变化), 在允许这种情况(或者回退到单用户模式,如果用户执行 CTRL+D),init 将首先询问 root 用户的密码。 如果该程序退出,内核将在多用户模式下重新执行 init。如果系统从多用户模式切换到单用户模式,还将碰到上述的情况。
如果内核加载后, init 不能被正常启动, 这将导致 panic 错误,此时系统将不可使用。想要通过 init 自身来改变 init 的路径,不同的版本情况不太一样(NetBSD中可执行 boot -a ; FreeBSD中利用 init_path 命令装载变量)。
其他风格
很多人一直努力地从某些方面改进传统的 init 守护进程,使它变得更完善。下面列出的是一些改进(排序不分先后):
SystemStarter, 用来替代 launchd — Apple Mac OS X开启进程
Initng, 完全代替 init,可以异步开启进程
Upstart, 完全代替 init,可以异步开启进程 曾由Ubuntu使用
Service Management Facility, 完全代替/重新设计 Solaris 启动 Solaris 10
runit, 跨平台的完全代替 init 可以并行启动服务
BootScripts, GoboLinux
Mudur, 用 Python 写成的 init 替代品,可以异步开启进程,Pardus Linux 发行版
systemd, 完全替代init,可并行启动服务,并能减少在shell上的系统开销,被认为是最邪恶的项目,已被大多数Linux发行版所使用
下面列出的项目还没有大范围的使用:
eINIT, 完全代替 init ,可以异步开启进程,但是完成这个过程可以不使用 shell 脚本
svscan 来自 daemontools 被用作 1 号进程 - 似乎将被 runit 替代
twsinit, 部分用 x86 汇编写成, 只是用来证明一种概念
OpenRC,Gentoo默认使用
关于 Linux 系统的运行级别(runlevel)
Linux 系统有 7 个运行级别,Linux 系统任何时候都运行在一个指定的运行级别上,不同的运行级别所运行的程序和服务不尽相同,所要完成的工作和要达到的目的也不相同:
运行级别 0 系统停机(halt)状态,系统的默认运行级别不能设为 0,否则不能正常启动
运行级别 1 单用户工作(single user)状态,root 权限,用于系统维护,禁止远程登陆
运行级别 2 多用户(multiuser)状态 (没有 NFS)
运行级别 3 完全的多用户(multiuser)状态 (有 NFS),登陆后进入控制台命令行模式
运行级别 4 系统未使用,保留
运行级别 5 X11 控制台 (xdm、gdm、kdm),登陆后进入图形 GUI 模式
运行级别 6 系统正常关闭并重启(reboot),默认运行级别不能设为 6,否则不能正常启动
在一些新的发行版本中这个规则已经变的无效了或不再被遵守了,在CentOS 7中这种定义已经不再有效了,从/etc/inittab中的内容已经变更为如下:
# inittab is no longer used when using systemd.
# ADDING CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM.
# Ctrl-Alt-Delete is handled by /usr/lib/systemd/system/ctrl-alt-del.target
# systemd uses 'targets' instead of runlevels. By default, there are two main targets:
# multi-user.target: analogous to runlevel 3
# graphical.target: analogous to runlevel 5
#
# To view current default target, run:
# systemctl get-default
#
# To set a default target, run:
# systemctl set-default TARGET.target
解读Linux启动过程
1. 概述
本文解读一下从CPU加电自检到启动init进程的过程,先通过下面这张图大致看一下Linux启动的整个过程,分析环境是GRUB 0.97 + Linux 2.6.18。
2. BIOS
CPU加电后首先工作在实模式并初始化CS:IP=FFFF:FFF0,BIOS的入口代码必须从该地址开始。BIOS完成相应的硬件检查并提供一系列中断服务例程,这些中断服务提供给系统软件访问硬件资源(比如磁盘、显示器等),最后选择一个启动盘加载第一个扇区(即:MBR,共512字节)数据到内存0x7C00处,并从这里开始执行指令(CS:IP=0000:7C00),对于笔者的电脑来说这就是GRUB的Stage1部分。
3. GRUB
GRUB的作用是bootloader,用来引导各种OS的。
3.1. Stage1
Stage1就是MBR,由BIOS把它从磁盘的0扇区加载到0x7c00处,大小固定位512字节,此时的CPU上下文如下:
eax=0011aa55 ebx=00000080 ecx=00000000 edx=00000080 esi=0000f4a0 edi=0000fff0
eip=00007c00 esp=00007800 ebp=00000000 iopl=0 nv up ei pl zr na po nc
cs=0000 ds=0000 es=0000 fs=0000 gs=0000 ss=0000 eflags=00000246
// 注: dl=启动磁盘号, 00H~7FH是软盘, 80H~FFH是硬盘。
因为只能是512字节,大小受限,它就干一件事,把Stage2的第一个512字节读取到0x8000,然后jmp到0x8000继续执行。
3.1.1. 读磁盘
磁盘扇区寻址有两种方式:
CHS方式:传统的方式,使用三元组(10位Cylinder, 8位Head, 6位Sector)来寻找扇区,最大只能找到(2^10) * (2^8) * (2^6) * 512 = 8GB的硬盘容量,现在的硬盘明显不够用了。
LBA方式:现在的方式,使用48位线性地址来寻找扇区,最大支持(2^48) * 512 = 128PB的硬盘空间。虽然机械上还是CHS的结构,不过磁盘的固件会自动完成LBA到CHS的转换。
因为CHS明显不适合现在的硬盘,所以LBA模式寻址是现在的PC的标配了吧!万一磁盘不支持LBA或者是软盘,需要我们手工转换成CHS模式。转换公式如下(就是三维空间定位一个点的问题):
磁道号C = LBA / 每磁道的扇区数SPT / 盘面总HPC
磁头号H = (LBA / 每磁道的扇区数SPT) mod HPC
扇区号S = (LBA mod SPT) + 1
判断是否支持LBA模式
/* check if LBA is supported */
movb $0x41, %ah
movw $0x55aa, %bx
int $0x13
如果返回成功(CF=1)并且BX值是0xAA55表示支持LBA寻址(用Extensions方法)。
注意:3.5英寸软盘需要使用CHS方式寻址,它的CHS参数是80个柱面、2个磁头、每个磁道18个扇区,每扇区512字节,共1.44MB容量。
LBA模式读的功能号是AH=42h,DL参数是磁盘号,DS:SI参数是Disk Address Packet(DAP)结构体的内存地址,定义如下:
struct DAP {
uint8_t sz; // 结构体大小
uint8_t unused;
uint16_t sector_cnt; // 需要都的扇区总数
struct dst_addr { // 内存地址,读到这里
uint16_t offset;
uint16_t segment;
};
uint64_t lba_addr; // 磁盘的LBA地址
};
参考:
https://en.wikipedia.org/wiki/Logical_block_addressing#CHS_conversion
https://en.wikipedia.org/wiki/INT_13H
3.2. Stage2
Stage2就是GRUB剩下的全部的代码了,包括BIOS中断服务的封装给C代码使用、键盘驱动、文件系统驱动、串口、网络驱动等等,它提供了一个小型的命令行环境,可以解析用户输入命令并执行对OS的启动。
3.2.1. start.S
首先Stage2的头512字节(start.S)被加载到0x8000,并在这里开始执行,此时的CPU上下文如下:
eax=00000000 ebx=00007000 ecx=00646165 edx=00000080 esi=00007c05 edi=0000fff0
eip=00008000 esp=00001ffe ebp=00000000 iopl=0 nv up ei pl zr na po nc
cs=0000 ds=0000 es=0800 fs=0000 gs=0000 ss=0000 eflags=00000246
start.S的工作是把Stage2的后续部分全部加载到内存中(从0x8200开始),有103KB大小。
3.2.2. asm.S
asm.S是0x8200处的代码,先看一下CPU上下文环境:
eax=00000e00 ebx=00000001 ecx=00646165 edx=00000080 esi=00008116 edi=000081e8
eip=00008200 esp=00001ffe ebp=000062d8 iopl=0 nv up ei pl zr na po nc
cs=0000 ds=0000 es=1ae0 fs=0000 gs=0000 ss=0000 eflags=00000246
3.2.2.1. 最开始的代码应该设置好段寄存器和栈
cli
/* set up %ds, %ss, and %es */
/* cs=0000 ds=0000 es=0000 fs=0000 gs=0000 ss=0000 */
xorw %ax, %ax
movw %ax, %ds
movw %ax, %ss
movw %ax, %es
/* set up the real mode/BIOS stack */
movl $STACKOFF, %ebp
movl %ebp, %esp
sti
此时:
cs=0000 ds=0000 es=0000 ss=0000 esp=00001ff0 ebp=00001ff0。
3.2.2.2. 保护模式和实模式
因为GRUB没有实现自己的中断服务,所以访问硬件资源还是使用BIOS的中断服务例程(实模式)。GRUB的命令行环境是工作在保护模式下的,所以当GRUB需要访问BIOS中断的时候需要切换回实模式,于是在GRUB执行过程中会有频繁的实模式和保护模式的互相切换操作,当切换回实模式后别忘了保存保护模式下的栈指针。
1 实模式进入保护模式
/* transition to protected mode */
DATA32 call EXT_C(real_to_prot)
/* The ".code32" directive takes GAS out of 16-bit mode. */
.code32
下图是实模式到保护模式的切换步骤:
GRUB没有设置分页机制和新的中断,所以GRUB的保护模式访问的是物理内存且是不能使用INT指令,不过对于bootloader来说够用了;因为需要切换到保护模式栈,原来的返回地址要放到新的栈上,以保证能够正常ret:
ENTRY(real_to_prot)
...
/* put the return address in a known safe location */
movl (%esp), %eax
movl %eax, STACKOFF ; 把返回地址保存起来备用
/* get protected mode stack */
movl protstack, %eax
movl %eax, %esp
movl %eax, %ebp ; 设置保护模式的栈
/* get return address onto the right stack */
movl STACKOFF, %eax
movl %eax, (%esp) ; 把返回地址重新放到栈上
/* zero %eax */
xorl %eax, %eax
/* return on the old (or initialized) stack! */
ret ; 正常返回
(2) 保护模式切换回实模式
/* enter real mode */
call EXT_C(prot_to_real)
.code16
下图说明了保护模式切换回实模式的步骤:
保护模式的栈需要保存起来以便恢复现场,让C代码正确运行,实模式的栈每次都重置为STACKOFF即可,和(1)一样,也要设置好返回地址:
ENTRY(prot_to_real)
...
/* save the protected mode stack */
movl %esp, %eax
movl %eax, protstack ; 把栈保存起来
/* get the return address */
movl (%esp), %eax
movl %eax, STACKOFF ; 返回地址放到实模式栈里
/* set up new stack */
movl $STACKOFF, %eax ; 设置实模式的栈
movl %eax, %esp
movl %eax, %ebp
...
3.2.2.3. 创建C运行时环境
C的运行环境主要包括栈、bss数据区、代码区。随着切换到保护模式,栈已经设置好了;随着Stage2从磁盘加载到内存,代码区和bss区都已经在内存了,最后还需要把bss区给初始化一下(清0),接下来即可愉快的执行C代码了。
3.2.2.4. 执行cmain()
先执行一个init_bios_info()获取BIOS的信息,比如被BIOS使用的内存空间(影响我们Linux映像加载的位置)、磁盘信息、ROM信息、APM信息,最后调用cmain()。cmain()函数在stage2.c文件中,其中最主要的函数run_menu()是启动一个死循环来提供命令行解析执行环境。
3.2.2.5. load_image()
如果grub.cfg或者用户执行kenrel命令,会调用load_image()函数来将内核加载到内存中。至于如何加载linux镜像在Documentation的boot.txt和zero-page.txt有详细说明。
load_image()是一个非常长的函数,它要处理支持的各种内核镜像格式。Linux镜像vmlinuz文件头是struct linux_kernel_header结构体,该结构体里头说明了这个镜像使用的boot协议版本、实模式大小、加载标记位和需要GRUB填写的一些参数(比如:内核启动参数地址)。
实模式部分:始终被加载到0x90000位置,并从0x90200开始执行(linux 0.11就这样做了)。
保护模式部分:我们现在使用的内核比较大(大于512KB),叫做bzImage,加载到0x100000(高位地址,1MB)开始的位置,可以任意大小了。否则小内核zImage放在0x10000到mbi.mem_lower * 1024(一般是0x90000)区域。
3.2.2.6. linux_boot()
我们正常的启动过程调用的是big_linux_boot()函数,把实模式部分copy到0x90000后,设置其他段寄存器值位0x9000,设置CS:IP=9020:0000开始执行(使用far jmp)。
至此GRUB的工作完成,接下来执行权交给Linux了。
4. setup.S
该文件在arch/i386/boot/setup.S,主要作用是收集硬件信息并进入保护模式head.S。初始的CPU上下文如下:
eax=00000000 ebx=00009000 ecx=00000000 edx=00000003 esi=002d8b54 edi=0009a000
eip=00000000 esp=00009000 ebp=00001ff0 iopl=0 nv up di pl zr na po nc
cs=9020 ds=9000 es=9000 fs=9000 gs=9000 ss=9000 eflags=00000046
4.1. 自身检查
先检查自己setup.S是否合法,主要是检查末尾的两个magic是否一致
# Setup signature -- must be last
setup_sig1: .word SIG1
setup_sig2: .word SIG2
4.2. 收集硬件信息
主要是通过BIOS中断来收集硬件信息。收集的信息包括内存大小、键盘、鼠标、显卡、硬盘、APM等等。收集的硬件信息保存在0x9000处:
# 设置ds = 0x9000,用来保存硬件信息
movw %cs, %ax # aka SETUPSEG
subw $DELTA_INITSEG, %ax # aka INITSEG
movw %ax, %ds
这里看一下如何获取内存大小,这样OS才能进行内存管理。这里用三种方法获取内存信息:
e820h:请求中断INT 15H,AX=E820H时返回可用的物理内存信息,e820由此得名,参考此处。由于内存的使用是不连续的,通过连续调用INT 15H得到所有可用的内存区域,每次查询得到的结果ES:DI是个struct address_range_descriptor结构体,返回的结果都是64位的,完全能够满足目前PC的需求了。
struct address_range_descriptor {
uint32_t base_addr_low; // 起始物理地址
uint32_t base_addr_high;
uint32_t length_low; // 长度
uint32_t length_high;
uint8_t type; // 1=OS可用的, 2=保留的,OS不可用
};
e801h:通过请求中断INT15h,AX=e801H返回结果,最高只能得到4GB内存结果。
88h:古老的办法,通过请求中断INT15h,AH=88H返回结果。最高只能得到16MB或者64MB的内存,现在的电脑不适用了。
扩展阅读:Detecting_Memory_(x86)#E820h
4.3. 启用A20
让CPU访问1MB以上的扩展内存,否则访问的是X mod 1MB的地址。下面列举三种开启A20的方法:
使用I/0端口92H,AL的将1-bit置1
inb $0x92, %al # Configuration Port A
orb $0x02, %al # "fast A20" version
andb $0xFE, %al # don't accidentally reset
outb %al, $0x92
使用BIOS中断INT 0x15, AX=0x2401
movw $0x2401, %ax
pushfl # Be paranoid about flags
int $0x15
popfl
使用键盘控制器
movb $0xD1, %al # command write
outb %al, $0x64
call empty_8042
movb $0xDF, %al # A20 on
outb %al, $0x60
call empty_8042
4.4. 进入保护模式
4.4.1. 临时的GDT和IDT
这里的IDT全部是0;Linux目前使用的GDT如下:
gdt:
.fill GDT_ENTRY_BOOT_CS,8,0
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9A00 # code read/exec
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
.word 0xFFFF # 4Gb - (0x100000*0x1000 = 4Gb)
.word 0 # base address = 0
.word 0x9200 # data read/write
.word 0x00CF # granularity = 4096, 386
# (+5th nibble of limit)
gdt_end:
这里只定义了两个DPL为0的代码段和数据段,只给内核使用的。
4.4.2. 设置CR0.PE
这里使用lmsw指令,它和mov cr0, X是等价的
movw $1, %ax # protected mode (PE) bit
lmsw %ax # This is it!
jmp flush_instr
4.5. 调转到head.S(CS:EIP=0x10:100000)
至此硬件信息就收集完成,这些收集到的硬件信息都保存在0x90000处,后续OS可以使用这些硬件信息来管理了。
5. head.S
该文件位于arch/i386/kernel/head.S,这个是内核保护模式的代码的起点,笔者电脑的位置在0x100000,此时CPU上下文是:
eax=00000001 ebx=00000000 ecx=0000ff03 edx=47530081 esi=00090000 edi=00090000
eip=00100000 esp=00008ffe ebp=00001ff0 iopl=0 nv up di pl nz na pe nc
cs=0010 ds=0018 es=0018 fs=0018 gs=0018 ss=0018 eflags=00000002
注:已经进入保护模式,CS的值是GDT表项的索引。
它的作用就是设置真正的分段机制和分页机制、启动多处理器、设置C运行环境,最后执行start_kernel()函数。
5.1. startup_32
5.1.1. 加载临时的分段机制
boot_gdt_table就是临时的GDT,其实和start.S的一样:
lgdt boot_gdt_descr - __PAGE_OFFSET
movl $(__BOOT_DS),%eax
movl %eax,%ds
movl %eax,%es
movl %eax,%fs
movl %eax,%gs
ENTRY(boot_gdt_table)
.fill GDT_ENTRY_BOOT_CS,8,0
.quad 0x00cf9a000000ffff /* kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* kernel 4GB data at 0x00000000 */
5.1.2. 初始化内核bss区和内核启动参数
为了让C代码正常运行,bss区全部清0,启动参数需要移动到boot_params位置。
5.1.3. 启动临时分页机制
临时的页表,只要能够满足内核使用就行。页目录表是swapper_pg_dir,它是一个4096大小的内存区域,默认全是0。一般__PAGE_OFFSET=0xC0000000(3GB),这是要把物理地址0x00000000映射到0xc0000000的地址空间(内核地址空间)。下面是页目录表和页表的初始化代码:
page_pde_offset = (__PAGE_OFFSET >> 20); // 3072,页目录的偏移
// 页目录表存放在pg0位置,arch/i386/kernel/vmlinux.lds中定义
movl $(pg0 - __PAGE_OFFSET), %edi
movl $(swapper_pg_dir - __PAGE_OFFSET), %edx // edx是页目录表的地址
movl $0x007, %eax /* 0x007 = PRESENT+RW+USER */
10:
// 创建一个页目录项
leal 0x007(%edi),%ecx /* Create PDE entry */
movl %ecx,(%edx) /* Store identity PDE entry */
movl %ecx,page_pde_offset(%edx) /* Store kernel PDE entry */
addl $4,%edx // 指向swapper_pg_dir的下一个项
movl $1024, %ecx // 每个页表1024个项目
11:
stosl // eax -> [edi]; edi = edi + 4
addl $0x1000,%eax // 每次循环,下一个页目录项
loop 11b
/* End condition: we must map up to and including INIT_MAP_BEYOND_END */
/* bytes beyond the end of our own page tables; the +0x007 is the attribute bits */
leal (INIT_MAP_BEYOND_END+0x007)(%edi),%ebp // 页表覆盖到这里就终止
cmpl %ebp,%eax
jb 10b
movl %edi,(init_pg_tables_end - __PAGE_OFFSET)
下面是对上面代码的翻译(这样更有利于理解):
extern uint32_t *pg0; // 初始值全0
extern uint32_t *swapper_pg_dir; // 初始值全0
void init_page_tables(){
uint32_t PAGE_FLAGS = 0x007; // PRESENT+RW+USER
uint32_t page_pde_offset = (_PAGE_OFFSET >> 20); // 3072
uint32_t addr = 0 | PAGE_FLAGS; // 内存地址+页表属性
uint32_t *pg_dir_ptr = swapper_pg_dir; // 页目录表项指针
uint32_t *pg0_ptr = pg0; // 页表项指针
for (;;) {
// 设置页目录项,同时映射两个地址,让物理地址和虚拟地址都能访问,
*pg_dir_ptr = pg0 | PAGE_FLAGS; // 0, 1
*(uint32_t *)((char *)pg_dir_ptr + page_pde_offset) = pg0 | PAGE_FLAGS; // 768, 769
pg_dir_ptr++;
// 设置页表项目
for (int i = 0; i < 1024; i++) {
*pg0++ = addr;
addr += 0x1000;
}
// 退出条件,实际上只映射了两个页目录就退出了(0,1,768, 769)
if (pg0[INIT_MAP_BEYOND_END] | PAGE_FLAGS) >= addr) {
init_pg_tables_end = pg0_ptr;
return;
}
}
};
5.1.4. 设置栈
/* Set up the stack pointer */
lss stack_start,%esp
ENTRY(stack_start)
.long init_thread_union+THREAD_SIZE
.long __BOOT_DS
/* arch/i386/kernel/init_task.c
* Initial thread structure.
*
* We need to make sure that this is THREAD_SIZE aligned due to the
* way process stacks are handled. This is done by having a special
* "init_task" linker map entry..
*/
union thread_union init_thread_union
__attribute__((__section__(".data.init_task"))) =
{ INIT_THREAD_INFO(init_task) };
内核最初使用的栈是init_task进程的,也就是0号进程的栈,这个进程是系统唯一一个静态定义而不是通过fork()产生的进程。
5.1.5. 设置真正的IDT和GDT
lgdt cpu_gdt_descr // 真正的GDT
lidt idt_descr //真正的IDT
ljmp $(__KERNEL_CS),$1f // 重置CS
1: movl $(__KERNEL_DS),%eax # reload all the segment registers
movl %eax,%ss # after changing gdt. // 重置SS
movl $(__USER_DS),%eax # DS/ES contains default USER segment
movl %eax,%ds
movl %eax,%es
xorl %eax,%eax # Clear FS/GS and LDT
movl %eax,%fs
movl %eax,%gs
lldt %ax
cld # gcc2 wants the direction flag cleared at all times
// push一个假的返回地址以满足 start_kernel()函数return的要求
pushl %eax # fake return address
对于IDT先全部初始化成ignore_int例程:
setup_idt:
lea ignore_int,%edx
movl $(__KERNEL_CS << 16),%eax
movw %dx,%ax /* selector = 0x0010 = cs */
movw $0x8E00,%dx /* interrupt gate - dpl=0, present */
lea idt_table,%edi
mov $256,%ecx
rp_sidt:
movl %eax,(%edi)
movl %edx,4(%edi)
addl $8,%edi
dec %ecx
jne rp_sidt
ret
ignore_int例程就干一件事,打印一个错误信息"Unknown interrupt or fault at EIP %p %p %p\n"。
对于GDT我们最关心的__KERNEL_CS、__KERNEL_DS、__USER_CS、__USER_DS这4个段描述符:
.quad 0x00cf9a000000ffff /* 0x60 kernel 4GB code at 0x00000000 */
.quad 0x00cf92000000ffff /* 0x68 kernel 4GB data at 0x00000000 */
.quad 0x00cffa000000ffff /* 0x73 user 4GB code at 0x00000000 */
.quad 0x00cff2000000ffff /* 0x7b user 4GB data at 0x00000000 */
至此分段机制、分页机制、栈都设置好了,接下去可以开心的jmp start_kernel了。
6. start_kernel
该函数在linux/init/main.c文件里。我们可以认为start_kernel是0号进程init_task的入口函数,0号进程代表整个linux内核且每个CPU有一个。这个函数开始做一系列的内核功能初始化,我们重点看rest_init()函数。
6.1.rest_init
这是start_kernel的最后一行,它启动一个内核线程运行init函数后就什么事情也不做了(死循环,始终交出CPU使用权)。
static void noinline rest_init(void){
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); // 启动init
……
/* Call into cpu_idle with preempt disabled */
cpu_idle(); // 0号进程什么事也不做
}
6.2. init()
该函数的末尾fork了”/bin/init”进程。这样1号进程init就启动了,接下去就交给init进程去做应用层该做的事情了!
// 以下进程启动后父进程都是0号进程
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n",
ramdisk_execute_command);
}
/*
* We try each of these until one succeeds.
*
* The Bourne shell can be used instead of init if we are
* trying to recover a really broken machine.
*/
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
附录1. 启动多核CPU
以上解读的内容只在0号CPU上执行,如果是多CPU的环境,还要初始化其他的CPU。多CPU启动的起点是start_kernel()->rest_init()->init()->smp_init()。而smp_init()函数给每个CPU上调用cpu_up()和do_boot_cpu()函数,每个CPU都要再走一遍head.S的流程,然后启动自己的idle进程(内核态0号进程)。
附录2. x64的不同
i386和x64的启动代码主要区别在head.S中。
页表格式不同,i386使用两级页表,x64使用4级页表。
多了兼容32位的代码段和数据段__USER32_CS、__USER32_DS和__KERNEL32_CS。
x64段寄存器用法和i386的不同:x64下面CS、DS、ES、SS不用了,始终为0。而FS、GS寄存器的用法倒像是实模式下的,主要考虑是保留两个作为基地址好让线性地址计算方便。FS:XX = MSR_FS_BASE + XX, GS:XX = MSR_GS_BASE + XX, 不是段描述符索引了(像实模式的分段)。
Linux系统构建之启动
1、Linux启动
Linux操作系统不仅仅有Linux的内核,更需要具备各种服务才能成为一个完整的操作系统。了解Linux系统的启动过程,对理解Linux系统是非常有益的.但是Linux系统涉及的服务众多, 不同的发行版又有差异,所以这里只描述一个轮廓,日后逐渐充实。
BIOS
按下电源的时候,CPU执行的第一条指令是BIOS(基本输入输出系统)。BIOS执行的最后将启动磁盘的第一个扇区(MBR)加载到内存中固定的位置(0000:7C00),然后CPU开始执行0000:7C00处的指令,也就是MBR中存储的指令。
MBR(Master Boot Record, 主引导记录),是磁盘的第一个扇区,前446字节存放引导程序,中间64字节存放磁盘分区表,最后两个字节固定为0x55、0xAA(0x550xAA是识别MBR的标记)。内存中0000:7C00之前的位置用来存放BIOS中断表等BIOS程序加载进去的内容。
引导程序
跳转到0000:7C00后,开始执行的就是引导程序。因为MBR只有512字节,所以引导程序不完成太多任务,主要是继续加载更多的内容。Linux系统的引导程序通常是grub。
grub2.0中,执行的第一条指令(加载到0000:7C00处的指令)在grub-2.00.tar\grub-2.00\grub-2.00\grub-core\boot\i386\pc\boot.S中.
.globl _start, start;
_start:
start:
/*
* _start is loaded at 0x7c00 and is jumped to with CS:IP 0:0x7c00
*/
/*
* Beginning of the sector is compatible with the FAT/HPFS BIOS
* parameter block.
*/
jmp LOCAL(after_BPB)
nop /* do I care about this ??? */
可以从boot.S开始梳理Grub的引导过程。
引导程序通过BIOS中断完成对硬件的操作。
grub的安装方式非常简单,假设我们要在磁盘sda中安装grub,并且将grub需要的文件存放在第一个分区sda1中。
mount /dev/sda1 /mnt
grub-install --root-directory=/mnt --no-floppy sda
执行上述命令后,sda的MBR就被写入了grub的引导程序,在/mnt目录下则多出一个boot目录,里面存放着grub需要的文件。这时候从sda启动就可以看到grub的执行(如果要看到完整的引导,需要在boot目录下进行grub的引导配置,指定内核和initrd)。
grub引导的具体过程涉及的内容比较复杂。其将内核加载,并加载了一个位于内存的根文件系统,作为内核看到的第一个根文件系统。
initrd
initrd是内核看到的第一个根文件系统,这个根文件系统中包含必要的驱动和设备文件,在这里加载驱动后,才能读取到最终的文件系统,这时切换到最终的根文件系统。可以将initrd解压,查看包含的内容和完成的操作。
CentOS系列的发行版可以在/boot下找到img文件,例如initrd-2.6.18-308.el5.img,这个就是initrd。在grub的grub.conf可以看到系统启动时使用的时那个img。
initrd-2.6.18-308.el5.img是一个gzip压缩文件,首先需要修改后缀,然后用gunzip解压, 再用cpio读取。
cp initrd-2.6.18-308.el5.img /tmp/initrd-2.6.18-308.el5.img.gz
cd /tmp
gunzip initrd-2.6.18-308.el5.img.gz
mkdir cpio;
cd cpio;
cpio -i -d < ../initrd-2.6.18-308.el5.img
initrd中包含一个init程序, 这是内核执行的第一个程序,CentOS中init是一个nash脚本。
CentOS中init的内容:
(挂载虚拟文件系统)
echo Creating /dev
mount -o mode=0755 -t tmpfs /dev /dev #dev使用虚拟文件系统tmpfs(内存)中
mkdir /dev/pts #/dev/pts存放ssh登陆时生成的虚拟终端设备文件
mount -t devpts -o gid=5,mode=620 /dev/pts /dev/pts #/dev/pts的文件系统类型devpts, 虚拟终端文件设备
mkdir /dev/shm #/dev/shm 只建立目录
mkdir /dev/mapper #/dev/mapper 只建立目录
(创建必要的设备文件)
echo Creating initial device nodes
mknod /dev/null c 1 3
mknod /dev/zero c 1 5
mknod /dev/urandom c 1 9
mknod /dev/systty c 4 0
...
...
mknod /dev/ttyS3 c 4 67
echo Setting up hotplug.
hotplug
echo Creating block device nodes.
mkblkdevs
(加载必要的驱动)
echo "Loading ehci-hcd.ko module"
insmod /lib/ehci-hcd.ko
echo "Loading ohci-hcd.ko module"
insmod /lib/ohci-hcd.ko
echo "Loading uhci-hcd.ko module"
insmod /lib/uhci-hcd.ko
mount -t usbfs /proc/bus/usb /proc/bus/usb
echo "Loading jbd.ko module"
insmod /lib/jbd.ko
echo "Loading vmxnet3.ko module"
...
...
insmod /lib/vmxnet3.ko
echo "Loading pvscsi.ko module"
insmod /lib/pvscsi.ko
echo "Loading vmxnet.ko module"
insmod /lib/vmxnet.ko
echo Waiting for driver initialization.
stabilized --hash --interval 1000 /proc/scsi/scsi
mkblkdevs
echo Scanning and configuring dmraid supported devices
resume LABEL=SWAP-sda2
(切换到最终的根文件系统)
echo Creating root device.
mkrootdev -t ext3 -o defaults,ro /dev/sda3 #定义root路径
echo Mounting root filesystem.
mount /sysroot #将实体根目录挂载到sysroot
echo Setting up other filesystems.
setuproot #将通过initrd的init建立的/proc /sys /dev目录中的资料转移到/sysroot
echo Switching to new root and running init.
switchroot #切入实体根目录,将原先系统的所有内容清空
initrd最后切换的最终的根文件系统,位于内存中的临时的根文件系统随之被清空。
系统的初始化
切换到最终的根文件系统后,开始了Linux操作系统的初始化;这时候就看到发行版之间的差异了,不同的发行版有不同的处理方式,初始化的内容也不尽相同。
初始化程序是一系列的shell脚本,繁多复杂,牵扯到太多的服务,如果能够全部了解,那么对Linux系统将会非常熟悉。
初始化脚本的组织方式主要分为System V(CentOS)和event-base(Ubuntu)两种风格。
在System V风格中,init通过/etc/inittab文件的内容得知初始化脚本、运行级别、每个级别的初始化脚本。
在event-base风格中,init遍历/etc/init目录中的事件文件,执行需要执行的脚本。
CentOS 5.8版本中使用的System V风格,6.4版本中换用了event-base风格。
sysvinit - System V风格
System V风格使用sysinit机制,所有init要执行的事项都在/etc/inittab文件中指明。
/etc/inittab是init的配置文件,指定了需要运行的脚本。可以查看对应的linux手册:man 5 inittab。/etc/inittab中的每一行都使用下面的格式:
id:runlevels:action:process
id是由1-4个字符组成,用来唯一标记每一行。
runlevels指定该行使用的运行级别.
action指定init应该采取的动作
respawn: 如果process运行结束,init立即重新调用process
wait: process执行一次,init等待process运行结束
once: process执行一次
boot: 在系统启动时运行process,忽略runlevel
bootwait: 在系统启动时运行process,init等待process运行结束
off: 什么都不做
ondemand:
initdefault: 默认的运行级别
sysinit: 系统初始化执行,在boot和bootwait之前执行
powerwait:
powerfail:
powerokwait:
powerfailnow:
ctrlaltdel: init收到ctl_alt_del组合键信号时执行
kbrequest:
process是要执行的命令.
下面是CentOS 5.8的/etc/inittab文件.
默认运行级别是5
init进程依次执行/etc/rc.d/rc.sysinit
对应运行级别的process(/etc/rd.c/rc 0)
重复运行/sbin/mingetty ttyXX.
id:5:initdefault:
# System initialization.
si::sysinit:/etc/rc.d/rc.sysinit
l0:0:wait:/etc/rc.d/rc 0
l1:1:wait:/etc/rc.d/rc 1
l2:2:wait:/etc/rc.d/rc 2
l3:3:wait:/etc/rc.d/rc 3
l4:4:wait:/etc/rc.d/rc 4
l5:5:wait:/etc/rc.d/rc 5
l6:6:wait:/etc/rc.d/rc 6
# Trap CTRL-ALT-DELETE
ca::ctrlaltdel:/sbin/shutdown -t3 -r now
# When our UPS tells us power has failed, assume we have a few minutes
# of power left. Schedule a shutdown for 2 minutes from now.
# This does, of course, assume you have powerd installed and your
# UPS connected and working correctly.
pf::powerfail:/sbin/shutdown -f -h +2 "Power Failure; System Shutting Down"
# If power was restored before the shutdown kicked in, cancel it.
pr:12345:powerokwait:/sbin/shutdown -c "Power Restored; Shutdown Cancelled"
# Run gettys in standard runlevels
1:2345:respawn:/sbin/mingetty tty1
2:2345:respawn:/sbin/mingetty tty2
3:2345:respawn:/sbin/mingetty tty3
4:2345:respawn:/sbin/mingetty tty4
5:2345:respawn:/sbin/mingetty tty5
6:2345:respawn:/sbin/mingetty tty6
upstart - event-base风格
event-base风格的初始化机制,称为upstart机制.初始化过程被拆分成一个个事件,每个事件都各自的事件文件,事件文件中指明事件执行的时机和要执行的操作。
事件文件存放在/etc/init目录下,启动时init进程到/etc/init目录扫描执行需要执行的事件文件。
可以指定事件直接的依赖关系,例如B事件必须在A事件完成后开始。
init进程发出第一个事件startup,然后执行在startup时执行的事件,这批事件执行结束后各自发出相应的事件(事件文件中的emits命令),从而又启动一批事件,直至完成。
下面是CentOS 6.4中的事件文件/erc/init/rcS.conf
指定了事件执行的时机(start on startup)
停止的时机(stop on runlevel)
事件类型(task)
信息打印输出的位置(console output)
事件执行前进行的操作(pre-start script .... end script)
事件(exec /etc/rc.d/rc.sysinit)
事件结束后执行的操作(post-stop script ... end script)
# rcS - runlevel compatibility
#
# This task runs the old sysv-rc startup scripts.
start on startup
stop on runlevel
task
# Note: there can be no previous runlevel here, if we have one it's bad
# information (we enter rc1 not rcS for maintenance). Run /etc/rc.d/rc
# without information so that it defaults to previous=N runlevel=S.
console output
pre-start script
for t in $(cat /proc/cmdline); do
case $t in
emergency)
start rcS-emergency
break
;;
esac
done
end script
exec /etc/rc.d/rc.sysinit
post-stop script
if [ "$UPSTART_EVENTS" = "startup" ]; then
[ -f /etc/inittab ] && runlevel=$(/bin/awk -F ':' '$3 == "initdefault" && $1 !~ "^#" { print $2 }' /etc/inittab)
[ -z "$runlevel" ] && runlevel="3"
for t in $(cat /proc/cmdline); do
case $t in
-s|single|S|s) runlevel="S" ;;
[1-9]) runlevel="$t" ;;
esac
done
exec telinit $runlevel
fi
end script
从上面的事件文件中可以看到,CentOS6.4将原先在inittab中指定的rc.sysinit定位为一个rcS.conf事件,并且从inittab中获取运行级别,通过这种方式兼容了以往的System V风格的初始化过程。
Ubuntu中事件文件使用同样的格式
查看事件文件的linux帮助手册:man 5 init
第一批事件
以下几个事件要在startup时执行。
freeoa@ubuntu:/etc/init$ grep startup *
hostname.conf:start on startup
module-init-tools.conf:start on (startup
mountall.conf:start on startup
udev-fallback-graphics.conf:start on (startup and
udev-finish.conf:start on (startup
udevmonitor.conf:start on (startup
udevtrigger.conf:start on (startup
过滤掉同时依赖别的事件的事件后,得到最早执行的两个事件:
hostname.conf
mountall.conf
通过下面分析可以知道,第一批事件完成了两个工作,设置主机名,加载文件系统(磁盘)。
hostname事件只是简单的设置系统的主机名。
# hostname - set system hostname
#
# This task is run on startup to set the system hostname from /etc/hostname,
# falling back to "localhost" if that file is not readable or is empty and
# no hostname has yet been set.
description "set system hostname"
start on startup
task
exec hostname -b -F /etc/hostname
mountall发出的多个事件,带动其他事件的执行。
# mountall - Mount filesystems on boot
#
# This helper mounts filesystems in the correct order as the devices
# and mountpoints become available.
description "Mount filesystems on boot"
start on startup
stop on starting rcS
expect daemon
task
emits virtual-filesystems
emits local-filesystems
emits remote-filesystems
emits all-swaps
emits filesystem
emits mounting
emits mounted
# temporary, until we have progress indication
# and output capture (next week :p)
console output
script
. /etc/default/rcS
[ -f /forcefsck ] && force_fsck="--force-fsck" # 环境变量指示是否需要修复文件系统
[ "$FSCKFIX" = "yes" ] && fsck_fix="--fsck-fix"
# set $LANG so that messages appearing in plymouth are translated
if [ -r /etc/default/locale ]; then
. /etc/default/locale
export LANG LANGUAGE LC_MESSAGES LC_ALL # 语言环境
fi
exec mountall --daemon $force_fsck $fsck_fix # mountall命令,加载fstab中指定的挂载
end script
post-stop script # 事件结束
rm -f /forcefsck 2>dev/null || true
end script
systemd
systemd supports SysV and LSB init scripts and works as a replacement for sysvinit
systemd
redhat intro
Rethinking PID 1
2、CentOS initrd分析
解压initrd
在/boot下找到initrd-2.6.18-308.el5.img,复制到另一个目录下(防止操作将原文件覆盖),并修改后缀名为.gz,如下:
cp initrd-2.6.18-308.el5.img /root/initrd/initrd-2.6.18-308.el5.img.gz
initrd-2.6.18-308.el5.img是一个gzip压缩文件,修改后缀后,使gunzip可以识别解压:
gunzip initrd-2.6.18-308.el5.img.gz
解压后得到.img文件,使用cpio解压.imgd到一个新目录中:
mkdir cpio;
cd cpio;
cpio -i -d < ../initrd-2.6.18-308.el5.img
在cpio目录下得到initrd中所有文件:
bin dev etc init lib proc sbin sys sysroot
cpio中的文件就是内核在initrd环境中运行时使用的根文件系统。其中的init时执行的第一个进程。CentOS 5.8的initrd中的cpio/init是一个nash脚本,完成到磁盘上的根文件系统的切换。
nash是专门用于inird的类似shell的解析程序,内置了一些必用的命令。
cpio/init完成以下工作:
挂载虚拟文件系统
echo Creating /dev
mount -o mode=0755 -t tmpfs /dev /dev #dev使用虚拟文件系统tmpfs(内存)中
mkdir /dev/pts #/dev/pts存放ssh登陆时生成的虚拟终端设备文件
mount -t devpts -o gid=5,mode=620 /dev/pts /dev/pts #/dev/pts的文件系统类型devpts, 虚拟终端文件设备
mkdir /dev/shm #/dev/shm 只建立目录
mkdir /dev/mapper #/dev/mapper 只建立目录
最初需要的设备文件
echo Creating initial device nodes
mknod /dev/null c 1 3
mknod /dev/zero c 1 5
mknod /dev/urandom c 1 9
mknod /dev/systty c 4 0
mknod /dev/tty c 5 0
mknod /dev/console c 5 1
mknod /dev/ptmx c 5 2
mknod /dev/rtc c 10 135
mknod /dev/tty0 c 4 0
mknod /dev/tty1 c 4 1
mknod /dev/tty2 c 4 2
mknod /dev/tty3 c 4 3
mknod /dev/tty4 c 4 4
mknod /dev/tty5 c 4 5
mknod /dev/tty6 c 4 6
mknod /dev/tty7 c 4 7
mknod /dev/tty8 c 4 8
mknod /dev/tty9 c 4 9
mknod /dev/tty10 c 4 10
mknod /dev/tty11 c 4 11
mknod /dev/tty12 c 4 12
mknod /dev/ttyS0 c 4 64
mknod /dev/ttyS1 c 4 65
mknod /dev/ttyS2 c 4 66
mknod /dev/ttyS3 c 4 67
echo Setting up hotplug.
hotplug
echo Creating block device nodes.
mkblkdevs
加载相关模块
echo "Loading ehci-hcd.ko module"
insmod /lib/ehci-hcd.ko
echo "Loading ohci-hcd.ko module"
insmod /lib/ohci-hcd.ko
echo "Loading uhci-hcd.ko module"
insmod /lib/uhci-hcd.ko
mount -t usbfs /proc/bus/usb /proc/bus/usb
echo "Loading jbd.ko module"
insmod /lib/jbd.ko
echo "Loading ext3.ko module"
insmod /lib/ext3.ko
echo "Loading scsi_mod.ko module"
insmod /lib/scsi_mod.ko
echo "Loading sd_mod.ko module"
insmod /lib/sd_mod.ko
echo "Loading scsi_transport_spi.ko module"
insmod /lib/scsi_transport_spi.ko
echo "Loading mptbase.ko module"
insmod /lib/mptbase.ko
echo "Loading mptscsih.ko module"
insmod /lib/mptscsih.ko
echo "Loading mptspi.ko module"
insmod /lib/mptspi.ko
echo "Loading libata.ko module"
insmod /lib/libata.ko
echo "Loading ata_piix.ko module"
insmod /lib/ata_piix.ko
echo "Loading dm-mem-cache.ko module"
insmod /lib/dm-mem-cache.ko
echo "Loading dm-mod.ko module"
insmod /lib/dm-mod.ko
echo "Loading dm-log.ko module"
insmod /lib/dm-log.ko
echo "Loading dm-region_hash.ko module"
insmod /lib/dm-region_hash.ko
echo "Loading dm-message.ko module"
insmod /lib/dm-message.ko
echo "Loading dm-raid45.ko module"
insmod /lib/dm-raid45.ko
echo "Loading vmxnet3.ko module"
insmod /lib/vmxnet3.ko
echo "Loading pvscsi.ko module"
insmod /lib/pvscsi.ko
echo "Loading vmxnet.ko module"
insmod /lib/vmxnet.ko
echo Waiting for driver initialization.
stabilized --hash --interval 1000 /proc/scsi/scsi
mkblkdevs
echo Scanning and configuring dmraid supported devices
resume LABEL=SWAP-sda2
切入实体系统
echo Creating root device.
mkrootdev -t ext3 -o defaults,ro /dev/sda3 #定义root路径
echo Mounting root filesystem.
mount /sysroot #将实体根目录挂载到sysroot
echo Setting up other filesystems.
setuproot #将通过initrd的init建立的/proc /sys /dev目录中的资料转移到/sysroot
echo Switching to new root and running init.
switchroot #切入实体根目录,将原先系统的所有内容清空
切换完成
切换完成后,首先执行的是实体系统中的init进程。
实验
重新打包成cpio
find . | cpio -o -H newc > ../initrd_new.img #将当前目录下所有文件打包成cpo
cd ..; gzip initrd_new.img
将得到的initrd_new.img.gz复制到/boot目录下。 在/boot/grub/grub.conf中增加新的启动项:
title initrd_new (2.6.18-308.el5)
root (hd0,0)
kernel /vmlinuz-2.6.18-308.el5 ro root=LABEL=/
initrd /initrd_new.img.gz
将busybox集成到initrd
解压,编译,安装
make menconfig #需要安装curses-devel,最好选用编译成static,不然需要添加依赖的动态库
make #如果内核版本还没直至ubi, 在配置中将ubi相关的选项关闭(.config中),ubi是新的flash文件系统
make CONFIG_PREFIX=/path/from/root install #安装到的目录
3、Linux系统定制
自己动手定制一个linux,可以解除很多的疑惑,破除种种神秘。首先要有一台Linux机器:分为安装grub、编译内核、制作initrd三步。
安装grub
假设目标磁盘的设备文件是sdb,首先sdb进行分区、格式化;假设分成sdb1、sdb2,然后将sdb1挂载到/mnt
执行grub安装
grub-install --root-directory=/mnt --no-floppy sdb
执行上述命令后, 在/mnt目录下会多出一个boot目录。将来内核、initrd都会存放在boot目录下。
在/mnt/boot/grub下配置grub.conf。
可以参照原有系统的grub配置文件(/boot/grub/grub.conf), 这时候从sdb启动时就会进入到grub。
制作initrd
新建一个目录initrd
mkdir initrd
将需要的东西复制到initrd目录中
例如将busybox安装到initrd中、创建必要的几个设备文件、init程序
可以查看当前使用的系统initrd,进行参照
解压方式: cpio -idmv < filename.img
生成img
find . | cpio -o -H newc |gzip -9 >initrd.img
特别注意这一步必须在initrd目录下进行,这样才能保证解压后的内容。
丰富initrd
当可以启动进入到initrd的时候, 就说明已经制作成功。接下来需要做的就是在其中添加各种新的程序。
4、参考
《深度探索Linux操作系统–系统构建和原理解析》 王柏生
《深入探索Linux操作系统的奥秘》