简单来讲,内核、文件系统、应用程序,三者构成一个完整可用的系统。发行版之间不同之处不在于内核啥的,而在于rootfs——它的一套系统工具;比如包管理,Ubuntu上面就是apt-get,Arch上面就是pacman,OpenWRT上面就是opkg,等等。
内核可以放在文件系统里面,如果bootloader支持文件系统的话就可以找到内核在哪里,树莓派啥的就是这样的风格。而不少嵌入式设备bootloader不会关注文件系统的事情,它只需有引导、更新内核的能力即可。所以不少板子直接将内核放在nand flash的某一个或几个block里面,如用uboot,则只需要nand read xxxx xxxx xxxx
就可以了。而对于ucpc这块板子来说,内核在片内flash里面就地运行,文件系统完全可以放在别的地方。
更完整的系统还需要包括各种运行时库(如C库)和链接器等,而且编译器是其中的重要软件,再不济也得有个解释器(比如Sylixos里面有个C解释器)。不过编译器在嵌入式设备上面几乎无用,即使性能强如智能手机也一样,没多少人愿意在板子上做native编译,毕竟上位机工具功能才全面。而对于ucpc这块板子来说,因为现成的arm-uclinuxeabi工具链都不能编译动态链接库,所以所有应用程序都是静态链接的,于是rootfs里面就连C库和链接器都不需要了。
helloworld例子
稍微大一点的操作系统,即使是nuttx、rtthread等;它们都有所谓的根文件系统rootfs,*nix的思想是任何东西都通过rootfs来访问:一切皆为文件,文件都在同一个命名空间
里;某物需先接入命名空间才能被识别、使用,这个过程叫挂载(mount)
。nuttx的rootfs可以空无一物——直接用VFS作为rootfs,因为它的应用程序可以是内核中內建的;而Linux的rootfs必须依赖于某个物理存在的文件系统,任何一个物理存在的文件系统都行,因为Linux内核中并不会內建任何工具,这也可以说体现了Linux的哲学吧:提供机制而不是策略。
ucpc上试过的根文件系统类型有:initramfs、romfs、nfs、yaffs。如果将SD卡、USB大容量设备等的驱动编译进内核的话,那么fatfs、ext4等等的东西也可以作为根文件系统,不然的话就只能手动mount然后chroot,才能说是所谓根文件系统了。
现成的initramfs可以从eLinux这里下载,里面是busybox的一堆命令以及/etc下面的简单的配置文件,如果不想自己配置一遍busybox编译的话可以直接解压这份rootfs来用。
下面我们做一个最最简单的rootfs作为例子,让内核启动之后打印hello world!
#include <stdio.h>
#include <unistd.h>
int main(void)
{
while (1) {
fprintf(stdout, "hello world!\n");
fflush(stdout);
sleep(10);
}
return 0;
}
死循环是为了防止init进程退出而kernel panic。之所以要fflush这么麻烦,是因为它不flush的话还真的没输出。。。
编译生成hello可执行文件。arm-uclinuxeabi工具链详见前面的文章:
$ arm-uclinuxeabi-gcc -o hello hello.c -march=armv7-m -mthumb
$ ls
hello hello.c hello.gdb
$
然后将hello放到rootfs里面,重命名为linuxrc,这个名字是因为设备树里面的参数init=linuxrc
:
$ mkdir rootfs
$ cp hello rootfs/linuxrc
$
另外rootfs里面还需要有/dev
文件夹,并且手动生成console节点;因为没有写mount函数,所以刚刚挂载这个rootfs时候是没有/dev和设备节点的,而init进程的stdin
、stdout
、stderr
都是/dev/console,没有的话printf就会没地方输出。
$ mkdir rootfs/dev
$ sudo mknod rootfs/dev/console c 5 1
$ find rootfs #然后看看rootfs里面有啥东西
rootfs/
rootfs/dev
rootfs/dev/console
rootfs/linuxrc
$
initramfs
Linux发行版最开始启动时挂载的rootfs一般是构建在内存中的,它里面的初始化脚本最终会chroot到真正的位于磁盘上的rootfs。实用的ramfs有两个:initrd和initramfs,相当相似,但是initrd是一个RAM disk,访问disk的话要经过block io,从而拉低了性能;而initramfs直接就是一个fs了,绕开了block io以及不必要的cache从而性能稍高,有助于提高开机速度。这两个机制都有保留,取决于传入内核的rootfs是一个镜像还是一个cpio,参考这篇博客。
initramfs内核默认就支持。我们直接将cpio编译进内核中,配置选项CONFIG_INITRAMFS_SOURCE
是一个字符串,它就是经过打包的cpio根文件系统的路径。
将上面做好的rootfs打包为cpio。这需要在rootfs目录下面进行:
$ cd rootfs
rootfs$ find . | cpio -o -H newc > ../hello.cpio
rootfs$
注意,cpio如果打包的是initramfs的话,必须-H newc
。
设备树里面,确认bootargs这样设置:root=/dev/ram rdinit=linuxrc。
然后编译、下载到板子上,就可以启动到hello world啦!
romfs
romfs需要用genromfs
工具,它由包管理安装。-d
指明打包的目录,-f
指明输出的文件:
$ genromfs -f hello.bin -d ./rootfs/
$
将这个romfs烧写到一个地址那里,不妨烧到0x081E0000处吧,它是stm32f429ii片内flash的最后一个block。
romfs需要开启以下的选项:
首先是ROMFS:
-> File systems
-> Miscellaneous filesystems
-> ROM file system support
然后是CONFIG_MTD_BLOCK
:
-> Device Drivers
-> Memory Technology Device (MTD) support
-> Caching block device access to MTD devices
然后是CONFIG_MTD_ROM
:
-> Device Drivers
-> Memory Technology Device (MTD) support
-> RAM/ROM/Flash chip drivers
-> Support for ROM chips in bus mapping
还有CONFIG_MTD_UCLINUX
-> Device Drivers
-> Memory Technology Device (MTD) support
-> Mapping drivers for chip access
-> Generic uClinux RAM/ROM filesystem support
上面最后一个选项编译的是drivers/mtd/maps/uclinux.c,负责将ROM mapped的东西注册为mtd设备。而开启了CONFIG_MTD_BLOCK之后它就能生成/dev/mtdblock0、1、2等等了。
uclinux.c里面有一个参数:
static unsigned long physaddr = -1;
module_param(physaddr, ulong, S_IRUGO);
这是romfs的地址。上面我们将它烧到了0x081E0000了,因此在设备树里面要将其传入bootargs中。
启动之后可以看到如下的printk:
[ 0.720000] uclinux[mtd]: probe address=0x81e0000 size=0x3000
[ 0.720000] Creating 1 MTD partitions on "rom":
[ 0.730000] 0x000000000000-0x000000003000 : "ROMfs"
/dev/mtdblock的编号是怎么来的呢?这取决于注册该设备的时间顺序,因此如果注册了很多个mtd块设备的话,要从头到尾翻看内核log,找“Creating x MTD partitions on xxx”这段话,数数它是第几个注册的。这里是第0个注册的,因此0x081e0000处的romfs镜像对应的就是/dev/mtdlock0。需要写入bootargs中。
因此内核参数如下:root=/dev/mtdblock0 init=/linuxrc uclinux.physaddr=0x081E0000
内核启动之后会有如下的printk:
[ 0.310000] romfs: ROMFS MTD (C) 2007 Red Hat, Inc.
...
[ 0.720000] uclinux[mtd]: probe address=0x81e0000 size=0x3000
[ 0.720000] Creating 1 MTD partitions on "rom":
[ 0.730000] 0x000000000000-0x000000003000 : "ROMfs"
...
[ 0.780000] VFS: Mounted root (romfs filesystem) readonly on device 31:0.
接下来就是hello world了!
yaffs
上一篇文章里讲述了给内核打补丁、添加nand flash分区、添加nand flash驱动、改动bootargs等工序。整个rootfs可以通过nfs来复制到nand上,从而启动该hello world。当然如果要量产的话,还是要老老实实在bootloader里面添加yaffs的逻辑,添加USB之类的驱动,从而可以快速烧写程序。
nfs
上一篇文章里讲述了如何搭建nfs、挂载nfs、内核配置、改动bootargs等工序。值得一提的是,如果主机用WiFi的话,nfs传输速度有可能会极慢,有些时候甚至都挂不上nfs,还是连网线大法好。
busybox
busybox是嵌入式Linux的瑞士军刀,它内置了诸如bash、ls、mount、cat、vi等等一大堆命令行工具,基本上编译通过之后丢进去就能跑通一个小系统了!
从官网上下载的源码无需任何修改,只需要配置就行了:
- 它有
CONFIG_NOMMU
的配置 CONFIG_CROSS_COMPILER_PREFIX
设置交叉编译工具链,需要用应用程序的工具arm-uclinuxeabi-,而不是裸机程序的工具arm-none-eabi-CONFIG_EXTRA_CFLAGS
设置为-march=armv7-m -mthumb
,使之生成thumb2的代码CONFIG_PREFIX
设置安装目录- 其他命令按需设置
编译时,需要SKIP_STRIP:
$ make SKIP_STRIP=y
$ make install
现在这个rootfs有/bin、/usr、/sbin这些目录了。还需要添加一些目录,比方说/etc、/sys、/proc等。规范的根目录布局参考“Filesystem Hierarchy Standard”规范。
/etc目录放的是一堆配置文件,可以参考busybox源码下面的examples/bootfloppy/etc
目录。其中:
/etc/fstab
是启动时候要挂载的文件系统。可以添加dev、sysfs的东西:
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
/etc/inittab
是init进程维护的东西,可以这样设置:
::sysinit:/etc/init.d/rcS
ttyS0::respawn:/bin/sh
tty0::respawn:/bin/sh
其中后面两行表示在ttyS0和tty0上面开启shell终端,而且这些终端退出之后可以重新登录,即respawn。另外第一行表示,它会执行/etc/init.d/rcS
脚本。
/etc/init.d/rcS
就是一个bash脚本,可以做任何想做的事情,比方说挂载各种各样的文件系统、加载内核驱动模块等等:
#! /bin/sh
# see also /etc/fstab
/bin/mount -a
# load other drivers
/sbin/modprobe usb-storage
/sbin/modprobe sd_mod
# and many other things....
内核模块放在/lib/modules/4.13.3/
目录下面。编译内核之后,将INSTALL_MOD_PATH
环境变量设置为那个目录,然后通过make modules_install来安装模块。
$ INSTALL_MOD_PATH=某某某 make modules_install
至此,就构建了一个带有shell的rootfs啦。