做嵌入式Linux的兄弟应该都干过这事:往板子上烧完内核,手搓了一个BusyBox根文件系统,结果启动到一半卡在“Creating 5 entries in /dev”或者挂载根文件系统之后VFS报一堆节点不存在,console登录不了,串口一片死寂。这种问题十有八九是dev目录没处理好——不是忘了建,就是建了但方式不对,设备节点在用户空间看不到,内核也不认。
这期就专门聊聊BusyBox环境下根文件系统里dev目录的正确创建方式。我把静态mknod、devtmpfs、mdev这三种主流方案从原理到实操全拆开讲,附带设备号速查和排错经验。无论你是刚入坑的应届生,还是被设备节点折磨过好几轮的老兵,这篇都值得花十分钟看完。
1. 为什么dev目录在BusyBox里这么特殊
1.1 /dev不是普通目录,它是设备访问的入口
先搞清一个概念:Linux下一切皆文件,硬件设备在用户空间就是/dev目录下的一个文件节点。你在应用层open("/dev/ttyS0"),内核VFS就会根据这个路径找到对应的设备驱动,然后把你的读写请求转给硬件。没有这个节点,驱动写得再好,应用程序也够不着设备。
在完整的发行版系统里,dev目录由udev或者systemd-udevd接管,插个U盘、加载个驱动,设备节点会自动出现。但嵌入式设备用BusyBox,图的就是体积小、依赖少,绝大多数场景不会把完整的udev机制搬进去。BusyBox只提供了一个轻量级的mdev,还经常有人不配置。这时候dev目录里的设备节点从哪来,就得你自己决定。
1.2 内核挂载根文件系统的先后顺序,决定了问题从哪来
这里要理解内核启动流程中的一个关键点。内核启动后期会向initrd/initramfs里的init进程交接,或者在直接挂载根文件系统后执行其中的/sbin/init。无论哪条路径,根文件系统上挂载时,VFS希望里面已经有足够的基础设备节点,尤其是/dev/console和/dev/null。因为init进程起来之前,内核要把控制台输出重定向到某个设备上,如果没有console节点,printk回显会出问题,甚至init直接崩溃。
所以你的根文件系统里dev目录从一开始就得存在,且至少要放console和null两个节点。这两个节点不是“建议有”,而是“没有大概率起不来”。我在课上带学生做实验时,第一个坑必踩在这。
1.3 没有守护进程,也没有设备管理器,谁来补位
发行版有udev在后台实时响应内核的uevent,创建设备节点。BusyBox环境通常没有这个后台机制。就算你用mdev,也得在rcS里显式启动mdev并设置热插拔处理函数。否则内核注册了一个新设备,sysfs里能看到,dev目录里却什么都没有。这就是很多人遇到的怪现象:/sys/class/xxx里有设备,/dev/xxx就是找不到。
所以,你要么在制作文件系统时就把常用的节点静态创建好,要么让内核帮忙把devtmpfs挂到/dev上,要么配置mdev让它扫描sysfs生成节点。三种思路各有利弊,下一节详细对比。
2. 三种主流方案对比与选型
2.1 方案一:静态mknod,最简单的板级方案
这是古老但确实可行的方式。你在构建根文件系统时,用mknod命令把目标板子上需要的所有设备节点预先创建出来,随文件系统一起烧录。
优点很直接:不需要任何运行时机制,节点一直都在,依赖SPI Flash、NAND这类介质上也不会因为用户态程序没起来而缺失。缺点也明显:不灵活。换内核、换驱动,设备主次设备号变了,你的静态节点就废了。而且嵌入式设备外设相对固定,所以这方案在小批量、硬件固定的产品里依然有生命力。
2.2 方案二:devtmpfs,内核帮你维护节点
devtmpfs从内核2.6.33开始引入,是一个由内核维护的虚拟文件系统。简单说,内核每注册一个驱动、发现一个设备,就自动在这个文件系统里创建设备节点;设备移除时自动删除。你不需要任何用户态守护进程。
使用上只需要内核开启CONFIG_DEVTMPFS和CONFIG_DEVTMPFS_MOUNT,然后在启动时把devtmpfs挂载到/dev。如果开启了自动挂载,内核会自己挂,连命令都省了。这是目前我见过最适合BusyBox轻量系统的方案,兼顾了动态性和零守护进程的开销。不过要注意:devtmpfs只会创建内核已知的设备节点,像某些纯粹的虚拟设备节点,可能还是需要手动补。
2.3 方案三:mdev,BusyBox自带的用户态热插拔
mdev是BusyBox里udev的简化替代品。它不像devtmpfs那样由内核直接生成节点,而是通过解析sysfs和uevent,由用户态程序负责创建。
配置相对繁琐:需要编写/etc/mdev.conf,需要在rcS里启动mdev,还需要把/sys挂载好,以及设置内核的uevent helper或者直接在rcS里执行mdev -s来扫描已有设备。它能实现比较细粒度的规则控制,比如给特定设备指定权限、属主,这是devtmpfs裸用做不到的。但因为依赖用户态程序,如果根文件系统阶段、rcS还没跑起来时设备节点就已经被需要了,那就晚了。因此通常把mdev和devtmpfs结合使用,这是比较专业的做法。
2.4 方案选型表
| 方案 | 动态性 | 实现复杂度 | 有无守护进程 | 适用场景 |
|---|---|---|---|---|
| 静态mknod | 无 | 低 | 无 | 硬件固定、批量产线 |
| devtmpfs | 高 | 低 | 无 | 大多数BusyBox系统,首选 |
| mdev | 高 | 中 | 有 | 需要自定义节点权限/规则 |
| devtmpfs + mdev | 高 | 中 | 有 | 兼顾动态与定制,专业推荐 |
3. 正确创建dev目录的详细实操
3.1 先搭好根文件系统的骨架
在用BusyBox制作根文件系统时,我习惯先用下面的命令把基础目录建好,避免后面缺目录临时补:
mkdir -p rootfs/{bin,sbin,etc/init.d,dev,proc,sys,tmp,usr/bin,usr/sbin,lib,root,mnt} chmod 1777 rootfs/tmp sudo chown -R root:root rootfs这个骨架里dev目录必须要提前建,不然后续根本没法往里面放节点。同时强调一下,整个根文件系统的属主和权限也有讲究,如果以非root用户创建了目录,打包进去后init启动时可能遇到权限问题。
3.2 把busybox编进去,并补齐动态链接库
BusyBox建议静态编译,这样省去拷贝库的麻烦。但如果你想用动态编译节省一点空间,就得用readelf -d busybox查它依赖的库,然后从工具链的sysroot里把对应的ld、libc拷贝到rootfs/lib下。这一步如果有遗漏,进入系统后执行任何命令都会报"No such file or directory",但不是文件不存在,而是动态链接器缺失。
3.3 静态节点方式的mknod完整操作
选择静态方案时,先把内核的文档或/proc/devices里列出的设备号确认一下,然后rootfs/dev目录下执行:
cd rootfs/dev mknod console c 5 1 mknod null c 1 3 mknod zero c 1 5 mknod random c 1 8 mknod urandom c 1 9 mknod ttyS0 c 4 64 mknod ttyS1 c 4 65 mknod mtd0 c 90 0 mknod mtd0ro c 90 1这些节点里面console和null是必须的,另外几个看你的板子。我拿一块全志的板子举例,串口对应ttyS0,Flash分区对应mtd0。如果你的平台串口名是ttyAMA0或者ttymxc0,需要同步调整。
设备节点的关键是类型和主次设备号,不能拍脑袋。比如串口是字符设备c,主设备号4,次设备号64对应ttyS0,65对应ttyS1。只用linux源码目录下Documentation/admin-guide/devices.txt,或者直接查内核源码的include/uapi/linux/major.h。
3.4 devtmpfs方式的内核配置和挂载
采用devtmpfs方案时,先确认内核配置:
CONFIG_DEVTMPFS=y CONFIG_DEVTMPFS_MOUNT=y这两个配置打开后,内核在初始化时会自动挂载devtmpfs到/dev,不需要根文件系统里做任何特殊操作。但为了安全,我习惯在rcS里再加一句挂载兜底:
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev mkdir -p /dev/pts mount -t devpts none /dev/pts这里有个细节:devpts和devtmpfs是两回事,devpts专门给pty伪终端用的。很多人在rcS里漏了devpts,结果ssh登录进去之后无法申请伪终端,shell直接报错。
3.5 mdev方案的配置细节
如果用mdev,建议在内核配置里确认CONFIG_DEVTMPFS=y,但不需要自动挂载,然后rcS里做下面的操作:
mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev mkdir -p /dev/pts mount -t devpts none /dev/pts echo /sbin/mdev > /proc/sys/kernel/hotplug mdev -s这里有一个很多人踩的坑:mdev -s必须在/sys挂载之后执行,因为它要扫描/sys下的设备信息。同时要注意,devtmpfs已经挂载的情况下,mdev -s的主要作用不是创建设备节点,而是根据/etc/mdev.conf修正节点权限、建立软链接。如果你只想靠mdev完全接管,不挂devtmpfs,比较复杂,我不推荐新手这么干。
3.6 /etc/mdev.conf规则示例
一份简化但实用的mdev.conf:
console 0:0 660 null 0:0 666 zero 0:0 666 urandom 0:0 444 ttyS0 0:0 600 ttyS1 0:0 600 mtd.* root:root 660格式基本是“设备名正则 属主 权限”。匹配规则支持正则,这就是mdev比devtmpfs灵活的点。比如监控一个U盘设备自动挂载,也是通过mdev.conf的规则触发脚本完成的,这个后面有机会单独讲。
4. init进程与dev目录的先后顺序问题
4.1 内核挂载devtmpfs的时机
如果你开启了CONFIG_DEVTMPFS_MOUNT,内核在初始化设备模型时会自动挂载devtmpfs,这个时间点远早于用户空间init进程启动。也就是说,不管你是用initramfs还是直接挂载根文件系统,/dev在init跑起来之前就已经是可用的了。这也是devtmpfs方案比静态方案更“正确”的核心原因之一:它卡在最合理的时机。
但注意,有一种情况会被坑:如果你没有开启自动挂载,也没有在initramfs阶段挂载devtmpfs,而你的init进程又依赖某个/dev节点,那就麻烦了。比如有些init脚本会在mount -a之前就要访问/dev/console,这时可能出问题。所以要么内核自动挂载,要么保证initramfs阶段正确挂载,两条路至少走一条。
4.2 rcS脚本里的挂载顺序,命门就在这里
rcS脚本是BusyBox系统里的第一个用户态初始化脚本。很多人习惯把一堆mount塞进去,但顺序错了也会出诡异问题。
我的经验是先挂proc和sysfs,再挂devtmpfs,再挂devpts,最后再去执行其他依赖设备节点的动作。原因很简单:mdev -s需要读取/sys里的设备信息,所以sysfs必须先就绪;而需要pty的应用程序必须等devpts就位。这套顺序经得起推敲,不是随便排的。
小型嵌入式系统常见的问题是,把普通根文件系统的数据分区(比如/etc或/var)也放进rcS里挂载。这时如果该设备节点还没出现,mount就会失败。如果用devtmpfs,通常节点已经由内核创建,问题不大;如果你用静态节点,而挂载的设备在静态列表里漏了,那就只能干瞪眼。所以这里也能看出devtmpfs的优势。
4.3 VFS层和sync,和dev目录有什么关系
先解释一下VFS和sync这两个热词和dev目录的关联。Linux一切皆文件靠的是VFS,设备节点在VFS里是一种特殊inode。mknod、挂载devtmpfs、open("/dev/xxx"),最终都会经VFS层转化成对具体文件系统或驱动框架的操作。
而sync是把内存中变更的dirty数据刷回持久存储。对dev目录来说,生产环境有一个非常实际的场景:你在开发机上用mknod在rootfs目录里创建了一堆静态设备节点,然后打包烧录。如果你在创建节点之后没有sync,开发机断电或者直接拔掉SD卡,节点可能因为page cache还没落盘而丢失。这个坑我朋友遇到过,现场演示时设备节点时有时无,查了半天,最后发现就是没sync导致数据没真正写进镜像。所以我现在每次构建完根文件系统,打包前都习惯性跑一下sync,形成肌肉记忆了。
5. 常见问题与排查技巧实录
5.1 启动时报错“Creating 2 entries in /dev”
看到这个错误,说明内核的devtmpfs已经在工作了,但根文件系统里的/dev是空的,或者初始化脚本尝试往里面创建节点却缺少必要工具。这个报错实际上来自内核的devtmpfs初始化逻辑。我遇到时一般先检查内核配置,确认CONFIG_DEVTMPFS_MOUNT是否开启,以及启动参数里有没有“rootfstype=xxx”把文件系统类型指定错。
5.2 设备节点存在,但open失败
节点在/dev下能看到,open却报Permission denied或者No such device。Permission denied一般是权限问题,ls -l看一下节点的权限位和属主。No such device相对复杂,表示VFS能找到这个inode,但驱动层不认这个设备,通常是主设备号对应的驱动没注册,或次设备号超出驱动管理范围。先用ls -l /dev/xxx确认主次设备号,然后到/proc/devices核对系统实际分配的设备号,不一致就用mknod重新创建。
5.3 内核配置了devtmpfs,但/dev下什么都没有
这种情况一般是没开启CONFIG_DEVTMPFS_MOUNT,内核只提供了devtmpfs的代码,但没人挂载它。此时需要你在initramfs阶段或者rcS脚本里手动执行mount -t devtmpfs none /dev。还有一种可能是文件系统里缺乏挂载点目录,如果/dev都不存在,mount当然失败。所以我在前面强调:根文件系统骨架里的dev目录绝对不能省。
5.4 使用mdev,模块加载后节点不出现
如果确认devtmpfs已经挂载,模块加载后节点还是看不到。先确认sysfs有没有挂载,再确认内核的hotplug机制里是否设置了uevent helper。手动执行一下mdev -s,如果能扫出节点,说明自动触发链路有问题。这种问题大概率是echo /sbin/mdev > /proc/sys/kernel/hotplug没执行,或者mdev.conf配置里把设备规则写成了不匹配的模式。
5.5 设备号速查表(常用)
| 设备节点 | 类型 | 主设备号 | 次设备号 |
|---|---|---|---|
| /dev/console | 字符 | 5 | 1 |
| /dev/null | 字符 | 1 | 3 |
| /dev/zero | 字符 | 1 | 5 |
| /dev/random | 字符 | 1 | 8 |
| /dev/urandom | 字符 | 1 | 9 |
| /dev/ttyS0 | 字符 | 4 | 64 |
| /dev/ttyS1 | 字符 | 4 | 65 |
| /dev/mtd0 | 字符 | 90 | 0 |
| /dev/fb0 | 字符 | 29 | 0 |
这张表最好存一份,开发时经常要翻。不同平台、不同内核版本可能会有差异,最终以内核源码里的设备号分配表为准。
5.6 常见排错速查表
| 现象 | 可能原因 | 排查/解决 |
|---|---|---|
| 启动后无法打开console | /dev/console不存在或类型错误 | 确认c 5 1节点存在 |
| 设备节点有,读写报错 | 主次设备号不对或驱动未加载 | cat /proc/devices核对 |
| mdev后节点还是少 | /sys未挂载或uevent helper未设置 | mount -t sysfs none /sys;设置hotplug |
| /dev目录不存在导致挂载失败 | 文件系统骨架漏建/dev | mkdir -p /dev,并打包前sync |
| init无法执行 | 动态链接库缺失或bin目录权限不对 | 静态编译busybox,或补齐运行时 |
最后再分享两个实际操作中的经验
每次构建根文件系统时,无论用哪种设备节点方案,我都坚持在打包前用find rootfs/dev -ls检查一遍关键节点是否在位,然后执行一次sync。这个习惯帮我避免过好几次线下调试时的突发意外。另外提一点,如果你打算长期维护一个嵌入式Linux产品,建议优先走上devtmpfs的路线,而不是在静态节点上堆mknod。开发初期看似省事,但后期每升级一次内核,设备号或多或少的变动都会让你头大。用内核的机制去管理内核维护的设备,总比自己手写列表更靠谱。希望这篇东西能帮你少走我当年走过的弯路。