把 100 MB 写进 /dev/null 只要 3.40 毫秒(29442 MB/秒)——因为它什么都不做,只把调用接住就返回。字符设备是【按字节流走、没有位置、不能回头】的一类文件。
7.9 认全了七种文件,这一节看第一种非普通文件:设备文件。它长得像文件,背后其实是一段驱动程序。
一、设备文件背后是驱动
打开 /dev/null 的时候,内核并不去磁盘上找内容。它看这个 inode 里的【主设备号】与【次设备号】,在驱动表里找到对应的那段代码,把操作转给它。
主设备号说【是哪一类驱动】(1 是内存设备),次设备号说【这一类里的哪一个】(3 是 null)。
所以 ls -l /dev/null 的大小那一栏是 1, 3——不是字节数,是【主, 次】。
这就是【一切皆文件】最直接的体现:你 open、write、close,根本不知道对面是一个磁盘还是一段代码。这也是为什么 echo hi > /dev/null 和 echo hi > file.txt 写得一模一样。
一句话:设备文件是【设备的门牌号】,不是【设备本身】。门牌号可以随便发,设备只有内核里那一个。
二、几个必知的设备
/dev/null:写进去扔掉,读出来立刻 EOF。用途:丢弃输出。它是所有设备里最快的——因为它什么都不做。
/dev/zero:读出来是无限个 0 字节。用途:给文件预清零、测试带宽。
/dev/full:写进去返回 ENOSPC。用途:故意测【磁盘满】的处理逻辑,比真的塞满一块盘安全得多。
/dev/random:真随机,熵池空的时候会阻塞。/dev/urandom:伪随机,永不阻塞。现代内核里两者在启动之后差别很小,日常用 urandom 就够(加密密钥也用 urandom 完全可以)。
/dev/tty:当前终端。它和 /dev/pts/0 的区别是:前者是【我这个进程的终端】,后者是【那个具体的终端】。
三、实测:devnull 有多快
写 100 MB 进 os.devnull:3.40 毫秒,折算 29442 MB/秒。这个数已经超过内存带宽了——因为数据根本没被搬走,只是被接住了。
对照:同一份数据写进内存里的 bytearray 要 478 毫秒。看起来内存比 devnull 慢几百倍?其实是那段代码在反复 +=,每次都要重新分配与搬数据。
这件事本身也是个教训:一次测量的【对比对象】如果造得不对,结论会完全反过来。这里的 478 毫秒测的是【字符串拼接的代价】,不是【内存的带宽】。
真正该记住的是:/dev/null 的写是【空操作】,唯一成本是一次系统调用。所以【把输出重定向到 /dev/null】省的是【终端渲染】,不是【程序的计算】。
四、一个能自杀的坑
cat /dev/zero > 文件:它会一直往后写,直到把磁盘写满为止。这是 shell 里最容易犯的自杀操作之一。同理还有 cat /dev/urandom > 文件。
为什么会这样:/dev/zero 的读【永远不是 EOF】,cat 就一直读下去。
要安全地用它,必须限量:head -c 100M /dev/zero > 文件,或者 dd if=/dev/zero of=文件 bs=1M count=100。
另一个相关的坑:> /dev/sda 之类的写法会直接毁掉磁盘,因为它绕过了文件系统,写到裸块设备上。这也是为什么危险命令要先 ls -l 确认那个设备文件。
五、动手 Demo:字符设备——把 100 MB 写进 /dev/null
这一节真写一次:同样 100 MB,一头写进字符设备,一头写进内存里的 bytearray,比一比。
Windows 上 os.devnull 就是 NUL,行为与 /dev/null 一样。
import os
import time
MB = 1024 * 1024
BUF = b"x" * MB
def write_null():
with open(os.devnull, "wb") as g:
for _ in range(100):
g.write(BUF)
def write_mem():
a = bytearray()
for _ in range(100):
a += BUF
return len(a)
for name, f in (("devnull", write_null), ("内存", write_mem)):
t0 = time.perf_counter()
f()
ms = (time.perf_counter() - t0) * 1000
print(name, "%.2f 毫秒" % ms, "%.0f MB/秒" % (100 / (ms / 1000)))
实跑输出:
--------------------------------------------------------------------
字符设备——把 100 MB 写进 /dev/null
--------------------------------------------------------------------
① 真写一次
写到哪 100 MB 耗时 折算速度
---------------------------------------------------------
os.devnull(字符设备) 3.09 毫秒 32397 MB/秒
内存里的 bytearray 423.17 毫秒 236 MB/秒
**写进 devnull 反而更快**——
**因为它【什么都不做】,只把调用接住就返回。**
**内存那份慢,是因为反复 `+=` 在不停地重新分配与搬数据。**
**这也顺便说明:【内存】不等于【免费】。**
② 几个典型字符设备
设备 读 写 用途
-----------------------------------------------------------------
/dev/null 立刻 EOF 收下就扔 丢弃输出
/dev/zero 无限个 0 字节 未定义 清零内存
/dev/full 未定义 返回 ENOSPC 测磁盘满
/dev/random 真随机,会阻塞 未定义 生成密钥
/dev/urandom 伪随机,不阻塞 未定义 日常随机数
/dev/tty 键盘输入 屏幕输出 当前终端
**它们长得像文件,其实背后是【驱动程序里的一段函数】。**
**你 write 的时候,内核按主设备号/次设备号找到那个驱动。**
**数据交给它之后,它爱怎么处理就怎么处理,扔了也行。**
**这就是字符设备的性格:【按字节流走,没有位置,不能回头】。**
**所以 `ls -l /dev/null` 会显示首字母 `c`。**
**而【大小】那一栏永远显示 `1, 3`(主设备号 1、次设备号 3),不是字节数。**
📋 **Linux 上验证**:`ls -l /dev/null /dev/zero /dev/random`;
`cat /dev/zero | head -c 16 | xxd` 看 16 个 0。
**还有一个真坑**:**`cat /dev/zero > 文件`。**
**它会一直往后写到把磁盘写满为止**——
**这是 shell 里最容易犯的自杀操作之一。**
写进 devnull 反而更快——因为它什么都不做,只把调用接住就返回。
内存那份慢,是因为反复 += 在不停地重新分配与搬数据。这也顺便说明:【内存】不等于【免费】。如果换成预分配好的 bytearray 加切片赋值,内存那边会快一个量级(7.13 就是这么测的)。
所以【devnull 的写】唯一成本就是一次系统调用——它既不动磁盘,也不动内存,数据从用户态拷进内核缓冲区就被扔掉了。
📋 Linux 上验证:ls -l /dev/null /dev/zero /dev/random,看大小那一栏是不是【主, 次】。
📋 cat /dev/zero | head -c 16 | xxd,看 16 个 0。
30 秒实验
ls -l /dev/null,记下它的主设备号与次设备号。time (cat /dev/zero | head -c 1G > /dev/null),量一次吞吐。- 想一想:为什么
> /dev/null不能加快程序本身?
常见误区
- 【/dev/null 是一个空文件】。它没有存储、没有大小;写进去的东西被丢掉,读出来立刻 EOF。
- 【/dev/random 比 /dev/urandom 安全】。现代内核上启动之后两者质量相当,random 只会因为等熵而卡住。
- 【设备文件可以直接删】。删了之后设备还在,只是没有门牌号;
mknod或udev能重新造出来。
自测三问(点开对答案)
1. `ls -l /dev/null` 大小那一栏的 `1, 3` 是什么意思? —— **主设备号 1、次设备号 3**——**前者说【哪一类驱动】,后者说【这一类里的哪一个】。****不是字节数。** 2. 为什么写 `/dev/null` 比写内存还快? —— **因为它什么都不做**:**数据拷进内核缓冲区就被丢掉**,**不落盘、不分配、不检查。** 3. `cat /dev/zero > 文件` 会怎样? —— **一直写到磁盘满**,因为 `/dev/zero` 永远不会 EOF。**要限量就得用 `head -c` 或 `dd count=`。**- ☐ 7.11 块设备
相关笔记
- 文件的种类 — 七种文件的全貌
- 块设备 — 另一种设备文件
- 页面缓存 — 为什么写常规文件看起来也很快
- 🐧 Linux是怎样工作的 · 索引