初识Linux初始化Init系统


init(为英语:initialization的简写)是 Unix 和类Unix 系统中用来产生其它所有进程的程序。它以守护进程的方式存在,其进程号为1。
Unix 系列中(如 System III 和 System V)init的作用,和研究中的 Unix 和 BSD 派生版本相比,发生了一些变化。大多数Linux发行版是和 System V 相兼容的,但是一些发行版如 Slackware 采用的是BSD风格,其它的如 Gentoo 是自己定制的。Ubuntu和其它一些发行版现在开始采用Upstart来代替传统的 init 进程(现也转向Systemd)。在 Linux 和其他类 Unix 系统中,init(初始化)进程是系统启动时由内核执行的第一个进程,其进程 ID(PID)为 1,并静默运行在后台,直到系统关闭。
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盘点
init 进程负责启动其他所有的进程,比如守护进程、服务和其他后台进程,因此它是系统中其它所有进程之母(但却叫做“父进程”)。某个进程可以启动许多个子进程,但在这个过程中,某个子进程的父进程结束之后,该子进程的父进程会变成 init 进程。这么多年过去了,许多的初始化系统在主流 Linux 脱颖而出,来一起盘一下 Linux 操作系统最好的初始化系统。
1. System V Init
System V (SysV) 是一个在类 Unix 系统中最为成熟而且大受欢迎的初始化方案,是 Unix/Linux 系统中所有进程的父进程。SysV 是第一个商业 Unix 系统设计的初始化方案。
除了 Gentoo 使用自主的初始化系统、Slackware 使用 BSD 风格的初始化方案外,几乎所有的 Linux 发行版都在早期率先使用 SysV 作为初始化方案。
随着时间的推移,由于一些设计上的缺陷,有几个 SysV 初始化替换方案已经开发出来,用以为 Linux 创建更加高效和完美的初始化系统。
尽管这些替代方案都超越了 SysV 并提供了更多新特性,但它们仍然和原始 SysV 初始化脚本保持兼容。
2. SystemD
SystemD 是一个 Linux 平台中相对较新的初始化方案。它由 Fedora v15 引入,集成了各类工具以便更好的管理系统。主要目的是:系统初始化、管理和跟踪引导进程中和系统运行时所有的系统进程。它全面有别于其他传统的 Unix 初始化系统,特别是在启动系统和服务管理方面。它同样兼容 SysV 和 LBS 初始化脚本。
其中较为突出的特性如下:
纯粹、高效的设计
启动时的并发和并行处理
更好的 API
开启可选进程的移除功能
使用 journald 来支持事件日志
使用 systemd calender timers 来支持任务计划
以二进制文件存储日志
保存 systemd 的状态以待今后查看
与 GNOME 更好整合实现等
3. Upstart
Upstart 是一个基于事件的初始化系统,由 Ubuntu 的制作团队开发的,用以替代 SysV。它可以启动不同的系统任务和进程、在系统运行时校验进程并在系统关闭时结束进程。
它是一个使用 SysV 和 Systemd 启动脚本的混合初始化系统,其值得一提的特性如下:
Ubuntu 的原生初始化系统,但可以运行在其他所有的发行版中
基于事件启动/结束的任务和服务
启动/结束任务和服务时生成事件
可以由其他系统进程发送事件
使用 D-Bus 和 init 进程通信
用户可以启动/结束其各自的进程
可以再现崩溃的进程等
4. OpenRC
OpenRC 是一个基于依赖关系的类 Unix 系统初始化方案,兼容 SysV。基本可以说是 SysV 的升级版,但必须要明白的是:OpenRC 并非只是完全替代 /sbin/init 文件。
它所提供的出色特性如下:
可运行在包括 Gentoo 和 BSD 在内的多数 Linux 系统之中
支持硬件触发的初始化脚本
支持单个配置文件
不支持单个服务配置文件
以守护进程的方式运行
并行服务启动等
5. Runit
runit 同样是一个跨平台初始化系统,可以运行在 GNU/Linux、Solaris、BSD 和 Mac OS X 中,用替代 SysV,同时提供服务监控。
相比于 SysV 和其他 Linux 初始化系统,它提供了一些好用和卓越的组件,如下:
服务监控:每个服务都关联一个服务目录
清理进程状态,以保证每个进程处于干净状态
可靠的日志机制
快速的系统启动和关闭
可移植
打包方便
代码体积小等
Linux 中的初始化系统负责启动和管理所有的进程。此外,SysV 是 Linux 系统中主要的初始化系统,但由于一些性能缺陷,系统开发者已经开发出几个替代品。
其他风格
很多人一直努力地从某些方面改进传统的 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默认使用
Gentoo的Wiki页面有一众init进程轻度对比(Comparison of init systems)。
进程管理工具
1、Supervisor
supervisorctl是Supervisor提供的命令行客户端,而Supervisor是一个进程控制系统。它用于在UNIX类操作系统上控制和监视进程,通常用于服务器环境中管理长时间运行的进程。它不是大多数 UNIX 系统的标准部分,因此通常需要单独安装。
2、Systemd
systemd 是现代 Linux 系统上的一个标准进程管理和初始化系统,它用于替代传统的 SysV init 系统。它通过使用称为 systemctl 的命令行工具来管理服务和系统资源。
3、init.d
在 systemd 出现之前,许多 UNIX 和 Linux 系统使用了名为 init.d 的传统初始化系统。init.d 脚本通常位于 /etc/init.d/ 目录下,可用于启动、停止、重启和管理各种服务。与之配套的管理工具较多如service、chkconfig等。
4、Upstart
Upstart 是一个由 Ubuntu 开发的事件驱动机制以替代传统 init 系统,而被主动放弃。它被设计为更灵活和适应更多现代计算环境的要求,尤其是在处理异步和并发启动的场景。
5、Monit
Monit 是一个小型的开源工具,用于管理和监控 UNIX 系统上的进程、程序、文件、目录和文件系统。它可以用来自动修复问题,如重启失败的服务,或者在检测到问题时发送警报。
6、Runit
runit 是一个跨平台的进程监视器和服务管理工具。它被设计为 SysV init 和其他重型管理工具的替代品,提供快速、可靠和简单的服务管理工具。
7、Daemontools
Daemontools 是一个用于监控和控制服务的集合工具,它提供了一种确保服务持续运行的简单方式。
该命令还支持以下参数:
-u : up, 如果services没有运行的话,启动它,如果services停止了,重启它。
-d : down, 如果services正在运行的话,给它发送一个TERM(terminate)信号,然后再发送一个CONT(continue)信号,在它停止后,不再启动它。
-o : once, 如果services没有运行,启动它,但是在它停止后不再启动了。就是只运行一次。
-p : pause, 给services发送一个停止信号。
-c : continue, 给services发送一个CONT信号。
-h : hang up, 给services发送一个HUP信号。
-a : alarm, 给services发送一个ALRM信号。
-i : interrupt, 给services发送一个INT信号。
-t : Terminate, 给services发送一个TERM信号。
-k : kill, 给services发送一个KILL信号。
-x : exit, supervise在services停止后会立刻退出,但是值得注意的是,如果在一个稳定的系统中使用了这个选项,已经开始犯错了:supervise被设计成为永远运行的。
Daemontools、Supervisor、Monit三者均不具备系统级init功能,但其提供了守护进程管理的功能,常驻后台的(守护)进程不小心挂掉后可以帮助重启该服务。当然Systemd也同样具备此功能。
关于 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)。在 Linux 和其他类 Unix 系统中,init(初始化)进程是系统启动时由内核执行的第一个进程,其进程 ID(PID)为 1,并静默运行在后台,直到系统关闭。
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盘点
init 进程负责启动其他所有的进程,比如守护进程、服务和其他后台进程,因此它是系统中其它所有进程之母(但却叫做“父进程”)。某个进程可以启动许多个子进程,但在这个过程中,某个子进程的父进程结束之后,该子进程的父进程会变成 init 进程。这么多年过去了,许多的初始化系统在主流 Linux 脱颖而出,来一起盘一下 Linux 操作系统最好的初始化系统。
1. System V Init
System V (SysV) 是一个在类 Unix 系统中最为成熟而且大受欢迎的初始化方案,是 Unix/Linux 系统中所有进程的父进程。SysV 是第一个商业 Unix 系统设计的初始化方案。
除了 Gentoo 使用自主的初始化系统、Slackware 使用 BSD 风格的初始化方案外,几乎所有的 Linux 发行版都在早期率先使用 SysV 作为初始化方案。
随着时间的推移,由于一些设计上的缺陷,有几个 SysV 初始化替换方案已经开发出来,用以为 Linux 创建更加高效和完美的初始化系统。
尽管这些替代方案都超越了 SysV 并提供了更多新特性,但它们仍然和原始 SysV 初始化脚本保持兼容。
2. SystemD
SystemD 是一个 Linux 平台中相对较新的初始化方案。它由 Fedora v15 引入,集成了各类工具以便更好的管理系统。主要目的是:系统初始化、管理和跟踪引导进程中和系统运行时所有的系统进程。它全面有别于其他传统的 Unix 初始化系统,特别是在启动系统和服务管理方面。它同样兼容 SysV 和 LBS 初始化脚本。
其中较为突出的特性如下:
纯粹、高效的设计
启动时的并发和并行处理
更好的 API
开启可选进程的移除功能
使用 journald 来支持事件日志
使用 systemd calender timers 来支持任务计划
以二进制文件存储日志
保存 systemd 的状态以待今后查看
与 GNOME 更好整合实现等
3. Upstart
Upstart 是一个基于事件的初始化系统,由 Ubuntu 的制作团队开发的,用以替代 SysV。它可以启动不同的系统任务和进程、在系统运行时校验进程并在系统关闭时结束进程。
它是一个使用 SysV 和 Systemd 启动脚本的混合初始化系统,其值得一提的特性如下:
Ubuntu 的原生初始化系统,但可以运行在其他所有的发行版中
基于事件启动/结束的任务和服务
启动/结束任务和服务时生成事件
可以由其他系统进程发送事件
使用 D-Bus 和 init 进程通信
用户可以启动/结束其各自的进程
可以再现崩溃的进程等
4. OpenRC
OpenRC 是一个基于依赖关系的类 Unix 系统初始化方案,兼容 SysV。基本可以说是 SysV 的升级版,但必须要明白的是:OpenRC 并非只是完全替代 /sbin/init 文件。
它所提供的出色特性如下:
可运行在包括 Gentoo 和 BSD 在内的多数 Linux 系统之中
支持硬件触发的初始化脚本
支持单个配置文件
不支持单个服务配置文件
以守护进程的方式运行
并行服务启动等
5. Runit
runit 同样是一个跨平台初始化系统,可以运行在 GNU/Linux、Solaris、BSD 和 Mac OS X 中,用替代 SysV,同时提供服务监控。
相比于 SysV 和其他 Linux 初始化系统,它提供了一些好用和卓越的组件,如下:
服务监控:每个服务都关联一个服务目录
清理进程状态,以保证每个进程处于干净状态
可靠的日志机制
快速的系统启动和关闭
可移植
打包方便
代码体积小等
Linux 中的初始化系统负责启动和管理所有的进程。此外,SysV 是 Linux 系统中主要的初始化系统,但由于一些性能缺陷,系统开发者已经开发出几个替代品。
其他风格
很多人一直努力地从某些方面改进传统的 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默认使用
Gentoo的Wiki页面有一众init进程轻度对比(Comparison of init systems)。
进程管理工具
1、Supervisor
supervisorctl是Supervisor提供的命令行客户端,而Supervisor是一个进程控制系统。它用于在UNIX类操作系统上控制和监视进程,通常用于服务器环境中管理长时间运行的进程。它不是大多数 UNIX 系统的标准部分,因此通常需要单独安装。
2、Systemd
systemd 是现代 Linux 系统上的一个标准进程管理和初始化系统,它用于替代传统的 SysV init 系统。它通过使用称为 systemctl 的命令行工具来管理服务和系统资源。
3、init.d
在 systemd 出现之前,许多 UNIX 和 Linux 系统使用了名为 init.d 的传统初始化系统。init.d 脚本通常位于 /etc/init.d/ 目录下,可用于启动、停止、重启和管理各种服务。与之配套的管理工具较多如service、chkconfig等。
4、Upstart
Upstart 是一个由 Ubuntu 开发的事件驱动机制以替代传统 init 系统,而被主动放弃。它被设计为更灵活和适应更多现代计算环境的要求,尤其是在处理异步和并发启动的场景。
5、Monit
Monit 是一个小型的开源工具,用于管理和监控 UNIX 系统上的进程、程序、文件、目录和文件系统。它可以用来自动修复问题,如重启失败的服务,或者在检测到问题时发送警报。
6、Runit
runit 是一个跨平台的进程监视器和服务管理工具。它被设计为 SysV init 和其他重型管理工具的替代品,提供快速、可靠和简单的服务管理工具。
7、Daemontools
Daemontools 是一个用于监控和控制服务的集合工具,它提供了一种确保服务持续运行的简单方式。
该命令还支持以下参数:
-u : up, 如果services没有运行的话,启动它,如果services停止了,重启它。
-d : down, 如果services正在运行的话,给它发送一个TERM(terminate)信号,然后再发送一个CONT(continue)信号,在它停止后,不再启动它。
-o : once, 如果services没有运行,启动它,但是在它停止后不再启动了。就是只运行一次。
-p : pause, 给services发送一个停止信号。
-c : continue, 给services发送一个CONT信号。
-h : hang up, 给services发送一个HUP信号。
-a : alarm, 给services发送一个ALRM信号。
-i : interrupt, 给services发送一个INT信号。
-t : Terminate, 给services发送一个TERM信号。
-k : kill, 给services发送一个KILL信号。
-x : exit, supervise在services停止后会立刻退出,但是值得注意的是,如果在一个稳定的系统中使用了这个选项,已经开始犯错了:supervise被设计成为永远运行的。
Daemontools、Supervisor、Monit三者均不具备系统级init功能,但其提供了守护进程管理的功能,常驻后台的(守护)进程不小心挂掉后可以帮助重启该服务。当然Systemd也同样具备此功能。
关于 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操作系统的奥秘》