Skip to content

Latest commit

 

History

History
1095 lines (747 loc) · 66.2 KB

File metadata and controls

1095 lines (747 loc) · 66.2 KB

五、构建根文件系统

根文件系统是嵌入式 Linux 的第四个也是最后一个元素。 一旦您阅读了本章,您将能够构建、引导和运行一个简单的嵌入式 Linux 系统。

我将在这里描述的技术被广泛地称为滚动您自己的Ryo。 在嵌入式 Linux 的早期,这是创建根文件系统的唯一方法。 仍然有一些适用于 Ryo 根文件系统的用例,例如,当 RAM 或存储量非常有限时,用于快速演示,或者用于标准构建系统工具不能(轻松)满足您的要求的任何情况。 尽管如此,这种情况还是相当罕见的。 让我强调一下,本章的目的是教育性的;它并不是构建日常嵌入式系统的秘诀:使用下一章中描述的工具来实现这一点。

第一个目标是创建给我们一个 shell 提示符的最小根文件系统。 然后,以此为基础,我们将添加脚本以启动其他程序,并配置网络接口和用户权限。 Beaglebone Black 和 QEMU 目标都有工作过的例子。 了解如何从头开始构建根文件系统是一项有用的技能,当我们在后面的章节中查看更复杂的示例时,它将帮助您理解正在发生的事情。

在本章中,我们将介绍以下主题:

  • 根文件系统中应该包含什么内容?
  • 将根文件系统传输到目标
  • 创建引导initramfs
  • init程序
  • 配置用户帐户
  • 更好的设备节点管理方式
  • 配置网络
  • 使用设备表创建文件系统映像
  • 使用 NFS 挂载根文件系统
  • 使用 TFTP 加载内核

技术要求

要按照示例操作,请确保您具备以下条件:

  • 一种基于 Linux 的主机系统
  • 一种 microSD 卡读卡器和卡
  • 第 4 章配置和构建内核中为 Beaglebone Black 准备的 microSD 卡
  • zImage和 QEMU 的 DTB(参见第 4 章配置和构建内核)
  • USB 转 TTL 3.3V 串行电缆
  • 比格尔博恩黑
  • 5V 1A 直流电源
  • 用于 NFS 和 TFTP 的以太网电缆和端口

本章的所有代码都可以在本书的 giHub 存储库的Chapter05文件夹中找到:https://github.com/PacktPublishing/Mastering-Embedded-Linux-Programming-Third-Edition

根文件系统中应该有什么?

内核将获得根文件系统,作为从引导加载器作为指针传递的initramfs,或者通过挂载内核命令行上由root=参数给出的块设备。 一旦拥有根文件系统,内核将执行第一个程序,默认名称为init,如第 4 章配置和构建内核中的早期用户空间部分所述。 然后,就内核而言,它的工作就完成了。 这取决于init程序开始启动其他程序并使系统恢复活力。

要创建最小根文件系统,您需要以下组件:

  • init:这是启动一切的程序,通常通过运行一系列脚本。 我将在第 13 章启动-init 程序中更详细地描述init是如何工作的。
  • Shell:您需要一个 Shell 来为您提供命令提示符,但更重要的是,您还需要一个 Shell 来运行init调用的 Shell 脚本和其他程序。
  • 守护进程:守护进程是向其他人提供服务的后台程序。 很好的例子是系统日志守护进程(syslogd)和安全外壳守护进程(sshd)。 init程序必须启动初始后台进程群才能支持主系统应用。 事实上,init本身就是一个守护进程:它是提供启动其他守护进程的服务的守护进程。
  • 共享库:大多数程序都与共享库链接,因此它们必须位于根文件系统中。
  • 配置文件init和其他守护进程的配置存储在一系列文本文件中,通常存储在/etc目录中。
  • 设备节点:这些是允许访问各种设备驱动程序的特殊文件。
  • proc 和 sys:这两个伪文件系统将内核数据结构表示为目录和文件的层次结构。 许多程序和库函数依赖于/proc/sys
  • 内核模块:如果您已经将内核的某些部分配置为模块,则需要将它们安装在根文件系统中,通常安装在/lib/modules/[kernel version]中。

此外,还有特定于设备的应用,这些应用使设备执行其预期的工作,以及它们生成的运行时数据文件。

重要音符

在某些情况下,您可以将前面的大多数程序压缩为一个静态链接的程序,然后启动该程序而不是init。 例如,如果您的程序名为/myprog,您可以将以下命令添加到内核命令行init=/myprog。 我只在安全系统中遇到过这样的配置一次,在安全系统中,fork系统调用已被禁用,因此无法启动任何其他程序。 这种方法的缺点是您不能使用通常进入嵌入式系统的许多工具。 你必须自己做所有的事情。

目录布局

有趣的是,除了由init=rdinit=命名的程序存在的之外,Linux 内核并不关心文件和目录的布局,因此您可以自由地将内容放在您喜欢的任何位置。 例如,将运行 Android 的设备的文件布局与桌面 Linux 发行版的文件布局进行比较:它们几乎完全不同。

然而,许多程序都希望某些文件位于特定位置,如果设备使用类似的布局(Android 除外),这对我们开发人员是有帮助的。 大多数 LINUX 系统的基本布局都是在文件系统层次标准(FHS)中定义的,该标准可在https://refspecs.linuxfoundation.org/fhs.shtml获得。 FHS 涵盖了 Linux 操作系统的所有实现,从最大到最小。 嵌入式设备往往根据需要使用子集,但通常包括以下内容:

  • /bin:对所有用户都必不可少的程序
  • /dev:设备节点和其他特殊文件
  • /etc:系统配置文件
  • /lib:基本共享库,例如,组成 C 库的共享库
  • /proc:有关表示为虚拟文件的进程的信息
  • /sbin:系统管理员必备的程序
  • /sys:有关以虚拟文件表示的设备及其驱动程序的信息
  • /tmp:放置临时或易失性文件的位置
  • /usr:分别位于 /usr/bin/usr/lib/usr/sbin目录中的其他程序、库和系统管理员实用程序
  • /var:可以在运行时修改的文件和目录的层次结构,例如日志消息,其中一些必须在引导后保留

这里有一些微妙的区别。 /bin/sbin之间的区别很简单,后者不需要包括在非 root 用户的搜索路径中。 Red Hat 派生发行版的用户应该对此很熟悉。 /usr的意义在于,它可能位于与根文件系统不同的分区中,因此它不能包含引导系统所需的任何内容。

临时目录

您应该从在主机上创建分段目录开始,您可以在该目录中汇编最终将传输到目标的文件。 在下面的示例中,我使用了~/rootfs。 您需要在其中创建一个骨架目录结构,例如,查看以下内容:

$ mkdir ~/rootfs
$ cd ~/rootfs
$ mkdir bin dev etc home lib proc sbin sys tmp usr var
$ mkdir usr/bin usr/lib usr/sbin
$ mkdir -p var/log

要更清楚地查看目录层次结构,可以使用以下示例中使用的便捷tree命令和-d选项仅显示目录:

$ tree -d
.
├── bin
├── dev
├── etc
├── home
├── lib
├── proc
├── sbin
├── sys
├── tmp
├── usr
│   ├── bin
│   ├── lib
│   └── sbin
└── var
    └── log

正如我们将看到的,并非所有目录都具有相同的文件权限,并且目录中的各个文件可以具有比目录本身更严格的权限。

POSIX 文件访问权限

每个进程(在本讨论的上下文中表示每个正在运行的程序)都属于一个用户和一个或多个组。 用户由称为用户 IDUID的 32 位数字表示。 关于用户的信息,包括从 UID 到名称的映射,保存在/etc/passwd中。 同样,组由信息保存在/etc/group中的组 IDGID表示。 始终存在 UID 为0root用户和 GID 为0的根组。 root用户也称为超级用户,因为在默认配置中,它绕过了大多数权限检查,可以访问系统中的所有资源。 在基于 Linux 的系统中,安全性主要是限制对帐户的访问。

每个文件和目录也有一个所有者,并且恰好属于一个组。 进程对文件或目录的访问级别由一组访问权限标志控制,称为文件的模式。 有 3 个 3 位集合:第一个集合适用于文件的所有者,第二个集合适用于与该文件属于同一组的成员,最后一个集合适用于其他所有人-世界的其余部分。 这些位用于文件的读取(r)、写入(w)和执行(x)权限。 由于 3 位恰好适合一个八进制数字,因此它们通常用八进制表示,如下图所示:

Figure 5.1 – File access permissions

图 5.1<>文件访问许可

有第四个前面的八进制数字,其值具有特殊意义:

  • SUID(4):如果文件是可执行的,它会在程序运行时将进程的有效 UID 更改为文件所有者的 UID。
  • sgid(2):类似于 SUID,它将进程的有效 GID 更改为文件组的有效 GID。
  • Sticky(1):在目录中,这会限制删除,使一个用户无法删除另一个用户拥有的文件。 这通常在/tmp/var/tmp上设置。

SUID 位可能是最常用的。 它为非 root 用户提供临时权限提升到超级用户以执行任务。 一个很好的例子是ping程序:ping打开一个原始套接字,这是一个特权操作。 为了让普通用户使用ping,它归用户root所有,并设置了 SUID 位,以便当您运行ping时,无论您的 UID 是什么,它都会使用 UID0执行。

要设置此前导八进制数字,请在chmod命令中使用值 4、2 或 1。 例如,要在分段root目录中的/bin/ping上设置 SUID,您可以将4设置为模式755,如下所示:

$ cd ~/rootfs
$ ls -l bin/ping
-rwxr-xr-x 1 root root 35712 Feb 6 09:15 bin/ping
$ sudo chmod 4755 bin/ping
$ ls -l bin/ping
-rwsr-xr-x 1 root root 35712 Feb 6 09:15 bin/ping

请注意,第二个ls命令显示模式的前 3 位为rws,而之前它们为rwx。 该s表示 SUID 位已设置。

转移目录中的文件所有权权限

出于安全和稳定性原因,非常重要的一点是要注意将放置在目标设备上的文件的所有权和权限。 一般来说,您希望将敏感资源限制为只能由root用户访问,并尽可能少地使用非 root 用户运行程序。 最好使用非 root 用户运行程序,这样,如果他们受到外部攻击的危害,他们向攻击者提供的系统资源就会尽可能少。 例如,名为/dev/mem的设备节点提供对系统内存的访问,这在某些程序中是必需的。 但是,如果它对每个人都是可读和可写的,那么就没有安全性了,因为每个人都可以访问内存中的所有内容。 因此,/dev/mem应该属于root,属于root组,并且具有600模式,该模式拒绝除所有者之外的所有人的读写访问。

不过,临时目录有一个问题。 您在那里创建的文件将归您所有,但当它们安装到设备上时,它们应该属于特定的所有者和组,主要是root用户。 一个明显的解决方法是在此阶段使用如下所示的命令将所有权更改为root

$ cd ~/rootfs
$ sudo chown -R root:root *

问题是您需要root权限才能运行chown命令,从那时起,您需要成为root才能修改临时目录中的任何文件。 不知不觉中,您正在以root身份登录进行所有开发,这不是一个好主意。 这是一个我们稍后再来讨论的问题。

根文件系统的程序

现在,到了开始向root文件系统填充它们运行所需的基本程序和支持库、配置和数据文件的时候了。 我将从概述您将需要的程序类型开始。

Init 程序

init是第一个运行的程序,因此它是根文件系统的重要部分。 在本章中,我们将使用 BusyBox 提供的 Simpleinit程序。

壳 / 炮弹 / 鞘翅 / 外皮

我们需要一个外壳来运行脚本,并给我们一个命令提示符,以便我们可以与系统交互。 交互式 shell 在生产设备上可能不是必需的,但对于开发、调试和维护很有用。 嵌入式系统中常用的外壳有多种:

  • bash:这是桌面 Linux 中我们都熟悉和喜爱的巨兽。 它是 Unix Bourne shell 的超集,具有许多扩展或bashism

  • ash:同样基于 Bourne shell,它与 Unix 的 BSD 变体有着悠久的历史。 BusyBox 有一个ash版本,该版本已进行了扩展,使其与bash更兼容。 它比bash小得多,因此是嵌入式系统非常流行的选择。

  • hush: This is a very small shell that we briefly looked at in Chapter 3, All about Bootloaders. It is useful on devices with very little memory. There is a version of hush in BusyBox.

    给小费 / 翻倒 / 倾覆

    如果您使用ashhush作为目标上的 shell,请确保在目标上测试 shell 脚本。 我们很容易只在主机上使用bash测试它们,然后在将它们复制到目标时却发现它们不起作用,这是非常诱人的。

名单上的下一个是公用事业。

功用 / 公用事业 / 实用 / 效用

Shell 只是启动其他程序的一种方式,而 Shell 脚本只不过是要运行的个程序的列表,带有一些流控制和在程序之间传递信息的方法。 要使 shell 有用,您需要 Unix 命令行所基于的实用程序。 即使对于基本的根文件系统,也需要大约 50 个实用程序,这会带来两个问题。 首先,找到每一个的源代码并对其进行交叉编译将是一项相当大的工作。 其次,由此产生的程序集合将占用数十兆字节,这在嵌入式 Linux 早期是一个真正的问题,当时您只有几兆字节。 为了解决这个问题,BusyBox 应运而生。

BusyBox 出手相救!

BusyBox的起源与嵌入式 Linux 无关。 这个项目是由 Bruce Perens 在 1996 年为 Debian 安装程序发起的,这样他就可以从一张 1.44MB 的软盘上启动 Linux。 巧合的是,这差不多是当代设备上的存储大小,所以嵌入式 Linux 社区很快就采用了它。 从那时起,BusyBox 就一直是嵌入式 Linux 的核心。

BusyBox 是从头开始编写的,用于执行那些基本的 Linux 实用程序的基本功能。 开发人员利用了 80:20 规则:程序中最有用的 80%是用 20%的代码实现的。 因此,BusyBox 工具实现了桌面等效项的一部分功能,但它们做的足够多,在大多数情况下都是有用的。

BusyBox 采用的另一个技巧是将所有工具组合到一个二进制文件中,这样就可以轻松地在它们之间共享代码。 它的工作原理是这样的:BusyBox 是一个小程序集合 ,每个小程序都以[applet]_main的形式导出其main函数。 例如,cat命令在coreutils/cat.c中执行,并导出cat_main。 BusyBox 的main函数本身根据命令行参数将调用分派给正确的小程序。

因此,要读取文件,可以使用要运行的小程序的名称启动 BusyBox,后跟小程序需要的任何参数,如下所示:

$ busybox cat my_file.txt

您还可以不带参数运行 BusyBox,以获得已编译 的所有 applet 的列表。

以这种方式使用 BusyBox 相当笨拙。 让 BusyBox 运行cat小程序的更好方法是创建从/bin/cat/bin/busybox的符号链接:

$ ls -l bin/cat bin/busybox
-rwxr-xr-x 1 root root 892868 Feb 2 11:01 bin/busybox
lrwxrwxrwx 1 root root 7      Feb 2 11:01 bin/cat -> busybox

当您在命令行中键入cat时,BusyBox 是实际运行的程序。 BusyBox 只需检查通过argv[0]传入的可执行文件的路径( 将是/bin/cat),提取应用名称cat,并进行表查找以匹配catcat_main。 所有这些都是在这段代码的libbb/appletlib.c中完成的(稍微简化):

applet_name = argv[0];
applet_name = bb_basename(applet_name);
run_applet_and_exit(applet_name, argv);

BusyBox 有 300 多个小程序,包括一个init程序,几个复杂程度不同的 shell,以及用于大多数管理任务的实用程序。 甚至还有一个简单版本的vi编辑器,您可以在设备上更改文本文件。 典型的 BusyBox 二进制文件只能启用几十个小程序。

总而言之,BusyBox 的典型安装由单个程序组成,每个小程序都有一个 符号链接,但它的行为就像是 个单独应用的集合。

构建 BusyBox

BusyBox 使用与内核相同的KconfigKbuild系统,因此交叉编译非常简单。 您可以通过克隆 BusyBox Git 存储库并签出您想要的版本(1_31_1是撰写本文时的最新版本)来获取源代码,如下所示:

$ git clone git://busybox.net/busybox.git
$ cd busybox
$ git checkout 1_31_1

您也可以从https://busybox.net/downloads/下载相应的 TAR 文件。

然后,从默认配置开始配置 BusyBox,这将启用 BusyBox 的几乎所有功能:

$ make distclean
$ make defconfig

此时,您可能希望运行make menuconfig来微调配置。 例如,您几乎肯定希望在Busybox Settings|Installation Options(CONFIG_PREFIX)中将安装路径设置为指向临时目录。 然后,您可以用通常的方式进行交叉编译。 如果您的目标是 Beaglebone Black,请使用以下命令:

$ make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf-

如果您的目标是多功能 PB 的 QEMU 仿真,请使用以下命令:

$ make ARCH=arm CROSS_COMPILE=arm-unknown-linux-gnueabi-

在任何一种情况下,结果都是可执行文件busybox。 对于这样的默认配置构建,大小约为 900 KiB。 如果这对您来说太大了,您可以通过更改配置来精简它,去掉不需要的实用程序。

要将 BusyBox 安装到临时区域,请使用以下命令:

$ make ARCH=arm CROSS_COMPILE=arm-cortex_a8-linux-gnueabihf- install

这将把二进制文件复制到在CONFIG_PREFIX中配置的目录,并创建指向它的所有符号链接。

现在我们将看一看 Busybox 的替代方案,称为 Toybox。

Toybox-BusyBox 的替代品

BusyBox 并不是镇上唯一的游戏。 此外,还有Toybox,您可以在http://landley.net/toybox/找到。 这个项目是由 Rob Landley 发起的,他之前是 BusyBox 的维护者。 Toybox 与 BusyBox 的目标相同,但更多地强调遵循标准,特别是 POSIX-2008 和 LSB 4.1,而不是与这些标准的 GNU 扩展兼容。 Toybox 比 BusyBox 小,部分原因是它实现的 applet 较少。 它的许可证是 BSD,而不是 GPL v2,这使得它与拥有 BSD 许可用户空间的操作系统(如 Android)兼容。 因此,Toybox 将与所有新的 Android 设备一起交付。 从最近的 0.8.3 版本开始,TOYBOX 的Makefile可以构建一个完整的 Linux 系统,在给定 Linux 和 Toybox 源代码的情况下,该系统可以引导到 shell 提示符。

根文件系统的库

程序与库链接在一起。 您可以将它们全部静态链接,在这种情况下,目标设备上将没有个库。 但是,如果您有两个或三个以上的程序,这会占用不必要的大量存储空间。 因此,您需要将共享库从工具链复制到临时目录。 你怎么知道哪些图书馆?

一个选项是从工具链的sysroot目录复制所有.so文件。 不要试图预测要包含哪些库,只需假设您的映像最终将需要它们。 这当然是合乎逻辑的,如果您正在创建一个供其他人用于一系列应用的平台,这将是正确的方法。 不过,请注意,一个完整的glibc是相当大的。 在glibc2.22 的 Crossstool-NG 构建的情况下,库、区域设置和其他支持文件的大小为 33MiB。 当然,您可以使用musl libcuClibc-ng大幅减少。

另一种选择是只挑选您需要的库,对于这些库,您需要一种发现库依赖关系的方法。 使用我们在第 2 章了解工具链中的一些知识,我们可以使用readelf命令执行此任务:

$ cd ~/rootfs
$ arm-cortex_a8-linux-gnueabihf-readelf -a bin/busybox | grep "program interpreter"
[Requesting program interpreter: /lib/ld-linux-armhf.so.3]
$ arm-cortex_a8-linux-gnueabihf-readelf -a bin/busybox | grep "Shared library"
0x00000001 (NEEDED) Shared library: [libm.so.6]
0x00000001 (NEEDED) Shared library: [libc.so.6]

第一个readelf命令在busybox二进制文件中搜索包含program interpreter的行。 第二个readelf命令在busybox二进制文件中搜索包含Shared library的行。 现在,您需要在工具链sysroot目录中找到这些文件,然后将它们复制到临时目录。 记住,您可以这样找到sysroot

$ arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot
/home/chris/x-tools/arm-cortex_a8-linux-gnueabihf/arm-cortex_a8-linux-gnueabihf/sysroot

为了减少打字量,我将在 shell 变量中保留一份副本:

$ export SYSROOT=$(arm-cortex_a8-linux-gnueabihf-gcc -print-sysroot)

如果您查看sysroot中的/lib/ld-linux-armhf.so.3,您会发现它实际上是一个符号链接:

$ cd $SYSROOT
$ ls -l lib/ld-linux-armhf.so.3
lrwxrwxrwx 1 chris chris 10 Mar 3 15:22 lib/ld-linux-armhf.so.3 -> ld-2.22.so

libc.so.6libm.so.6重复该练习,最终将得到一个包含三个文件和三个符号链接的列表。 现在,您可以使用cp -a复制每个文件,这将保留符号链接:

$ cd ~/rootfs
$ cp -a $SYSROOT/lib/ld-linux-armhf.so.3 lib
$ cp -a $SYSROOT/lib/ld-2.22.so lib
$ cp -a $SYSROOT/lib/libc.so.6 lib
$ cp -a $SYSROOT/lib/libc-2.22.so lib
$ cp -a $SYSROOT/lib/libm.so.6 lib
$ cp -a $SYSROOT/lib/libm-2.22.so lib

对每个程序重复此程序。

给小费 / 翻倒 / 倾覆

只有在获得尽可能最小的嵌入式内存占用量时,才值得这样做。 您可能会错过通过dlopen(3)调用加载的库-主要是插件。 在本章后面的配置网络接口时,我们将查看一个使用名称服务交换机(NSS)库的示例。

通过剥离来减小尺寸

库和程序通常使用存储在符号表中的一些信息进行编译,以帮助调试和跟踪。 在生产系统中很少需要这些。 节省空间的一种快捷方法是剥离符号表的二进制文件。 此示例显示剥离前的libc

$ file rootfs/lib/libc-2.22.so
lib/libc-2.22.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 4.3.0, not stripped
$ ls -og rootfs/lib/libc-2.22.so
-rwxr-xr-x 1 1542572 Mar 3 15:22 rootfs/lib/libc-2.22.so

现在,让我们看看剥离调试信息的结果:

$ arm-cortex_a8-linux-gnueabihf-strip rootfs/lib/libc-2.22.so
$ file rootfs/lib/libc-2.22.so
rootfs/lib/libc-2.22.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (GNU/Linux), dynamically linked (uses shared libs), for GNU/Linux 4.3.0, stripped
$ ls -og rootfs/lib/libc-2.22.so
-rwxr-xr-x 1 1218200 Mar 22 19:57 rootfs/lib/libc-2.22.so

在本例中,我们保存了 324,372 字节,约为剥离前文件大小的 20%。

给小费 / 翻倒 / 倾覆

在剥离内核模块时要小心。 模块加载器需要一些符号来重新定位模块代码,因此如果剥离这些符号,模块将无法加载。 使用此命令删除调试符号,同时保留用于重新定位的符号:strip --strip-unneeded <module name>

设备节点

Linux 中的大多数设备都由设备节点表示,这与 Unix 的理念一致,即一切都是一个文件(除了网络接口,它是套接字)。 设备节点可以指块设备或字符设备。 块设备是大容量存储设备,如 SD 卡或硬盘驱动器。 同样,字符设备几乎是其他任何东西,除了网络接口。 设备节点的传统位置是名为/dev的目录。 例如,串行端口可以由称为/dev/ttyS0的设备节点表示。

设备节点是使用名为mknod(make node的缩写)的程序创建的:

mknod <name> <type> <major> <minor>

mknod的参数如下:

  • name是要创建的设备节点的名称。
  • type对于字符设备是c,对于块设备是b
  • majorminor是内核用来将文件请求路由到适当的设备驱动程序代码的一对数字。 在Documentation/devices.txt文件的内核源代码中有一个标准主号和次号的列表。

您需要为您的 系统上要访问的所有设备创建设备节点。 您可以使用mknod命令手动创建它们,正如我将在这里说明的那样,或者 您可以在运行时使用我 稍后将提到的设备管理器之一自动创建它们。

在真正的最小根文件系统中,使用 BusyBox 引导只需要两个节点:consolenull。 控制台只需要设备节点的所有者root可以访问,因此访问权限为600 (rw-------)。 每个人都可以读写null设备,因此模式为666 (rw-rw-rw-)。 您可以使用mknod-m选项在创建节点时设置模式。 您需要是root才能创建设备节点,如下所示:

$ cd ~/rootfs
$ sudo mknod -m 666 dev/null c 1 3
$ sudo mknod -m 600 dev/console c 5 1
$ ls -l dev
total 0
crw------- 1 root root 5, 1 Mar 22 20:01 console
crw-rw-rw- 1 root root 1, 3 Mar 22 20:01 null

您可以使用标准的rm命令删除设备节点。 没有rmnod命令,因为一旦创建,它们就只是文件。

proc 和 sysfs 文件系统

procsysfs是两个伪文件系统,它们提供了了解内核内部工作的窗口。 它们都将内核数据表示为目录层次结构中的文件:当您读取其中一个文件时,您看到的内容并不是来自磁盘存储;它是由内核中的一个函数动态格式化的。 有些文件也是可写的,这意味着使用您写入的新数据调用内核函数,如果它是正确格式的,并且您有足够的权限,它将修改内核内存中存储的值。 换句话说,procsysfs提供了与设备驱动程序和其他内核代码交互的另一种方式。 应该将procsysfs文件系统挂载在名为/proc/sys的目录中:

# mount -t proc proc /proc
# mount -t sysfs sysfs /sys

虽然它们在概念上非常相似,但它们执行的功能不同。 proc从早期就是 Linux 的一部分。 它最初的目的是向用户空间公开有关进程的信息,因此得名。 为此,每个名为/proc/<PID>的进程都有一个目录,其中包含有关其状态的信息。 进程列表命令ps读取这些文件以生成其输出。 此外,还有一些文件提供有关内核其他部分的信息,例如,/proc/cpuinfo告诉您有关 CPU 的信息,/proc/interrupts提供有关中断的信息,等等。

最后,在/proc/sys中,有一些文件可以显示和控制内核子系统的状态和行为,特别是调度、内存管理和联网。 手册页是您将在proc目录中找到的文件的最佳参考,您可以通过键入man 5 proc查看该目录。

另一方面,sysfs的角色是将内核驱动程序模型呈现给用户空间。 它导出与设备和设备驱动程序以及它们彼此连接方式相关的文件层次结构。 当我在第 11 章与设备驱动程序接口中描述与设备驱动程序的交互时,我将更详细地介绍 Linux 驱动程序模型。

挂载文件系统

mount命令允许我们将一个文件系统附加到另一个文件系统中的目录,从而形成文件系统的层次结构。 顶部的文件系统称为根文件系统,它是在 内核引导时挂载的。 mount命令 的格式如下:

mount [-t vfstype] [-o options] device directory

mount的参数如下:

  • vfstype是文件系统的类型。
  • options是逗号分隔的mount选项列表。
  • device是文件系统驻留的块设备节点。
  • directory是要挂载文件系统的目录。

-o之后可以提供各种选项;有关更多信息,请参阅手册页mount(8)。 例如,如果您想要将第一个分区中包含ext4文件系统的 SD 卡挂载到名为/mnt的目录中,则需要键入以下代码:

# mount -t ext4 /dev/mmcblk0p1 /mnt

假设挂载成功,您将能够在/mnt目录中看到 SD 卡上存储的文件。 在某些情况下,您可以省略文件系统类型,让内核探测设备以找出存储在那里的内容。 如果挂载失败,您可能首先需要卸载分区,以防您的 Linux 发行版配置为在插入 SD 卡时自动挂载 SD 卡上的所有分区。

在挂载proc文件系统的示例中,有一点很奇怪:没有像/dev/proc这样的设备节点,因为它是伪文件系统而不是真正的文件系统。 但是mount命令需要一个device参数。 因此,我们必须给出一个字符串device的位置,但是这个字符串是什么并不重要。 这两个命令可实现完全相同的结果:

# mount -t proc procfs /proc
# mount -t proc nodevice /proc

mount命令忽略procfsnodevice字符串。 在挂载伪文件系统时,使用文件系统类型代替设备是相当常见的。

内核模块

如果您有内核模块,则需要使用modules_install内核 make 目标将它们安装到根文件系统中,如我们在第 4 章配置和构建内核中看到的那样。 这会将它们连同modprobe命令所需的配置文件一起复制到名为/lib/modules/<kernel version>的目录中。

请注意,您刚刚在内核和根文件系统之间创建了一个依赖项。 如果您更新其中一个,则必须更新另一个。

既然我们已经了解了如何从 SD 卡挂载文件系统,让我们来看看挂载根文件系统的不同选项。 替代方案(ramdisk 和 NFS)可能会让您大吃一惊,特别是如果您不熟悉嵌入式 Linux 的话。 内存磁盘可保护原始源图像免受损坏和损坏。 我们将在第 9 章创建存储策略中了解有关闪存软件的更多信息。 网络文件系统允许更快速的开发,因为文件更改可以立即传播到目标。

将根文件系统传输到目标

在在临时目录中创建了主干根文件系统之后,下一个任务是将其传输到目标。 在接下来的几节中,我将描述三种可能性:

  • initramfs:也称为 ramdisk,这是由引导加载程序加载到 RAM 中的文件系统映像。 内存磁盘很容易创建,并且不依赖于大容量存储驱动程序。 当主根文件系统需要更新时,可以在备用维护模式下使用它们。 它们甚至可以用作小型嵌入式设备中的主要根文件系统,并且在主流 Linux 发行版中通常用作早期用户空间。 请记住,根文件系统的内容是易失性的,您在运行时对根文件系统所做的任何更改都将在系统下次引导时丢失。 您需要另一种存储类型来存储永久数据,如配置参数。
  • 磁盘映像:这是根文件系统的副本,已格式化,可以加载到目标上的大容量存储设备上。 例如,它可以是准备复制到 SD 卡上的ext4格式的图像,也可以是准备通过引导加载程序加载到闪存中的jffs2格式的图像。 创建磁盘映像可能是最常见的选项。 在第 9 章创建存储策略中有关于不同类型的大容量存储的更多信息。
  • 网络文件系统:临时目录可以通过 NFS 服务器导出到网络,并在引导时由目标挂载。 这通常是在开发阶段完成的,而不是创建磁盘映像并将其重新加载到大容量存储设备的重复周期,这是一个相当缓慢的过程。

我将从 ramdisk 开始,并使用它来说明对根文件系统的一些改进,比如添加用户名和设备管理器以自动创建设备节点。 然后,我将向您展示如何创建磁盘映像以及如何使用 NFS 通过网络挂载根文件系统。

创建引导 initramfs

初始 RAM 文件系统或initramfs是压缩的cpio归档。 cpio是一种旧的 Unix 存档格式,类似于 tar 和 ZIP,但更容易解码,因此在内核中需要的代码更少。 您需要使用CONFIG_BLK_DEV_INITRD配置内核以支持initramfs

碰巧,有三种不同的方法可以创建引导 ramdisk:作为独立的cpio归档文件、作为嵌入在内核映像中的cpio归档文件,以及作为内核构建系统作为构建的一部分进行处理的设备表。 第一个选项提供了最大的灵活性,因为我们可以根据自己的需要混合和匹配内核和内存磁盘。 然而,这意味着您需要处理两个文件,而不是一个文件,而且并不是所有的引导加载器都具有加载单独的 ramdisk 的功能。 稍后我将向您展示如何在内核中构建一个。

独立 initramfs

下面的指令序列创建档案,压缩档案,并添加准备加载到目标上的 U-Boot 标头:

$ cd ~/rootfs
$ find . | cpio -H newc -ov --owner root:root > 
../initramfs.cpio
$ cd ..
$ gzip initramfs.cpio
$ mkimage -A arm -O linux -T ramdisk -d initramfs.cpio.gz uRamdisk

请注意,我们使用--owner root:root选项运行cpio。 这是对先前在临时目录部分的文件所有权权限中提到的文件所有权问题的快速修复。 它使cpio存档中的所有内容都具有0的 UID 和 GID。

uRamdisk文件的最终大小约为 2.9MB,没有内核模块。 加上内核zImage文件的 4.4MB 和 U-Boot 的 440KB,总共需要 7.7MB 的存储空间来引导此板。 我们离开始这一切的 1.44MB 软盘还有一点距离。 如果大小确实是个问题,您可以使用以下选项之一:

  • 去掉不需要的驱动程序和函数,让内核变得更小。
  • 去掉不需要的实用程序,让 BusyBox 变得更小。
  • musl libcuClibc-ng代替glibc
  • 静态编译 BusyBox。

现在我们已经组装了一个initramfs,让我们引导归档文件。

引导 initramfs

我们能做的最简单的事情是在控制台上运行一个 shell,这样我们就可以与目标交互。 我们可以通过将rdinit=/bin/sh添加到内核命令行来做到这一点。 接下来的两个部分将展示如何在 QEMU 和 Beaglebone Black 上做到这一点。

使用 QEMU 引导

QEMU 具有名为-initrd的选项,可将initramfs加载到内存中。 从第 4 章配置和构建内核、使用arm-unknown-linux-gnueabi工具链编译的zImage以及用于多功能 PB 的设备树二进制文件中,您应该已经有了。 从本章开始,您应该已经创建了一个initramfs,其中包括使用相同工具链编译的 BusyBox。 现在,您可以使用MELP/Chapter05/run-qemu-initramfs.sh中的脚本或使用以下命令启动 QEMU:

$ QEMU_AUDIO_DRV=none \
qemu-system-arm -m 256M -nographic -M versatilepb    
-kernel zImage
-append "console=ttyAMA0 rdinit=/bin/sh" \ 
-dtb versatile-pb.dtb 
-initrd initramfs.cpio.gz

您应该会得到一个带有提示符/ #的根 shell。

启动 Beaglebone Black

对于 Beaglebone Black,我们需要在第 4 章配置和构建内核中准备的 microSD 卡,以及使用arm-cortex_a8-linux-gnueabihf工具链构建的根文件系统。 将您在本节前面创建的uRamdisk复制到 microSD 卡上的引导分区,然后使用它将 Beaglebone Black 引导到出现 U-Boot 提示符的位置。 然后,输入以下命令:

fatload mmc 0:1 0x80200000 zImage
fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb fatload mmc 0:1 0x81000000 uRamdisk
setenv bootargs console=ttyO0,115200 rdinit=/bin/sh
bootz 0x80200000 0x81000000 0x80f00000

如果一切顺利,您将在串行控制台上获得一个带有提示符/ #的根 shell。 完成此操作后,我们将需要在两个平台上安装proc

安装流程

您会发现ps命令在两个平台上都不起作用。 这是因为proc文件系统尚未挂载。 尝试挂载它:

# mount -t proc proc /proc

现在,再次运行ps,您将看到进程列表。

对此设置的改进是编写一个挂载proc的 shell 脚本,以及在引导时需要执行的任何其他操作。 然后,您可以在引导时运行此脚本,而不是运行 。 下面的代码片段让您了解它的工作原理:

#!/bin/sh
/bin/mount -t proc proc /proc
# Other boot-time commands go here
/bin/sh

最后一行/bin/sh启动一个新的 shell,它会给您一个交互式的 root shell 提示。 以这种方式将 shell 用作init对于快速破解非常方便,例如,当您想要用损坏的init程序拯救系统时。 但是,在大多数情况下,您将使用init程序,我们将在本章后面介绍该程序。 但是,在此之前,我想看看加载initramfs的另外两种方法。

在内核镜像中构建 initramfs

到目前为止,我们已经创建了一个压缩的initramfs作为单独的文件,并使用引导加载程序将其加载到内存中。 某些引导加载程序不能以这种方式加载initramfs文件。 为了处理这些情况,可以将 Linux 配置为将initramfs合并到内核映像中。 为此,请更改内核配置,并将CONFIG_INITRAMFS_SOURCE设置为您之前创建的cpio归档的完整路径。 如果您使用的是menuconfig,则它位于常规设置|Initramfs 源文件中。 请注意,它必须是以.cpio结尾的未压缩的cpio文件,而不是gzipped版本。 然后,构建内核。

引导过程与之前相同,只是没有 ramdisk 文件。 对于 QEMU,命令如下所示:

$ QEMU_AUDIO_DRV=none \
qemu-system-arm -m 256M -nographic -M versatilepb \
-kernel zImage \
-append "console=ttyAMA0 rdinit=/bin/sh" \
-dtb versatile-pb.dtb

对于 Beaglebone Black,在 U-Boot 提示符下输入以下命令:

fatload mmc 0:1 0x80200000 zImage
fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb
setenv bootargs console=ttyO0,115200 rdinit=/bin/sh
bootz 0x80200000 – 0x80f00000

当然,您必须记住在每次更改根文件系统的内容然后重新构建内核时重新生成cpio文件。

使用设备表构建 initramfs

设备表是一个文本文件,其中列出了进入存档或文件系统映像的文件、目录、设备节点和链接。 压倒性的优势在于,它允许您在归档文件中创建属于root用户或任何其他 UID 的条目,而无需您自己拥有 root 权限。 您甚至可以在不需要 root 权限的情况下创建设备节点。 所有这些都是可能的,因为存档只是一个数据文件。 只有当 Linux 在引导时将其展开时,才会使用您指定的属性创建真正的文件和目录。

内核有一个特性,允许我们在创建initramfs时使用设备表。 您编写设备表文件,然后将CONFIG_INITRAMFS_SOURCE指向它。 然后,当您构建内核时,它从设备表中的指令创建cpio存档。 在任何情况下,您都不需要root访问权限。

下面是我们的简单rootfs的设备表,但缺少大多数指向 BusyBox 的符号链接以使其易于管理:

dir /bin 775 0 0
dir /sys 775 0 0
dir /tmp 775 0 0
dir /dev 775 0 0
nod /dev/null 666 0 0 c 1 3
nod /dev/console 600 0 0 c 5 1
dir /home 775 0 0
dir /proc 775 0 0
dir /lib 775 0 0
slink /lib/libm.so.6 libm-2.22.so 777 0 0
slink /lib/libc.so.6 libc-2.22.so 777 0 0
slink /lib/ld-linux-armhf.so.3 ld-2.22.so 777 0 0
file /lib/libm-2.22.so /home/chris/rootfs/lib/libm-2.22.so 755 0 0
file /lib/libc-2.22.so /home/chris/rootfs/lib/libc-2.22.so 755 0 0
file /lib/ld-2.22.so /home/chris/rootfs/lib/ld-2.22.so 755 0 0

语法相当明显:

  • dir <name> <mode> <uid> <gid>
  • file <name> <location> <mode> <uid> <gid>
  • nod <name> <mode> <uid> <gid> <dev_type> <maj> <min>
  • slink <name> <target> <mode> <uid> <gid>

dirnodslink命令使用给定的名称、模式、用户 ID 和组 ID 在initramfs cpio存档中创建文件系统对象。 file命令将文件从源位置复制到存档中,并设置模式、用户 ID 和组 ID。

通过usr/gen_initramfs_list.sh中内核源代码中的脚本从给定目录创建设备表,从从头开始创建initramfs设备表的任务变得更加容易。 例如,要为rootfs目录创建initramfs设备表,并将用户 ID1000和组 ID1000拥有的所有文件的所有权更改为用户和组 ID0,您可以使用以下命令:

$ bash linux-stable/scripts/gen_initramfs_list.sh -u 1000 \ 
-g 1000 
rootfs > initramfs-device-table

使用此脚本的-o选项可以创建压缩的initramfs文件,其格式取决于-o之后的文件扩展名。

请注意,该脚本仅适用于bash外壳。 如果您的系统具有不同的默认 shell,就像大多数 Ubuntu 配置一样,您会发现脚本失败。 因此,在前面给出的命令中,我显式地使用了bash来运行脚本。

旧的 initrd 格式

Linux ramdisk 有一种旧格式,称为initrd。 它是 Linux 2.6 之前唯一可用的格式,如果您正在使用 Linux 的非 MMU 变体 uClinux,则仍然需要它。 这是相当模糊的,我不会在这里报道它。 在Documentation/initrd.txt中有更多关于内核源代码的信息。

一旦我们的initramfs启动,系统就需要开始运行程序。 第一个运行的程序是init程序。 接下来让我们来看看这一点。

init 程序

对于简单的情况,在引导时运行 shell,甚至是 shell 脚本是很好的,但实际上您需要一些更灵活的东西。 通常,Unix 系统运行一个名为init的程序,该程序启动并监视其他程序。 多年来,已经有许多init程序,我将在第 13 章启动-init 程序中描述其中一些程序。 现在,我将简要介绍 BusyBox 的init程序。

init程序从读取配置文件/etc/inittab开始。 这里有一个简单的例子,足以满足我们的需求:

::sysinit:/etc/init.d/rcS
::askfirst:-/bin/ash

启动init时,第一行运行 shell 脚本rcS。 第二行打印消息请按 Enter 将此控制台激活到控制台,并在按Enter时启动外壳程序。 /bin/ash前面的-表示它将成为一个登录 shell,它在给出 shell 提示之前获取/etc/profile$HOME/.profile。 这样启动 shell 的好处之一是启用了作业控制。 最直接的效果是可以使用Ctrl+C来终止当前程序。 也许你之前没有注意到,但是等到你运行了ping程序,你会发现你无法阻止它!

如果根文件系统中不存在任何内容,BusyBoxinit会提供默认值inittab。 它比前一个稍微广泛一些。

名为/etc/init.d/rcS的脚本用于放置需要在引导时执行的初始化命令,例如挂载procsysfs文件系统:

#!/bin/sh
mount -t proc proc /proc 
mount -t sysfs sysfs /sys

确保将rcS设置为可执行文件,如下所示:

$ cd ~/rootfs
$ chmod +x etc/init.d/rcS

通过如下更改-append参数,您可以在 QEMU 上尝试:

-append "console=ttyAMA0 rdinit=/sbin/init"

对于 Beaglebone Black,您需要在 U-Boot 中设置bootargs变量,如下所示:

setenv bootargs console=ttyO0,115200 rdinit=/sbin/init

现在,让我们仔细看看init在启动过程中读取的inittab

启动守护进程

通常,希望在启动时运行某些后台进程。 让我们以日志守护进程syslogd为例。 syslogd的目的是积累来自其他程序(主要是其他守护进程)的日志消息。 当然,BusyBox 为此提供了一个小程序!

启动守护进程非常简单,只需将如下一行添加到etc/inittab

::respawn:/sbin/syslogd -n

respawn表示如果程序终止,它将自动重启;-n表示它应该作为前台进程运行。 日志将写入/var/log/messages

重要音符

您可能还想以同样的方式启动klogdklogd将内核日志消息发送到syslogd,以便可以将它们记录到永久存储中。

接下来,我们将学习如何配置用户帐户。

配置用户帐户

正如我已经暗示的那样,以root身份运行所有程序并不是一种好的做法,因为如果 一个程序受到外部攻击的危害,那么整个系统就处于危险之中。 最好创建非特权用户帐户,并在不需要 Fullroot 的地方使用它们。

用户名在/etc/passwd中配置。 每个用户一行,由冒号分隔的七个信息字段按顺序排列如下:

  • 登录名
  • 用于验证密码的散列码,或者,更常见的情况是,验证x以指示密码存储在/etc/shadow
  • 用户 ID
  • 组 ID
  • 注释字段,通常为空
  • 用户的home目录
  • 此用户将使用的外壳(可选)

下面是一个简单的示例,其中 UID 为0的用户root和 UID 为1的用户daemon

root:x:0:0:root:/root:/bin/sh
daemon:x:1:1:daemon:/usr/sbin:/bin/false

将用户daemon的 shell 设置为/bin/false可确保使用该名称登录的任何尝试都将失败。

各种程序必须读取/etc/passwd才能查找用户名和 UID,因此文件必须是完全可读的。 如果密码散列也存储在那里,这将是一个问题,因为恶意程序将能够复制一份副本,并使用各种破解程序发现实际密码。 因此,为了减少此敏感信息的暴露,密码存储在/etc/shadow中,并将x放在密码字段中以指示情况确实如此。 名为/etc/shadow的文件只需要由root访问,因此只要root用户没有受到攻击,密码就是安全的。 影子密码文件由每个用户一个条目组成,由九个字段组成。 下面是一个镜像上一段中所示密码文件的示例:

root::10933:0:99999:7:::
daemon:*:10933:0:99999:7:::

前两个字段是用户名和密码散列。 其余七个字段与密码老化有关,这在嵌入式设备上通常不是问题。 如果您对全部细节感兴趣,请参考shadow(5)的手册页。

在本例中,root的密码为空,这意味着 root 用户无需提供密码即可登录。 为root设置空密码在开发过程中很有用,但在生产过程中并不有用。 您可以通过在目标系统上运行passwd命令来生成或更改密码散列,这将向/etc/shadow写入一个新的散列。 如果希望所有后续根文件系统都使用相同的密码,可以将此文件复制回临时目录。

组名在/etc/group中的存储方式类似。 每组有一行,由冒号分隔的四个字段组成。 字段如下:

  • 组的名称
  • 群密码,通常为x字符,表示没有 群密码
  • GID 或组 ID
  • 属于此组的可选用户列表,用逗号分隔

下面是一个例子:

root:x:0:
daemon:x:1:

现在我们已经了解了如何配置用户帐户,让我们看看如何将其添加到根文件系统。

向根文件系统添加用户帐户

首先,您必须将文件etc/passwdetc/shadowetc/group添加到您的临时目录中,如上一节所示。 确保etc/shadow的权限为0600。 接下来,您需要通过启动一个名为getty的程序来启动登录过程。 BusyBox 中有一个版本的getty。 您可以使用关键字respawninittab启动它,该关键字在终止登录 shell 时重新启动getty。 您的inittab应如下所示:

::sysinit:/etc/init.d/rcS
::respawn:/sbin/getty 115200 console

然后,重建内存磁盘并像以前一样使用 QEMU 或 Beaglebone Black 进行测试。

在本章的前面部分,我们学习了如何使用mknod命令创建设备节点。 现在,让我们看一下创建设备节点的一些更简单的方法。

更好的设备节点管理方式

使用mknod静态创建设备节点是一项相当繁重且不灵活的工作。 还可以通过其他方式按需自动创建设备节点:

  • devtmpfs:这是您在引导时通过/dev挂载的伪文件系统。 内核使用内核当前知道的所有设备的设备节点填充它,并在运行时检测到新设备时为其创建节点。 节点归root所有,默认权限为0600。 一些众所周知的设备节点(如/dev/null/dev/random)会将缺省值覆盖为0666。 要确切了解这是如何完成的,请查看 Linux 源文件drivers/char/mem.c,看看struct memdev是如何初始化的。

  • mdev:这是一个 BusyBox 小程序,用于使用设备节点填充目录并根据需要创建新节点。 有一个配置文件/etc/mdev.conf,它包含有关节点所有权和模式的规则。

  • udev: This is the mainstream equivalent of mdev. You will find it on desktop Linux and in some embedded devices. It is very flexible and a good choice for higher-end embedded devices. It is now part of systemd.

    重要音符

    虽然mdevudev都是自己创建设备节点,但是让devtmpfs来完成这项工作并使用mdev/udev作为顶层来实现设置所有权和权限的策略会更容易一些。 devtmpfs方法是在用户空间启动之前生成设备节点的唯一可维护方式。

让我们看一些使用这些工具的示例。

使用 devtmpfs 的示例

devtmpfs文件系统的支持由内核配置变量CONFIG_DEVTMPFS控制。 在 ARM Versatile PB 的默认配置中没有启用,因此如果您想要使用此目标来尝试以下功能,则必须返回到内核配置并启用此选项。 尝试使用devtmpfs非常简单,只需输入以下命令:

# mount -t devtmpfs devtmpfs /dev

之后,您会注意到在/dev中有更多的设备节点。 要获得永久修复,请将以下内容添加到/etc/init.d/rcS

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev

如果在内核配置中启用CONFIG_DEVTMPFS_MOUNT,内核将在挂载根文件系统后立即自动挂载devtmpfs。 但是,此选项在引导initramfs时不起作用,就像我们在这里所做的那样。

使用 mdev 的示例

虽然mdev的设置有点复杂,但它确实允许您在创建设备节点时修改它们的权限。 首先运行带有-s选项的mdev,这会导致它扫描/sys目录,查找有关当前设备的信息。 根据该信息,它使用相应的节点填充/dev目录。 如果您希望跟踪新设备上线并为其创建节点,则需要通过写入/proc/sys/kernel/hotplug使mdev成为热插拔客户端。 /etc/init.d/rcS的这些新增功能将实现所有这些功能:

#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

默认模式为660,所有权为root:root。 您可以通过在/etc/mdev.conf中添加规则来更改此设置。 例如,要赋予nullrandomurandom设备正确的模式,您可以将以下内容添加到/etc/mdev.conf

null root:root 666
random root:root 444
urandom root:root 444

该格式记录在docs/mdev.txt中的 BusyBox 源代码中,在名为examples的目录中还有更多示例。

静态设备节点到底有那么糟糕吗?

与运行设备管理器相比,静态创建的设备节点确实有一个优势:它们在引导过程中不需要任何时间来创建。 如果最大限度地减少引导时间是当务之急,那么使用静态创建的设备节点将节省相当多的时间。

检测到设备并创建其节点后,启动顺序的下一步通常是配置网络。

配置网络

接下来,让我们看看一些基本的网络配置,以便我们可以与外部世界通信。 我是,假设有一个以太网接口eth0,并且我们只需要一个简单的 IPv4 配置。

这些示例使用 BusyBox 中的网络实用程序,它们对于使用旧而可靠的ifupifdown程序的简单用例来说已经足够了。 您可以阅读这两个版本的手册页以了解详细信息。 主网络配置存储在/etc/network/interfaces中。 您需要在临时目录中创建以下目录:

etc/network
etc/network/if-pre-up.d
etc/network/if-up.d
var/run

对于静态 IP 地址,/etc/network/interfaces如下所示:

auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
    address 192.168.1.101
    netmask 255.255.255.0
    network 192.168.1.0

对于使用 DHCP 分配的动态 IP 地址,/etc/network/interfaces如下所示:

auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp

您还必须配置 DHCP 客户端程序。 BusyBox 有一个名为udchpcd的。 它需要一个应该放在/usr/share/udhcpc/default.script中的 shell 脚本。 在examples/udhcp/simple.script目录中的 BusyBox 源代码中有一个合适的默认值。

Glibc 的网络组件

glibc使用称为名称服务开关(NSS)的机制来控制将名称解析为网络和用户编号的方式。 例如,用户名可以通过文件/etc/passwd解析为 UID,而诸如 HTTP 之类的网络服务可以通过/etc/services解析为服务端口号。 所有这些都是由/etc/nsswitch.conf配置的;有关详细信息,请参阅手册页nss(5)。 下面是一个简单的示例,它可以满足大多数嵌入式 Linux 实现的需要:

passwd:    files
group:     files
shadow:    files
hosts:     files dns
networks:  files
protocols: files
services:  files

除了主机名以外,所有内容都由/etc中相应命名的文件解析,如果它们不在/etc/hosts中,还可以通过 DNS 查找来解析。

要实现这一点,您需要用这些文件填充/etc。 所有 Linux 系统上的网络、协议和服务都是相同的,因此可以从您的开发 PC 中的/etc复制它们。 /etc/hosts至少应包含环回地址:

127.0.0.1 localhost

前面在配置用户帐户部分描述了其他文件passwdgroupshadow

拼图的最后一块是执行名称解析的库。 它们是根据nsswitch.conf的内容根据需要加载的插件,这意味着如果使用readelfldd,它们不会显示为依赖项。 您只需从工具链的sysroot复制它们:

$ cd ~/rootfs
$ cp -a $SYSROOT/lib/libnss* lib
$ cp -a $SYSROOT/lib/libresolv* lib

我们的临时目录现在已经完成,所以让我们从它生成一个文件系统。

使用设备表创建文件系统映像

我们在前面的创建引导 initramfs一节中看到,内核有一个使用设备表创建initramfs的选项。 设备表非常有用,因为它们允许非 root 用户创建设备节点,并将任意 UID 和 GID 值分配给任何文件或目录。 同样的概念也应用于创建其他文件系统映像格式的工具,如文件系统格式到工具的映射所示:

  • jffs2mkfs.jffs2
  • ubifsmkfs:ubifs
  • ext2genext2fs

当我们查看闪存的文件系统时,我们将查看第 9 章创建存储策略中的jffs2ubifs。 第三,ext2是通常用于管理包括 SD 卡的 d 闪存的格式。 下面的示例使用ext2创建可以复制到 SD 卡的磁盘映像。

要开始,您需要在主机上安装genext2fs工具。 在 Ubuntu 上,要安装的程序包名为genext2fs

$ sudo apt install genext2fs

genext2fs<name> <type> <mode> <uid> <gid> <major> <minor> <start> <inc> <count>格式的设备表文件,各字段含义如下:

  • name

  • type: One of the following:

    f:常规文件

    Колибри:一个目录

    c:字符专用设备文件

    b:块专用设备文件

    p:FIFO(命名管道)

  • uid:文件的 UID

  • gid:文件的 GID

  • majorminor:设备号(仅限设备节点)

  • startinccount:允许您从 Start 中的次要编号开始创建一组设备节点(仅限设备节点)

您不必指定每个文件,就像您对内核initramfs表所做的那样。 您只需指向一个目录-临时目录-并列出需要在最终文件系统映像中进行的更改和例外。

下面是一个为我们填充静态设备节点的简单示例:

/dev d 755 0 0 - - - - - 
/dev/null c 666 0 0 1 3 0 0 -
/dev/console c 600 0 0 5 1 0 0 -
/dev/ttyO0 c 600 0 0 252 0 0 0 -

然后,您可以使用genext2fs生成一个 4MB 的文件系统映像(即 4096 个默认大小的块,1024 字节):

$ genext2fs -b 4096 -d rootfs -D device-table.txt -U rootfs.ext2

现在,您可以将生成的图像rootfs.ext2复制到 SD 卡或类似的卡中,这是我们下一步要做的。

启动 Beaglebone Black

名为MELP/format-sdcard.sh的脚本在 microSD 卡上创建了两个分区:一个用于引导文件,另一个用于根文件系统。 假设您已经按照上一节所示创建了根文件系统映像,您可以使用dd命令将其写入第二个分区。 像往常一样,在将文件直接复制到这样的存储设备时,请绝对确保您知道哪张是 microSD 卡。 在本例中,我使用的是内置读卡器,即名为/dev/mmcblk0的设备,因此命令如下所示:

$ sudo dd if=rootfs.ext2 of=/dev/mmcblk0p2

请注意,主机系统上的读卡器可能有不同的名称。

然后,将 microSD 卡插入 Beaglebone Black,并将内核命令行设置为root=/dev/mmcblk0p2。 U-Boot 命令的完整序列如下:

fatload mmc 0:1 0x80200000 zImage
fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb
setenv bootargs console=ttyO0,115200 root=/dev/mmcblk0p2
bootz 0x80200000 – 0x80f00000

这是一个从普通块设备(如 SD 卡)挂载文件系统的示例。 同样的原则也适用于其他文件系统类型,我们将在第 9 章创建存储策略中更详细地介绍这些原则。

使用 NFS 挂载根文件系统

如果您的设备有网络接口,在开发期间通过网络挂载根文件系统通常很有用。 它允许您访问主机上几乎无限的存储空间,因此您可以添加调试工具和具有大型符号表的可执行文件。 作为额外的好处,在开发机器上对根文件系统所做的更新可以立即在目标系统上使用。 您还可以从主机访问目标的所有日志文件。

首先,您需要在主机上安装和配置 NFS 服务器。 在 Ubuntu 上,要安装的包名为nfs-kernel-server

$ sudo apt install nfs-kernel-server

NFS 服务器需要被告知哪些目录正在被导出到网络;这由/etc/exports控制。 每个导出对应一行。 格式在手册页exports(5)中进行了说明。 例如,要导出我主机上的根文件系统,我具有以下内容:

/home/chris/rootfs *(rw,sync,no_subtree_check,no_root_squash)

*将目录导出到我的本地网络上的任何地址。 如果您愿意,您可以在此时提供单个 IP 地址或范围。 下面是括在圆括号中的选项列表。 在*和左括号之间不能有任何空格。 以下是选项:

  • rw:这将以读写方式导出目录。
  • sync:此选项选择 NFS 协议的同步版本,它比async选项更健壮,但速度稍慢。
  • no_subtree_check:此选项禁用子树检查,这会带来轻微的安全隐患,但在某些情况下可以提高可靠性。
  • no_root_squash:此选项允许处理来自用户 ID0的请求,而不会挤压到其他用户 ID。有必要允许目标正确访问root拥有的文件。

/etc/exports进行更改后,重新启动 NFS 服务器以应用它们。

现在,您需要设置目标以通过 NFS 挂载根文件系统。 为此,您的内核必须使用CONFIG_ROOT_NFS配置。 然后,您可以将 Linux 配置为在引导时挂载,方法是将以下内容添加到内核命令行:

root=/dev/nfs rw nfsroot=<host-ip>:<root-dir> ip=<target-ip>

选项如下:

  • rw:这会以读写方式挂载根文件系统。

  • nfsroot:指定主机的 IP 地址,后跟导出的根文件系统的路径。

  • ip: This is the IP address to be assigned to the target. Usually, network addresses are assigned at runtime, as we have seen in the Configuring the network section. However, in this case, the interface has to be configured before the root filesystem is mounted and init has been started. Hence, it is configured on the kernel command line.

    重要音符

    Documentation/filesystems/nfs/nfsroot.txt中有关于内核源代码中的 NFS 根挂载的更多信息。

接下来,让我们启动一个映像,其中包含 QEMU 上的根文件系统和 Beaglebone Black。

使用 QEMU 进行测试

下面的脚本使用一对静态 IPv4 地址在主机上的名为tap0的网络设备和目标上的eth0之间创建一个虚拟网络,然后使用参数启动 QEMU,以将tap0用作模拟接口。

您需要将根文件系统的路径更改为分段目录的完整路径,如果 IP 地址与您的网络配置冲突,则可能还需要更改 IP 地址:

#!/bin/bash
KERNEL=zImage
DTB=versatile-pb.dtb
ROOTDIR=/home/chris/rootfs
HOST_IP=192.168.1.1
TARGET_IP=192.168.1.101
NET_NUMBER=192.168.1.0
NET_MASK=255.255.255.0
sudo tunctl -u $(whoami) -t tap0
sudo ifconfig tap0 ${HOST_IP}
sudo route add -net ${NET_NUMBER} netmask ${NET_MASK} dev tap0
sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"
QEMU_AUDIO_DRV=none \
qemu-system-arm -m 256M -nographic -M versatilepb -kernel ${KERNEL} -append "console=ttyAMA0,115200 root=/dev/nfs rw nfsroot=${HOST_IP}:${ROOTDIR} ip=${TARGET_IP}" -dtb ${DTB} -net nic -net tap,ifname=tap0,script=no

脚本在MELP/Chapter05/run-qemu-nfsroot.sh中可用。

它应该像以前一样引导,现在直接通过 NFS 导出使用临时目录。 您在该目录中创建的任何文件将立即对目标设备可见,并且在该设备中创建的任何文件将对开发 PC 可见。

使用 Beaglebone Black 进行测试

以类似的方式,您可以在 Beaglebone Black 的 U-Boot 提示符下输入以下命令:

setenv serverip 192.168.1.1
setenv ipaddr 192.168.1.101
setenv npath [path to staging directory]
setenv bootargs console=ttyO0,115200 root=/dev/nfs rw nfsroot=${serverip}:${npath} ip=${ipaddr}
fatload mmc 0:1 0x80200000 zImage
fatload mmc 0:1 0x80f00000 am335x-boneblack.dtb
bootz 0x80200000 - 0x80f00000

MELP/Chapter05/uEnv.txt中有一个 U-Boot 环境文件,其中包含所有这些命令。 只需将其复制到 microSD 卡的引导分区,U-Boot 将完成其余工作。

文件权限问题

您复制到转移目录中的文件将归您登录的用户的 UID 所有,通常为1000。 但是,目标不知道该用户。 此外,目标创建的任何文件都将归目标配置的用户所有,通常是root用户。 整件事一团糟。 不幸的是,没有简单的出路。 最佳解决方案是使用sudo chown -R 0:0 *命令复制临时目录,并将所有权更改为 UID,将 GID 更改为0。 然后,将此目录导出为 NFS 挂载。 它消除了仅在开发系统和目标系统之间共享根文件系统的一个副本的便利性,但至少文件所有权将是正确的。

在嵌入式 Linux 中,将设备驱动程序静态链接到内核而不是在运行时将其作为模块从根文件系统动态加载的情况并不少见。 那么,在修改内核源代码或 DBS 时,我们如何从 NFS 提供的快速迭代中获得同样的好处呢? 答案是 TFTP。

使用 TFTP 加载内核

既然我们已经了解了如何使用 NFS 通过网络挂载根文件系统,您可能会想知道是否有办法通过网络加载内核、设备树和initramfs。 如果我们可以这样做,那么唯一需要写入目标存储的组件就是引导加载器。 其他一切都可以从主机加载。 这将节省时间,因为您不需要不断刷新目标,甚至可以在闪存驱动程序仍在开发的情况下完成工作(这种情况正在发生)。

普通文件传输协议(TFTP)就是这个问题的答案。 TFTP 是一种非常简单的文件传输协议,旨在易于在 U-Boot 等引导加载程序中实现。

首先,您需要在主机上安装 TFTP 守护程序。 在 Ubuntu 上,要安装的包名为tftpd-hpa

$ sudo apt install tftpd-hpa

默认情况下,tftpd-hpa授予对/var/lib/tftpboot目录中文件的只读访问权限。 安装并运行tftpd-hpa后,将想要复制到目标的文件复制到/var/lib/tftpboot中,对于 Beaglebone Black,它将是zImageam335x-boneblack.dtb。 然后在 U-Boot 命令提示符下输入以下命令:

setenv serverip 192.168.1.1
setenv ipaddr 192.168.1.101
tftpboot 0x80200000 zImage
tftpboot 0x80f00000 am335x-boneblack.dtb
setenv npath [path to staging]
setenv bootargs console=ttyO0,115200 root=/dev/nfs rw nfsroot=${serverip}:${npath} ip=${ipaddr}
bootz 0x80200000 - 0x80f00000

您可能会发现tftpboot命令挂起,不停地打印字母T,这意味着 TFTP 请求超时。 发生这种情况的原因有很多,最常见的原因如下:

  • serverip的 IP 地址不正确。
  • 服务器上没有运行 TFTP 守护程序。
  • 服务器上存在阻止 TFTP 协议的防火墙。 默认情况下,大多数防火墙确实会阻止 TFTP 端口69

一旦您解决了问题,U-Boot 就可以从主机加载文件并以通常的方式引导。 您可以通过将命令放入uEnv.txt文件来自动执行该过程。

摘要

Linux 的优势之一是它可以支持广泛的根文件系统,因此可以进行定制以满足广泛的需求。 我们已经看到,可以使用少量组件手动构建简单的根文件系统,BusyBox 在这方面特别有用。 通过一步一步地完成这个过程,它让我们深入了解了 Linux 系统的一些基本工作原理,包括网络配置和用户帐户。 然而,随着设备变得越来越复杂,这项任务很快就变得难以管理。 而且,人们一直担心在执行过程中可能存在我们没有注意到的安全漏洞。

在下一章中,我将向您展示如何使用嵌入式构建系统使 创建嵌入式 Linux 系统的过程更加容易和可靠。 我将从 Buildroot 开始,然后看看更复杂但功能更强大的 Yocto 项目。

进一步阅读