一、busybox 源码分析1
1、源码目录梳理
2、整个程序入口的确认
(1) 分析一个程序,不管多庞大还是小,最好的路线都是 按照程序运行时的逻辑顺序来。所以找到一个程序的入口至关重要。
(2) 学 C 语言的时候都知道,程序的主函数 main 函数就是整个程序的入口。这种情况适应于操作系统下工作的应用程序 的情况。
(3) 在 uboot 和 linux kernel 这两个大的 C 语言的项目中,main 函数都没有,都不是入口。在我们这种裸机程序中,入口不是 main 函数,而是由链接脚本来指定的。
(4) busybox 是 linux 启动起来后工作的一个应用程序,因此其中必然有 main 函数,而且 main 就是入口。
3、busybox 中 main 函数全解析
(1) busybox 入口就是 main 函数,其中有很多个 main,但是只有一个起作用了,其他的是没起作用的。真正的 busybox 工作时的入口是 libbb/appletlib.c 中的 main 函数。
(2) busubox 中有很多 xxx_main 的函数,这些 main 函数每一个都是 busybox 支持的一个命令的真正入口。
譬如 ls_main 函数,就是 busybox 当作 ls 函数使用时的入口程序。
(3) ls 或者 cd 等命令,其实都是 busybox 一个程序,但是实际执行时的效果却是各自的效果。busybox 是如何实现一个程序化身万千还能各自工作的?答案就是 main 转 xxx_main。
也就是说,busybox 每次执行时,都是先执行其 main,在 main 函数中识别(靠 main 函数的传参 argv[0] 来识别)我们真正要执行的函数(譬如 ls),然后去调用相应的 xxx_main(譬如 ls_main)来具体实现这个命令。
二、busybox源码分析2
1、inittab 解析与执行
(1) inittab 的解析是在 busybox/init/init.c/init_main 函数中。
(2) 执行逻辑是:先通过 parse_inittab 函数解析 /etc/inittab(解析的重点是,将 inittab 中的各个 action 和 process 解析出来),然后后面先直接执行 sysinit 和 wait 和 once(注意这里只执行一遍),然后在 while(1) 死循环中去执行 respwan 和 askfirst。
2、pwd 命令执行路径分析
(1) 根据上节讲的,我们在 busybox 命令行下执行 pwd 命令时,实际执行的是 pwd_main 这个函数。
3、busybox 的体积优势原理
(1) busybox 实际上就是把 ls、cd、mkdir 等很多个 linux 中常用的 shell 命令集成在一起了。集成在一起后有一个体积优势:就是 busybox 程序的大小,比 busybox 中实现的那些命令的大小加起来 要小很多。
(2) busybox 体积变小的原因主要有 2 个:
第一个是 busybox 本身提供的 shell 命令是阉割版的(busybox 中的命令支持的参数选项比发行版中要少,譬如 ls 在发行版中可以有几十个 -x 选项,但是在 busybox 中只保留了几个常用的选项,不常用的都删除掉了);
第二个是 busybox 中因为所有的命令的实现代码都在一个程序中实现,而各个命令中有很多代码函数都是通用的(譬如 ls 和 cd、 mkdir 等命令都会需要去操作目录,因此在 busybox 中实现目录操作的函数,就可以被这些命令共用),共用会降低重复代码出现的次数,从而减少总的代码量和体积。
(3) 经过分析,busybox 的体积优势 是嵌入式系统本身的要求和特点造成的。
三、rcS 文件介绍1
0、/etc/init.d/rcS
/etc/init.d/rcS 文件,是 linux 的运行时配置文件中最重要的一个,其他的一些配置都是由这个文件引出来的。这个文件可以很复杂,也可以很简单,里面可以有很多的配置项。
这是 ubuntu 发行版的 rcS 文件:
root@ubuntu:# cat /etc/init.d/rcS
#! /bin/sh
#
# rcS
#
# Call all S??* scripts in /etc/rcS.d/ in numerical/alphabetical order
#
exec /etc/init.d/rc S
root@ubuntu:# vim /etc/init.d/rc
这是 busybox 最简单的 rcS 文件:
1、PATH=xxx
(1) 首先从 shell 脚本的语法角度分析,这一行定义了一个变量PATH,值等于后面的字符串。
(2) 后面用 export 导出了这个 PATH,那么 PATH 就变成了一个环境变量。
(3) PATH 这个环境变量,是 linux 系统内部定义的一个环境变量,含义是操作系统去执行程序时,会默认到 PATH 指定的各个目录下去寻找。
如果找不到,就认定这个程序不存在,如果找到了就去执行它。将一个可执行程序的目录导出到 PATH,可以让我们不带路径 来执行这个程序。
(4) rcS 中为什么要先导出 PATH?就是因为我们希望一旦进入命令行时,PATH 环境变量中就有默认的 /bin /sbin /usr/bin /usr/sbin 这几个常见的可执行程序的路径,这样我们进入命令行后就可以 ls、 cd 等直接使用了。
(5) 为什么我们的 rcS 文件还没添加,系统启动就有了 PATH 中的值?原因在于 busybox 自己用代码硬编码,为我们导出了一些环境变量,其中就有 PATH。
2、runlevel=
(1) runlevel 也是一个 shell 变量,并且被导出为环境变量。
(2) runlevel 这个环境变量到底有什么用?
(3) runlevel=S 表示将系统设置为单用户模式。
3、umask=
(1) umask 是 linux 的一个命令,作用是设置 linux 系统的 umask 值。
(2) umask 值决定当前用户在创建文件时的默认权限。
4、mount -a
(1) mount 命令是用来挂载文件系统的。
(2) mount -a 是挂载所有的应该被挂载的文件系统,在 busybox 中 mount -a 时,busybox 会去查找一个文件 /etc/fstab 文件,这个文件按照一定的格式列出来所有应该被挂载的文件系统(包括了虚拟文件系统)
ubuntu 发行版的 /etc/fstab:
busybox 的 fstab:
四、 rcS文件实战1
1、PATH&runlevel
在 rootfs 根文件系统创建 init.d 目录,然后拷贝 rcS 到 init.d 目录下:
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# cd init.d/
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc/init.d# vim rcS
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc/init.d#
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc/init.d# cat rcS
#!/bin/sh
PATH=/sbin:/bin:/usr/sbin:/usr/bin
runlevel=S
prevlevel=N
umask 022
export PATH runlevel prevlevel
mount -a
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc/init.d#
拷贝 fstab 文件到 etc 目录下:
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# ls
fstab init.d inittab
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# vim fstab
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# cat fstab
# /etc/fstab: static file system information.
#
# Use 'vol_id --uuid' to print the universally unique identifier for a
# device; this may be used with UUID= as a more robust way to name devices
# that works even if disks are added and removed. See fstab(5).
#
# <file system> <mount point> <type> <options> <dump> <pass>
proc /proc proc defaults 0 0
sysfs /sys sysfs defaults 0 0
tmpfs /var tmpfs defaults 0 0
tmpfs /tmp tmpfs defaults 0 0
tmpfs /dev tmpfs defaults 0 0
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc#
(1) 我们实战发现,rcS 文件明明存在,但是却提示不存在。问题原因就是,rcS 文件在 windows 下创建的,行尾换行符为 ‘\r\n’,多了点东西。但是因为 ubuntu 中的 vi 对行尾做了优化,所以在 ubuntu 中是看不出来多了东西的。但是在 securecrt 下一看就发现,每一行末尾多出来了一个^M。
手动修改删除多余的 ^M。
重新启动,不再报错 rcS 找不到。
(2) 这个故事告诉我们:shell 脚本文件如果格式不对,运行时可能会被提示文件不存在。
(3) 扩展讲一个:有时候一个应用程序执行时,也会提示文件不存在,问题可能是这个程序所调用的一个动态链接库找不到。
开发板的 runlevel 无效:
正常的 ubuntu 发行版的 runlevel 命令能正常工作:
(4) 测试结果:PATH 本来在 busybox 中就已经用代码导出过了,所以 rcS 中再次导出没有任何明显的现象,因此看不出什么差别;runlevel 实际执行结果一直是 unknown,问题在于 busybox 并不支持 runlevel 这个特性。
2、umask 测试
(1) umask是022的时候,默认touch创建一个文件的权限是644。
(2) umask是044的时候,默认touch创建一个文件的权限是622。
(3) umask是444的时候,默认touch创建一个文件的权限是222。
总结:umask 的规律就是:umask 值和默认创建文件的权限值加起来,是666.
3、mount 测试
(1) 挂载时全部出错:
mount: mounting proc on /proc failed: No such file or directory
mount: mounting sysfs on /sys failed: No such file or directory
mount: mounting tmpfs on /var failed: No such file or directory
mount: mounting tmpfs on /tmp failed: No such file or directory
mount: mounting tmpfs on /dev failed: No such file or directory
(2) 原因是因为,根文件系统中找不到挂载点。
所谓挂载点, 就是我们要将目标文件系统(当然这里都是虚拟文件系统)挂载到当前文件系统中的某一个目录中,这个目录就是挂载点。
(3) 解决方案就是,自己在制作的 rootfs 根目录下 创建这些挂载点目录即可。
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs# mkdir proc sys tmp var dev
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs# ls
bin dev etc linuxrc proc sbin sys tmp usr var
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs#
(4) 验证是否挂载成功,可以看挂载时输出信息;还可以启动后去看 proc 和 sys 文件夹,如果有文件出现则证明挂载成功了,如果没东西就证明失败了。
五、rcS文件介绍2
1、mdev
(1) mdev 是 udev 的嵌入式简化版本,udev/mdev 是用来配合 linux 驱动工作的一个应用层的软件,udev/mdev 的工作就是配合 linux 驱动生成相应的 /dev 目录下的设备文件。
(2) 因为这个问题涉及到驱动,因此详细讲解要等到驱动部分。这里我们只是通过一些直观的现象来初步理解 udev/mdev 的工作效果。
(3) 在 rcS 文件中没有启动 mdev 的时候,/dev 目录下启动后是空的;在 rcS 文件中添加上 mdev 有关的 2 行配置项后,再次启动系统后发现,/dev 目录下生成了很多的设备驱动文件。
(4) /dev 目录下的设备驱动文件就是 mdev 生成的,这就是 mdev 的效果和意义。
2、hostname
(1) hostname 是 linux 中的一个 shell 命令。命令(hostname xxx)执行后,可以用来设置当前系统的主机名为 xxx,直接 hostname 不加参数,可以显示当前系统的主机名。
(2)
/bin/hostname -F /etc/sysconfig/HOSTNAME
-F 来指定一个主机名配置文件(这个文件一般文件名叫 hostname 或者 HOSTNAME)
3、ifconfig
(1) 有时候我们希望开机后进入命令行时,ip 地址就是一个指定的 ip 地址(譬如192.168.1.30),这时候就可以在 rcS 文件中:
ifconfig eth0 192.168.1.30
六、profile 文件和用户登录理论
1、profile 文件添加
(1) 之前添加了 /bin/hostname 在 /etc/sysconfig/HOSTNAME 文件中定义了一个 hostname(aston210),实际效果是:命令行下 hostname 命令查到的 host 名字确实是 aston210。但是问题就是,命令行的提示符是没有显示的。
(2) 这个问题的解决就要靠 profile 文件。将提供的 profile 文件放入 /etc/ 目录下即可。
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs# cd etc/
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc#
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# cp /mnt/hgfs/linux_win_shared/etc/profile .
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# ls
fstab init.d inittab profile sysconfig
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# cat profile
# Ash profile
# vim: syntax=sh
# No core files by default
ulimit -S -c 0 > /dev/null 2>&1
USER="`id -un`"
LOGNAME=$USER
PS1='[\u@\h \W]\# '
PATH=$PATH
HOSTNAME=`/bin/hostname`
export USER LOGNAME PS1 PATH
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc#
(3) 添加了 profile 之后的实验现象:命令行提示符前面显示:[@aston210 ]#
结论是:
第一,profile 文件起了作用,hostname 显示出来了。
第二,还有个问题,登录用户名没显示出来。
原因就是我们直接进入了命令行,而没有做登录。等我们添加了用户登录功能,并且成功登陆后这个问题就能解决。
(4) profile 文件工作原理是:profile 文件也是被 busybox(init 进程)自动调用的,所以是认名字的。
2、如何看到用户登录界面
(1) linux 中有一个原则就是:用一个小程序来完成一个功能。如果我们产品确实需要很复杂的综合型的功能,我们倾向于先使用很多个小程序完成其中的一个功能,然后再将这些小程序集成起来完成整个大功能的产品。
(2) 这种集成很多个小程序来完成一个大的功能,有很多种技术实现。譬如 shell 脚本,还有一些别的技术,譬如 linux 启动中的 inittab。
(3) 因为我们之前 intttab 中有一个配置项 ::askfirst:-/bin/sh,这个配置项作用就是:当系统启动后,就去执行 /bin/sh,执行这个就会出现命令行。
因此我们这样的安排就会直接进入命令行,而不会出现登录界面。
(4) 我们要出现登录界面, 就不能直接执行 /bin/sh,而应该执行一个负责出现登录界面,并且负责管理用户名和密码的一个程序,busybox 中也集成了这个程序(就是 /bin/login 和 /sbin/gettty),因此我们要在 inittab 中用 /bin/login 或者 /sbin/getty ,去替代 /bin/sh。
3、用户名和密码的设置
(1) 用户名和密码的设置,是和登录程序有关联的,但是 /bin/login和 /sbin/getty 在用户名和密码的管理上是一样的。 其实常见的所有的 linux 系统的用户名和密码的管理几乎都是一样的。
(2) 密码一般都是用加密文字的,而不是用明文。意思就是系统中的密码肯定是在系统中的一个专门用来存密码的文件中存储的,用明文存密码有风险,因此 linux 系统都是用密文来存储密码的。关于密文密码的使用下节课实践时会详细讲。
七、用户登录实战
1、添加 /bin/login 到 sysinit
(1) 在 inittab 中修改,去掉 /bin/sh,换上 /bin/login,则系统启动后出现登录界面。可以输入用户名和密码。
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# vim inittab
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc# cat inittab
#first:run the system script file
::sysinit:/etc/init.d/rcS
#::askfirst:-/bin/sh
::sysinit:/bin/login
::ctrlaltdel:-/sbin/reboot
#umount all filesystem
::shutdown:/bin/umount -a -r
#restart init process
::restart:/sbin/init
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/rootfs/etc#
(2) 实验现象:成功出现用户登录界面,但是死活密码不对。
成功出现登陆界面:
2、添加 passwd 和 shadow 文件
(1) 为什么用户名和密码不对?因为我们根本没有为 root 用户设置密码。
(2) linux 系统中用来描述用户名和密码的文件是 passwd 和 shadow 文件,这两个文件都在 etc 目录下。passwd 文件中存储的是用户的密码设置,shadow 文件中存储的是加密后的密码。
(3) 我们直接复制 ubuntu 系统中的 /etc/passwd 和 /etc/shadow 文件到当前制作的 rootfs 目录下,然后再做修改即可。
同理,删除 shadow 文件除了 root 之外的 其他用户密码:
(4) /etc/passwd 和 /etc/shadow 修改好后,shadow 中默认有一个加密的密码口令,这个口令和你拷贝的shadow本身有关,像我的 ubuntu 中, root 用户的密码就是 root,因此复制过来后登陆时的密码还是 root。
3、重置密码实践
(1) ubuntu 刚装好的时候,默认登录是用普通用户登录的,默认 root 用户是关闭的。普通用户的密码是在装系统的时候设置的,普通用户登陆后可以使用 su passwd root 给 root 用户设置密码,设置了密码后,root 用户才可以登录。
(2) 其实这个原因就是,root 用户在 /etc/shadow 文件中加密口令是空白的。所以是不能登录的。
(3) busybox 中因为没有普通用户,所以做法是:默认 root 用户如果加密口令是空的,则默认无密码直接登录。
等我们登陆了之后,还是可以用 passwd root 给 root 用户设置密码。
解决报错:-sh:can’t access tty; job control turned off
(4) 平时有时候我们忘记了自己的操作系统的密码,怎么办?
有一种解决方法就是,用其他系统(WindowsPE 系统或者 ubuntu 的单用户模式等···)来引导启动,启动后挂载到我们的硬盘上,然后找到 /etc/shadow 文件,去掉密文密码后保存。然后再重启系统后密码就没了。
4、getty 实战
(1) 大家后面做项目会发现,inittab 中最常见的用于登录的程序不是 /bin/login,反而是 /sbin/getty。
(2) 这两个的差别不详,但是在 busybox 中这两个是一样的。这两个其实都是 busybox 的符号链接而已。因此不用严格区分这两个。
(3) 我们可以在 inittab 中,用 getty 替换 login 程序来实现同样的效果。
重启登录效果没有差别:
八、动态链接库的拷贝
1、静态编译链接 helloworld 程序并执行
(1) 任务:自己写一个 helloworld 程序,然后交叉编译连接,然后丢到开发板根文件系统中,开机后去运行。
(2) C 程序如果使用 gcc 来编译,则可以在主机 ubuntu 中运行,但是不能在开发板运行;
要在开发板运行,需要用 arm-linux-gcc 来交叉编译,但是这时候就不能在主机 ubuntu 中运行了。
我们可以用 file xx 命令来查看一个 elf 可执行程序是哪个架构的。
(3) 静态链接:
arm-linux-gcc hello.c -o hello_satic -static
制作 Makefile 进行程序编译:
(4) 实验结果:静态编译连接后生成的 hello_satic 已经可以成功运行。
2、动态编译连接helloworld程序并执行
(1) 动态链接:
arm-linux-gcc hello.c -o hello_dynamic
(2) 实验结果:-sh: ./hello_dynamic: not found 。运行时提示找不到程序。
(3) 错误分析:动态链接的 hello 程序中调用到了 printf 函数,而 printf 函数在动态连接时,要在运行时环境(开发板的 rootfs)中,去寻找对应的库文件(开发板 rootfs 中部署的动态链接库中,包含了 printf 函数的那个库文件)。如果找到了,则 printf 函数就会被成功解析,然 后hello_dynamic 程序就会被执行;如果找不到,则程序就不能被执行,命令行会提示错误信息 -sh: ./hello_dynamic: not found。
(4) 解决方案:将 arm-linux-gcc 的动态链接库文件,复制到开发板 rootfs 的 /lib 目录下即可解决。
3、找到并复制动态链接库文件到rootfs中
(1) 我们用的 arm-2009q3 这个交叉编译工具链的动态链接库,在 /usr/local/arm/arm-2009q3/arm-none-linux-gnueabi/libc/lib 目录下。
其他的一些交叉编译工具链中动态链接库的目录不一定在这里,要去找一下。找的方法就是 find。
(2) 复制动态链接库,到 roots/lib 目录下。复制时要注意参数用 -rdf,主要目的就是符号链接复制过来还是符号链接。
复制命令:
cp ../lib/ /home/aston/workspace/porting_x210/rootfs/rootfs/. -rfd
动态库的大小总共 3.8M:
(3) 现在再去测试 ./hello_dynamic 看看是否可以运行,实验结果是可以运行。
4、使用strip工具去掉库中符号信息
动态链接库 so 文件中,包含了调试符号信息,这些符号信息在运行时是没用的(调试时用的),这些符号会占用一定空间。
在传统的嵌入式系统中,flash 空间是有限的,为了节省空间常常把这些符号信息去掉。这样节省空间并且不影响运行。
去掉符号命令:
arm-linux-strip *so*
实际操作后发现库文件由 3.8M 变成了 3.0M,节省了 0.8M 的空间。
九、开机自启动与主流rcS格式介绍
1、修改 rcS 实现开机自启动
(1) 开机自启动指的是,让一些应用程序能够开机后自动执行。
(2) 开机自启动的实现原理就是,在开机会自动执行的脚本 rcS 中,添加上执行某个程序的语句代码即可。
2、前台运行与后台运行
(1) 程序运行时占用了当前的控制台,因此这个程序不结束我们都无法使用控制台,这就叫前台运行。默认执行程序就是前台运行的。
(2) 后台运行就是让这个程序运行,并且同时让出控制台。这时候运行的程序还能照常运行,而且还能够不影响当前控制台的使用。
(3) 让一个程序后台运行的方法就是 ./xxx &
3、实际开发中 rootfs 的 rcS 是怎样的
(1) 我们以 X210 开发板九鼎科技做的 rootfs 中 rcS 部分来分析。
(2) 分析 inittab 发现:sysinit 时执行 rcS,shutdown 时执行 rcK。
(3) 分析 /etc/init.d/rcS 和 rcK 文件发现,rcS 和 rcK 都是去遍历执行 /etc/init.d/ 目录下的 S 开头的脚本文件,区别是 rcS 传参是 start,rcK 传参是 stop。
(4) 由此可以分析出来,正式产品中的 rcS 和 rcK 都是一个引入,而不是真正干活的。真正干活的配置脚本是 /etc/init.d/S??*。这些文件中肯定有一个判断参数是 start 还是 stop,然后 start 时去做一些初始化,stop 时做一些清理工作。
十、制作 ext2 格式的镜像并烧录启动
1、确定文件夹格式的rootfs可用
(1) 设置 bootargs 为 nfs 启动方式,然后从主机 ubuntu 中做好的文件夹格式的 rootfs 去启动,然后看启动效果,作为将来的参照物。
2、动手制作ext2格式的镜像
(1)
# bs=1024 count=10240 相乘就是 10MB.
dd if=/dev/zero of=rootfs.ext2 bs=1024 count=10240
losetup /dev/loop1 rootfs.ext2
mke2fs -m 0 /dev/loop1 10240
mount -t ext2 /dev/loop1 ./ext2_rootfs/
(2) 向 ./rootfs 中复制内容
root@ubuntu:/home/aston/workspace/porting_x210/rootfs#cd ext2_rootfs/
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/ext2_rootfs# ls
lost+found
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/ext2_rootfs#
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/ext2_rootfs#
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/ext2_rootfs# cp ../rootfs/* . -rf
root@ubuntu:/home/aston/workspace/porting_x210/rootfs/ext2_rootfs# ls
(3) 回到 ext2_rootfs 的上级路径,卸载 ext2_rootfs 根文件系统:
umount /dev/loop1
losetup -d /dev/loop1
(4)完成后得到的rootfs.ext2就是我们做好的rootfs镜像。拿去烧录即可。
cp rootfs.ext2 /mnt/hgfs/linux_win_shared/. -rf
拷贝 rootfs.ext2 文件,到 Windows 主机:
将 rootfs.ext2 文件拷贝到 Windows 的 fastboot 软件目录中:
3、烧录镜像并设置合适的bootargs
(1) 使用 fastboot 烧录制作好的 rootfs.ext2 到开发板 inand 中
fastboot flash system rootfs.ext2
烧录完成后重启系统
-
开发板进入 fastboot 模式:
-
在 Windows fastboot 这边输入命令:
(2) 设置bootargs为:
set bootargs console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext2
(3) 启动后发现,现象和之前 nfs 方式启动挂载 rootfs 后一样的,至此 rootfs 制作实验圆满完成。
源自朱有鹏老师.