根文件系统
根文件系统的构建,是Linux移植三大组成部分的最后一步,根文件系统构建好后,就构成了一个基础的、可以运行的嵌入式Linux最小系统
1. 根文件系统简介
Linux的根文件系统一般也叫做 rootfs,Linux的根文件系统更像是一个文件夹或者叫做目录,在这个目录里面会有很多的子目录。根目录下和子目录中会有很多的文件,这些文件是Linux运行所必须的,比如库、常用的软件和命令、设备文件、配置文件等等。
根文件系统的这个“根”字就说明了这个文件系统的重要性,它是其他文件系统的根,没有这个“根” ,其他的文件系统或者软件就别想工作。比如我们常用的 ls、mv、ifconfig 等命令其实就是一个个小软件,只是这些软件没有图形界面,而且需要输入命令来运行。这些小软件就保存在根文件系统中。
在构建根文件系统之前,先来看一下根文件系统里面都有些什么内容,根文件系统的目录名字为‘/’ ,就是一个斜杠。下面以Ubuntu为例,来看看根文件系统里都有些什么内容
一些常用的子目录介绍如下表示
目录 | 描述 |
---|---|
/bin | 此目录下存放着系统需要的可执行文件,一般都是一些命令,比如 ls、mv 等命令 |
/dev | device的缩写,此目录下的文件都是和设备有关的。在Linux下一切皆文件,即使是硬件设备,也是以文件的形式存在的,比如/dev/ttymxc0就表示串口0 |
/etc | 此目录下存放着各种配置文件 |
/lib | lib是library的简称,也就是库的意思,因此此目录下存放着Linux所必须的库文件 |
/mnt | 临时挂载目录,一般是空目录,可以在此目录下创建空的子目录,比如/mnt/sd、/mnt/usb,这样就可以将SD卡或者U盘挂载到/mnt/sd 或者/mnt/usb 目录中 |
/proc | 此目录一般是空的,当Linux系统启动以后会将此目录作为proc文件系统的挂载点,proc是个虚拟文件系统,没有实际的存储设备。proc里面的文件都是临时存在的,一般用来存储系统运行信息文件 |
/usr | usr不是user的缩写,而是Unix Software Resource的缩写,即Unix操作系统软件资源目录。Linux 一般被称为类Unix操作系统。既然是软件资源目录,因此/usr 目录下也存放着很多软件,一般系统安装完成以后此目录占用的空间最多 |
/var | 此目录存放一些可以改变的数据 |
/sbin | 此目录页用户存放一些可执行文件, 但是此目录下的文件或者说命令只有管理员才能使用,主要用于系统管理 |
/sys | 系统启动以后此目录作为 sysfs 文件系统的挂载点,sysfs是一个类似于 proc文件系统的特殊文件系统,sysfs也是基于RAM的文件系统,也就是说它也没有实际的存储设备。此目录是系统设备管理的重要目录 |
/opt | 可选的文件、软件存放区,由用户选择将哪些文件或软件放到此目录中 |
2. BusyBox构建根文件系统
2.1 BusyBox简介
BusyBox是一个集成了大量的Linux命令(如ls、mv、ifconfig 等命令)和工具的软件。借助BusyBox,进行配置和编译,就可以方便的构建一个嵌入Linux平台所需要的根文件系统。
可在BusyBox官网 https://busybox.net/ 下载源码,如下图
左侧的“Get BusyBox”栏有一行“Download Source” ,点击“Download Source”即可打开 BusyBox 的下载页
目前最新的 BusyBox 版本是1.33.1,但这里使用正点原子提供的1.29.0版本的BusyBox(busybox-1.29.0.tar.bz2)
2.2 编译BusyBox构件根文件系统
一般在Linux驱动开发的时候都是通过NFS挂载根文件系统的,当调试好之后再将根文件系统烧写到 EMMC或者NAND中,因此需要先在ubuntu虚拟机中构建NFS服务,构建方法请参考环境搭建第二节的介绍
在nfs服务器目录中创建一个名为rootfs的子目录,用来存放我们的根文件系统。
将busybox-1.29.0.tar.bz2发送到Ubuntu中的合适位置(我存放在 /home/xxpcb/myTest/imx6ull/dts)并解压:
tar -vxjf busybox-1.29.0.tar.bz2
解压后的文件如下:
- 修改Makefile添加编译器
#修改Makefile的目的是为了在编译时,可以不用在指定编译器的架构
#从而可以缩短手动输入指令的长度
CROSS_COMPILE ?= /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/bin/arm-linux-gnueabihf-
......
ARCH ?= arm
# CORSS_COMPILE使用了绝对路径!是为了防止编译出错
- busybox中文字符支持
如果直接编译busybox的,在使用串口工具的时候是不支持中文显示的,中文字符会显示为“?” ,可以通过busybox源码,来取消 busybox对中文显示的限制,打开文件busybox-1.29.0/libbb/printable_string.c,找到函数printable_string,把某些程序注释掉,修改后的函数内容如下:
/********** printable_string.c代码段 **********/
const char* FAST_FUNC printable_string(uni_stat_t *stats, const char *str)
{
char *dst;
const char *s;
s = str;
while (1) {
......
if (c < ' ')
break;
/* 注释掉下面这个两行代码,禁止字符大于0X7F以后 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;
/* 修改下面代码,禁止字符大于0X7F以后输出‘?’ */
/* if (c < ' ' || c >= 0x7f) */
if( c < ' ')
*d = '?';
d++;
}
......
#endif
return auto_string(dst);
}
接着打开文件busybox-1.29.0/libbb/unicode.c,修改如下内容:
/********** unicode.c代码段 **********/
static char* FAST_FUNC unicode_conv_to_printable2(uni_stat_t *stats, const char *src, unsigned width, int flags)
{
char *dst;
unsigned dst_len;
unsigned uni_count;
unsigned uni_width;
if (unicode_status != UNICODE_ON) {
char *d;
if (flags & UNI_FLAG_PAD) {
d = dst = xmalloc(width + 1);
......
/* 修改下面一行代码 */
/* *d++ = (c >= ' ' && c < 0x7f) ? c : '?'; */
*d++ = (c >= ' ') ? c : '?';
src++;
}
*d = '\0';
} else {
d = dst = xstrndup(src, width);
while (*d) {
unsigned char c = *d;
/* 修改下面一行代码 */
/* if (c < ' ' || c >= 0x7f) */
if(c < ' ')
*d = '?';
d++;
}
}
......
return dst;
}
......
return dst;
}
- 配置busybox
busybox有以下几种配置选项
– defconfig:缺省配置,也就是默认配置选项
– allyesconfi:全选配置,也就是选中 busybox 的所有功能
– allnoconfig:最小配置
一般使用默认配置即可
make defconfig #使用默认配置
make menuconfig #打开图形化配置界面
设置Settings -> Build static binary (no shared libs),用来决定是静态编译还是动态编译,静态编译的话就不需要库文件,但是编译出来的库会很大。动态编译的话要求根文件系统中有库文件,但是编译出来的 busybox 会小很多。这里我们不使用静态编译,所以保持默认不选即可
设置Settings -> vi-style line editing commands
配置Linux Module Utilities -> Simplified modutils
配置Linux System Utilities -> mdev (16 kb)
设置Settings -> Support Unicode,使能busybox的unicode编码以支持中文
- 编译busybox:配置好busybox以后就可以编译了,输入如下命令
make
make install CONFIG_PREFIX=/home/andyxi/linux/nfs/rootfs
#CONFIG_PREFIX指定编译结果的存放目录
编译完成以后,busybox的所有工具和文件就会被安装到rootfs目录中,如下图;rootfs目录下有bin、sbin和usr三个目录,以及linuxrc文件。Linux内核linit进程最后会查找用户空间的init程序,找到以后就会运行这个用户空间的init程序,从而切换到用户态。如果bootargs设置init=/linuxrc,那么linuxrc就是可以作为用户空间的init程序
2.3 向根文件系统添加lib库
busybox编译完成后,此时的根文件系统还不能使用, 还需要一些其他的文件
- 向rootfs/lib中添加库文件:上面的busybox使用的是动态库编译,所以还需要向根文件系统中添加动态库
先在rootfs中创建一个名为“lib”的文件夹。lib库文件从交叉编译器中获取,之前搭建交叉编译环境的时候将交叉编译器存放到了“/usr/local/arm/”目录中,进入对应的目录:
cd /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib
此目录下有很多的so和.a 文件,这些就是库文件,将此目录下所有的so和.a文件都拷贝到 rootfs/lib 目录中:
cp *so* *.a /home/andyxi/linux/nfs/rootfs/lib/ -d #-d表示拷贝符号链接
### 特殊库文件:ld-linux-armhf.so.3(软连接文件,即快捷方式) 的处理
rm ld-linux-armhf.so.3 #先删除rootfs/lib中的这个软链接
# 然后重新拷贝ld-linux-armhf.so.3
cp /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/lib/ld-linux-armhf.so.3 .
继续进入如下目录中,此目录下也有很多的的so和.a 库文件,也将其也拷贝到 rootfs/lib 目录中
cd /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/lib
cp *so* *.a /home/andyxi/linux/nfs/rootfs/lib/ -d #-d表示拷贝符号链接
- 向rootfs/usr/lib中添加库文件
在rootfs/usr目录下创建一个名为lib的目录, 将如下目录中的so和.a 库文件拷贝到rootfs/usr/lib目录中
cd /usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf/arm-linux-gnueabihf/libc/usr/lib
cp *so* *.a /home/andyxi/linux/nfs/rootfs/usr/lib/ -d
至此,根文件系统的库文件就全部添加好了,可以在rootfs目录下使用“du”命令来查看一下/lib和/usr/lib 这两个目录的大小:
du ./lib ./usr/lib/ -sh
2.4 创建其他文件夹
在根文件系统中创建其他文件夹,如 dev、proc、mnt、sys、tmp 和 root 等,创建完后的效果:
3. 根文件系统初步测试
使用NFS挂载的方式来测试上面创建好的根文件系统rootfs。uboot里面的bootargs环境变量会设置root的值,需要将root的值改为NFS挂载,设置格式如为:
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,存放根文件系统的Ubuntu的IP地址,比如我的192.168.5.105。
- root-dir:根文件系统的存放路径,比如我的就是/home/xxpcb/myTest/nfs/rootfs。
- nfs-options:NFS 的其他可选选项,一般不设置。
- client-ip>:客户端IP ,开发板的IP地址,Linux内核启动以后就会使用此IP地址来配置开发板。我的为92.168.5.102。
- gw-ip:网关地址,我的就是 192.168.5.1。
- netmask:子网掩码,我的就是 255.255.255.0。
- hostname:客户机的名字,一般不设置,此值可以空着。
- device:设备名,也就是网卡名,一般是 eth0,eth1….,正点原子的开发板的ENET2为eth0,ENET1为eth1。这里我们使用ENET2,所以网卡名就是 eth0。
- autoconf:自动配置,一般不使用,所以设置为 off。
- dns0-ip:DNS0 服务器 IP 地址,不使用。
- dns1-ip:DNS1 服务器 IP 地址,不使用
根据上面的格式bootargs环境变量的root值如下:
root=/dev/nfs nfsroot=192.168.5.105:/home/xxpcb/myTest/nfs/rootfs,proto=tcp rw ip=192.168.5.102:192.168.5.105:192.168.5.1:255.255.255.0::eth1:off
启动开发板,串口连接开发板,进入uboot命令行模式,然后设置bootargs环境变量,命令如下:
setenv bootargs 'console=ttymxc0,115200 root=/dev/nfs nfsroot=192.168.5.105:/home/xxpcb/myTest/nfs/rootfs,proto=tcp rw ip=192.168.5.102:192.168.5.105:192.168.5.1:255.255.255.0::eth1:off'
saveenv
设置好以后使用“boot”命令启动Linux内核,进入根文件系统,结果如下图示
输入“ls”命令进行测试,发现ls命令正常工作。但是此时rootfs并没有制作成功,注意以下错误提示,提示无法运行“/etc/init.d/rcS”这个文件,说明rootfs仍然不够完善
can't run '/etc/init.d/rcS': No such file or directory
4. 完善根文件系统
4.1 创建/etc/init.d/rcS文件
rcS 是个 shell 脚本,Linux 内核启动以后需要启动一些服务,而 rcS 就是规定启动哪些文件的脚本文件。在rootfs中创建/etc/init.d/rcS 文件,命令如下:
mkdir -p etc/init.d/
cd etc/init.d/
touch rcS
然后在rcS文件中输入如下内容(注意每行前面的行数不要输入,这里加上行数为了讲解代码):
1 #!/bin/sh
2
3 PATH=/sbin:/bin:/usr/sbin:/usr/bin:$PATH
4 LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
5 export PATH LD_LIBRARY_PATH
6
7 mount -a
8 mkdir /dev/pts
9 mount -t devpts devpts /dev/pts
10
11 echo /sbin/mdev > /proc/sys/kernel/hotplug
12 mdev -s
- 第 1 行,表示这是一个 shell 脚本。
- 第 3 行,PATH 环境变量保存着可执行文件可能存在的目录,这样我们在执行一些命令或者可执行文件的时候就不会提示找不到文件这样的错误。
- 第 4 行,LD_LIBRARY_PATH 环境变量保存着库文件所在的目录。
- 第 5 行,使用 export 来导出上面这些环境变量,相当于声明一些“全局变量”。
- 第 7 行,使用 mount 命令来挂载所有的文件系统,这些文件系统由文件/etc/fstab 来指定,所以我们一会还要创建/etc/fstab 文件。
- 第 8 和 9 行,创建目录/dev/pts,然后将 devpts 挂载到/dev/pts 目录中。
- 第 11 和 12 行,使用 mdev 来管理热插拔设备,通过这两行,Linux 内核就可以在/dev 目录下自动创建设备节点。关于 mdev 的详细内容可以参考 busybox 中的 docs/mdev.txt 文档。
创建好rcS文件后,赋予其可执行权限,命令如下:
chmod 777 rcS
4.2 创建/etc/fstab文件
fstab文件是在 Linux 开机以后自动配置哪些需要自动挂载的分区,格式如下:
<file system> <mount point> <type> <options> <dump> <pass>
- file system:要挂载的特殊的设备,也可以是块设备,比如/dev/sda 等等。
- mount point:挂载点必须为当期已经存在的目录。
- type:文件系统类型,比如 ext2、ext3、proc、romfs、tmpfs 等等。
- options:挂载选项,用于设置挂载参数,一般使用 defaults,也就是默认选项。
– defaults:rw, suid, dev, exec, auto, nouser, and async。
– auto:系统自动挂载。fstab默认就是这个选项。
– noauto :开机不自动挂载。
– nouser:只有超级用户可以挂载。
– ro:按只读权限挂载。
– rw:按可读可写权限挂载。
– user:任何用户都可以挂载。- dump:为 1 的话表示允许备份,为 0 不备份,一般不备份,因此设置为 0。
- pass:磁盘检查设置,为 0 表示不检查。根目录‘/’设置为 1,其他的都不能设置为 1,其他的分区从 2 开始。一般不在 fstab 中挂载根目录,因此这里一般设置为 0。
首先返回到文件系统根目录下(/home/topeet/work/busybox/busybox-1.29.0/rootfs),创建fstab文件,命令如下:
cd etc/
touch fstab
fstab文件内容如下:
#<file system> <mount point> <type> <options> <dump> <pass>
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
4.3 创建/etc/inittab文件
inittab 的详细内容可以参考 busybox 下的文件 examples/inittab。init 程序会读取/etc/inittab这个文件,inittab 由若干条指令组成。每条指令的结构都是一样的,由以“:”分隔的 4 个段组成,格式如下:
<id>:<runlevels>:<action>:<process>
- id:每个指令的标识符,不能重复。但是对于 busybox 的 init 来说,有着特殊意义。
对于 busybox 而言用来指定启动进程的控制 tty,一般我们将串口或者 LCD 屏幕设置为控
制 tty。- runlevels:对 busybox 来说此项完全没用,所以空着。
- action:动作关键字,用于指定 process 可能用到的动作。具体动作如下图标所示
- process:具体的动作,比如程序、脚本或命令等。
参考 busybox 的 examples/inittab 文件,我们也创建一个/etc/inittab文件,命令如下:
cd etc
touch inittab
inittab文件内容如下(注意每行前面的行数不要输入,这里加上行数为了讲解代码):
1 #etc/inittab
2 ::sysinit:/etc/init.d/rcS
3 console::askfirst:-/bin/sh
4 ::restart:/sbin/init
5 ::ctrlaltdel:/sbin/reboot
6 ::shutdown:/bin/umount -a -r
7 ::shutdown:/sbin/swapoff -a
- 第 2 行,系统启动以后运行/etc/init.d/rcS 这个脚本文件。
- 第 3 行,将 console 作为控制台终端,也就是 ttymxc0。 第 4 行,重启的话运行/sbin/init。 > - 第 5 行,按下 ctrl+alt+del 组合键的话就运行/sbin/reboot,看来 ctrl+alt+del 组合键用于重
启系统。- 第 6 行,关机的时候执行/bin/umount,也就是卸载各个文件系统。
- 第 7 行,关机的时候执行/sbin/swapoff,也就是关闭交换分区。
/etc/inittab 文件创建好以后就可以重启开发板即可,至此!根文件系统要创建的文件就已经全部完成了。接下来就要对根文件系统进行其他的测试,比如我们自己编写的软件运行是否正常、是否支持软件开机自启动、中文支持是否正常以及能不能链接等
5. 根文件系统其他功能测试
根文件系统已经完善了,接下来就来继续测试根文件系统是否好用
5.1 软件运行测试
先来编写一个简单的c语言程序运行一下,验证库文件是否能用。
先在ubuntu的rootfs中(可以先创建一个单独的test文件夹用来测试)创建一个hello.c:
#include <stdio.h>
int main(void)
{
while(1)
{
printf("hello world!\r\n"); //打印
sleep(2); //休眠2秒
}
}
然后使用交叉编译链来编译程序:编译后会生成名为hello的可执行文件,可以使用“file”命令查看文件类型以及编码格式,见下图。hello是个32位的LSB可执行文件,ARM架构的,并且是动态链接的
arm-linux-gnueabihf-gcc hello.c -o hello
在ubunt中编译完后,重启开发板,在开发板的串口中(串口软件),运行hello文件:程序每隔2秒打印一次,运行正常,说明我根文件系统中的动态库没有问题。按下“ctrl+c”组合键即可中止该程序
程序运行起来后,会占用交互窗口,可以让hello进入后台运行,方法是在运行软件的时候加上“&”,即:./hello &
注意:程序在后台运行时,交互串口仍是有打印的,只是这时我们可以敲回车键来输入命令了,与程序的输出互不影响,唯一的影响是程序的输出会打断我们的输入,但可以不理会打断,继续输入命令仍是可以执行命令的。
在后台运行的程序,可以使用ps命令来查看各个进程的id,然后使用kill -9 pid(进程 ID)命令来关闭掉当前运行的程序。如下图,输入ps命令,可以看到hello程序的进程为101,此时先敲回车键,弹出井号提示符,然后输入kill -9 101,再回车,就可以看到hello进程被杀掉了
5.2 中文字符测试
在Ubuntu中的rootfs目录下新建一个“中文测试”文件夹,并新建一个“测试文档.txt”,里面输入任意中文,下载到开发板后,在开发板命令行模式中,是用cat命令查看中文是否显示正确
5.3 开机自启动测试
一般做好产品以后都是需要开机自动启动相应的软件,实现开机自启动的原理也很简单,linux在启动时,有一个默认的开机启动脚本(/etc/init.d/rcS这个shell文件),因此修改这个脚本,添加自启动相关内容即可
########## /etc/init.d/rcS 文件代码 ##########
#!/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/lib:/usr/lib
runlevel=S
umask 022
export PATH LD_LIBRARY_PATH runlevel
mount -a
mkdir /dev/pts
mount -t devpts devpts /dev/pts
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
#开机自启动
cd /drivers
./hello &
cd /
自启动代码添加完成以后,重启开发板,可以看到hello这个软件已经开机时自动运行了:
5.4 外网连接测试
这里的外网是相对于局域网这个内外而言的,即测试板子是否能访问百度、QQ这样的网站,先使用ping指令来测试一下:
ping www.baidu.com
在网络连接正常的情况下,无法ping通这些网址,是因为缺少域名解析服务,需要配置域名解析服务器的IP地址。一般域名可以设置为所处网络的网关地址,比如我的局域网的网关是192.168.5.1,也可以设置为运营商的域名解析服务器地址:114.114.114.114
在rootfs中新建文件/etc/resolv.conf,然后在里面输入如下内容:
nameserver 114.114.114.114
nameserver 192.168.5.1
修改保存退出,再次ping一下百度网站,可以看出ping QQ成功了!
至此!我们的根文件系统就彻底的制作完成,这个根文件系统最好打包保存一下,防止以后做实验不小心破坏了根文件系统而功亏一篑,又得从头制作根文件系统。
uboot、 Linux kernel、 rootfs 这三个共同构成了一个完整的Linux 系统,现在的系统至少是一个可以正常运行的系统, 后面我们就可以在这个系统上完成Linux 驱动开发的学习。