简单来讲,内核、文件系统、应用程序,三者构成一个完整可用的系统。发行版之间不同之处不在于内核啥的,而在于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进程的stdinstdoutstderr都是/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根文件系统的路径。

initramfs
initramfs

将上面做好的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啦!

helloworld
helloworld

romfs

romfs需要用genromfs工具,它由包管理安装。-d指明打包的目录,-f指明输出的文件:

$ genromfs -f hello.bin -d ./rootfs/
$ 

将这个romfs烧写到一个地址那里,不妨烧到0x081E0000处吧,它是stm32f429ii片内flash的最后一个block。

romfs1
romfs1

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了!

romfs1
romfs1

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啦。

busybox
busybox