深度探索Linux操作系统 —— 构建initramfs

简介: 深度探索Linux操作系统 —— 构建initramfs

前言

    一般而言,桌面、服务器等通用系统都使用 initramfs。部分嵌入式系统中,也会使用 initramfs,甚至有的使用 initramfs 作为最终的根文件系统。那么什么是 initramfs 呢?很难用一句话将 initramfs 的作用描述清楚,或许可以将 initramfs 定位为内核通往根文件系统的桥梁。


一、为什么需要 initramfs

   鸡和蛋的问题:内核要加载这些模块或者运行这些程序才能正确识别根文件系统所在的设备,但是保存这些模块或者程序的根文件系统又存储在这些设备上。


   内核开发者们设计了 initramfs 机制。initramfs 是一个临时的文件系统,其中包含了必要的设备如硬盘、网卡、文件系统等的驱动以及加载驱动的工具及其运行环境,比如基本的 C 库,动态库的链接加载器等等。同时,那些处理根文件系统在 RAID 、网络设备上的程序也存放在 initramfs 中。由第三方程序(如 Bootloader )负责将 initramfs 从硬盘装载进内存。以驱动硬盘为例,内核就不必再从硬盘,而是从已经加载到内存的 initramfs 中获取硬盘控制器等相关驱动了,继而可以驱动硬盘,访问硬盘上的根文件系统,从而解决了前面提到的鸡和蛋的矛盾。


   在初始化的最后,内核运行 initramfs 中的 init 程序,该程序将探测硬件设备、加载驱动,挂载真正的文件系统,执行文件系统上的 /sbin/init,进而切换到真正的用户空间。真正的文件系统挂载后,initramfs 即完成了使命,其占用的内存也会被释放。

二、initramfs原理探讨

   在 2.4 以及更早版本的内核中,内核使用的是 initrd。initrd 是基于ramdisk技术的,而 ramdisk 就是一个基于内存的块设备,因此 initrd 也具有块设备的一切属性。比如 initrd 容量是固定的,一旦创建 initrd 时设定了一个大小,就不能再进行动态调整。


   ramfs 与 ramdisk 有着本质的区别,ramdisk 本质上是基于内存的一个块设备,而 ramfs 是基于缓存的一个文件系统。因此,ramfs 去除了前述块设备的一些限制。比如,ramfs 根据其中包含的文件大小可自由伸缩;增加文件时,自动分配内存;删除文件时,自动释放内存。更重要的是,ramfs 是基于已有的缓存机制,因此不必再像 ramdisk 那样需要和缓存之间进行多余的复制一环。


   从 2.6 开始,内核开发人员基于 ramfs 开发了 initramfs 替代 initrd 。


   当 2.6 版本的内核引导时,在挂载真正的根文件系统之前,首先将挂载一个名为 rootfs 的文件系统,并将 rootfs 的根作为虚拟文件系统目录树的总根。那么为什么要使用 rootfs 这么一个中间过程呢?原因之一还是为了解决鸡和蛋的问题。内核需要根文件系统上的驱动以及程序来驱动和挂载根文件系统,但是这些驱动和程序有可能没有编译进内核,而在根文件系统上。如果不借助第三方,内核是没有办法挂载真正的根文件系统的。而 rootfs 虽然名称为 rootfs ,但是并不是什么新的文件系统,事实上,其就是一个 ramfs,只不过换了一个名称。换句话说,rootfs 是在内存中的,内核不需要特殊的驱动就可以挂载 rootfs,所以内核使用 rootfs 作为一个过渡的桥梁。


   在挂载了 rootfs 后,内核将 Bootloader 加载到内存中的 initramfs 中打包的文件解压到 rootfs 中,而这些文件中包含了驱动以及挂载真正的根文件系统的工具,内核通过加载这些驱动、使用这些工具,实现了挂载真正的根文件系统。此后,rootfs 也完成了历史使命,被真正的根文件系统覆盖(overmount)。但是 rootfs 作为虚拟文件系统目录树的总根,并不能被卸载。但是这没有关系,前面我们已经谈到了,rootfs 基于 ramfs,删除其中的文件即可释放其占用的空间。

三、构建基本的initramfs

# 1
mkdir initramfs
cd initramfs

# 2
# /vita/initramfs/init
#!/bin/bash
echo "Hello Linux!"
exec /bin/bash

# 3
mkdir bin
cp ../sysroot/bin/bash bin/

bash 依赖

vita@baisheng:/vita/initramfs$ ldd bin/bash
  libdl.so.2 => /vita/sysroot/lib/libdl.so.2
  libgcc_s.so.1 =>/vita/cross-tool/i686-none-linux-gnu/lib/libgcc_s.so.1
  libc.so.6 => /vita/sysroot/lib/libc.so.6

vita@baisheng:/vita/initramfs$ ldd lib/libdl.so.2
  libc.so.6 => /vita/sysroot/lib/libc.so.6
  ld-linux.so.2 => /vita/sysroot/lib/ld-linux.so.2
  
vita@baisheng:/vita/initramfs$ ldd lib/libc.so.6
  ld-linux.so.2 => /vita/sysroot/lib/ld-linux.so.2
  
vita@baisheng:/vita/initramfs$ ldd lib/ld-linux.so.2

vita@baisheng:/vita/initramfs$ ldd lib/libgcc_s.so.1
  libc.so.6 => /vita/sysroot/lib/libc.so.6

    bash 依赖于libc、libdl 以及 libgcc_s.so.1,因此,我们需要在 initramfs 中安装这三个库,以及安装加载动态库的动态加载/链接器。

    根据依赖关系可见,libdl 依赖 libc 和动态链接器,libgcc 只依赖 libclibc 仅依赖动态链接器,而动态链接器不依赖其他任何库,因此,我们不再需要安装其他库到 initramfs 中。

四、将硬盘驱动编译为模块

1、配置devtmpfs

  Linux 从 2.6.18 开始采用 udev,/dev 目录使用了基于内存的文件系统 tmpfs 管理设备文件。


   2009 年初,开发人员又提出了 devtmpfs ,并在同年年底被 Linux 2.6.32 正式收录。内核引导时,devtmpfs 将所有注册的设备在 devtmpfs 中建立相应的设备文件,一旦进入用户空间,在启动 udev 前,就可以将 devtmpfs 挂载到 /dev 目录下。


   也就是说,在启动 udev 前,devtmpfs 中已经建立了初步的设备文件,一般启动程序不必再等待 udev 建立设备节点,甚至在某些嵌入式系统上,不再需要 udev 创建设备节点,因为这个基本的 /dev 已经足够,从而缩短了系统的启动时间。同 rootfs 类似,devtmpfs 也不是新设计的文件系统,如果内核配置支持 tmpfs ,那么其就是 tmpfs;否则,devtmpfs 就是 ramfs,只不过换了一个名字而已。

2、将硬盘控制器驱动配置为模块

    接下来重新编译内核和模块。内核和模块可以使用单独的命令分开编译,也可以使用一条 make 命令同时编译内核和模块。 编译完成后,将模块暂时安装在 “/vita/sysroot/lib/modules” 目录下。

vita@baisheng:/vita/build/linux-3.7.4$ make bzImage
vita@baisheng:/vita/build/linux-3.7.4$ make modules
vita@baisheng:/vita/build/linux-3.7.4$ make \
    INSTALL_MOD_PATH=$SYSROOT modules_install

最终安装的硬盘控制器驱动模块包括:

vita@baisheng:/vita$ ls sysroot/lib/modules/3.7.4/kernel/drivers/ata/

ahci.ko ata_piix.ko libahci.ko

我们将其复制到 initramfs 中。

3、自动加载硬盘控制器驱动

   从 2.6 版内核开始,Linux 采用 udev 管理驱动模块的加载以及设备节点的管理。每当内核发现新的设备,便通过 NETLINK 向用户空间发送新设备事件,该事件中记录了设备的相关信息。用户空间的 udev 服务进程收到内核事件后,根据事件中携带的信息,首先判断该设备的驱动是否已经加载,如果没有,则加载驱动。驱动加载后,内核会再次向用户空间报告发现新设备事件,这时设备已经成功驱动了,并且主次设备号等信息也已经准备好了,udev 收到事件后,或者为设备建立节点,或者执行某些特定的操作。整个过程如图4-19所示。

(1)内核向用户空间发送事件

    PC 机上的硬盘控制器,无论是 IDE 接口的,还是 SATA 接口的,一般都是通过 PCI 总线连接到计算机上的。内核在引导时,PCI 子系统将进行初始化,枚举总线上的设备,并尝试为设备匹配驱动;然后将收集到的设备相关信息组织为 uevent 事件;接着调用 kobject_uevent ,通过 NETLINK 将组织好的 uevent 发送到用户空间,通知 udev 有新设备了。简单地讲,内核的工作就是探测并收集设备信息,将其包装到 uevent 事件中,然后发送到用户空间。


   事实上,无论是发现新的设备,还是有新的驱动载入,抑或是用户向 sysfs 中的 uevent 写入字符串,内核都将调用函数 kobject_uevent 向用户空间发送事件。


   结构体 kobj_uevent_env 用来保存收集到的设备相关信息,所以在函数 kobject_uevent_env 中,首先为 kobj_uevent_env 申请了一块内存,即变量 env 指向的内存,用来临时存放准备发送到用户空间的设备相关信息。


   然后向该内存中添加了三个默认的变量,包括 ACTION、DEVPATH 和 SUBSYSTEM 。 其中 ACTION 指的是热插拔的动作,如 “add”,“remove”,“change” 等。DEVPATH 指的是设备在 sysfs 文件系统中注册的设备路径,比如笔者的硬盘 sda 的 DEVPATH 是 “/devices/pci0000:00/0000:00:1f.2/ata1/host0/target0:0:0/0:0:0:0/block/sda” 。SUBSYSTEM 一般是指设备所在的总线,比如笔者的硬盘是挂在 PCI 总线上的,因此该变量的值是 “pci”。


   pci_uevent 又向 uevent 中追加了 pci class 、 vendor id 、 device id 以及 MODALIAS 等变量,其中 MODALIAS 需要重点关注,其是由设备所在总线、vendor ID、device ID 等相关参数连接而成的一个字符串。在接下来的章节中,读者将看到,用户空间的 udev 恰恰就是根据这个变量为设备匹配驱动模块的。


   除了总线外,如果硬盘控制器所属的 class 或者 type 也需要继续向 uevent 中追加变量,则继续调用硬盘控制器所属的 class 或者 type 中的相应的函数,这里不再继续分析了。最终,内核向用户空间发送的 uevent 事件包含的大致的内容如下,其中不同变量之间使用 “\0” 进行分隔。

(2)udev加载驱动和建立设备节点

   udev 是用户空间动态管理设备的机制,包括加载驱动、管理设备节点等。udev 机制的核心是其服务进程 udevd。当启动过程进入用户空间阶段后,udevd 将被启动。udevd 启动后,首先读取并分析所有的规则文件,并将其缓存在内存中。一般情况下,系统默认的规则文件存在 /lib/udev/rules.d 目录下,用户自定义的规则存放在 /etc/udev/rules.d 目录下。每当动态地增加、删除或者改变某个规则文件时,udevd 将更新其缓存在内存中的规则。然后,udevd 通过 NETLINK 协议,监听并处理来自内核的 uevent 事件。每当 udevd 收到一个内核的 uevent ,udevd 均创建一个单独的子进程处理 uevent 。


   对于每个内核报告的 uevent,udevd 根据 uevent 中的变量逐个匹配规则。规则文件通常以数字开头,数字小的先进行匹配。若每个规则文件中包含若干个规则,同一规则不允许断行,每个规则至少包含一个 key-value 对,每个 key-value 对之间使用逗号分隔。可以将规则理解为由匹配条件和赋值动作组成,当所有的匹配条件都满足后,赋值动作就会发生。规则中可以加载驱动模块;规定如何给设备接点命名、建立符号连接;设备连接和断开时分别执行指定的程序等。


   前面我们看到内核在发现新设备时会将设备的一些信息通过 NETLINK 发送到用户空间,udev 接收到事件后,如果发现设备尚未被驱动,将尝试加载驱动模块。那么 udev 如何确定设备对应的驱动模块呢?一般而言,根据设备的 vender ID 和 device ID 就可以标识一类设备,当然有的也需要根据 subvendor ID 和 subdevice ID 进一步细分。而在驱动代码中,恰恰使用这些设备信息明确声明了其可以支持的设备。


   内核将 ID table 中的每一项中的信息按照一定的格式组合起来,作为驱动的一个别名。这些别名存储在编译好的驱动模块中,模块安装后,需要使用工具 depmod 将其提取出来并存储在 /lib/modules/‘uname-r’ 目录下的 modules.alias.bin/modules.alias 中,如同前面讨论的 modules.dep 和 modules.dep.bin 的关系一样,modules.alias.bin 与 modules.alias 完全相同,只不过 modules.alias.bin 是为了加快搜索速度采用 Trie 树存储的。很多读者可能会说,编译安装模块时从来没有显示执行 depmod 啊,那是因为 make 等安装脚本已经替我们调用了这个命令。

(3)处理冷插拔设备

   前面我们讨论了动态加载驱动的整个过程。但是不知道读者想过没有,对于磁盘这种非热插拔设备,如果驱动没有编译进内核,那么当内核引导枚举设备时,系统运行在内核空间,尚未进入用户空间,更谈不上启动用户空间的 udev 服务了,因此内核发送到用户空间的 uevent 自然会被丢掉,更别提加载硬盘驱动模块和建立设备节点了。


   为了解决这个问题,开发人员基于 sys 文件系统设计了一种巧妙的机制。在 Linux 操作系统进入用户空间,udevd 启动后,通过 sys 文件系统请求内核重新发出 uevent 。此时 udevd 已经启动了,就会收到 uevent ,然后结合这些事件和规则,完成驱动的加载、设备节点的建立等。我们可以将这个过程看作是内核和 udev 导演的一出戏,对于冷插拔的设备,模拟了一遍热插拔的过程。


   下面我们简单探讨一下这个机制的原理。


   当新设备注册时,内核将调用 device_create_file 在 sys 文件系统中为设备注册一个名字为 uevent 的文件,当用户空间的程序读取该文件时,内核将调用函数 show_uevent 处理用户的读操作,而当用户空间的程序向该文件写入时,内核将调用函数 store_uevent 处理用户的写操作。我们以函数 store_uevent 为例,看看内

核是如何处理用户的写操作的。函数 store_uevent 代码如下:


   也就是说,当用户空间的程序向该属性文件写入字符串 “add” 时,函数 kobject_action_type 认为用户空间的程序要求 KOBJ_ADD 类型的事件,于是调用 kobject_uevent 向用户空间发送 KOBJ_ADD 类型的 uevent 。


   利用这种机制,我们可以在用户空间的 udev 服务程序启动后,向所有设备的属性文件 uevent 写入 “add” 字符串,请求内核重新发送一遍 KOBJ_ADD 事件,模拟一遍热插拔动作。如此,udevd 就可以收到这些事件,完成驱动加载、设备节点创建等工作。


   为此,udev 提供了一个管理工具 udevadm,我们可以使用这个工具请求内核重新发送设备相关事件。假设请求内核对全部设备模拟一遍热插拔,即重新发送事件 KOBJ_ADD ,则使用如下命令:

udevadm trigger --action=add
目录
相关文章
|
6月前
|
Ubuntu 物联网 Linux
从零安装一个Linux操作系统几种方法,以Ubuntu18.04为例
一切就绪后,我们就可以安装操作系统了。当系统通过优盘引导起来之后,我们就可以看到跟虚拟机中一样的安装向导了。之后,大家按照虚拟机中的顺序安装即可。 好了,今天主要介绍了Ubuntu Server版操作系统的安装过程,关于如何使用该操作系统,及操作系统更深层的原理,还请关注本号及相关圈子。
|
6月前
|
监控 Ubuntu Linux
什么Linux,Linux内核及Linux操作系统
上面只是简单的介绍了一下Linux操作系统的几个核心组件,其实Linux的整体架构要复杂的多。单纯从Linux内核的角度,它要管理CPU、内存、网卡、硬盘和输入输出等设备,因此内核本身分为进程调度,内存管理,虚拟文件系统,网络接口等4个核心子系统。
397 0
|
6月前
|
Unix 物联网 Linux
都什么年代了,你还不懂啥是Linux操作系统
至于华为鸿蒙操作系统是不是独树一帜,这个留给各位阅读本文的网友们来讨论
173 0
|
6月前
|
安全 Linux iOS开发
linux属于什么操作系统
Linux是一种自由和开放源代码的操作系统,具有高度的灵活性和可定制性。与常见的操作系统如Windows和macOS相比,Linux具有自由、安全和稳定等优势。Linux已广泛应用于服务器、桌面电脑、超级计算机和嵌入式设备等领域,并且在未来的发展前景广阔。由于其自由和开放源代码的特性,Linux还促进了计算机技术和社区的发展,为全球的计算机用户提供了更多的选择和可能性。
|
6月前
|
安全 Ubuntu Unix
关于Linux操作系统,你必须要知道的事
我们可以看到无论是Debian还是Buildroot都有各自的特点,为客户提供了更大的选择空间和灵活性,大家可以根据自己的需求选择合适的版本来满足终端用户的体验和功能需求。从平技术将会一直关注更多更安全、灵敏、易于开发的Linux版本,做好适配工作,不断为客户带来“简单开发、方便应用”的使用体验。
|
6月前
|
安全 Ubuntu Linux
如何安装Linux操作系统?
此时,您可以选择重新启动计算机,然后从硬盘上的Linux系统启动。以上是一个大致的安装过程。请注意,不同的Linux发行版可能会在细节上有所差异,因此在进行安装之前,请确保您阅读并理解了相应发行版的安装指南或文档。
|
6月前
|
Ubuntu Linux 开发者
Linux发行版比较:选择适合你的操作系统
在做出选择之前,建议您先在虚拟机或双系统环境中尝试不同的发行版,根据自己的体验和需求做出决策。选择适合自己的Linux发行版是一个个人化和主观的过程,最重要的是找到符合自己需求和喜好的发行版,让您在使用Linux系统时感到舒适和方便。
|
6月前
|
Ubuntu Unix Linux
玩机强化技能,动手安装Ubuntu Linux操作系统
(13)Ubuntu重启过程中,你将在关机画面中看到提示文字“Please remove the installation medium, then press ENTER:”,按下“Enter”键即可重启电脑。
|
6月前
|
Web App开发 Ubuntu Linux
Linux之Ubuntu操作系统安装
1、在Ubuntu系统下,可以使用自带的U盘启动制作软件制作启动盘。使用方法和rufus类似。 2、或者用dd命令制作启动盘,关于dd命令的使用方法可以百度查看。经过亲自实践,archlinux系统和红旗9.0系统的启动U盘就是用dd命令制作成功。
|
6月前
|
Ubuntu Linux Android开发
Ubuntu——一款备受瞩目的Linux操作系统
目前,要在Ubuntu上使用Windows下常用的工具,一个理想的解决方案是采用Deepin-wine。Deepin-wine是Windows模拟环境,允许用户在Ubuntu上轻松运行许多Windows软件。安装Deepin-wine后,您将能够使用以下列出的众多软件: