折腾完内核和设备树之后我踩到了教科书上不会告诉你的一步内核起来只是“心脏开始跳”想让整个系统站住还得有一副完整的“骨架”——也就是根文件系统 Rootfs。最近在给一块国产 Cortex-A 系列开发板做系统移植前面几篇分别解决了交叉编译环境、bootloader 和内核启动到了第四篇内核已经能正常解压并跑到挂载根文件系统的阶段结果因为目录结构完全不存在直接停在经典的 “No init found”。这篇就把 Rootfs 目录结构创建的完整思路、操作命令和踩坑记录一次说清楚。开篇先把结论放这Rootfs 目录创建不是“多敲几个 mkdir 的事”目录缺哪个、权限给多少、特殊文件建不建直接决定你的 sbin/init 能不能跑起来、设备节点能不能出现、最终系统能不能登录。1. 为什么必须先建目录Rootfs 的本质思考1.1 从内核启动流程看目录的意义很多人以为内核启动完成后系统就“自动”有了文件系统其实这是最大的误解。Linux 内核本身是个光杆司令设备驱动、硬件初始化、内存管理都由内核负责但用户态的进程、shell、配置统统来自根文件系统。内核启动流程走到最后一步会去尝试挂载 rootfs然后查找并执行第一个用户态程序。这个程序默认路径有好几个候选最常见的是 /sbin/init如果指定了 init 参数也可以换成 /bin/sh。无论哪个内核都会用一个绝对路径去找这个文件。问题是如果没有根文件系统或者目录结构不完整内核就会在挂载点看不到这些路径然后打印 “Kernel panic - not syncing: No init found. Try passing init option to kernel.”。这说明了一个很底层的事实Linux 的根文件系统不是一个普通数据仓库它本身是内核启动链路中的一环。目录结构就是这张链路的骨架/sbin 下必须有 init 或者指向 busybox 的链接/lib 下必须能找到动态链接器和共享库/dev 下必须有 console 和 null 这两个基础设备节点/etc 下要有 inittab 或 rcS 之类的配置脚本。少了任何一环系统都站不起来。我在第一次试验时光顾着把 busybox 编出来丢进 rootfs却漏了 /dev/console 设备节点启动后串口完全没有输出屏幕黑压压一片。查了很久才发现内核在启动阶段要打开 /dev/console 作为标准输入输出这个节点不存在console 上的所有打印就直接丢了。这种问题不是程序逻辑错误而是文件系统目录“骨架”缺失导致的底层故障。1.2 FHS 标准与嵌入式精简的平衡Linux 文件系统有一套公开的目录规范叫 FHSFilesystem Hierarchy Standard也就是文件系统层次标准。它规定了 /bin、/usr/bin、/etc、/var、/tmp、/home 这些顶层目录的用途。桌面发行版几乎完全遵循这套规范目录非常多光 /usr 下面就有十多个子目录连 /usr/games 都有。但嵌入式 Rootfs 不能照搬这套全量目录原因很简单空间和复杂度都受不了。一块成熟的嵌入式方案整个 Rootfs 往往只有几十 MB甚至几 MB。在这个体积下把桌面发行版的完整目录树搬过来是灾难。轻则占用大量存储重则各种无关配置和启动脚本互相干扰导致系统行为诡异。不过“精简”不等于“乱删”。我见过有些移植资料把目录砍到只剩 bin、sbin、etc、dev、lib、proc、sys、tmp、usr连 /home、/root、/var 都不要。短期内这样确实能启动但后患无穷你装第一个需要写日志的应用程序时就傻眼了因为没有 /var/log你想用 root 用户正常登录shell 会要 /root 目录作为 HOME缺了就报各种配置错误。嵌入式 Rootfs 的目录精简应该把握一个原则在满足 FHS 兼容性的前提下去掉明显用不到的目录保留所有基础用户态程序运行所依赖的目录。我实际开发中推荐的底线是 /bin、/sbin、/lib、/etc、/usr、/var、/home、/root、/tmp、/proc、/sys、/dev、/mnt后面需要 /opt 或 /media 再加也不迟。1.3 目录结构决定后续移植的“影响半径”目录结构创建这一步的“影响范围”比表面看起来大得多。它直接决定后面所有组件的落地位置和运行方式我结合自己的移植经验梳理了几条影响第一个用户态程序的查找。内核要执行 /sbin/init你的 init 或 busybox 必须出现在这个路径上否则系统根本起不来。影响动态库的搜索路径。交叉编译成的动态程序默认会去 /lib 找加载器和 so 文件目录没有或者 so 放错位置程序会报 “No such file or directory” 或 “cannot open shared object file”。影响挂载点的可用性。/proc、/sys、/dev 是内核伪文件系统的挂载点目录不存在就算内核支持这些文件系统也无法挂载。影响用户管理。passwd 文件里每个用户都有家目录路径如 /home/xxx 或 /root目录不存在登录成功也会被踢回默认路径或者出现各种权限异常。影响程序运行期数据。日志写到 /var/log、锁文件放 /var/run、临时文件放 /tmp这些目录缺失或者权限不对应用会在运行时报错而且报错信息经常很迷惑。影响后续扩展。Builder 系统、软件包管理器、应用程序安装脚本都假定标准目录存在。如果你手工创建的目录不全后面每装一个软件都要为“没有标准目录”买单。所以别把 Rootfs 目录创建看成一步“走个过场”的命令执行它其实是整个移植方案的地基。地基歪一寸上层楼就歪一丈。2. 动手前必须明白核心目录各自的角色2.1 必建目录与功能对照如果你用的目标平台是 ARM 32 位还是 ARM64目录结构创建上没有任何区别顶层目录名字都一样区别只在于交叉编译工具链和库文件架构。我这边用 arm-linux-gnueabihf- 作为工具链前缀先列出我最终创建的必建目录与对应作用目录用途嵌入式开发中的必要性/bin用户基本命令sh、ls、cat、mv 等必须/sbin系统管理命令init、mount、ifconfig 等必须/usr/bin额外的用户程序建议保留/usr/sbin额外的系统管理程序建议保留/lib动态链接器、共享库、内核模块必须/etc系统配置inittab、fstab、passwd、rcS必须/home普通用户家目录建议保留/root管理员家目录必须/var可变数据日志、锁、临时运行数据必须/tmp临时文件必须/procprocfs 挂载点进程/内核信息必须/syssysfs 挂载点设备与驱动信息必须/devdevtmpfs 挂载点或设备节点目录必须/mnt临时挂载点建议保留/opt第三方软件的安装目录可选这里有个容易忽视的点/var 下面至少要拆出 /var/log、/var/run、/var/tmp 三个子目录。很多 init 脚本和应用程序默认往这三个路径写东西不建的话启动脚本第一步就可能报 “Unable to open /var/log/messages”。还有一个细节是 /usr。busybox 安装时默认会生成 /usr/bin、/usr/sbin、/usr/lib 这些链接所以即使你觉得暂时用不到预留 /usr 及其子目录会更省事避免后面手动补目录时权限五花八门。2.2 proc、sys、dev三座特殊的“桥”/bin、/etc 这些目录是真实存储在存储介质上的普通目录而 /proc、/sys、/dev 这三个目录比较特殊它们本质上是“挂载点”对应的是内核提供的伪文件系统数据并不存在磁盘里。创建目录只是准备工作真正让它们工作起来要等到系统启动后执行 mount。/procprocfs进程和内核运行状态都在这里ps、top、free 这些命令要正常工作前提就是 /proc 被挂载。挂载命令是 mount -t proc proc /proc。/syssysfs设备和驱动模型、总线、块设备拓扑等都在这里。mdev 或 udev 这类设备管理程序依赖 /sys 扫描设备并创建设备节点。/dev设备节点目录。最理想的情况是用 devtmpfs 自动管理内核启动时自动填充设备节点。挂载命令通常是 mount -t devtmpfs devtmpfs /dev。如果内核没开 devtmpfs你就得靠 mdev/udev 或手工 mknod。这三个目录如果不挂载最明显的现象是ps 命令报错找不到 /proc/stat插入 USB 设备没反应mdev 收不到热插拔事件/dev 下只有几个手动创建的节点其他设备一律不出现。我在第一次跑系统时把 /proc 和 /sys 的空目录建好后就没管了结果进入 shell 后敲 ps内核直接打印 “cant open /proc/stat” 之类的报错。当时还以为是 busybox 编译问题后来才发现纯粹是没挂载伪文件系统。2.3 权限和属主骨架的“关节强度”目录结构建好不等于骨架耐用权限和属主决定了这套骨架能不能被正常使用。Linux 系统对目录权限的判定和文件略有不同目录必须有 x 权限才能被进入必须有 r 权限才能列出内容必须有 w 权限才能在里面创建/删除文件。基础建议是所有顶层目录统一属主 root:root目录权限保持 755即 rwxr-xr-x这能保证 root 和普通用户都能读、能进入但只有 root 能创建文件。有几个目录要单独调整/root 权限设 700。管理员家目录通常不允许其他用户查看设成 700 避免普通用户窥探 root 的配置文件和私密数据。/tmp 权限设 1777。1 表示 sticky bit也就是粘滞位。这个权限的作用是所有用户都能在 /tmp 里创建文件但只能删除自己创建的文件避免互相误删。/dev 下的设备节点权限视情况而定。比如 /dev/null 一般是 666方便所有用户丢弃输出/dev/console 一般 600只有 root 能访问控制台。权限出问题时的表现很“诡异”。我遇到过某个工作目录缺 x 权限结果 mount 命令执行时报 “Permission denied”但 mount 本身是有权限的还有一次 /tmp 权限设成了 755一个普通用户程序写临时文件时一直报错排查了很久才发现是目录权限过严。所以在创建目录后我建议立刻用 ls -ld 把关键目录的权限列一遍而不是等到系统起不来再怀疑。3. 实操把 Rootfs 骨架从零搭起来3.1 准备工作区环境变量与宿主机注意事项在宿主机上创建 Rootfs 第一步不是 mkdir而是先在习惯的工作目录下建一个统一的项目根目录。我一般在 Linux 开发机上用一个非 root 用户的目录比如 /home/dev/work/然后在这个目录下新建 rootfs 作为 Rootfs 的顶层目录。这里有个重要经验不要在 root 用户的主目录下直接开建也不要用已经用过的发行版 rootfs 来改。发行版 rootfs 里有大量与目标平台无关的服务、systemd 目录、启动脚本混在一起后期排查起来痛苦不堪。从零手工创建反而更可控一切都是你自己加进去的。建议先定义环境变量省得后面每次敲全路径export ROOTFS/home/dev/work/rootfs后面所有命令都可以用 $ROOTFS 指代 Rootfs 顶层目录。这个变量在当前 shell 里有效如果你在其他终端操作记得重新 export 一下或者直接写一个环境变量文件 source。3.2 创建目录的完整命令与逐条解读创建目录我用的是 mkdir -p配合 shell 的花括号展开一次完成。直接贴我当时用的完整命令sudo mkdir -p $ROOTFS/{bin,sbin,lib,etc,root,home,var,var/log,var/run,var/tmp,tmp,proc,sys,dev,dev/pts,mnt,opt,usr,usr/bin,usr/sbin,usr/lib,usr/local}逐段理解一下{bin,sbin,...} 是 shell 的花括号展开等价于逐个 mkdir。dev/pts 被创建出来是为了后面挂载 devpts 伪终端文件系统。master/slave 伪终端节点都在 /dev/pts 下没有挂载点则 SSH 或 telnet 登录这类需要终端设备的服务可能异常。var/log、var/run、var/tmp 是新增的三个子目录它们都用于可变数据后续日志、锁文件、临时文件都有家可归。usr 和 usr/bin、usr/sbin、usr/lib 是给标准用户态程序使用的busybox 安装脚本会默认往这些路径放内容。创建完目录接着设置基础权限和属主。建议先统一把顶层目录设为 root:root再对特殊目录单独调整sudo chown -R root:root $ROOTFS sudo find $ROOTFS -type d -exec chmod 755 {} \; sudo chmod 1777 $ROOTFS/tmp sudo chmod 700 $ROOTFS/root这里 chown -R root:root 的作用是把 Rootfs 里所有文件和目录的属主统一为 root这样后续从 NFS 启动时不会因为属主不一致出现权限异常。find 的 -exec chmod 755 是批量把目录都设置成标准权限。随后 /tmp 覆盖为 1777/root 覆盖为 700。还有一个容易被遗漏的操作创建设备节点。如果你打算依赖 devtmpfs 或 mdev 来管理 /dev可以跳过这一步但为了稳妥我建议一开始就把基础节点建好防止 devtmpfs 没有配置或 mdev 还没运行时系统在启动早期就缺少 /dev/consolesudo mknod -m 600 $ROOTFS/dev/console c 5 1 sudo mknod -m 666 $ROOTFS/dev/null c 1 3mknod 命令中的 c 表示字符设备后面的 5 1 是主设备号和次设备号。console 的主设备号是 5次设备号是 1null 的主设备号是 1次设备号是 3。这是内核约定好的标准分配不能写错。写错之后设备文件就算存在内核打开它时也会报错或者直接当普通文件处理。3.3 校验骨架树形结构、权限与特殊文件目录建完别急着往里面放文件。先把“骨架”校验一遍确认没有遗漏。常用命令有 tree 和 find如果宿主机没装 tree直接用 find 足够tree -L 2 $ROOTFS或者find $ROOTFS -maxdepth 2 -type d | sort还要看一下关键目录和设备的权限ls -ld $ROOTFS/tmp $ROOTFS/root $ROOTFS/var/log ls -l $ROOTFS/dev/console $ROOTFS/dev/null我当时注意到一个细节用 mknod 创建的 /dev/console 和 /dev/null 在ls -l里显示类型是 c而不是普通文件的-。看到 c 开头的权限位就说明特殊设备文件建对了。如果后面要从 NFS 启动验证挂载点可以先把宿主机的 /proc、/sys、/dev 用 mount --bind 临时绑到 Rootfs 对应目录这样在宿主机里就可以直接观察 Rootfs 启动后的完整文件系统视图。这个操作只是宿主机侧验证不是最终目标系统必需的启动流程sudo mount --bind /proc $ROOTFS/proc sudo mount --bind /sys $ROOTFS/sys sudo mount --bind /dev $ROOTFS/dev这种临时挂载的好处是你可以直接在宿主机上 chroot 进入 Rootfs做快速验证。chroot 进去之前我建议先检查 /bin/sh 或 /sbin/init 是否存在如果还没有那就先进入下一步把 BusyBox 装进去。4. 骨架验证放上 BusyBox 并启动最小系统4.1 BusyBox 的编译与安装要点目录结构只是一个空壳真正让系统“能跑”的第一个程序通常是 BusyBox。BusyBox 是一个把常用 Unix 命令打包成单个二进制的程序通过符号链接或参数模拟 ls、sh、mount、init 等命令。嵌入式系统里它几乎是事实标准。编译 BusyBox 时我特别强调一个关键选项静态编译。很多人第一次交叉编译 busybox默认配置是动态编译装到 Rootfs 后一运行就报 “/bin/sh: not found”。其实 busybox 文件明明在那里问题在于动态链接器 /lib/ld-linux-armhf.so.3 或系统需要的基础库文件还没放进去。如果你还没有搭好动态库环境最省事的方法是选静态编译。配置命令大致如下export ARCHarm export CROSS_COMPILEarm-linux-gnueabihf- make defconfig make menuconfig进入 menuconfig 后在 BusyBox Settings - Build Options 里找到 “Build static binary (no shared libs)” 并选中。然后编译安装make -j$(nproc) make install CONFIG_PREFIX$ROOTFSCONFIG_PREFIX 指向你的 Rootfs 目录BusyBox 会把 busybox 二进制本身放入 /bin 或 /usr/bin取决于配置同时创建 /sbin/init、/bin/sh、/bin/ls 等大量符号链接都指向 /bin/busybox。这一步就是在给骨架填充第一块“肌肉”。安装完成后验证一下骨架是否具备启动最小系统的条件ls -l $ROOTFS/sbin/init $ROOTFS/bin/sh $ROOTFS/bin/busybox如果看到类似 “/sbin/init - ../bin/busybox” 的符号链接就说明基础命令已经就位了。4.2 通过 NFS 挂载验证 RootfsRootfs 创建好后最常用的快速验证方式是通过 NFS 让目标板直接挂载宿主机上的 Rootfs。这样你不用反复把文件系统烧写到 SD 卡直接从网络启动改代码、调页面都非常方便开发效率会高很多。宿主机侧导出 Rootfssudo apt install nfs-kernel-server # Debian/Ubuntu 系安装 NFS 服务 echo $ROOTFS *(rw,sync,no_root_squash,no_subtree_check) | sudo tee -a /etc/exports sudo exportfs -ra内核启动参数我通常这样设置root/dev/nfs nfsroot192.168.1.100:/home/dev/work/rootfs,vers3,tcp rw ipdhcp这里 192.168.1.100 是宿主机的 IP后面是 Rootfs 的路径。vers3 表示使用 NFSv3tcp 是传输协议rw 表示可读写。ipdhcp 表示目标板通过 DHCP 获取 IP。如果你只是想快速看目录结构能不能正常工作还可以用更粗暴的方式在 U-Boot 里临时给内核传 init/bin/sh跳过完整 init 流程直接进入 shell。进入后手动挂载伪文件系统mount -t proc proc /proc mount -t sysfs sysfs /sys mount -t devtmpfs devtmpfs /dev然后查看 /proc/cpuinfo、/sys/class 的内容是否存在再尝试向 /tmp 写入文件。如果这些操作全部成功说明 Rootfs 的目录骨架在目标架构上已经基本可用接下来要做的就是往里填配置文件和应用了。关于挂载参数还有个细节NFS 挂载时最好在 bootargs 里加上 rootwait。这个参数让内核在检测网络设备时多等待一段时间避免因为网络链路还没准备好根文件系统挂载失败。我试过不止一次不加 rootwait 时系统随机卡在挂载 NFS 阶段加了之后几乎百发百中。4.3 目录不全时最容易见到的启动报错验证过程中我遇到过的与目录直接相关的启动报错可以总结成几类每一类都指向目录结构里某个具体的缺失缺 /sbin/init内核启动最后阶段打印 “No init found. Try passing init option to kernel.”。这个报错最常见的原因是 /sbin/init 不存在或是指向的 busybox 无法执行。解决方法是确认 /sbin/init 符号链接是否存在以及 /bin/sh 是否真的能跑起来。缺 /dev/console启动过程中登录进程或 init 脚本报 “cant open /dev/console”。这个现象很容易被误判为串口配置问题但实际往往是 /dev/console 设备节点缺失或主次设备号不对。缺 /proc 或 /proc 未挂载进入 shell 后敲 ps、top、free 这类命令输出内容为空或直接报错。因为这个信息来自 /proc 文件系统目录存在但没挂载就等于没有数据源。缺 /tmp 或权限不当启动脚本执行到一半报错说无法创建临时文件。解决方法是确保 /tmp 存在并且权限包含写入和粘滞位。这些报错都不难修但要修得快前提是你已经清楚目录结构里哪个缺了。这也是我把校验放在实操里的原因多花两分钟看 ls能省下后面至少一小时的排查时间。5. 高频问题与排查技巧实录5.1 “No init found” 这类问题的完整排查路径“No init found” 是我见过最容易被表象误导的报错。第一次看到这个提示时我以为是内核配置错了或者 bootargs 没写对后来才发现是 Rootfs 里根本没有 /sbin/init。排查路径建议按这个顺序走第一步查看 Rootfs 里是否存在 /sbin/init 或配置了 init 参数指定的程序ls -l $ROOTFS/sbin/init如果文件不存在说明 BusyBox 没安装进去或者安装时 CONFIG_PREFIX 指向的路径不对。第二步检查 busybox 本身能否在目标架构上执行file $ROOTFS/bin/busybox输出应该是 “ELF 32-bit LSB executable, ARM, ...”。如果显示 x86 或 unknown那就意味着交叉编译失败或安装错了工具链。第三步检查动态链接依赖。如果是动态编译的 busybox用 readelf 看它依赖哪些共享库readelf -d $ROOTFS/bin/busybox | grep NEEDED输出里可能有一串 libc.so.6、ld-linux-armhf.so.3 之类的库名。这些库必须存在于 /lib否则内核加载 busybox 时会报类似 “cannot open shared object file” 的错误。第四步检查 inittab。就算 init 找到了busybox init 默认会读取 /etc/inittab没有这个文件时它会走默认动作可能会试图执行 /etc/init.d/rcS。如果 rcS 也不存在又会出现另一类启动失败。5.2 dev 目录“闹鬼”设备节点不出现Rootfs 启动后进入 shell但 /dev 下只有空空的几个文件USB 设备插上去也没反应这是很多嵌入式新手会踩的坑。根本原因通常是 devtmpfs 没有挂载或者 mdev 没有配置。内核有 devtmpfs 支持时建议直接在启动脚本里挂载mount -t devtmpfs devtmpfs /dev如果内核配置里没开 CONFIG_DEVTMPFS那就得靠 mdev 来扫描和创建设备节点。做法是确保 /dev 已经挂载或为空然后在启动脚本里运行echo /sbin/mdev /proc/sys/kernel/hotplug mdev -smdev -s 的作用是扫描 /sys 目录根据设备信息在 /dev 下创建对应的节点。如果没有 /sys 支持mdev 什么也扫不到所以顺序必须是先挂载 /sys再跑 mdev。还有一个容易混淆的点如果你手动用 mknod 创建了 /dev/console 或 /dev/null然后又在启动脚本里 mount -t devtmpfsdevtmpfs 挂载时会覆盖整个 /dev 目录把你手动创建的节点清掉。所以要么依赖 devtmpfs 自动生成要么依赖 mdev 扫描生成不要混用否则排查时你会觉得自己创建的东西“神秘消失”。5.3 权限、属主和只读挂载的坑权限类问题有时候比目录缺失更隐蔽。我举一个真实的例子有一次目标板启动后程序写日志时总是报 “Permission denied”但 /var/log 明明存在。用 ls -ld 一看权限是 755属主是 root。普通用户程序在 /var/log 下没有写权限日志自然写不进去。解决方式是给 /var/log 设置合适的权限。考虑日志通常由 root 或专门的日志程序写我一般设 755 然后让相关进程以 root 运行如果你想普通用户也能写就得改成 1777但又可能带来安全性问题。最好按实际需求设权限不要一刀切。还有一类常见问题是 NFS 挂载时 root_squash 导致权限异常。NFS 服务端开启 root_squash 后客户端 root 的请求会被映射成 nobody 用户这样即使 Rootfs 里目录是 root 属主远程的 root 进程也拿不到真正的写权限。嵌入式开发期间我建议在 NFS 导出配置里加 no_root_squash$ROOTFS *(rw,sync,no_root_squash,no_subtree_check)这样宿主机和目标板的 root 身份基本等价调试起来顺畅很多。只读挂载是另一个大坑。如果 bootargs 里没有写 rw内核会尝试以只读方式挂载根文件系统这时 /tmp、/var 全部变成不可写很多启动脚本中途就失败。我调试时习惯在 bootargs 里明确写 rw避免依赖默认挂载选项。5.4 高频问题速查表现象可能原因快速排查与解决Kernel panic: No init found/sbin/init 不存在或无法执行ls -l /sbin/init、file /bin/busybox确认 ARCH 和工具链/bin/sh: not foundbusybox 动态编译且库缺失改为静态编译或补齐 /lib 下的链接器和库串口无输出/dev/console 缺失设备节点mknod -m 600 /dev/console c 5 1或启用/挂载 devtmpfsps/top 报错或输出为空/proc 未挂载mount -t proc proc /proc插入 USB 无反应/sys 未挂载或 mdev 未运行挂载 sysfs运行 mdev -s配置内核 hotplug日志写入失败/var/log 权限不当或根文件系统只读chmod 755 /var/log确认 bootargs 带 rw临时文件创建失败/tmp 缺失或权限不足mkdir -p /tmpchmod 1777 /tmp程序报“Permission denied”目录缺 x 权限或属主不对ls -ld 检查目录权限chown root:root 或按需调整设备节点手动创建后消失devtmpfs 挂载覆盖了目录改为一律使用 devtmpfs或一律使用 mdev不要混用写到这里再分享一点我自己的体会。做过几次从零移植之后我对“文件系统骨架”这四个字的理解已经完全变了它不是一个名词而是一套启动信任链。内核信任 /sbin/init 能被执行init 信任 /etc/inittab 能被读取shell 信任 /tmp 有可写空间mdev 信任 /sys 能反映设备状态。每一条信任都建立在目录存在、权限正确、特殊节点齐全的基础上。所以每次创建完 Rootfs 目录我都会像查清单一样逐项核对权限和关键节点宁愿多花十分钟也不愿在板子上靠串口日志反复猜谜。这套流程走通之后系统能顺利进入 shell 的那一刻你会觉得之前所有关于目录的较真都是值得的。