Linux系统的构建过程

作者: 李佶澳   转载请保留:原文地址   发布时间:2014/03/02 20:24:28

Linux启动

Linux操作系统不仅仅有Linux的内核, 更需要具备各种服务才能成为一个完整的操作系统. 了解Linux系统的启动过程, 对理解Linux系统是非常有益的.但是Linux系统涉及的服务众多, 不同的发行版又有差异, 所以这里只描述一个轮廓, 日后逐渐充实.

BIOS

按下电源的时候, CPU执行的第一条指令是BIOS(基本输入输出系统).

对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中断完成对硬件的操作.

中断表大全 http://www.oldlinux.org/Linux.old/docs/interrupts/int-html/int.htm

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的使用)

grub引导的具体过程涉及的内容比较深入, 目前还不甚明了, 日后有需要时补充.

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目录中的事件文件,执行需要执行的脚本.

CentOS5.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是要执行的命令.    

下面是CentOS5.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命令),从而又启动一批事件,直至完成.

下面是centOS6.4中的事件文件/erc/init/rcS.conf.

指定了事件执行的时机(start on startup)V
停止的时机(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时执行.

lja@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

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时执行的第一个进程。Cento5.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

下载busybox源码: http://www.busybox.net/

解压,编译,安装

make menconfig   #需要安装curses-devel,最好选用编译成static,不然需要添加依赖的动态库
make 		 #如果内核版本还没直至ubi, 在配置中将ubi相关的选项关闭(.config中),ubi是新的flash文件系统
make CONFIG_PREFIX=/path/from/root install  #安装到的目录

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的时候, 就说明已经制作成功。接下来需要做的就是在其中添加各种新的程序。

参考

  1. 《深度探索Linux操作系统–系统构建和原理解析》 王柏生
  2. 《深入探索Linux操作系统的奥秘》

欢迎加微信,最好备注姓名和方向

QQ交流群

区块链实践互助QQ群:576555864

Kubernetes实践互助QQ群:947371129

Prometheus实践互助QQ群:952461804

API网关Kong实践互助QQ群:952503851

Ansible实践互助QQ群:955105412

Copyright @2011-2018 All rights reserved. 转载请添加原文连接,合作请加微信lijiaocn或者发送邮件: lijiaocn@foxmail.com,备注网站合作 友情链接: lijiaocn github.com