0%

Linux驱动学习实战(3) BusyBox 构建根文件系统

是什么

  Linux中的根文件系统更像是一个文件夹或者叫做目录(在我看来就是一个文件夹,只不过是特殊的文件夹),在这个目录里面会有很多的子目录。根目录下和子目录中会有很多的文件,这些文件是Linux运行所必须的,比如库、常用的软件和命令、设备文件、配置文件等等。根文件系统是Linux内核启动以后挂载(mount)的第一个文件系统,然后从根文件系统中读取初始化脚本,比如rcS,inittab等。根文件系统和Linux内核是分开的,单独的Linux内核是没法正常工作的,必须要搭配根文件系统。如果不提供根文件系统,Linux内核在启动的时候就会提示内核崩溃(Kernel panic)的提示。

目录 作用
/bin 可执行文件
/dev 此目录下的文件都是设备 文件。在 Linux 下一切皆文件,即使是硬件设备,也是以文件的形式存在的
/etc 各种配置文件
/lib Linux所必需的库文件
/mnt 临时挂载目录
/usr Unix 操作系 统软件资源目录
/var 此处存放一些可以改变的数据
/sys 系统启动以后此目录作为 sysfs 文件系统的挂载点
/opt 可选的文件、软件存放区,由用户选择将哪些文件或软件放到此目录中。

干什么

  正点原子提供了 BusyBox的教程,我们就依据此教程和 BusyBox 工具构建一个根文件系统。BusyBox 就是一 个大的工具箱,这个工具箱里面集成了 Linux 的许多工具和命令。一般下载 BusyBox 的源码, 然后配置 BusyBox,选择自己想要的功能,最后编译即可。

怎么干

下载

  打开官网 https://busybox.net/FAQ.html ,在页面左侧找到 Download Source,找到1.37.0版本并下载。下载好后将压缩包拖入虚拟机中,并使用 tar 命令解压。

图 1 busybox目录

编译 BusyBox 构建根文件系统

构建 nfs 文件系统

  一般我们在Linux 驱动开发的时候都是通过 nfs 挂载根文件系统的,当产品最终上市开卖 的时候才会将根文件系统烧写到 EMMC 或者 NAND 中,因此在设置的 nfs 服务器目录中创建一个名为 rootfs 的子目录。

编辑 Makefile

  同移植 UBoot 和 Linux 内核一样,先打开顶层Makefile添加 ARCH 和 CROSS_COMPILE。

CROSS_COMPILE ?= /home/hcw/toolchain/gcc-arm-11.2-2022.02-x86_64-arm-none-linux-gnueabihf/bin/arm-none-linux-gnueabihf-
ARCH ?= arm

添加中文支持

  接下来,我们添加中文支持。如果默认直接编译 busybox 的话,在使用 SecureCRT 的时候中文字符是显示不正常的,中文字符会显示为“?”,比如你的中文目录,中文文件都显示为“?”。不知道从哪个版本开始 busybox 中的 shell 命令对中文输入即显示做了限制,即使内核支持中文但在 shell 下也依然无法正确显示。打开 busybox-1.37.0/libbb/printable_string.c ,做出如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
const char* FAST_FUNC printable_string2(uni_stat_t *stats, const char *str)
{
char *dst;
const char *s;

s = str;
while (1) {
unsigned char c = *s;
if (c == '\0') {
/* 99+% of inputs do not need conversion */
if (stats) {
stats->byte_count = (s - str);
stats->unicode_count = (s - str);
stats->unicode_width = (s - str);
}
return str;
}
if (c < ' ')
break;
// 删除限制
// if (c >= 0x7f)
// break;
s++;
}

#if ENABLE_UNICODE_SUPPORT
dst = unicode_conv_to_printable(stats, str);
#else
{
char *d = dst = xstrdup(str);
while (1) {
unsigned char c = *d;
if (c == '\0')
break;
//if (c < ' ' || c >= 0x7f)
if (c < ' ')
*d = '?';
d++;
}
if (stats) {
stats->byte_count = (d - dst);
stats->unicode_count = (d - dst);
stats->unicode_width = (d - dst);
}
}
#endif
return auto_string(dst);
}

紧接着,打开 busybox-1.37.0/libbb/unicode.c ,找到下面这一行:

*d++ = (c >= ' ' && c < 0x7f) ? c : '?';

并改为

*d++ = (c >= ' ') ? c : '?';

还有这一行

if (c < ' ' || c >= 0x7f)

改为

if(c < ' ')

配置 busybox

  跟我们编译 Uboot、Linux kernel 一样,我们要先对 busybox 进行默认的配置,有以下几种
配置选项:
①、defconfig,缺省配置,也就是默认配置选项。
②、allyesconfig,全选配置,也就是选中 busybox的所有功能。
③、allnoconfig,最小配置。

我们一般使用默认配置即可,因此使用如下命令先使用默认配置来配置一下 busybox:

make defconfig

busybox 也支持图形化配置,通过图形化配置我们可以进一步选择自己想要的功能,输入如下命令打开图形化配置界面:

make menuconfig

配置路径如下

Location:
-> Settings 
-> Build static binary (no shared libs)

选项“Build static binary (no shared libs)”用来决定是静态编译busybox 还是动态编译,静态编译的话就不需要库文件,但是编译出来的库会很大。动态编译的话要求根文件系统中有库文件,但是编译出来的busybox会小很多。

继续配置如下路径配置项:

Location: 
-> Settings
-> vi-style line editing commands

选中此选项,命令行进入 vi 模式,有 插入模式 / 普通模式,快捷键遵循 vi 习惯。

继续配置如下路径配置项:

Location: 
->Linux System Utilities 
-> mdev (20 kb)  //确保下面的全部选中,默认都是选中的

最后就是使能 busybox 的 unicode 编码以支持中文,配置路径如下:

Location:
-> Settings 
    -> Support Unicode //选中
        -> Check $LC_ALL, $LC_CTYPE and $LANG environment variables //选中

编译

  配置好以后busybox 以后就可以编译了,我们可以指定编译结果的存放目录,我们肯定要将编 译结果存放到前面创建的 rootfs 目录中,输入如下命令:

make
make install CONFIG_PREFIX=/home/hcw/learn/nfs/rootfs

向根文件系统添加 lib 库

  Linux 中的应用程序一般都是需要动态库的,当然你也可以编译成静态的,但是静态的可 执行文件会很大。如果编译为动态的话就需要动态库,所以我们需要向根文件系统中添加动态 库。在 rootfs 中创建一个名为“lib”的文件夹。

  1ib库文件从交叉编译器中获取,前面我们搭建交叉编译环境的时候将交叉编译器存放到了“/usr/local/arm/”目录中。交叉编译器里面有很多的库文件,这些库文件具体是做什么的我们作为初学者肯定不知道,既然我不知道那就简单粗暴的把所有的库文件都放到我们的根文件系统中。

进入如下路径对应的目录:

/usr/arm-linux-gnueabihf/lib/
cp *so* *.a /home/hcw/learn/nfs/rootfs/lib/ -d

后面的“-d”表示拷贝符号链接,这里有个比较特殊的库文件:ld-linux-armhf.so.3,此库文件也是个符号链接,相当于 Windows 下的快捷方式。

图 2 查看文件链接

  ld-linux-armhf.so.3后面有个“->”,表示其是个软连接文件,链接到文件ld-2.19-2014.08-1-git.so,因为其是一个“快捷方式”,因此大小只有24B。但是,ld-linux-armhf.so.3不能作为符号链接,否则的话在根文件系统中执行程序无法执行!所以我们需要ld-linux-armhf.so.3完成逆袭,由“快捷方式”变为“本尊”,方法很简单,那就是重新复制ld-linux-armhf.so.3,只是不复制软链接即可,先将rootfs/lib 中的ld-linux-armhf.so.3文件删除掉。然后我们重新进入刚才的目录重新复制删除的文件,这次不带 -d。

rootfs 的 “usr/lib” 目录添加库文件

  在 Ubuntu 24.04 + apt cross 工具链 这一套体系下:不需要也找不到一个“现成的、像教程那样”的 /usr/lib/arm-linux-gnueabihf 来整体拷贝。根文件系统里的 /usr/lib,是“按需生成”的,不是“整体复制”的。因此到后期,我们在根文件系统中按需添加自己的库文件即可。

根文件系统初步测试

  接下来我们使用测试一下前面创建好的根文件系统rootfs,测试方法就是使用NFS挂载,uboot里面的bootargs环境变量会设置“root”的值,所以我们将root的值改为NFS挂载即可。root环境变量格式如下

root=/dev/nfs nfsroot=[<server-ip>:]<root-dir>[,<nfs-options>] ip=<client-ip>:<server-ip>:<gw -ip>:<netmask>:<hostname>:<device>:<autoconf>:<dns0-ip>:<dns1-ip>

<server-ip>:服务器IP地址,也就是存放根文件系统主机的IP地址,那就是Ubuntu的IP地址,比如我的Ubuntu主机IP地址为192.168.1.250。
<root-dir>:根文件系统的存放路径,比如我的就是/home/zuozhongkai/linux/nfs/rootfs。
<nfs-options>:NFS的其他可选选项,一般不设置。
<client-ip>:客户端IP地址,也就是我们开发板的IP地址,Linux内核启动以后就会使用此IP地址来配置开发板。此地址一定要和Ubuntu主机在同一个网段内,并且没有被其他的设备使用,在Ubuntu中使用ping命令ping一下就知道要设置的IP地址有没有被使用,如果不能ping通就说明没有被使用,那么就可以设置为开发板的IP地址,比如我就可以设置为192.168.1.251.
<server-ip>:服务器IP地址,前面已经说了。
<gw-ip>:网关地址,我的就是192.168.1.1。
<netmask>:子网掩码,我的就是255.255.255.0。
<hostname>:客户机的名字,一般不设置,此值可以空着。
<device>:设备名,也就是网卡名,一般是ethO,ethl…..正点原子的I.MX6U-ALPHA开发板的ENET2为ethO,ENET1为eth1。如果你的电脑只有一个网卡,那么基本只能是eth0。这里我们使用ENET2,所以网卡名就是eth0。
<autoconf>:自动配置,一般不使用,所以设置为off。
<dns0-ip>:DNS0服务器IP地址,不使用。
<dns1-ip>:DNS1服务器IP地址,不使用。

根据上面的格式设置 bootargs 环境变量的命令如下:

setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.10.3:/home/hcw/learn/nfs/rootfs,vers=3,tcp ip=192.168.10.2::192.168.10.1:255.255.255.0:::off rw'
saveenv

然后启动 Linux 内核,成功进入。但可以看到还是有一行错误提示

can't run '/etc/init.d/rcS': No such file or directory

提示很简单,说是无法运行“/etc/init.d/rcS”这个文件,因为这个文件不存在。看来我们的 rootfs 还是缺文件啊,没什么说的,一步一步的完善吧。

图 3 成功

完善根文件系统

创建 /etc/init.d/rcS 文件

  rcS 是个 shell 脚本,Linux 内核启动以后需要启动一些服务,而 rcS 就是规定启动哪些文件 的脚本文件。在 rootfs 中创建/etc/init.d/rcS 文件,然后在 rcS 中输入如下所示内容:

1
2
3
4
5
6
7
8
9
10
11
12
#!/bin/sh

PATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
export PATH LD_LIBRARY_PATH

mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts

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

  这个脚本的主要作用是网络初始化、挂载存储、启动守护进程。完成后通过 chmod 命令给予权限。

创建 /etc/fstab 文件

  在 rootfs 中创建/etc/fstab 文件, fstab 在 Linux 开机以后自动配置哪些需要自动挂载的分区,格式如下:

<file system>    <mount point>   <type>    <options>     <dump>    <pass>

<filesystem>:要挂载的特殊的设备,也可以是块设备,比如/dev/sda等等。
<mount point>:载点。
<type>:文件系统类型,比如ext2、ext3、proc、romfs、tmpfs等等。
<options>:挂载选项,在Ubuntu中输入“manmount”命令可以查看具体的选项。一般使用defaults,也就是默认选项,defaults包含了rw、suid、dev、exec、auto, nouser 和 async
<dump>:为1的话表示允许备份,为0不备份,一般不备份,因此设置为0。
<pass>:磁盘检查设置,为0表示不检查。根目录‘’设置为1,其他的都不能设置为1,其他的分区从2开始。一般不在fstab中挂载根目录,因此这里一般设置为0。

按照上述格式,在 fstab 文件中输入如下内容:

1
2
3
4
#<file system> <mount point>     <type>      <options> <dump> <pass> 
proc /proc proc defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
sysfs /sys sysfs defaults 0 0

创建 /etc/inittab 文件

  inittab 的详细内容可以参考 busybox 下的文件 examples/inittab。init 程序会读取/etc/inittab 这个文件,inittab 由若干条指令组成。每条指令的结构都是一样的,由以“:”分隔的 4 个段组成,格式如下:

<id>:<runlevels>:<action>:<process>

<id>:每个指令的标识符,不能重复。但是对于 busybox 的 init 来说,<id>有着特殊意义。 对于 busybox 而言<id>用来指定启动进程的控制 tty,一般我们将串口或者 LCD 屏幕设置为控制 tty。
<runlevels>:对 busybox 来说此项完全没用,所以空着。
<action>:动作,用于指定<process>可能用到的动作。
<process>:具体的动作,比如程序、脚本或命令等。

参考 busybox 的 examples/inittab 文件,我们也创建一个/etc/inittab,在里面输入如下内容:

1
2
3
4
5
6
7
#etc/inittab 
::sysinit:/etc/init.d/rcS
console::askfirst:-/bin/sh
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
::shutdown:/bin/umount -a -r
::shutdown:/sbin/swapoff -a

  第2行,系统启动以后运行/etc/init.d/rcS这个脚本文件。
  第3行,将console作为控制台终端,也就是tymxc0。
  第4行,重启的话运行/sbin/init。
  第5行,按下ctrl+alt+del组合键的话就运行/sbin/reboot,看来ctrl+alt+del组合键用于重启系统。
  第6行,关机的时候执行/bin/umount,也就是卸载各个文件系统。
  第7行,关机的时候执行/sbin/swapoff,也就是关闭交换分区。
  /etc/initab文件创建好以后就可以重启开发板即可,至此!根文件系统要创建的文件就已经全部完成了。接下来就要对根文件系统进行其他的测试,比如我们自己编写的软件运行是否正常、是否支持软件开机自启动、中文支持是否正常以及能不能链接等。

测试根文件系统

  在根文件系统中新建一个hello.c文件,实现循环输出 hello world,输入以下指令编译

arm-linux-gnueabihf-gcc hello.c -o hello

效果如下

图 4 Hello world