explorer

万丈高楼平地起,勿在浮沙筑高台

0%

[What]Building a Root Filesystem

学习书籍:Mastering Embedded Linux Programming: Create fast and reliable embedded solutions with Linux 5.4 and the Yocto Project 3.1 (Dunfell), 3rd Edition

通过阅读这部书,将整个嵌入式 Linux 的开发知识串联起来,以整理这些年来所学的杂乱知识。

  • 开发主机:ubuntu 20.04 LTS
  • 开发板:myc-c8mmx-c imx8mm(4核 A53 + M4)
  • 系统:Linux 5.4
  • yocto:3.1

重新来梳理一下根文件系统编译。

根文件系统里面有什么?

内核挂载根文件系统,可以以initramfs的方式,或者通过root=参数指定的设备来挂载,然后执行其init程序来进行接下来的初始化。

最小的根文件系统包含下面这些基本组件:

  • init:用于初始化系统基本环境的程序,通常会调用一系列的脚本
  • shell:提供一个用于交互的命令行环境,以执行其他的程序
  • Daemons:守护进程为其他程序提供基础服务
  • Shared libraries:很多程序都会使用到共享库,所以这个是必须的
  • Configuration files:对守护进程对应的配置文件,通常位于/etc目录下
  • Device nodes:设备节点提供应用程序的访问设备驱动的通道
  • proc and sys:提供对内核参数的检测和控制文件夹
  • Kernel modules:内核模块会被安装于/lib/modules/<kernel versoin>/

目录的分布

为满足 FHS(Filesystem Hierarchy Standard)标准,一般目录分布如下:

  • /bin:对所有用户都适用的基础命令
  • /dev/:存放设备节点和其他特殊文件
  • /etc/:系统配置文件
  • /lib:系统基本的共享库
  • /proc:对进程等内核参数进行交互的虚拟文件
  • /sbin:对系统管理员所适用的基础命令
  • /sys:描述设备何其驱动对应关系的虚拟文件
  • /tmp:用于存放临时文件的 RAM fs
  • /usr:更多的命令、库、管理员工具等
  • /var:存放在运行时会被改变的文件

对于 procsysfs是需要挂载的:

1
2
3
4
5
$ mount [-t vfstype] [-o options] device directory
# 挂载存储设备时,大部分情况下不用主动指明文件系统
# 而对于 proc,sysfs 这种伪文件系统,则需要指明
# mount -t proc proc /proc
# mount -t sysfs sysfs /sys

创建staging文件夹

所谓的staging文件夹,就是一个根文件系统的基础框架,在最开始可以创建它:

1
2
3
4
5
6
7
$ 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
# 对于 ARM64 lib 引用的是 lib64,其实只需要为 lib 创建软链接即可
$ ln -s lib lib64

接下来就是要考虑一些文件的权限问题了,对于一些重要文件应该限制为root用户才能操作。而其他程序应该运行在普通用户模式。

目录中具有的程序

init程序

init程序是进入根文件系统后运行的第一个程序。

对于 busybox 而言,就是/sbin/init,最终还是指向 busybox 这个独立可执行程序。

init程序首先会读取/etc/inittab中的配置,然后依次启动对应的程序。

Shell

Shell用户运行脚步,和用户交互等。在嵌入式系统中,有这么几个常用的Shell

  • bash:功能强大,但是体积占用也大,一般运行于桌面系统中。
  • ash:和bash兼容性很好,且体积占用小,适合于嵌入式系统。
  • hush:用于 bootloader,占用很小的 shell。

其实只要空间不紧张,嵌入式也使用bash就好,因为和桌面系统完全一致,避免在桌面可以正常运行的脚本在嵌入式端运行就不正常了。

工具程序

工具程序用于支撑其他程序的正常运行。

BusyBox

原理

这些程序要是手动编译一个个放入文件系统会累死,而BusyBox就将这些工具精简编译到一个可执行程序中,这个程序就包含了常用的命令。

1
2
3
4
5
6
7
8
9
10
11
busybox.nosuid
cat -> /bin/busybox.nosuid
chgrp -> /bin/busybox.nosuid
chmod -> /bin/busybox.nosuid
chown -> /bin/busybox.nosuid
cp -> /bin/busybox.nosuid
cpio -> /bin/busybox.nosuid
date -> /bin/busybox.nosuid
dd -> /bin/busybox.nosuid
df -> /bin/busybox.nosuid
dmesg -> /bin/busybox.nosuid

当用户输入cat时,实际上是调用了busybox这个可执行文件,该文件按照如下流程处理:

  • 获取argv[0]得到字符串cat
  • 然后根据该字符串获取到对应入口函数cat_main
  • 执行cat_main

构建 BusyBox

首选获取源码:

1
$ git clone https://git.busybox.net/busybox

然后切换到最新稳定版:

1
$ git checkout 1_34_stable

按照惯例,当然是先clean 一下:

1
$ make distclean

使用其默认配置即可:

1
$ make defconfig

然后使用make menuconfig 进入Settings -> Destination path for make install来设置安装路径到前面的 staging 目录。

接下来便是编译及安装:

1
2
3
4
$ export CROSS_COMPILE=aarch64-unknown-linux-gnu-
$ export ARCH=arm64
$ make
$ make install

可以看到 staging 目录中已经安装好了,且那些文件都以软连接的形式指向了busybox这个可执行文件。

根文件系统中的库

应用程序要运行,就要依赖部分编译工具链中的库,简单粗暴的解决方式就是把这些库都拷贝到 staging 目录中。

1
2
# 以 SYSROOT 存储路径,比较方便
$ export SYSROOT=$(aarch64-unknown-linux-gnu-gcc -print-sysroot)

其中lib文件夹存储得是共享链接库,将它们复制进去即可:

1
2
# 使用 -a ,不破坏其软连接
cec@imx8:~/myb/rootfs$ cp -aR ${SYSROOT}/lib/** ./lib/

设备节点

创建设备节点使用命令mknod

1
2
# 依次是设备节点名称,类型,主设备号,次设备号
$ mknod <name> <type> <major> <minor>

主设备号和次设备号可以在 Documentation/devices.txt文件中找到

对于 BusyBox 而言,所需要的两个节点是consolenull

1
2
3
4
# null 节点所有用户都可以读写,所以权限是 666
$ sudo mknod -m 666 dev/null c 1 3
# console 节点只允许 root 操作,所以权限是 600
$ sudo mknod -m 600 dev/console c 5 1

内核模块

内核模块也需要被安装在根文件系统中,需要被内核设置INSTALL_MOD_PATH

1
2
# 由于前面已经设置了 ARCH 和 CROSS_COMPILE 所以这里就不用设置了
$ make INSTALL_MOD_PATH=/home/cec/myb/rootfs modules_install

可以看到模块都被安装到了根文件系统的lib/modules/<kernel_version>目录下了。

但是会发现还安装了sourcebuild文件夹,这个是嵌入式中不需要的,可以把它们删除。

创建 initramfs

在使用initramfs之前需要确保CONFIG_BLK_DEV_INITRD=y,以表示内核支持initramfs

创建initramfs有以下 3 种方法:

  1. 独立打包为cpio格式的文件包:这种方式最为灵活
  2. initramfs嵌入到内核镜像文件中
  3. 由内核构建系统将其编译进去

创建一个独立包

先打包到上级目录:

1
2
# 指定了 GID 和 UID 都是 root
cec@imx8:~/myb/rootfs$ find . | cpio -H newc -ov --owner root:root > ../initramfs.cpio

然后再进行一次压缩:

1
cec@imx8:~/myb$ gzip initramfs.cpio

最后使用工具mkimage来为文件加入头:

1
2
3
4
5
6
7
8
9
cec@imx8:~/myb$ ./bootloader/myir-imx-uboot/tools/mkimage -A arm64 -O linux -T ramdisk -d initramfs.cpio.gz uRamdisk

disk
Image Name:
Created: Fri Sep 10 13:53:24 2021
Image Type: AArch64 Linux RAMDisk Image (gzip compressed)
Data Size: 193404777 Bytes = 188871.85 KiB = 184.45 MiB
Load Address: 00000000
Entry Point: 00000000

需要注意的是:initramfs 包体积不能太大,因为压缩包和解压后的文件都会全部存在于内存中!

这篇文章有讲到,initramfs包最好小于内存的 25%

启动独立包

拷贝进 SD 卡

作为测试目的,我们可以将uRamdisk也拷贝到 SD 卡的第一分区,然后在 U-boot 中载入。

载入到 DDR

然后需要将initramfs载入到 DDR 中,前面我们将:

  • Image 载入到 0x40480000
  • FDT 载入到 0x43000000

而 FDT 目前大小只有 42KB,那么可以将uRamdisk载入到0x43800000

1
2
3
4
5
$ env set initrd_addr 0x43800000
$ env save
$ fatload mmc ${mmcdev}:${mmcpart} ${initrd_addr} uRamdisk

193404841 bytes read in 2230 ms (82.7 MiB/s)
  1. 指定启动的init程序

需要在bootargs中加入启动程序是 shell:rdinit=/bin/sh

1
2
$ env set bootargs console=ttymxc1,115200 earlycon=ec_imx6q,0x30890000,115200 rdinit=/bin/sh
$ env save
  1. 使用 booti 启动

也就是说在原来的基础上,加上initramfs的地址即可:

1
$ booti ${loadaddr} ${initrd_addr} ${fdt_addr}

这个时候会发现没有工作控制流而退出 shell:

1
/bin/sh: can't access tty; job control turned off

将 initramfs 嵌入内核

initramfs嵌入内核非常简单:在General setup -> Initramfs source file(s)中指定未压缩的 cpio 文件,然后再次运行 make 即可。

这样设置以后,便可以在 bootloader 中指定内核和设备树地址就行了。

这里需要注意内核 + initramfs 所占用的空间,设备树需要预留足够多的空间以避免相互覆盖。

比如当前内核 + initramfs 就有 55MB,那么设备树的载入位置需要再往后放一点。

当前开发板具有 2GB 内存,那 DDR 寻址范围是 0x40000000 ~ 0xC0000000。

所以设备树的位置预留足够位置即可,比如放置在 0x4C800000 处,就预留了 200MB 的空间。

以设备列表的形式构建 initramfs

设备列表就是一个配置文件,用以列出文件、文件夹、设备节点、链接等等。

在构建内核的时候,也就会生成按照设备列表配置的 cpio 文件。

和上面的方式一样,在内核的Initramfs source file(s)处指向该配置文件。

下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# dir <name> <mode> <uid> <gid>
dir /bin 775 0 0
dir /sys 775 0 0
dir /tmp 775 0 0
dir /dev 775 0 0
# nod <name> <mode> <uid> <gid> <dev_type> <maj> <min>
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 <name> <target> <mode> <uid> <gid>
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 <name> <location> <mode> <uid> <gid>
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

完整启动 initramfs

前面的启动过程,会由于 initramfs 缺少文件而退出 shell,而正确的启动流程是:

  1. 内核启动/sbin/init程序
  2. /sbin/init读取/etc/inittab确定启动级别及运行 shell
  3. 根据/etc/inittab中的内容找到/etc/init.d/rcS然后依次运行对应脚本进行环境初始化

busybox在其源码examples/bootfloppy/etc/中就提供了通用的示例,将其拷贝到我们创建的rootfs中是比较简单的方法:

1
cec@imx8:~/myb/rootfs$ cp -aR ../busybox/examples/bootfloppy/etc/** etc/

inittab 修改

作为测试目的,对其进行简单修改:

1
2
3
4
# 启动初始化脚本为 /etc/init.d/rcS
::sysinit:/etc/init.d/rcS
# 虚拟终端作为 shell
::askfirst:-/bin/sh

rcS 修改

rcS脚本中,需要至少挂载proc,sys两个虚拟文件系统:

1
2
3
#!/bin/sh
mount -t proc proc /proc
mount -t sysfs sysfs /sys

增加用户配置

busybox默认会支持 shadow 特性,这需要添加用户配置文件。

用户名及相关信息被配置于/etc/passwd文件中,每个用户一行,中间以冒号分开,依次是:

  • 用户名

  • x代表密码存储于/etc/shadow

    /etc/passwd是所有人可读的,而/etc/shadow则只能是 root 用户和组可以读,以此来保证安全性。

  • 用户 ID

  • 组 ID

  • 注释

  • 用户的home目录

  • 用户所使用的 shell

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

组名称则存储于/etc/group中,也是每个组一行,中间以冒号分开:

  • 组名
  • 组密码,x代表该组没有密码
  • 组 ID
  • 那些用于属于该组
1
2
root:x:0:
daemon:x:1:

启动

最后还需要在console中修改启动程序rdinit=/sbin/init

创建设备节点更好的方法

mknod创建设备节点比较繁琐,还有其他更好的办法:

  • devtmpfs:这是在启动时被挂载到/dev的伪文件系统。内核通过它来动态的增加和删除设备节点。
  • mdev:由 busybox 提供的工具,通过读取/etc/mdev.conf来达到自动挂载节点的目的
  • udev:功能和udev类似,现在属于systemd的一部分

在实际使用中,一般是通过devtmpfs来自动创建节点,而mdev/udev来设置节点的属性。

devtmpfs

在使用devtmpfs之前,需要确保内核已经使能了CONFIG_DEVTMPFS

如果使能了 CONFIG_DEVTMPFS_MOUNT 内核会自动挂载该文件系统,只是不适用于 initramfs

然后在启动脚本中挂载devtmpfs

1
mount -t devtmpfs devtmpfs /dev

mdev

在使用mdev之前,需要在启动脚本中将其设置为接收内核发送的hotplug事件,然后再启动mdev

1
2
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s

mdev会根据/etc/mdev.conf文件来配置节点的属性:

1
2
3
4
# file /etc/mdev.conf
null root:root 666
random root:root 444
urandom root:root 444

关于 mdev更多说明,参考 busybox 源码中的 docs/mdev.txt文件。

网络配置

基本配置

与网络配置相关的文件有:

1
2
3
4
5
etc/network
etc/network/if-pre-up.d
etc/network/if-up.d
etc/network/interfaces
var/run

其中interfaces中是对网络的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
auto lo
iface lo inet loopback

# 配置 eth0 为静态 IP
auto eth0
iface eth0 inet static
address 192.168.1.101
netmask 255.255.255.0
network 192.168.1.0

# 也可以这样配置,使用动态 IP
# auto eth0
# iface eth0 inet dhcp
# iface eth1 inet dhcp

对于动态 IP,busybox 使用 udchpcd运行/usr/share/udhcpc/default.script来完成配置。

字符串映射

glibc 使用 name service switch(NSS)来实现从名称到特定数值的转换。

比如从用户名转换到 UID,从服务器名称转换到端口号,从主机名称转换到 IP 地址等。

这些配置被存储于/etc/nssswitch.conf文件中:

1
2
3
4
5
6
7
passwd:    files # 查询 UID ,就在 /etc/passwd
group: files
shadow: files
hosts: files dns # 查询主机就在 /etc/hosts,如果查询不到就从 DNS 查询
networks: files
protocols: files
services: files # 查询端口号,就在 /etc/services