🐧 第七章 文件系统
📄 一句话概括

把 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 秒实验

  1. ls -l /dev/null,记下它的主设备号与次设备号。
  2. time (cat /dev/zero | head -c 1G > /dev/null),量一次吞吐。
  3. 想一想:为什么 > /dev/null 不能加快程序本身?

常见误区

  1. 【/dev/null 是一个空文件】。它没有存储、没有大小;写进去的东西被丢掉,读出来立刻 EOF。
  2. 【/dev/random 比 /dev/urandom 安全】。现代内核上启动之后两者质量相当,random 只会因为等熵而卡住。
  3. 【设备文件可以直接删】。删了之后设备还在,只是没有门牌号;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=`。**

相关笔记