深度探索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
目录
相关文章
|
2天前
|
存储 安全 Linux
探索Linux操作系统的心脏:内核
在这篇文章中,我们将深入探讨Linux操作系统的核心—内核。通过简单易懂的语言和比喻,我们会发现内核是如何像心脏一样为系统提供动力,处理数据,并保持一切顺畅运行。从文件系统的管理到进程调度,再到设备驱动,我们将一探究竟,看看内核是怎样支撑起整个操作系统的大厦。无论你是计算机新手还是资深用户,这篇文章都将带你领略Linux内核的魅力,让你对这台复杂机器的内部运作有一个清晰的认识。
12 3
|
2天前
|
存储 数据挖掘 Linux
服务器数据恢复—Linux操作系统网站服务器数据恢复案例
服务器数据恢复环境: 一台linux操作系统服务器上跑了几十个网站,服务器上只有一块SATA硬盘。 服务器故障: 服务器突然宕机,尝试再次启动失败。将硬盘拆下检测,发现存在坏扇区
|
4天前
|
安全 Android开发 数据安全/隐私保护
构建未来:移动应用开发与操作系统的融合之道
在数字化浪潮中,移动应用和操作系统如同现代文明的双子星座。本文将探索它们之间的紧密联系,揭示如何通过技术创新和设计哲学的融合,共同塑造我们的数字生活。从用户体验到系统性能,我们将一窥这些看似简单却复杂的互动是如何影响我们日常生活的。
|
12天前
|
Java 开发工具 Android开发
移动应用开发之旅:探索移动操作系统与应用构建的奥秘
【8月更文挑战第33天】在数字时代的浪潮中,移动应用已成为我们日常生活的一部分。本文将带您深入理解移动操作系统的工作原理,并揭示如何在这个多姿多彩的平台上开发出引人入胜的应用。我们将从基础概念出发,逐步深入到高级编程技巧,最终通过一个实际的代码示例,展示如何将理论应用于实践。无论您是初学者还是有经验的开发者,这篇文章都将为您提供宝贵的见解和灵感。让我们一起踏上这场激动人心的移动应用开发之旅吧!
|
13天前
|
机器学习/深度学习 vr&ar Android开发
构建未来:移动应用开发与操作系统的革新之路
【9月更文挑战第1天】 在数字时代的浪潮中,移动应用和操作系统是推动技术前进的重要力量。本文将深入探讨移动应用开发的新趋势、移动操作系统的创新演进,以及它们如何共同塑造我们的未来生活。通过分析当前技术发展的现状和挑战,我们将一窥即将到来的技术变革,并理解这些变化对个人和社会的深远影响。
28 1
|
14天前
|
Android开发 数据安全/隐私保护 iOS开发
构建未来:移动应用开发与操作系统的协同进化
【8月更文挑战第31天】在数字时代的浪潮中,移动应用与操作系统共同编织了一幅互动丰富的技术画卷。本文将探讨移动应用开发的新趋势、移动操作系统的创新特性,以及它们如何相互影响,推动着技术的进步。通过实际代码示例,我们将深入了解这一动态进化过程,揭示背后的技术细节和设计理念。
|
14天前
|
人工智能 Java Android开发
构建未来:移动应用开发与操作系统的融合之旅
【8月更文挑战第31天】本文将带您踏上一段探索移动应用开发和操作系统交互的旅程。我们将从基础概念出发,逐步深入到实际的开发案例,最后探讨未来的趋势。文章不仅包含理论知识,还结合了代码示例,旨在为初学者和有经验的开发者提供实用指南。
|
14天前
|
安全 Linux 开发工具
探索Linux操作系统:从命令行到脚本编程
【8月更文挑战第31天】在这篇文章中,我们将一起潜入Linux操作系统的海洋,从最基础的命令行操作开始,逐步深入到编写实用的脚本。无论你是初学者还是有一定经验的开发者,这篇文章都将为你提供新的视角和实用技能。我们将通过实际代码示例,展示如何在日常工作中利用Linux的强大功能来简化任务和提高效率。准备好了吗?让我们一起开启这段旅程,探索Linux的奥秘吧!
|
14天前
|
开发者 API 开发框架
Xamarin 在教育应用开发中的应用:从课程笔记到互动测验,全面解析使用Xamarin.Forms构建多功能教育平台的技术细节与实战示例
【8月更文挑战第31天】Xamarin 作为一款强大的跨平台移动开发框架,在教育应用开发中展现了巨大潜力。它允许开发者使用单一的 C# 代码库构建 iOS、Android 和 Windows 应用,确保不同设备上的一致体验。Xamarin 提供广泛的 API 支持,便于访问摄像头、GPS 等原生功能。本文通过一个简单的教育应用示例——课程笔记和测验功能,展示了 Xamarin 在实际开发中的应用过程。从定义用户界面到实现保存笔记和检查答案的逻辑,Xamarin 展现了其在教育应用开发中的高效性和灵活性。
24 0
|
14天前
|
人工智能 Java 物联网
构建未来:移动应用开发与操作系统的融合之旅
【8月更文挑战第31天】在数字时代的浪潮中,移动应用和操作系统成为我们日常生活和工作的基石。本文将深入探讨移动应用开发的核心概念、移动操作系统的架构,并通过一个实际的开发案例,展示如何将创新理念融入应用设计之中。我们将从基础出发,逐步深入到高级话题,旨在为读者提供一个全面而深刻的技术洞见。