说实话,我第一次认真研究 mknod 这个命令,是因为在某个精简容器镜像里调程序,程序一启动就要打开 /dev/null,结果容器里根本没有这个设备节点,连重定向都时报错。当时主机上明明一切正常,容器里却像被切掉了半个系统,那一刻我才意识到:/dev 目录不是天生就有的,它背后有一套独立的设备节点创建机制,而 mknod 就是这套机制里最底层的那个工具。
这篇文章适合所有需要跟 Linux 打交道的人,尤其是做容器封装、嵌入式系统、内核驱动调试,或者维护过老系统的朋友。很多情况下你并不需要动用 mknod,因为 udev、devtmpfs 会帮你自动搞定;但当你遇到设备节点缺失、被误删,或者需要在没有 udev 的启动早期阶段手动铺设备文件时,mknod 就是那个能让你免于重启机器、直接解决问题的关键命令。下面我把设备文件的原理、命令参数、真实使用场景和常见的坑一次讲透。
1. 设备文件为什么特殊:先搞懂“字符”和“块”
1.1 设备文件并不“存”数据
用 ls -l 看 /dev/null,你会看到一行非常奇怪的信息:
bash复制ls -l /dev/null
crw-rw-rw- 1 root root 1, 3 1月 1 00:00 /dev/null
注意中间那一列,不是文件大小,而是“1, 3”。这就是设备文件和其他文件最本质的区别:设备文件在磁盘上不占数据块,它只是文件系统里的一个特殊 inode,用来记录“类型”和“设备号”。
你可以把它想象成酒店房间门口挂着的门牌,门牌本身不值钱,也不住人,但它告诉你这扇门后面对应哪条走廊、哪个房间。设备文件就是那个门牌,真正的数据读写发生在内核驱动和硬件之间,文件系统层面只负责把“打开 /dev/null”这个操作翻译成“去找主设备号 1、次设备号 3 对应的那个驱动”。
所以在排查问题时,你首先要建立这个观念:设备文件不存在了,不等于设备没了;设备还在,也不代表你随便创建一个节点就能用。文件系统里的节点只是入口,入口背后还得有驱动和硬件支撑。
1.2 字符设备与块设备到底差在哪
mknod 支持三种类型:b、c、p。其中 b 和 c 是最核心的,p 是 FIFO 命名管道。很多人对 b 和 c 的区别只有一个模糊印象:b 是块设备,c 是字符设备。但实际操作中,这个区别直接决定了你该用哪个类型去创建节点。
块设备(b)按固定大小的块进行读写,数据会经过内核的页缓存和块设备调度层。硬盘、SSD、U 盘、loop 设备都属于块设备。块设备的特点是有缓冲区、有请求队列,读写可以合并、排序,所以性能优化空间更大。
字符设备(c)则按字节流处理,每次读写直接对应驱动里的一次操作,没有块设备那种通用缓冲层。终端、串口、鼠标、声卡、随机数生成器都属于字符设备。
判断一个设备该用 b 还是 c,最可靠的方法是看主机上现成的节点,或者查内核文档。我见过有人把 /dev/null 按块设备创建,结果程序打开是能打开,但行为完全不对,因为 null 驱动只实现了字符设备的接口。类型错了,后面全是坑。
1.3 主设备号、次设备号:哪个驱动和哪一份实例
设备文件的核心信息是“主设备号”和“次设备号”。主设备号决定内核去找哪个驱动程序,次设备号决定同一驱动下的哪一个具体设备实例。
举几个例子:
- /dev/null 是 c 1 3,主设备号 1 属于内存设备类驱动,次设备号 3 代表 null 设备。
- /dev/zero 是 c 1 5,同一个驱动,次设备号不同。
- /dev/sda 是 b 8 0,/dev/sda1 是 b 8 1,主设备号 8 归 SCSI/SATA 磁盘驱动,次设备号区分整个磁盘和第一个分区。
你可以把主设备号理解为快递公司的客服总机,次设备号就是分机号。打总机永远是同一家公司接,但分机不同,处理的具体事务就不同。创建节点时,这两个数字必须和内核驱动注册的值完全一致,写错任何一个,即使节点创建成功,打开时也会报错。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. mknod 的完整用法与容易被忽略的细节
2.1 语法与最基础的示例
mknod 的基本语法是:
bash复制mknod [选项]... 名称 类型 [主设备号 次设备号]
创建字符设备节点、块设备节点和 FIFO 的写法分别是:
bash复制# 字符设备,例如自定义位置创建 null 设备
mknod /tmp/null c 1 3
# 块设备,例如创建 sda1 的节点
mknod /tmp/sda1 b 8 1
# FIFO 命名管道,不需要主次设备号
mknod /tmp/mypipe p
创建完之后,用 ls -l 验证,可以看到文件类型那一列分别是 c、b、p:
bash复制ls -l /tmp/null /tmp/sda1 /tmp/mypipe
crw-r--r-- 1 root root 1, 3 ... /tmp/null
brw-r--r-- 1 root root 8, 1 ... /tmp/sda1
prw-r--r-- 1 root root 0 ... /tmp/mypipe
注意 FIFO 的设备号列显示为 0,因为管道不依赖任何驱动,它只是内核里的一种同步机制。
2.2 权限位与 umask:为什么每次都建议加 --mode
mknod 创建出来的节点权限默认受进程 umask 影响。很多人在脚本里写 mknod 时不带权限参数,结果创建出来的节点是 644,导致普通用户无法写入,尤其是 /dev/null 这种需要所有人都能读写的设备。
我在实际项目里吃过这个亏。当时为了给一个隔离环境补设备节点,只执行了 mknod 没加权限,后来所有非 root 进程都报 Permission denied,排查了半天才想到 umask 的锅。
推荐做法是创建时直接指定权限:
bash复制mknod -m 666 /dev/null c 1 3
mknod -m 600 /dev/console c 5 1
-m 参数接受八进制权限位,跟 chmod 的写法一致。另外,节点创建出来之后,属主是当前有效的 uid,通常你需要再配合 chown 把属主改成 root 或者特定组,否则某些服务会因为属主不对而拒绝使用。
2.3 用 mkfifo 与 mknod 创建 FIFO 的边角区别
mkfifo 命令看起来和 mknod p 很像,很多人疑惑到底有什么区别。从系统调用层面看,mkfifo 底层调用的就是 mknod 或者 mknodat 系统调用,只不过参数固定为 FIFO 类型。也就是说,mkfifo name 和 mknod name p 基本等价。
但在命令行习惯上,我建议分清楚场合:创建管道用 mkfifo,因为它语义清晰,选项也更友好;创建字符设备或块设备节点才用 mknod,因为 mkfifo 根本不支持 b/c 类型。混着用不会出错,但会让读脚本的人觉得你意图不明。
3. 真正需要手动 mknod 的几个场景
3.1 容器里缺设备节点:比你想的更常见
容器运行时做得再好,也没法保证镜像里内置完整的 /dev。很多精简镜像只带了最基本的几个节点,甚至一个都没有。你拉下来一个 scratch 镜像,想跑个需要打开 /dev/null 或者 /dev/urandom 的静态编译程序,大概率会遇到 File not found。
这种场景下的标准操作是:
bash复制# 先看看容器里现有节点情况
ls -l /dev
# 补齐基础节点
mknod -m 666 /dev/null c 1 3
mknod -m 666 /dev/zero c 1 5
mknod -m 666 /dev/random c 1 8
mknod -m 666 /dev/urandom c 1 9
如果你在容器里需要访问宿主机磁盘,但没有显式挂载设备,那还要自己创建对应节点。比如要用 loop 设备挂载镜像:
bash复制mknod /dev/loop0 b 7 0
losetup /dev/loop0 ./guest.img
mount /dev/loop0 /mnt
不过这里有个前提:容器得有权限执行 mknod,而且设备的 cgroup 允许访问。这个问题我在第 5 部分展开讲。
3.2 initramfs 与最小启动环境:udev 还没起来的那几分钟
系统启动早期,内核挂载 initramfs 的时候,/dev 目录往往还是空的,因为 udev 尚未启动,devtmpfs 也没完成自动填充。但内核和 init 脚本又需要访问 /dev/console 来输出日志,需要 /dev/null 来丢弃不需要的输出。
所以你会看到很多 initramfs 构建脚本里有类似这样的片段:
bash复制mknod /dev/console c 5 1
mknod /dev/null c 1 3
mknod /dev/ram0 b 1 0
嵌入式开发更是如此。如果内核没开 CONFIG_DEVTMPFS,或者你正在做一个非常精简的根文件系统,那启动脚本里手动创建设备节点就是唯一办法。别看这命令老旧,关键时刻比什么自动化工具都可靠。
3.3 设备文件损坏或者被误删:免重启重建
我遇到过一次很鬼畜的问题:某台机器上的 /dev/null 被某个启动脚本用普通文件覆盖了,结果所有依赖 /dev/null 的程序全部异常。正常思路是重启恢复,但那是台不能随便重启的机器。
解决办法很简单,把错误的文件删掉,重建节点即可:
bash复制rm -f /dev/null
mknod -m 666 /dev/null c 1 3
chown root:root /dev/null
类似的情况还有 /dev/random 节点被错误 bind mount,或者 /dev/console 节点权限不对导致 sshd 起不来。只要你知道正确的设备号,重建节点基本是秒级修复,比重启快得多。
4. 从 /proc/devices 到 mknod:设备号背后的映射逻辑
4.1 内核到底怎么知道设备号对应哪个驱动
当你打开一个设备节点时,内核根据 inode 里记录的主设备号去查找已注册的设备驱动。这个注册表不是写死在某个文件里的,而是驱动在初始化时通过 register_chrdev、register_blkdev 等接口动态注册的。
你可以通过 /proc/devices 查看当前内核里已经注册了哪些主设备号:
bash复制cat /proc/devices
Character devices:
1 mem
4 tty
5 console
10 misc
...
Block devices:
7 loop
8 sd
9 md
...
这里的信息非常关键。如果你要创建 /dev/loop0 节点,先看 /proc/devices 里有没有 7 loop 这一行。如果连这一行都没有,说明 loop 驱动根本没加载,就算创建了节点,打开时也会报 No such device or address。
4.2 常用主设备号速查
以下是我平时用得比较多的对应关系,记下来能省不少查文档的时间:
| 设备节点 | 类型 | 主设备号 | 次设备号 |
|---|---|---|---|
| /dev/null | c | 1 | 3 |
| /dev/zero | c | 1 | 5 |
| /dev/full | c | 1 | 7 |
| /dev/random | c | 1 | 8 |
| /dev/urandom | c | 1 | 9 |
| /dev/console | c | 5 | 1 |
| /dev/ptmx | c | 5 | 2 |
| /dev/tty | c | 5 | 0 |
| /dev/loop0 | b | 7 | 0 |
| /dev/sda1 | b | 8 | 1 |
| /dev/tun | c | 10 | 200 |
需要说明的是,这个表不是绝对真理。比如 NVMe 设备的主设备号在不同内核版本里有变动,还有 SCSI 磁盘在某个内核大版本里也调整过一次。严格来说,以当前系统的 /proc/devices 和 /sys 信息为准,这个表只适合应付日常判断。
4.3 如何正确取得设备号:三个可靠来源
第一,看主机上现成的节点。这是最简单也最不容易出错的方式:
bash复制ls -l /dev/sda1
brw-rw---- 1 root disk 8, 1 ... /dev/sda1
第二,查 /proc/devices,确认驱动是否已经注册。第三,用 udevadmin 查询设备属性:
bash复制udevadm info -q property -n /dev/sda1 | grep -E 'MAJOR|MINOR'
学到这个习惯之后,我再也没有凭记忆力去猜设备号。设备号写错的隐蔽性很强,节点创建成功,驱动也能加载,但因为数字不对,打开就是报 ENXIO,这种问题排查起来非常费时间。
5. 权限、安全与特权边界:这些坑我替你踩过了
5.1 为什么明明是 root,却提示 Operation not permitted
mknod 能不能执行,取决于进程是否拥有 CAP_MKNOD 能力。在一些容器运行时、或者被 seccomp 策略限制的环境里,即使你是 root,也可能没有这个能力,mknod 直接返回 Operation not permitted。
排查方法:
bash复制grep CapEff /proc/self/status
然后用 capsh 解析 CapEff 里是否包含 mknod:
bash复制capsh --decode=$(grep CapEff /proc/self/status | awk '{print $2}')
如果确实缺少 mknod 能力,就需要在容器启动参数里加回对应能力,或者改用其他方案。在主机上遇到这种情况,还要检查 SELinux 上下文和挂载选项,有些安全加固策略会明确禁止创建设备节点。
5.2 容器与设备 cgroup:创建了节点也未必能访问
即使你成功执行了 mknod,创建了 /dev/sda1 节点,容器里读取这个设备的操作还会受到设备 cgroup 的限制。设备 cgroup 是内核里独立的访问控制层,它规定了当前进程可以访问哪些设备号。
在大多数容器运行时里,默认只允许少数几个基本设备,比如 /dev/null、/dev/zero、/dev/random、/dev/urandom。你手动创建 /dev/sda1 节点,打开时大概率还是报 Operation not permitted,原因不在节点创建,而在设备 cgroup。
这就解释了为什么很多人在容器里手动 mknod 不生效:节点是有了,但内核在 open 阶段直接把你挡住了。正确的做法是通过容器的设备挂载参数把宿主机设备带进容器,让运行时自动创建节点并更新设备 cgroup。
5.3 安全的 mknod 使用习惯
手动创建设备节点的风险在于,一个错误的设备号可能让你意外获得访问敏感设备的权限。比如创建了 /dev/mem 节点,配错权限,普通用户就能读写物理内存,那种风险是非常直接的。
我的建议很简单:
- 创建后立刻用 chown、chmod 收紧权限,不要依赖默认值。
- 优先使用 -m 参数显式指定权限位,避免 umask 干扰。
- 能用 devtmpfs、udev 自动管理的场景,不要手动 mknod,尤其不要在长期运行的系统上留下一堆手写节点。
- 在容器环境里,先确认能力和设备 cgroup 的限制,再决定要不要手动建节点。
6. 常见错误信息与排查思路
6.1 逐个拆解:EPERM、EEXIST、EINVAL、ENXIO
我在实际调试中经常遇到的报错,基本可以归成下面几类:
| 报错或现象 | 根本原因 | 解决方向 |
|---|---|---|
| Operation not permitted | 缺少 CAP_MKNOD,或被 seccomp、SELinux 拦截 | 检查 CapEff,调整容器能力,检查安全策略 |
| File exists | 同名文件或已被设备节点占用 | 先确认不是重要文件,rm 后重建 |
| Invalid argument | 参数类型写错、主次设备号越界、文件系统不支持设备节点 | 重新核对语法,参考 /proc/devices 确认设备号 |
| No such device or address | 节点存在但对应驱动未注册 | 加载模块,或者检查设备是否真的存在 |
| Permission denied | 节点权限不足,或设备 cgroup 未放行 | chmod、chown,或者调整容器设备限制 |
其中 EINVAL 值得多说一句。NFS、FAT、某些 FUSE 文件系统不支持创建设备节点,你在这些文件系统上执行 mknod 会直接失败。判断的方法很简单:看这个路径落在哪个文件系统,df -T 或者 mount 看一眼就明白了。
6.2 排查一个“节点打不开”的完整链路
假设你现在遇到了节点打不开的问题,我建议按这个顺序查:
- 确认文件类型和设备号:ls -l 看首字母和主次设备号。
- 确认驱动是否注册:cat /proc/devices 看对应的主设备号有没有出现。
- 确认设备本身是否存在:ls /sys/class/ 看有没有相关设备目录。
- 尝试用 dd 读取,观察 dmesg:
bash复制dd if=/dev/mytestnode of=/dev/null bs=1 count=1
dmesg | tail
dd 的报错信息和内核日志往往能直接指出是驱动问题、权限问题还是设备不存在。如果你在容器环境里,还要考虑设备 cgroup 和能力问题,这两个层面的拦截不会在 dmesg 里留什么痕迹。
6.3 用 stat 快速确认设备类型和号
与其靠 ls -l 去猜,不如直接让 stat 给你打印结构化信息:
bash复制stat -c '%F 主设备号:%Hd 次设备号:%Ld' /dev/null
字符特殊文件 主设备号:1 次设备号:3
这个命令在脚本里也很好用,可以用来批量校验一批设备节点的类型和设备号是否和预期一致。
最后分享一个小习惯:建完节点之后,我总会随手拿 stat 和主机上对应节点对比一次,防止手滑把主次设备号写反。毕竟 mknod 本身不校验设备号是否真实,它只管记账,账记错了,后面所有 open 操作都会跟着错。养成这个确认动作之后,我在设备节点这个问题上少踩了至少一半的坑。
