🐧 第二章 用户模式实现的功能
📄 一句话概括

标准库最关键的作用不是提供函数,是攒着一起交:同一个文件写 300 个字节,带缓冲与每次直写差出一个数量级;它把你的一百次改成一两次,省下的是过路费,不是算法。

📝 本节和前面几节的关系

2.1 是门、2.2 是传达室,这一节是屋里的办事员——它自己也不进内核,只是把你的请求攒起来、整理好,再一次性交给传达室。

一、三层叠在一起长什么样

从下往上数:系统调用(过门搬数据)→ 包装函数(翻错误、查参数)→ C 标准库(缓冲、格式、分配)。 你在 C 里写 printf("hello"),这一句要经过:标准库把字符串塞进自己的缓冲区→ 缓冲区满了才调 write → write 是包装函数 → 最后真的过门。 没满的时候,一次也没进内核。这就是缓冲的意思:先攒着。

二、缓冲为什么这么值钱

第 1 章那笔账还记得:过门有固定开销,与搬多少数据无关。 所以 300 个字节分 300 次写,就是 300 次过门;攒起来一次写,只过 1 次门。 下面 Demo 量了这个差:带缓冲与每次直写,差出一个数量级。 而且这还不是最极端的情况——真正的差别在磁盘上:一次写 4KB 与写 300 次 1 字节,落到盘上是【一次寻道】与【三百次寻道】的差别。

缓冲区大小是有讲究的:太小省不下钱,太大浪费内存,还要考虑和文件系统块大小、页大小对齐。标准库默认取的值(通常是 4KB~8KB)就是这套权衡的结果。

三、标准库还管哪些事

格式化:printf 家族的格式串解析、scanf 的反向解析。这部分其实很贵——一次 printf 里那点解析可能比一次 write 还费。 字符串:strlen、strcpy、memcpy。这些通常被编译器内联或换成更快的指令,所以别自己手写循环去【优化】它们。 数学:sin、sqrt。很多 CPU 有对应指令,标准库只是包了一层。 内存分配:malloc/free。这是最容易被误解的一个——它大部分时候不进内核,只是从自己手里的空闲链表里切一块给你;真的不够了才向内核要(brk/mmap)。 所以 malloc 快、free 慢、小块多要很多次也不一定慢——这些现象都能用它【在用户态攒着】解释。

四、Python 里为什么也看得见这一层

print 有缓冲:输出到终端时行缓冲,重定向到文件时块缓冲——所以你在管道里看到的输出顺序可能和终端里不一样。 sys.stdout.flush() 就是【别攒了,现在就交】。 读文件也一样:open() 返回的对象自带缓冲,f.read(1) 读 1024 次可能只进内核一两次。想每次都真读,得用 os.read。

这件事有个实际后果:如果你用 f.read(1) 去量系统调用,量到的是 Python 的缓冲,不是内核的速度——第 1 章那个 Demo 特意绕开了它。

五、动手 Demo:C 标准库站在哪一层——缓冲这件事最能说明问题

同一个文件写 300 个字节:一次走标准库的缓冲,一次绕过它直接 os.write。 这两次写的字节数完全一样,差别只在【过几次门】。

# ① 走标准库缓冲:300 次 write 攒在对象里,最后一次性交
with open(p, "w", encoding="utf-8") as f:
    for _ in range(300):
        f.write("x")

# ② 绕开缓冲:每次 os.write 都要过门
fd = os.open(p, os.O_WRONLY | os.O_CREAT | os.O_TRUNC)
for _ in range(300):
    os.write(fd, b"x")
os.close(fd)

实跑输出:

--------------------------------------------------------------------
C 标准库站在哪一层——缓冲这件事最能说明问题
--------------------------------------------------------------------
① 写 300 个字节,两种写法

  写法             总耗时       进内核几次
  -----------------------------------------
  标准库带缓冲     1.7 毫秒     少数几次
  每次直接写       3.4 毫秒     300 次

      前者比后者省                   2.0 倍

  **标准库最关键的作用不是【提供函数】,是【攒着一起交】。**
  **同一件事,攒 300 次交一次,和跑 300 趟,差的不是代码长短,是过路费。**

② 三层各管什么,一句话

  层           例子                    它替你办的事
  ----------------------------------------------------------
  系统调用     open / read / write     只负责过门搬数据
  包装函数     glibc 的 open()         翻错误码、管参数
  C 标准库     fopen / printf          再攒一层缓冲、管格式
  你的程序     读一行、写一句          只管业务

  **一本书讲【用户模式实现的功能】,讲的就是最上面这三层。**
  它们都在用户模式里跑,一次也没进内核——除了那几次过门。

两次写出的文件一模一样,字节数一样、内容一样。差的只是 300 次过门与 1 次过门。 这就是标准库真正的价值:它不教你新算法,它替你攒。 记住这条判断法:当你发现【同样的活儿,写法不同但结果一样、速度差很多】,先问一句——是不是一个在批量、一个在逐次?

📋 在真 Linux 上验证:strace -e trace=write ./程序,带缓冲的版本只会打印一两次 write,直接写的版本会打印三百行。这是肉眼可见的【谁在攒】。

30 秒实验

  1. python -c "print('a'); print('b')" > /tmp/o.txt 然后 cat /tmp/o.txt,把输出重定向到终端与重定向到文件各跑一次,感受行缓冲与块缓冲的区别。
  2. strace -e trace=write python -c "print('x')",数一下这一句 print 实际发了几次 write。
  3. 在 Python 里对同一个文件分别用 f.read(1) 循环 1000 次与 os.read(fd, 1) 循环 1000 次,用 strace -c 对比系统调用次数。

常见误区

  1. 【printf 就是往屏幕上写字】。它先把字排进缓冲区,屏幕是终端程序画的,内核只负责送字节到 tty。
  2. 【malloc 每次都要进内核】。大部分 malloc 只是在用户态的堆里切一块,只有手里的地不够了才向内核要。所以小块频繁申请不一定慢。
  3. 【缓冲越大越好】。太大浪费内存、增加延迟(数据躺在缓冲区里还没落盘),而且断电时缓冲区里的东西会丢——所以关键数据要 flush 或 fsync。
自测三问(点开对答案) 1. 标准库最关键的作用是什么?为什么它值钱? —— **攒着一起交**(缓冲)。因为过门开销固定,把 N 次合成 1 次,省下的全是固定开销。 2. malloc 为什么大部分时候不进内核? —— 它先从自己维护的空闲链表里切一块(用户态),只有不够了才用 `brk`/`mmap` 向内核要。 3. 用 f.read(1) 量系统调用速度,量到的是什么? —— 量到的是 Python 对象的缓冲,**不是内核**。要量真系统调用必须用 `os.read`。

相关笔记