启动过程关键点分类如下:
bios -> grub -> initrd -> real system
这里 bios->grub->initrd 的过程在每一个使用 grub 引导的发行版中执行的过程大同小异,本文主要针对 debian 系统启动过程,重点在 initrd-> real system 这一阶段。
内核启动的入口是 start_kernel 函数,这与常规的程序不同。常规程序中使用 main 函数作为程序执行的入口,这是由 libc 中的初始化历程调用决定的,crtx.o 中保存的那些在 main 函数之前执行的函数、汇编最终调用了 main 这个符号,这让我们看上去 main 函数像是程序执行的入口,其实在 main 函数之前还有一些代码要执行,main 函数并不是严格意义上的函数执行入口。
嵌入式中一般程序执行的起点是大多数是 reset_handler,在这里执行栈与 bss 段、data 段等程序执行环境的创建过程,然后使用跳转指令跳转到 main 或其它函数中执行。
从 start_kernel 函数开始
撤了这么多其实想说明的是在 start_kernel 之前还有很多代码要执行。想想 start_kernel 作为一个函数,需要依赖函数执行的环境,创建这些环境肯定需要执行其它的代码,并且这也只是其中的一部分。
为了进一步说明这一点,我特地搜了下 linux 调用 start_kernel 的位置,相关的代码如下:
void __init x86_64_start_reservations(char *real_mode_data)
{
/* version is always not zero if it is copied */
if (!boot_params.hdr.version)
copy_bootdata(__va(real_mode_data));
x86_early_init_platform_quirks();
switch (boot_params.hdr.hardware_subarch) {
case X86_SUBARCH_INTEL_MID:
x86_intel_mid_early_setup();
break;
default:
break;
}
start_kernel();
}
start_kernel 中有一堆关于初始化的代码,在最后调用了 rest_init 函数。rest_init 中创建了一个内核线程来执行另外一些初始化任务,这属于进程上下文的执行流。
此内核线程执行 kernel_init 函数,在启用 ramfs 的情况下,它会执行 ramdisk 中指定的初始化命令,这一般是 /init 命令。
在我的虚拟机中,dmesg 能够看到如下输出:
[ 1.310303] Run /init as init process
上述输出就是在执行 ramdisk 中的 /init 程序前的打印。
/init 位于解压出来的 initrd 文件中,它实际是一个 shell 脚本。
/init 脚本
init 脚本首先创建一些必要的系统目录,然后挂载 sys、proc、pts、dev 目录。
挂载 sys 与 proc 目录的命令如下:
mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,noexec,nosuid proc /proc
挂载 pts 目录的命令如下:
mkdir /dev/pts
mount -t devpts -o noexec,nosuid,gid=5,mode=0620 devpts /dev/pts || true
pts 是伪终端文件系统,我曾经就遇到过因为没有挂载 pts 伪终端文件系统导致 ssh 无法分配一个 tty 的问题。
挂载 /dev 目录的命令:
mount -t devtmpfs -o nosuid,mode=0755 udev /dev
上述命令执行后 /dev 下面会生成一些设备文件,这些设备文件应当是 udev 自动创建的。
执行前只有一个 console 文件,执行后多出了很多设备文件,截图如下;
init 脚本中对 /proc/cmdline 进行了解析,设置不同的变量值。相关代码截取如下:
for x in $(cat /proc/cmdline); do
case $x in
init=*)
init=${x#init=}
;;
root=*)
ROOT=${x#root=}
if [ -z "${BOOT}" ] && [ "$ROOT" = "/dev/nfs" ]; then
BOOT=nfs
fi
;;
rootflags=*)
ROOTFLAGS="-o ${x#rootflags=}"
;;
rootfstype=*)
ROOTFSTYPE="${x#rootfstype=}"
;;
rootdelay=*)
ROOTDELAY="${x#rootdelay=}"
case ${ROOTDELAY} in
*[![:digit:].]*)
ROOTDELAY=
;;
esac
;;
nfsroot=*)
# shellcheck disable=SC2034
NFSROOT="${x#nfsroot=}"
;;
initramfs.runsize=*)
RUNSIZE="${x#initramfs.runsize=}"
;;
ip=*)
IP="${x#ip=}"
;;
boot=*)
BOOT=${x#boot=}
;;
ubi.mtd=*)
UBIMTD=${x#ubi.mtd=}
;;
resume=*)
RESUME="${x#resume=}"
case $RESUME in
UUID=*)
RESUME="/dev/disk/by-uuid/${RESUME#UUID=}"
esac
;;
resume_offset=*)
resume_offset="${x#resume_offset=}"
;;
noresume)
noresume=y
;;
drop_capabilities=*)
drop_caps="-d ${x#drop_capabilities=}"
;;
panic=*)
panic="${x#panic=}"
case ${panic} in
*[![:digit:].]*)
panic=
;;
esac
;;
ro)
readonly=y
;;
rw)
readonly=n
;;
debug)
debug=y
quiet=n
if [ -n "${netconsole}" ]; then
log_output=/dev/kmsg
else
log_output=/run/initramfs/initramfs.debug
fi
set -x
;;
debug=*)
debug=y
quiet=n
set -x
;;
break=*)
break=${x#break=}
;;
break)
break=premount
;;
blacklist=*)
blacklist=${x#blacklist=}
;;
netconsole=*)
netconsole=${x#netconsole=}
[ "x$debug" = "xy" ] && log_output=/dev/kmsg
;;
BOOTIF=*)
BOOTIF=${x#BOOTIF=}
;;
fastboot|fsck.mode=skip)
fastboot=y
;;
forcefsck|fsck.mode=force)
forcefsck=y
;;
fsckfix|fsck.repair=yes)
fsckfix=y
;;
fsck.repair=no)
fsckfix=n
;;
esac
done
注意这里的 init 与 root 变量,这两个变量是 init 的核心。
systemd-udevd 守护进程
init 中调用 scripts/init-top/udev 脚本来启动 systemd-udevd,相关代码如下:
SYSTEMD_LOG_LEVEL=$log_level /lib/systemd/systemd-udevd --daemon --resolve-names=never
udevadm trigger --type=subsystems --action=add
udevadm trigger --type=devices --action=add
udevadm settle || true
执行了上述步骤后,lsmod 能够看到自动加载了很多模块。这实际是由 systmed-udevd 调用 modprobe 加载的。
systemd-udevd 如何确定加载那些内核模块?
这里有一个问题是 systemd-udevd 怎么知道该加载那些内核模块呢?是用户写配置文件吗?还是其它机制吗?
比较容易想到的是用户配置文件,根据配置文件来加载需要的模块,但这对用户产生了依赖,不同的硬件环境需要用户手动创建不同的配置文件,这种方式对用户来说不太友好。
实际上 systmed-udevd 中使用了另外一种完全不同的机制,这种机制通过 netlink 与内核通信,获取设备信息来检索 /lib/modules 目录中的 modules.alias 表获取到设备对应的驱动,然后制作参数,使用 modprobe 加载。
具体的过程如下:
- 内核创建设备树,此时还没有加载任何的驱动
- 内核在创建设备树的过程中会将每一个设备节点的信息通过 netlink 发送,netlink 有内部的队列,这些信息在队列中缓存起来
- init 脚本执行 systmed-udevd 守护程序,此程序通过 netlink 与内核通信,读取队列中缓存的设备描述信息
- systmed-udevd 解析信息,然后使用某种能够唯一标识设备的字段来查询 /lib/modules 中的 modules.alias 匹配驱动,匹配成功则调用 modprobe 来加载之
- systmed-udevd 在 init 执行完成后也一直存在,它会检测热插拔时间并做出响应
netlink 的使用
对于 netlink 的使用,我从 NETLINK 的使用示例 链接中 copy 到如下代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/un.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <linux/types.h>
#include <linux/netlink.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#define UEVENT_BUFFER_SIZE 2048
static int init_hotplug_sock()
{
const int buffersize = 1024;
int ret;
struct sockaddr_nl snl;
bzero(&snl, sizeof(struct sockaddr_nl));
snl.nl_family = AF_NETLINK;
snl.nl_pid = getpid();
snl.nl_groups = 1;
int s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
if (s == -1)
{
perror("socket");
return -1;
}
setsockopt(s, SOL_SOCKET, SO_RCVBUF, &buffersize, sizeof(buffersize));
ret = bind(s, (struct sockaddr *)&snl, sizeof(struct sockaddr_nl));
if (ret < 0)
{
perror("bind");
close(s);
return -1;
}
return s;
}
int main(int argc, char* argv[])
{
int hotplug_sock = init_hotplug_sock();
while(1)
{
/* Netlink message buffer */
char buf[UEVENT_BUFFER_SIZE * 2] = {0};
recv(hotplug_sock, &buf, sizeof(buf), 0);
printf("%s\n", buf);
/* USB 设备的插拔会出现字符信息,通过比较不同的信息确定特定设备的插拔,在这添加比较代码 */
}
return 0;
}
学习过 socket 网络编程的朋友可能对以上的过程感到熟悉,其实上述代码与普通的 udp 客户段代码类似,只不过有一些针对 netlink 的不同配置。
注意上述代码中的如下行:
int s = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT);
该行调用了 socket 系统调用创建一个网络套接字。注意这里的 PF_NETLINK 表示使用 netlink 类型的套接字,SOCK_DGRAM 是原始数据报的意思,NETLINK_KOBJECT_UEVENT 标识了 netlink 的类型。
编译并执行后,我插拔 usb 接口,终端有如下输出信息:
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/0003:046D:C52B.0026
unbind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/usbmisc/hiddev0
remove@/usbmisc
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/hidraw/hidraw1
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/0003:046D:4052.0029/input/input61/mouse1
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/0003:046D:4052.0029/input/input61/event5
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/0003:046D:4052.0029/input/input61
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/0003:046D:4052.0029/hidraw/hidraw2
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/0003:046D:4052.0029/power_supply/hidpp_battery_7
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/0003:046D:4052.0029/power_supply/hidpp_battery_6
unbind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/0003:046D:4052.0029
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028/0003:046D:4052.0029
unbind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.0028
unbind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2
unbind@/devices/pci0000:00/0000:00:14.0/usb1/1-1
remove@/devices/pci0000:00/0000:00:14.0/usb1/1-1
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0/0003:046D:C52B.002A
bind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.0
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1/0003:046D:C52B.002B
bind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.1
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C
add@/class/usbmisc
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/usbmisc/hiddev0
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C/hidraw/hidraw1
bind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C
bind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2
bind@/devices/pci0000:00/0000:00:14.0/usb1/1-1
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C/0003:046D:4052.002D
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C/0003:046D:4052.002D/input/input62
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C/0003:046D:4052.002D/input/input62/mouse1
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C/0003:046D:4052.002D/input/input62/event5
add@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C/0003:046D:4052.002D/hidraw/hidraw2
bind@/devices/pci0000:00/0000:00:14.0/usb1/1-1/1-1:1.2/0003:046D:C52B.002C/0003:046D:4052.002D
可以看到程序接收到内核发送的 remove、unbind、add、bind usb 接口的信息,这里标识出了具体的事件与发生事件的设备。
这之后我尝试在 init 脚本执行 systmed-udevd 守护程序之前来执行上述程序,想看看能够接收到怎样的信息。
我修改了 initrd,添加了编译后的程序,并修改 init 脚本,让其在执行 systmed-udevd 守护程序前执行 /bin/sh 命令进入一个 shell。
结果我发现程序一直阻塞,没有任何打印。确认 systmed-udevd 确实没有执行。简单研究了下没有搞定就先跳过这个问题了。
挂载根目录
init 脚本中非常关键的一步是挂载根目录,它是通过执行 local 脚本中的 mountroot 函数来实现。
相关代码信息如下:
mountroot()
{
local_mount_root
}
local_mount_root()
{
local_top
if [ -z "${ROOT}" ]; then
panic "No root device specified. Boot arguments must include a root= parameter."
fi
local_device_setup "${ROOT}" "root file system"
ROOT="${DEV}"
# Get the root filesystem type if not set
if [ -z "${ROOTFSTYPE}" ] || [ "${ROOTFSTYPE}" = auto ]; then
FSTYPE=$(get_fstype "${ROOT}")
else
FSTYPE=${ROOTFSTYPE}
fi
local_premount
if [ "${readonly?}" = "y" ]; then
roflag=-r
else
roflag=-w
fi
checkfs "${ROOT}" root "${FSTYPE}"
# Mount root
# shellcheck disable=SC2086
if ! mount ${roflag} ${FSTYPE:+-t "${FSTYPE}"} ${ROOTFLAGS} "${ROOT}" "${rootmnt?}"; then
panic "Failed to mount ${ROOT} as root file system."
fi
}
上述脚本的主要执行过程如下:
- 根据 /proc/cmdline 中指定的 root=xxx 获取根目录所在分区及分区使用的文件系统
- 选择性的执行 fsck 来扫描分区
- 挂载根目录到 /root 中
/proc/cmdline 中 root 变量的一个常见设置内容如下;
root=UUID=180490b0-df1c-4632-8727-7824e5d2a8c6
这里使用 UUID 来标识出具体的分区。程序不能直接使用 UUID,它需要通过 blkid 命令获取 uuid 对应的分区的设备文件字符串。
示例代码如下:
sudo blkid -l -t UUID=180490b0-df1c-4632-8727-7824e5d2a8c6 -o device
/dev/nvme0n1p5
可以看到上述命令打印了 /dev/nvme0n1p5 这个设备文件字符串,这个设备文件就是我的系统中根目录所在的分区。
获取根分区使用的文件系统类型通过执行 get_fstype 函数来完成。相关代码如下:
get_fstype ()
{
local FS FSTYPE
FS="${1}"
# blkid has a more complete list of file systems,
# but fstype is more robust
FSTYPE="unknown"
eval "$(fstype "${FS}" 2> /dev/null)"
if [ "$FSTYPE" = "unknown" ]; then
FSTYPE=$(blkid -o value -s TYPE "${FS}") || return
fi
echo "${FSTYPE}"
return 0
}
上述代码首先使用 initrd 中的 fstype 命令来获取,获取失败则调用 blkid 重新获取。
fstype 命令执行示例如下:
[longyu@debian-10:10:34:19] initrd $ sudo /usr/lib/klibc/bin/fstype /dev/nvme0n1p5
FSTYPE=ext4
FSSIZE=32212254720
[longyu@debian-10:10:34:19] sudo blkid -o value -s TYPE /dev/nvme0n1p5
ext4
从上面的输出中可以看到我的系统中根目录所在的分区的文件系统类型是 ext4。获取到需要的信息后执行 mount 命令挂载根分区到 /root 中,注意这里并没有执行 chroot 切换根目录,在此之前还有一些工作需要处理。
- 移动 sys 挂载点到 /root/sys 中
- 移动 proc 挂载点到 /root/proc 中
- 调用 run-init 命令执行 chroot 并调用 /sbin/init 命令来完成下一步的初始化工作
移动挂载点执行的命令如下:
mount -n -o move /sys ${rootmnt}/sys
mount -n -o move /proc ${rootmnt}/proc
/sbin/init 命令实际上是 systmed 程序,这之后的初始化过程由 systmed 来完成,并且会将 initramfs 释放。
run-init 命令调用代码如下:
exec run-init ${drop_caps} "${rootmnt}" "${init}" "$@" <"${rootmnt}/dev/console" >"${rootmnt}/dev/console" 2>&1
好了对于启动过程的研究就到此为止,后续 systmed 的工作有时间再继续研究。
参考链接:
https://www.cnblogs.com/hoys/archive/2011/04/09/2010759.html