你可能永远用不到mknod,但真正需要它的那天,你会发现自己根本绕不开它。在Linux系统里,设备节点正常情况下都由系统自动管理,插上U盘、接入硬盘,对应的设备文件就会自己出现在/dev目录下。但一旦你进入容器环境、系统救援模式,或者遇到驱动静态注册设备号的情况,会发现/dev目录里连个影子都没有,这时候mknod就是你唯一能依赖的工具。它专门用来创建字符设备文件、块设备文件,以及FIFO命名管道,简单来说就是给一个真实的硬件设备在系统里“建档案”,让内核知道这个设备节点对应哪个驱动、由谁处理。这篇文章会把mknod讲透,从设备文件原理到主次设备号规则,再到四个实战场景和常见坑的排查方法,看完你就能在实际工作中稳稳上手。适合正在搞嵌入式开发、容器运维、设备驱动调试的朋友,也适合所有想真正理解Linux设备管理机制的人。
1. 先搞懂mknod背后的设备文件设计逻辑
1.1 设备文件到底是什么,为什么它不占磁盘空间
Linux有一个经典的设计哲学:一切皆文件。设备也不例外,但这里的“文件”和你在/home目录下看到的普通文件完全不是一回事。设备文件实际上是一个访问入口,存放在/dev目录下,它不占用任何磁盘空间,而是由内核在内存中维护的一种虚拟文件对象,内部记录着这个设备节点对应的主设备号、次设备号以及设备类型。
当你对设备文件执行open、read、write、close这些操作时,内核并不会像处理普通文件那样去磁盘上找数据,而是根据设备文件里记录的主次设备号,去查找对应的设备驱动程序,然后把你的读写请求转交给驱动,由驱动和真实硬件交互。整个链路看起来就是在操作一个文件,但底层的机制完全不同。
初学Linux的时候,我有一阵子没搞明白为什么ls -l /dev/sda显示出来的大小不是容量大小,而是两个逗号隔开的数字。后来才知道,那两个数就是主设备号和次设备号。理解了这一点,后面再学mknod就会非常顺——它做的事情,本质上就是替你创建这样一个“设备档案”。
1.2 字符设备与块设备的本质区别
mknod可以创建两种硬件设备文件:字符设备文件(c或u)和块设备文件(b)。两者最核心的区别在于数据传输方式。
字符设备按字节流传输数据,一次一个字节按顺序读写,没有缓冲机制,数据量小、但时序敏感。典型的字符设备有键盘、鼠标、串口、终端、声卡等。你在终端上敲下的按键,就是一个字符一个字符送到系统里的。字符设备的优点是实现简单、响应直接,但吞吐能力有限,不适合大规模数据传输。
块设备则不同,它以固定大小的数据块(block)为单位进行传输,每次读写都涉及一批字节,而且内核为块设备提供了页缓存机制,读写数据会先在内存里做缓冲和调度,再批量写入磁盘。典型的块设备包括硬盘、U盘、SSD、光盘等。块设备追求的是高吞吐量,适合存储类场景。
打个比方:字符设备就像便利店,你来买一瓶水,我直接递给你一瓶,交易简单但每次的量就那么大;块设备就像大型仓储超市,进货出货都是整托盘、整集装箱来,追求的是批量效率。
这个区别直接决定了你用mknod时必须给对类型。如果你把一个字符设备错误地创建成块设备文件,那么这个节点即使创建成功,打开它访问数据时驱动根本没法处理你的请求,报错几乎是必然的。所以动手之前,先确认你要操作的那类设备到底属于什么类型。
1.3 主设备号与次设备号的作用
创建设备文件时必须指定主设备号和次设备号,而这两个数字就是设备文件最核心的属性。
主设备号用来标识设备对应的驱动程序。内核中每个已注册的驱动都会被分配一个主设备号,当系统访问设备文件时,内核凭借主设备号找到对应的驱动。次设备号则用来区分同一个驱动管理下的不同设备实例。举例来说,一块SCSI磁盘被分成多个分区,它们的驱动都是同一套SCSI驱动,主设备号都是8,但每个分区的次设备号不同,内核才能区分你操作的是哪块磁盘的哪个分区。
用生活里的例子类比:主设备号相当于公司的总机电话号码,次设备号是总机转接各分机的分机号。你拨通总机(找到驱动),再拨分机号(找到具体设备),才能建立联系。设备文件创建过程就是在告诉内核:“这个设备由你管,具体管哪一个实例,看次设备号。”
在/dev目录里执行ls -l,你会看到类似这样的输出:
brw-rw---- 1 root disk 8, 1 Jun 22 10:11 sda1 crw-rw-rw- 1 root root 1, 3 Jun 22 10:11 null第一列开头是b,说明sda1是块设备;是c,则代表null是字符设备。在后面用逗号隔开的8, 1和1, 3,就是这对主次设备号。
1.4 何时需要手动创建:几个真实应用场景
有人会问:现代Linux都有udev,它会自动管理/dev目录,还有必要手动执行mknod吗?答案是有,而且一些场景只有mknod能救场。
第一,系统进入救援模式或单用户模式时,/dev目录可能不完整,关键节点缺失。如果连/dev/null、/dev/console都没有,很多系统工具直接罢工,你需要手动创建基础节点才能继续排查。
第二,容器和chroot环境。大多数容器不会运行udev,/dev目录的内容是创建容器时预先放进去的。一旦某个镜像精简过头,缺少程序运行所需的设备节点,你就得自己mknod补上。
第三,嵌入式开发和驱动调试。设备驱动如果采用静态设备号分配,内核加载时不会自动生成节点。驱动加载成功了,但/dev下没有对应文件,应用层完全无法访问,这时候mknod就是连接驱动和应用的桥梁。
第四,批量导出同一驱动的多个实例节点。比如一个驱动管理了4路ADC或8个LED,你以为系统会帮你生成/dev/myled0到/dev/myled7?实际上如果不配置udev规则,它们根本不会出现。写一个for循环配合mknod,一秒就把所有节点建好了。
第五,自定义设备节点路径。你想在某个特定的目录下建一个指向现有设备的节点,或者临时做一个和设备模型无关的节点,用mknod是最直接的方式。
2. 掌握设备号规则:mknod语法与分配方式
2.1 命令语法与参数详解
mknod的语法非常简洁,整个命令的所有参数都是一次性给的:
mknod [选项] 设备名 类型 [主设备号 次设备号]设备名指的是要创建的节点路径,比如/dev/mychar。类型有三种:
- b:块设备文件
- c或u:字符设备文件(u是历史遗留写法,兼容旧系统)
- p:FIFO命名管道,创建时不需要指定主次设备号
常用选项主要有两个:
- -m 权限:创建设备文件的同时指定权限,例如-m 666会直接生成rw-rw-rw-的权限,省去后续chmod的步骤。
- -v:显示执行过程的详细信息,确认命令执行结果。
举个例子来说明。创建字符设备文件:
sudo mknod /dev/mychar c 240 0意思是创建一个名叫/dev/mychar的字符设备节点,主设备号240,次设备号0。创建块设备文件:
sudo mknod /dev/myblock b 7 200意思是创建块设备节点/dev/myblock,主设备号7,次设备号200。7号主设备对应的是loop设备驱动,所以这个节点可以当成一个loop设备来使用。
创建FIFO更简单:
mknod /tmp/myfifo pFIFO不关联真实硬件,只做进程间通信,用普通用户权限就能创建(前提是对目标目录有写权限)。它在语义上比匿名管道更方便,因为所有进程都能通过路径找到它。
2.2 三种方法精确确定主次设备号
手动创建设备节点最让人头疼的问题就是:主设备号和次设备号填什么?填错了就算节点建出来也是废的。我整理了三种可靠的方法。
方法一:查看/proc/devices。这个文件记录了当前内核里已经注册的所有字符设备和块设备的主设备号,输出内容大致如下:
Character devices: 1 mem 4 /dev/vc/0 5 /dev/tty 7 vcs 10 misc 13 input 21 sg ... Block devices: 7 loop 8 sd 9 md 11 sr 254 device-mapper左边是主设备号,右边是驱动名称。看到某个设备的驱动在这个列表里,你就知道它的主设备号是多少了。次设备号则需要根据你要访问的具体实例来确定,一般从0开始递增。
方法二:参考现有的同类设备节点。比如你想在另一个目录创建一个和/dev/sda1指向相同设备的节点,直接执行ls -l /dev/sda1,从输出里抄下主设备号8和次设备号1就行。这个方法最稳妥,因为照着已有节点抄,设备号绝对不会错。要注意的是,设备名和类型也要和实际一致,别抄着抄着把sda写成了sdb。
方法三:查内核源码或驱动文档。如果你在编写或调试自己的驱动,驱动代码里会明确指定静态分配的主设备号;如果是动态分配,驱动加载后通过/proc/devices就能查到分配结果,然后按需创建节点。嵌入式开发里这个场景非常常见,驱动作者通常会把建议的创建命令写在README里。
2.3 常用设备号速查表
为了在实操中节省时间,我把Linux中常见的设备主设备号整理成了速查表。注意,不同内核版本和架构下数值可能有差异,最终以/proc/devices为准,这张表作为参考足够了。
| 主设备号 | 对应驱动类型 | 典型节点示例 | 节点类型 |
|---|---|---|---|
| 1 | 内存设备 | /dev/null、/dev/zero、/dev/mem | 字符设备 |
| 3 | 传统IDE硬盘 | /dev/hda(新系统较少见) | 块设备 |
| 4 | 终端设备 | /dev/tty0、/dev/tty1 | 字符设备 |
| 5 | 替代设备 | /dev/tty、/dev/console、/dev/ptmx | 字符设备 |
| 7 | loop设备 | /dev/loop0、/dev/loop1 | 块设备 |
| 8 | SCSI/SAS/SATA/USB存储 | /dev/sda、/dev/sdb | 块设备 |
| 9 | 软件RAID设备 | /dev/md0 | 块设备 |
| 10 | 杂项设备 | /dev/misc | 字符设备 |
| 13 | 输入设备 | /dev/input/mice、/dev/input/event0 | 字符设备 |
| 21 | SCSI磁带机 | /dev/st0 | 字符设备 |
| 29 | 帧缓冲设备 | /dev/fb0 | 字符设备 |
| 90 | 音频设备 | /dev/dsp(老旧系统) | 字符设备 |
| 179 | SD/MMC设备 | /dev/mmcblk0 | 块设备 |
| 180 | USB字符设备 | /dev/ttyUSB0、/dev/hidraw0 | 字符设备 |
| 188 | USB串口设备 | /dev/ttyUSB0 | 字符设备 |
| 254 | Device Mapper设备 | /dev/dm-0(LVM逻辑卷) | 块设备 |
这张表里有几行容易混淆。首先是USB串口设备的180和188,我在某些系统上看到过不同的分配方式,所以最靠谱的方法是查/proc/devices确认,不要硬记。其次是ttyUSB0,有的发行版主设备号用的188,有的用180,建错直接用不了。
3. 四个实战场景带你上手mknod
3.1 场景一:在容器环境中创建基础设备节点
先讲一个最常见的实战需求:在容器或chroot环境中补全设备节点。很多精简镜像里/dev目录几乎是空的,程序跑起来缺设备节点,报出一堆看不懂的错误。其实mknod几分钟就能搞定。
假设我在宿主机上模拟一个chroot环境,目录是/tmp/chroot-test,在/dev为空的情况下,先创建最基础的那几个节点:
mkdir -p /tmp/chroot-test/dev sudo mknod /tmp/chroot-test/dev/null c 1 3 sudo mknod /tmp/chroot-test/dev/zero c 1 5 sudo mknod /tmp/chroot-test/dev/random c 1 8 sudo mknod /tmp/chroot-test/dev/urandom c 1 9 sudo mknod /tmp/chroot-test/dev/tty c 5 0这些主次设备号是从哪来的?我并没有凭记忆硬填,而是先在自己的系统上执行ls -l /dev/null确认了null的主设备号是1、次设备号是3,然后照搬到目标目录。不同系统的设备号可能有细微差异,这是一个非常稳妥的习惯:先参考现有节点,再创建新节点。
节点创建出来后还要设置权限。因为mknod默认创建出来的节点,权限受umask影响,不一定是程序需要的权限,所以应该主动调整:
sudo chmod 666 /tmp/chroot-test/dev/null sudo chmod 666 /tmp/chroot-test/dev/zero sudo chmod 666 /tmp/chroot-test/dev/random sudo chmod 666 /tmp/chroot-test/dev/urandom sudo chmod 666 /tmp/chroot-test/dev/tty也可以直接在mknod时用-m参数一步到位,比如sudo mknod -m 666 /tmp/chroot-test/dev/null c 1 3,效果一样。
验证环节很简单。写入null设备测试:
echo "hello" > /tmp/chroot-test/dev/null没有报错,说明null设备正常。再用dd从zero设备读4个字节,用xxd查看内容全是00,说明zero设备也正常:
dd if=/tmp/chroot-test/dev/zero of=/tmp/test_zero.bin bs=1 count=4 xxd /tmp/test_zero.bin注意: 在容器环境里,即使你手动创建了设备节点,容器本身还必须具备访问对应设备的权限。如果容器不是运行在privileged模式下,也没有通过cgroup规则放宽设备访问限制,那么操作设备时依然会被内核拒绝。mknod只是创建了节点,不代表权限也同时打通了。这个坑很多人踩过,排查的时候先看容器启动参数和cgroup规则。
3.2 场景二:创建一个可用的loop设备
loop设备在Linux里用途广泛,挂载镜像文件全靠它。正常情况下udev会自动生成/dev/loop0、/dev/loop1等节点,但如果你处在一个精简的容器环境里,或者系统启动时某些节点没被创建,就需要手动补齐。
先检查当前系统里loop设备的情况:
ls /sys/block/ | grep loop如果没有任何输出,说明loop节点完全缺失。创建方法:
sudo mknod /dev/loop0 b 7 0 sudo mknod /dev/loop1 b 7 1 sudo mknod /dev/loop2 b 7 2主设备号7是所有loop设备驱动的统一编号,次设备号从0开始递增,对应loop0、loop1、loop2。权限建议设置为660:
sudo chmod 660 /dev/loop0 /dev/loop1 /dev/loop2创建完别急着收工,验证一下这个节点能不能真正工作。先制作一个最小镜像:
dd if=/dev/zero of=/tmp/test.img bs=1M count=1 mkfs.ext4 /tmp/test.img然后关联到loop设备:
sudo losetup /dev/loop0 /tmp/test.img sudo fdisk -l /dev/loop0如果fdisk能识别出这个设备,哪怕提示没有分区表,也说明loop设备节点工作正常。这个验证流程能帮你确认自己使用mknod创建出来的节点真的有在跟内核驱动通信,而不只是存在一个空文件。
3.3 场景三:创建节点后正确设置权限并验证有效性
设备文件创建出来之后,权限设置是很关键的一步。因为默认创建的节点,根据你当时的umask,权限可能很保守。如果你的应用需要任意用户都能访问,就必须主动设置权限。
我创建一个测试节点然后查看它的初始状态:
sudo mknod /tmp/testdev c 240 0 ls -l /tmp/testdev像这样的输出很常见:
crw-r--r-- 1 root root 240, 0 Apr 8 20:10 /tmp/testdev第一列的c代表字符设备,后面的240, 0就是设备号。权限是crw-r--r--,意思是root用户可读写,其他用户只能读,而很多设备需要读写双向操作,因此要执行:
sudo chmod 666 /tmp/testdev ls -l /tmp/testdev此时权限就变成crw-rw-rw-了。为了减少步骤,也可以创建时直接指定:
sudo mknod -m 666 /tmp/testdev c 240 0验证节点有没有真正连上驱动,可以执行:
stat /tmp/testdev输出里会明确显示设备类型是字符设备、主设备号240、次设备号0。然后配合cat /proc/devices,确认240对应的驱动确实存在:
grep 240 /proc/devices有输出说明驱动在线,这个节点是活的。如果没有输出,说明内核里根本没注册这个设备号对应的驱动,节点建得再标准也无法工作。这个排查思路很重要,能帮你快速区分“节点问题”和“驱动问题”。
3.4 场景四:用for循环批量创建同主设备号的节点
在驱动开发或嵌入式调试中,经常需要一次性创建一批设备节点。比如某个驱动管理了4路ADC、8个LED,如果一个个手敲mknod,效率太低还容易出错。用for循环配合变量就可以完美解决。
假设这个ADC驱动的主设备号是240,次设备号从0开始递增,共4路设备:
for i in 0 1 2 3; do sudo mknod -m 666 /dev/adc$i c 240 $i done执行完,/dev/adc0到/dev/adc3四个节点全部创建完成。想更灵活一点,次设备号按某个步长递增也行,比如每路设备间隔两个次设备号:
for i in 0 1 2 3; do minor=$((i * 2)) sudo mknod -m 660 /dev/ctl$i c 240 $minor done这种批量创建方式也适合给多个loop设备建块设备节点。如果同时需要两类节点(字符和块),可以在循环里加条件判断。注意一点:批量创建时要确认设备号范围不与其他已存在的节点冲突,否则会出现两个文件名指向同一设备的尴尬情况。
4. 避坑指南:常见错误与排查方法
4.1 权限不够怎么办:Operation not permitted
使用mknod时最常见的报错就是Operation not permitted。普通用户默认无法创建块设备或字符设备文件,必须使用root权限。解决办法很简单:加上sudo,或者切换到root用户操作。
但还有两个隐蔽原因会触发同样的报错。第一,文件系统挂载了nodev选项。如果你在一个nodev挂载点的目录里执行mknod,即使你有root权限,系统也会拒绝创建设备文件。检查方法:
mount | grep /tmp如果输出里带有nodev字样,说明该目录不允许创建设备文件。此时换个挂载选项正常的目录就可以。第二,安全模块限制。某些系统配置了SELinux或AppArmor策略,即使root用户也可能没有创建设备节点的能力。排查时先检查SELinux状态:
getenforce如果是Enforcing状态,可以临时用setenforce 0排查问题,确认后就调整策略或路径。生产环境不建议长期关闭安全模块,找到合适的策略配置才是正道。
还有一个低级但常见的错误:在已有同名普通文件或目录的路径上执行mknod,会提示File exists。这时候先确认旧文件真的没用了再删除:
rm -f /tmp/testdev sudo mknod /tmp/testdev c 240 0千万别在/dev目录里随手rm文件,如果不确定一个设备节点是否被系统引用,先查询、再确认、最后才动手。
4.2 设备号对不上:No such device or address
设备文件创建成功了,一访问就报No such device or address,这种情况非常有意思——节点本身没毛病,但主设备号对应的驱动不存在。说白了就是设备号填错了。
拿ttyUSB0举例。我想为USB转串口设备创建节点:
sudo mknod /dev/ttyUSB0 c 188 0标准Linux里,USB串口的主设备号是188,大多数系统上这么建是对的。但如果你的系统里这个驱动的主设备号实际是180,或者已经被其他驱动占用,那么创建出来的节点打开时就会报错。
解决办法是:先用ls /sys/class/tty/看看有没有ttyUSB0这个设备。如果sysfs里能看到设备但/dev下没有节点,说明硬件确实存在,只是节点缺失。然后参考/proc/devices确认设备号,再创建:
grep usb /proc/devices输出里会列出实际的主设备号,按这个数来创建节点就不会错。
还有一种情况需要留意:内核里有两个不同的驱动使用了相同的设备号,这会导致设备节点被错误的驱动接管。这种冲突很难从用户空间排查,通常需要检查内核日志。多了解一些设备和驱动的对应关系,对排查这类问题有帮助。
4.3 mknod创建的设备在重启后失效怎么办
不少朋友遇到过这样的疑问:我明明用mknod创建了设备节点,当时工作正常,重启之后节点就不见了。这其实是正常现象,不是命令出了问题。
现代Linux发行版在启动过程中会启动udev,udev会按照规则重新构建整个/dev目录。你在udev规则之外手动添加的“野节点”,重启后自然会被清除。udev不认这些节点,因为它不知道它们该不该存在。
如果你希望某个设备节点永久保留,有几种做法。最推荐的是写udev规则。在/etc/udev/rules.d/目录下新建一个规则文件,比如99-custom-device.rules,内容可以是这样:
KERNEL=="mydev", MODE="0666"这样udev在遇到名为mydev的内核设备时,会自动创建节点并设置权限。如果你的场景是驱动开发,更彻底的做法是在驱动内部调用device_create()函数创建设备接口,让udev自动接管节点生命周期。mknod适合应急和排障,但产品级交付还是应该交给设备模型自动管理。
4.4 FIFO使用的三个隐性陷阱
FIFO创建本身非常简单,mknod /tmp/myfifo p一行命令就搞定,没有主次设备号的烦恼。但在实际使用中,FIFO有几个非常容易踩的坑,我在这里单独拎出来说。
第一个坑是打开FIFO时的阻塞行为。默认情况下,以只读方式打开FIFO会一直阻塞,直到有另一个进程以写方式打开同一个FIFO。反过来,以只写方式打开也会阻塞,直到有进程以只读方式打开。这个设计是为了保证管道两端同步,但如果你只开了一端,进程就会卡住,看起来像死锁一样。解决办法是使用非阻塞标志O_NONBLOCK打开FIFO,或者以O_RDWR方式打开(后者不阻塞,但破坏了FIFO的语义,平时不建议)。写多进程通信代码时,一定要考虑清楚两端的打开顺序。
第二个坑是写端关闭后,读端会收到EOF。比如一个进程在持续读取FIFO,如果写端进程意外退出,读端下一次read操作会返回0,表示数据流结束。没有经验的人会把这个EOF误当成正常的数据结束,直接退出循环,导致后续逻辑错误。所以在设计协议时,要明确区分正常结束和异常断开。
第三个坑是权限问题。FIFO创建时权限受umask影响,如果两个不同用户的进程需要通信,创建FIFO时就要赋予双方都能读写权限。可以用:
mknod -m 666 /tmp/shared_fifo p或者用mkfifo -m 666命令,效果一样。这种细节放到生产环境里,可能就是一次线上事故的源头,提前想到能省很多麻烦。
4.5 mkfifo vs mknod:如何选择
mknod和mkfifo都支持创建FIFO,但使用习惯上,多数人更推荐mkfifo。原因很简单:mkfifo专门做FIFO创建这件事,语法更直观,默认行为也更贴近常规工具的习惯。
mkfifo -m 666 /tmp/myfifo一行命令就完成了,而mknod需要你记得用p类型参数:
mknod -m 666 /tmp/myfifo p功能上是等价的,但mkfifo更容易读、不容易误解。如果你只是创建FIFO,直接用mkfifo就好;如果是在脚本里统一处理多种设备文件类型(字符、块、FIFO),用一个mknod搞定所有需求也完全可以。工具选型的核心原则是“在合适的场景用合适的工具”,没有一个命令在所有场景里都是最优解。
最后再分享一点经验
mknod的语法简单到只有一条命令、几个参数,但它背后连接的是一整套Linux设备模型的设计逻辑。用了这些年,我最深的体会是:真正难的不是敲命令,而是搞清楚设备号怎么对、udev什么时候会接管、权限怎么给才安全、以及异常情况下怎么快速恢复。这几个层面每个都能写一篇长文,mknod只是设备管理体系里的冰山一角。
建议大家平时在虚拟机里做一个小练习:手动用mknod创建/dev/null、/dev/zero、/dev/random、/dev/tty这几个基础节点,然后分别尝试读写,直观感受一下设备文件到底是怎么和内核驱动交互的。另外,动手改动/sys和/dev目录之前,先收集好完整的现场信息,别急着删和建。真到了系统救援、容器排障、驱动调试的关键时刻,扎实的mknod功底能帮你省下宝贵时间。