0%

整理自 MIT OCW 6.004 Computation Structures(Spring 2017)L02 注解幻灯片。

源网页:2.1 Annotated Slides | The Digital Abstraction

讲师:Chris Terman。图片直接引用 OCW 原站链接。

L02:数字抽象(The Digital Abstraction)

上一讲讨论了如何把信息编码成比特序列。本讲转向:为比特寻找有用的物理表示,这是构建信息处理器件的第一步。

1. 编码信息(Encoding Information)

Encoding Information

好的比特表示应具备哪些性质?

  • 小且便宜:随身携带数十亿比特(如音乐文件),网上还有海量比特可供访问,因此比特必须体积小、成本低。
  • 长期稳定:一旦是 0,就应长期保持为 0。罗塞塔石碑(Rosetta Stone,约公元前 196 年)近 2000 年后仍可辨读——但石刻稳定却难改写。
  • 便于操作:能快速访问、变换、组合、传输与存储所编码的信息。

自然界的启发:DNA 用腺嘌呤(Adenine)、胸腺嘧啶(Thymine)、鸟嘌呤(Guanine)、胞嘧啶(Cytosine)编码遗传信息,分子尺度满足“小”的要求,也有人研究用生命化学做大规模计算。但我们既不想提着黏糊糊的 DNA,也不想带着石凿——那么该用什么表示比特?

2. 电来救急(Electricity to the Rescue)

Electricity to the Rescue

可用带电粒子相关的电现象表示信息:

  • 电荷造成电势差 → 电压(voltage)
  • 电荷流动 → 电流(current)
  • 电磁场的相位与频率 → 无线通信的基础

本课程用电压表示比特。例如 0V0\,\mathrm{V} 表示 0,1V1\,\mathrm{V} 表示 1;比特序列可用多根线上的多路电压,或单线上随时间变化的电压序列。

电压表示的优点:市电与电池供应相对便宜可靠;百余年积累的工程知识使我们能造出极小、极低功耗的存取与处理电路——稳态下信息不变时,功耗可接近零。

挑战:电压易受环境电磁场影响;远距离传输需导线;改变线上电压需时间,由导线的电阻与电容决定 RC 时间常数(现代集成电路中很小,但非零)。这些问题有成熟工程对策。

3. 用电压表示信息(Representing Information with Voltage)

Representing Information with Voltage

考虑用电压表示黑白图像:每个 (x,y)(x,y) 点有强度(黑最弱、白最强)。可把强度映到电压,如 0V0\,\mathrm{V} 为黑、1V1\,\mathrm{V} 为白,中间强度用中间电压。

每个点含多少信息?取决于能区分多少强度(电压)。若能分辨任意小差异,则每点信息量理论上无穷;工程上可分辨的差异存在下界。

要用 NN 比特表示的信息量,需在 0V0\,\mathrm{V}1V1\,\mathrm{V} 内区分 2N2^N 个电压。例如 N=2N=2 需区分四个电平(如 001/31/32/32/31V1\,\mathrm{V}),廉价电压表即可。NN 任意大在理论上可行,但微伏乃至纳伏级精度既昂贵又慢,热噪声等还会模糊“瞬时电压”的含义。

因此,电压编码能力受限于能否可靠、快速地区分某一时刻的电压。

4. 用电压编码一幅图(Using Voltages to Encode a Picture)

Using Voltages to Encode a Picture

按约定光栅顺序(如左→右、上→下)扫描图像,把强度转为电压,得到随时间变化的电压序列。早期电视即如此:画面编成在黑白表示之间变化的电压波形,并扩展电压范围以携带行同步、帧同步等同步信号(sync signals)。这种可取指定范围内任意值的波形称为连续波形(continuous waveform)。

5. 信息处理 = 计算(Information Processing = Computation)

Information Processing = Computation

用两个简单处理模块搭系统:

  • COPY:输出复现输入电压 → 图像不变
  • INVERTING:输入为 VV 时输出 1V1-V → 黑白反转

使用预封装模块是构建大电路的常见方式:按规则连接即可,无需理解每个模块内部细节;不同配置下也可根据模块行为预测系统行为——像搭积木一样组装,即使不熟悉模拟电路细节的程序员也能搭建处理任务。前提是:组件正确、连接遵守规则时,系统行为应可预期。

6. 动手搭一个系统!(Let’s Build a System!)

Let's Build a System

用若干 COPY 与 INVERTING 模块组成图像处理系统。COPY 不改图像,INVERTING 为偶数个,理论上输出应与输入相同。

实际上输出略糊:强度有偏差,剧烈变化被抹平。出了什么问题?

7. 系统为何失败?(Why Did Our System Fail?)

Why Did Our System Fail

COPY / INVERTING 很难严格服从数学描述:制造偏差与环境差异使 COPY 对 VV 输入输出 V+εV+\varepsilon,INVERTING 同理。在连续值强度表示下,V+εV+\varepsilon 仍是合法输出——只是对应另一幅略不同的合法图像。我们无法区分“略受损的信号”与“略不同图像的完美信号”。

更致命的是:误差会沿链路累积。系统越大,累积误差越大。若必须限制“还能做多少次计算才结果不可用”,系统将很难扩展。

噪声与不精确不可避免;无法可靠复现无限信息。必须设计系统在允许一定误差时仍能可靠处理信息——要能察觉处理引入的误差,并在误差累积前恢复正确值。这就是下一主题。

8. 数字抽象(The Digital Abstraction)

The Digital Abstraction

引入数字抽象(digital abstraction):用连续的电压世界表示一个小的有限值集合——此处即二进制的 0 与 1。世界本身并非天生数字;我们是用连续物理现象去工程出数字行为。

旁注:有些物理量天然离散(如电子自旋);量子计算正研究如何利用量子物理做计算。本课程聚焦于如何用经典连续现象构建数字系统。

9. 用电压做数字表示(Using Voltages Digitally)

Using Voltages Digitally

核心:约定信令,每次只编码 1 比特(0 或 1),系统中各组件与导线使用统一表示。到达可用方案需三轮尝试。

第一版:用阈值 VthV_{\mathrm{th}} 把电压范围一分为二——V<VthV < V_{\mathrm{th}} 为 0,VVthV \ge V_{\mathrm{th}} 为 1。数学上简洁,但阈值附近的电压极难可靠判读:电路需精密元件与严格受控环境,与低成本、多环境使用目标不符。→ 不可行(大红叉)

第二版:引入两个阈值 VLV_{\mathrm{L}}VHV_{\mathrm{H}}

  • VVLV \le V_{\mathrm{L}} → 解释为 0
  • VVHV \ge V_{\mathrm{H}} → 解释为 1
  • VLV_{\mathrm{L}}VHV_{\mathrm{H}} 之间为禁区(forbidden zone):系统可将该区电压解为 0 或 1,不必一致,甚至可不给出解释

这样可用高增益运放加禁区内粗略参考电压做“快而糙”的电压→比特转换;参考不必极准(如 10% 精度电阻分压),温度与电源漂移也可容忍——只需保证在 VLV_{\mathrm{L}} 以下或 VHV_{\mathrm{H}} 以上时行为正确。→ 暂给绿勾;稍后再做一次小修正。

10. 组合器件(Combinational Devices)

Combinational Devices

满足以下四条的器件称为组合器件(combinational device):

  1. 数字输入:按信令约定,VVLV \le V_{\mathrm{L}} 为 0,VVHV \ge V_{\mathrm{H}} 为 1
  2. 数字输出:输出 0 时电压 VL\le V_{\mathrm{L}},输出 1 时电压 VH\ge V_{\mathrm{H}}
  3. 功能规格:对每种可能的数字输入组合,规定各输出的值(例:三输入有 23=82^3=8 种组合,可用 8 行真值表)
  4. 时序规格:至少给出传播延迟(propagation delay)tPDt_{\mathrm{PD}}——从输入到达稳定有效数字值,到输出保证稳定有效的时间上界

合称静态纪律(static discipline),所有组合器件必须遵守。

11. 组合数字系统(A Combinational Digital System)

A Combinational Digital System

由组合组件组成更大组合系统的规则:

  1. 每个组件本身是组合器件
  2. 每个组件的每个输入:接系统输入、恰好接另一器件的一个输出,或接表示 0/1 的恒定电压
  3. 互连无有向环——从系统输入到输出的路径中,任一组件最多出现一次

主张:按此规则构建的系统本身也是组合器件;系统可任意大,仍服从静态纪律(不同于本节开头脆弱的模拟链路)。

12. 这是组合器件吗?(Is This a Combinational Device?)

Is This a Combinational Device

以由组合器件 A、B、C 组成的系统为例,验证整体是否服从静态纪律:

  1. 数字输入? 是——系统输入即某些组件的输入,组件组合 ⇒ 系统输入数字。
  2. 数字输出? 是——同理由组件继承。
  3. 功能规格? 可——无环,按拓扑顺序用各组件功能规格逐步求出内部信号与输出。
  4. tPDt_{\mathrm{PD}} 可——枚举输入到输出的有限路径,路径延迟为沿途各组件 tPDt_{\mathrm{PD}} 之和;系统 tPDt_{\mathrm{PD}} 取所有路径中的最大值(最长路径)。

因此整体是组合器件。可用组合规则构建任意复杂度的组合器件。

13. 应对噪声(Dealing With Noise)

Dealing With Noise

定稿信令前还有问题:上游器件输出略低于 VLV_{\mathrm{L}} 的合法 0;线上噪声使下游看到略高于 VLV_{\mathrm{L}} 的电压——不再是有效数字输入,下游组合行为不再有保证。

对策:让输出约束严于输入——合法输出可叠加一定噪声后,仍落在合法输入范围内。能否靠“消灭噪声”一劳永逸?用电学组件时做不到。

14. 噪声从哪来?(Where Does Noise Come From?)

Where Does Noise Come From

电压噪声(偏离标称电压)来源包括:

  • 电学效应:导体 IR 压降(欧姆定律)、导体间电容耦合、引线电感与变电流引起的 L(dI/dt)L(\mathrm{d}I/\mathrm{d}t)
  • 制造偏差:器件参数相对标称值的偏差导致器件间电学行为差异
  • 环境因素:热噪声、外部电磁场等

许多噪声来自电路正常工作或材料/工艺固有性质,无法消除;但可估计幅度并相应调整信令规格。

15. 噪声容限(Noise Margins)

Noise Margins

最终信令:输入与输出使用不同阈值。

输出

  • 送出 0:电压 VOL\le V_{\mathrm{OL}}
  • 送出 1:电压 VOH\ge V_{\mathrm{OH}}

输入

  • 电压 VIL\le V_{\mathrm{IL}} → 解释为 0
  • 电压 VIH\ge V_{\mathrm{IH}} → 解释为 1

约束:VILV_{\mathrm{IL}} 严格大于 VOLV_{\mathrm{OL}}VIHV_{\mathrm{IH}} 严格小于 VOHV_{\mathrm{OH}}。输入与输出阈值之间的间隙称为噪声容限(noise margins):

  • 低噪声容限:VILVOLV_{\mathrm{IL}} - V_{\mathrm{OL}}
  • 高噪声容限:VOHVIHV_{\mathrm{OH}} - V_{\mathrm{IH}}

二者中较小者称为该信令规格的噪声免疫(noise immunity)。服从该规格的组合器件会在误差累积前“清洗”输入噪声——数字信令不再重蹈前文模拟例子的覆辙。

16. 电压传输特性(Voltage Transfer Characteristic)

Voltage Transfer Characteristic

以最简单的组合器件——缓冲器(buffer,单输入单输出,输出经传播延迟后复现输入数字值)为例测量。它服从静态纪律,并使用含高低噪声容限的修订信令。

测量:将输入设为从 0V0\,\mathrm{V} 到电源电压的一系列值,每次等待输出稳定(即等满 tPDt_{\mathrm{PD}}),在横轴 VINV_{\mathrm{IN}}、纵轴 VOUTV_{\mathrm{OUT}} 上描点,得到电压传输特性(voltage transfer characteristic, VTC)。

静态纪律约束合法组合器件的 VTC:有效输入必须产生有效输出(“valid in, valid out”)。图上可标出禁区——有效数字输入却对应无效数字输出;合法器件的 VTC 不得落入这些区域。缓冲器的实测曲线(黑线)不穿过阴影区,符合要求。这些测量刻画的是静态行为,不直接给出速度。

17. VTC 推论(VTC Deductions)

VTC Deductions

关于 VTC 的两点观察:

  1. 中心白色区域对应输入在 VILV_{\mathrm{IL}}VIHV_{\mathrm{IH}}(禁区)。静态纪律只约束有效输入,禁区输入下输出可任意。
  2. 由正噪声容限,VOHVOLV_{\mathrm{OH}} - V_{\mathrm{OL}} 严格大于 VIHVILV_{\mathrm{IH}} - V_{\mathrm{IL}},故中心白区高大于宽。穿过该区的曲线必有一段斜率绝对值 大于 1——输入小变化引起输出更大变化,即器件增益(gain)>1>1<1<-1

若组件可互连,则 VINV_{\mathrm{IN}}VOUTV_{\mathrm{OUT}} 范围相同,VTC 图画在正方形内。因存在 斜率>1|\text{斜率}|>1 的区段,又不能全程 斜率>1|\text{斜率}|>1,曲线斜率必须改变——这种器件称为非线性器件(nonlinear devices)。

结论:仅用电阻、电容、电感等线性器件无法构成组合器件;需要增益 >1>1 的非线性器件。寻找这类器件是下一讲主题。

18. VTC 实例(VTC Example)

VTC Example

给定某器件的 VTC,能否选 VOLV_{\mathrm{OL}}VILV_{\mathrm{IL}}VIHV_{\mathrm{IH}}VOHV_{\mathrm{OH}} 使其成为合法组合反相器?

反相器:数字 0 入 → 数字 1 出,反之亦然。该器件低输入时输出高,有希望。

选取示例:

  • 器件最低输出约 0.5V0.5\,\mathrm{V} ⇒ 取 VOL0.5VV_{\mathrm{OL}} \ge 0.5\,\mathrm{V}(取 0.5V0.5\,\mathrm{V}
  • 输入高于约 3V3\,\mathrm{V} 时输出 VOL\le V_{\mathrm{OL}} ⇒ 取 VIH=3VV_{\mathrm{IH}} = 3\,\mathrm{V}(尽量低以留高噪声容限)
  • 设噪声容限 N=0.5VN = 0.5\,\mathrm{V},则 VIL=VOL+N=1VV_{\mathrm{IL}} = V_{\mathrm{OL}} + N = 1\,\mathrm{V}VOH=VIH+N=3.5VV_{\mathrm{OH}} = V_{\mathrm{IH}} + N = 3.5\,\mathrm{V}

在图上标出阈值与禁区后,VTC 合法。因此可用规格 VOL=0.5VV_{\mathrm{OL}}=0.5\,\mathrm{V}VIL=1VV_{\mathrm{IL}}=1\,\mathrm{V}VIH=3VV_{\mathrm{IH}}=3\,\mathrm{V}VOH=3.5VV_{\mathrm{OH}}=3.5\,\mathrm{V} 把该器件当作组合反相器。

19. 小结(Summary)

Summary

本讲要点:

  • 用电压表示信息可行,但连续模拟链路中误差会累积,难以扩展
  • 数字抽象:用连续电压只表示有限符号(0/1),并引入禁区与统一信令
  • 组合器件服从静态纪律:数字 I/O、功能规格、传播延迟 tPDt_{\mathrm{PD}}
  • 组合组件按规则(无环等)互连 → 更大组合系统
  • 输入/输出分离阈值 → 噪声容限,抗噪声累积
  • 合法组合器件的 VTC 避开禁区,且必含增益 >1>1 的非线性区段;下一讲寻找可用的物理器件(CMOS)

整理自 MIT OCW 6.004 Computation Structures(Spring 2017)L01 注解幻灯片。

源网页:1.1 Annotated Slides | Basics of Information

讲师:Chris Terman。图片直接引用 OCW 原站链接。

L01:信息基础(Basics of Information)

本讲从工程视角定义信息与不确定性,引入 Shannon 信息量与熵,讨论定长/变长编码、Huffman、整数与补码表示,以及 Hamming 距离下的检错与纠错。

1. 什么是信息?(What is Information?)

What is Information

工程定义:信息 = 被传送或接收、并能消除关于某事实/情形之不确定性的数据。消除的不确定性越大,传达的信息越多。

例:从 52 张牌中随机抽一张。无任何数据时有 52 种可能。若得知:

  • 是红心 → 剩 13 种
  • 不是黑桃 A → 剩 51 种
  • 是人头牌(J/Q/K)→ 剩 12 种
  • 是“自杀国王”(红心 K)→ 完全确定

哪条数据信息量最大/最小?下节用公式回答。

2. 量化信息(Quantifying Information)

Quantifying Information

用离散随机变量 XX 建模:可取 NN 个值 {x1,,xN}\{x_1,\ldots,x_N\},概率分别为 p1,,pNp_1,\ldots,p_N。概率越小,该取值越不确定。

Shannon 定义:得知 X=xiX=x_i 时收到的信息量为

I(xi)=log21pi bits.I(x_i)=\log_2\frac{1}{p_i}\ \textrm{bits}.

1/pi1/p_i 刻画不确定性;log2\log_2 把度量落到 bit(可取 0/1 的量)。可把信息量理解为“编码该选择所需的比特数”。

3. 数据传达的信息(Information Conveyed by Data)

Information Conveyed by Data

数据未必消除全部不确定性。推广为

I(data)=log21pdata bits.I(\textrm{data})=\log_2\frac{1}{p_{\textrm{data}}}\ \textrm{bits}.

例:得知是红心,p=13/52=0.25p=13/52=0.25

I(heart)=log210.25=2 bits.I(\textrm{heart})=\log_2\frac{1}{0.25}=2\ \textrm{bits}.

等概 NN 种选择被缩到 MM 种时:p=M/Np=M/N

I(NM)=log2NM bits.I(N\rightarrow M)=\log_2\frac{N}{M}\ \textrm{bits}.

4. 信息量例子(Example: Information Content)

Example: Information Content
  • 公平硬币正反面:212\to 1log2(2/1)=1\log_2(2/1)=1 bit
  • 抽牌得知红心:521352\to 13log2(52/13)=2\log_2(52/13)=2 bits
  • 两枚骰子(红+绿)36 种结果:36136\to 1log2365.17\log_2 36\approx 5.17 bits

分数比特含义:单次结果数字系统需用整比特(如 6 bit);但若记录 10 次投掷,公式说总共约 52 bit 即可无损编码,而非 10×6=6010\times 6=60——能否达到下界是编码问题。

5. 概率与信息量(Probability and Information Content)

Probability and Information Content

回到抽牌例子:表格列出各数据事件的概率与信息量,与直觉一致——消除不确定性越多,信息量越大。自杀国王信息量最大;“不是黑桃 A”信息量最小。

6. 熵(Entropy)

Entropy

离散随机变量 XX H(X)H(X) 是得知 XX 取值时的平均信息量

H(X)=E(I(X))=ipilog21pi.H(X)=E(I(X))=\sum_i p_i\log_2\frac{1}{p_i}.

例:{A,B,C,D}\{A,B,C,D\} 概率分别为 1/3,1/2,1/12,1/121/3,1/2,1/12,1/12

H(X)=(1/3)(1.58)+(1/2)(1)+(1/12)(3.58)+(1/12)(3.58)=1.626 bits.H(X)=(1/3)(1.58)+(1/2)(1)+(1/12)(3.58)+(1/12)(3.58)=1.626\ \textrm{bits}.

提示:聪明编码平均可比“每符号固定 2 bit”更短。

7. 熵的含义(Meaning of Entropy)

Meaning of Entropy

XX 的取值序列:

  • 平均每符号少于 H(X)H(X) bit → 不足以消除不确定性(无法无歧义描述)
  • 平均多于 H(X)H(X) bit → 资源未用尽,可能还能压缩
  • 恰好 H(X)H(X) → 理想编码(实践上多求接近)

熵是无歧义传输所需比特数的下界。

8. 编码(Encodings)

Encodings

编码 = 比特串与待编码集合元素之间的无歧义映射

  • 定长编码(fixed-length):如 A=00, B=01, C=10, D=11;“ABBA”→ 00 01 01 00
  • 变长编码(variable-length):如 A=01, B=1, C=000, D=001;适合概率不均
  • 坏例子:A=0, B=1, C=10… → “ABBA”编成 0110 可解成 ABBA / ADA / ABC 等多种 → 有歧义,非法编码

9. 编码与二叉树(Encodings as Binary Trees)

Encodings as Binary Trees

无歧义编码 ↔ 二叉树:边标 0/1,符号只在叶子;内部节点无符号。

解码:从根出发,按比特下行到叶子输出符号,再回根继续。例:01111 → B, A, A(“BAA”)。

10. 定长编码(Fixed-length Encodings)

Fixed-length Encodings

符号等概(或无先验)时用定长:所有叶子到根距离相同。优点:随机访问——第 nn 个符号可跳过固定比特数后解码。

等概 NN 种结果:H(X)=log2NH(X)=\log_2 N

  • BCD:10 个十进制数字 → 4 bit 定长;H=log2103.322H=\log_2 10\approx 3.322,1000 位数字用 4000 bit,熵提示或可压到约 3322 bit
  • ASCII:94 可打印字符,H=log2946.555H=\log_2 94\approx 6.555 → 常用 7 bit 定长

11. 编码正整数(Encoding Positive Integers)

Encoding Positive Integers

无符号整数用二进制(base-2):各位权重 2N1,,202^{N-1},\ldots,2^0。例:12 位 011111010000 = 1024+512+256+128+64+16=20001024+512+256+128+64+16=2000

NN bit 范围:002N12^N-1。固定字长(如 32/64 bit)系统对超大数需多步运算。

12. 十六进制记法(Hexadecimal Notation)

Hexadecimal Notation

长二进制串易抄错 → 用 hex(radix-16):每 4 bit 一个十六进制数字(0–9, A–F)。从最低位起按 4 位分组。

例:0111 1101 00000x7D0。前缀 0x 标明十六进制(多语言惯例)。

13. 编码有符号整数(Encoding Signed Integers)

Encoding Signed Integers

原码(signed magnitude):最高位作符号(0 正 / 1 负),其余为幅度。例:2000-2000 = 符号位 1 + 2000 的二进制。

问题:存在 +0+00-0 两套零;加减法电路不同(与小学加减分法类似)。

14. 补码编码(Two’s Complement Encoding)

Two's Complement Encoding

现代系统多用 two’s complementNN bit 最高位权重为 2N1-2^{N-1}。最高位 1 → 负数(兼作符号位)。

  • 最负:2N1-2^{N-1}(仅最高位为 1)
  • 最正:2N112^{N-1}-1
  • 8 bit:128127-128\sim 127
  • 全 1:1-1;全 0:唯一的 00

15. 补码更多性质(More Two’s Complement)

More Two's Complement

1+1-1+1 用普通二进制加法得全 0 → 补码算术统一。BA=B+(A)B-A=B+(-A)

A-A:因 A+(A)=0=1+(1)A+(-A)=0=1+(-1),且 1-1 为全 1,故

A=A+1-A=\sim A+1

(按位取反再加 1)。只需会二进制加法与补码取负即可练习。

16. 变长编码(Variable-length Encodings)

Variable-length Encodings

概率不等时,定长非最优。看期望码长ipi(len of xi)\sum_i p_i\cdot(\textrm{len of }x_i)。希望逼近 H(X)H(X)

策略:高概率(信息量小)→ 短码;低概率 → 长码 → 变长编码

17. 变长编码例子(Example: Variable-length Encoding)

Example: Variable-length Encoding

A,B,C,D 概率如前;编码使符号皆在叶子 → 无歧义。例:0 100 11 0 11 101 → BCABAD。

期望码长 =2(1/3)+1(1/2)+3(1/12)+3(1/12)=5/31.667=2\cdot(1/3)+1\cdot(1/2)+3\cdot(1/12)+3\cdot(1/12)=5/3\approx 1.667 bit。

1000 符号:定长需 2000 bit;变长期望 1667;1000H(X)=16261000\cdot H(X)=1626 为下界——已更接近,是否还有更优?下一节 Huffman。

18. Huffman 算法(Huffman’s Algorithm)

Huffman's Algorithm

给定符号与概率,Huffman 算法自底向上建最优变长码(逐符号编码时期望码长最短):

  1. 选概率最小的两个符号/子树,并成一棵子树(边标 0/1 任意)
  2. 用子树替换二者,根概率为二者之和
  3. 重复直到合成一棵树

0/1 标签互换得到不同码,但期望码长相同(取决于到根距离)。

19. 还能更好吗?(Can We Do Better?)

Can We Do Better

逐符号 Huffman 已最优;若对符号对/更长块编码,可进一步逼近熵。例:按对编码期望约 1.646 bit/符号(优于 1.667)。

现代压缩(如 LZW)自适应发现高频序列并赋短码,对自然语言等重复多的数据效果显著。

20. 检错(Error Detection)

Error Detection

比特可能在传输中翻转。简单编码:heads=0, tails=1。Bob 发 0 被翻成 1 → Alice 收到 tails,无法区分“无错的 tails”与“出错的 heads”→ 无法检测单比特错。

21. Hamming 距离(Hamming Distance)

Hamming Distance

Hamming distance:两等长编码对应位不同的个数。例:两 7 bit 码差第 3、5 位 → 距离 2。距离 0 → 完全相同。

22. Hamming 距离与比特错误(Hamming Distance and Bit Errors)

Hamming Distance and Bit Errors

单比特错把码字推到 Hamming 距离为 1 的邻点。若合法码字之间最小距离也是 1(如 01),则错误把合法码变成另一合法码 → 不可检测。图中箭头表示距离 1 的邻接。

23. 单比特检错(Single-bit Error Detection)

Single-bit Error Detection

要检测单错:合法码字间最小 Hamming 距离至少为 2。做法:加奇偶校验位(parity bit)。偶校验使码字中 1 的个数为偶:heads 000,tails 111,最小距离升为 2。

24. 奇偶校验(Parity Check)

Parity Check

单错:0001/10,皆非法 → 可检出。合法码偶个 1,出错后奇个 1 → parity error。数 1 的个数(或用 XOR)即可校验。

偶次比特错会保持偶校验 → 奇偶校验主要对单错有效;多错需更强编码。

25. 检测多比特错误(Detecting Multi-bit Errors)

Detecting Multi-bit Errors

一般:检测至多 EE 个错误,需最小 Hamming 距离 E+1\ge E+1。例:000111 距离 3 → 可检测最多 2 错(长度 2 的路径到不了另一合法码)。

26. 纠错(Error Correction)

Error Correction

最小距离升到 3:各合法码的单错邻域互不重叠 → 可纠正单错(收到 001 则判原码为 000)。

一般:纠正至多 EE 错,需最小距离 2E+1\ge 2E+1。编码理论研究如何系统构造此类码;本课只需把握:码字在 Hamming 空间中“隔得够远”即可检错乃至纠错。

27. 小结(Summary)

Summary

要点回顾:

  • 信息量化:I=log2(1/p)I=\log_2(1/p);熵 H(X)H(X) 是平均信息量与编码下界
  • 定长/变长编码;Huffman 在逐符号意义下最优;块编码可更逼近熵
  • 整数:二进制、hex、补码(取负 =A+1=\sim A+1
  • Hamming 距离:距离 E+1\ge E+1 可检 EE 错;2E+1\ge 2E+1 可纠 EE 错;奇偶校验是简单单错检测

现象

同一多线程工具、同一版本、同一份输入,在两台机器上跑完后,日志里的峰值内存对不上:

机器 Peak RSS Peak Virt Virt / RSS
机器 A 473 GB 518 GB 1.09
机器 B 478 GB 5485 GB 11.5

RSS 只差约 5 GB,Virt 却差约 4967 GB(近 5 TB)。两次运行都正常结束(exit status 0),结果也正确。机器 B 多带了 -mc,并行度更高,但后文估算表明两机线程数接近(约 144),单凭执行路径解释不了 Virt 差这么多。

问题变成:日志里的 Virt 异常,是不是真出了内存问题?

排查

排除 OOM 与泄漏

机器 freeSwap / totalSwap 退出码
机器 A 126 GB / 128 GB 0
机器 B 128 GB / 128 GB 0

RSS 接近、swap 几乎没动、结果正确——更像监控数字异常,而不是进程把物理内存吃爆了。

对比 ulimit

机器 ulimit -s(kbytes) 约合每线程栈
机器 A 42768 ~42 MB
机器 B 33554432 32 GB

栈限制相差约 785 倍。若程序用的是默认线程栈,这条差异值得顺着查。

定量估算

输入相同、RSS 接近,可近似认为线程数 (N) 差不多。若线程走默认栈,Virt 差应主要来自「每线程栈预留」之差:

1
2
3
ΔVirt          = 5485 − 518 = 4967 GB
Δstack/thread = 32.000 − 0.041 = 31.959 GB
N(上界) = 4967 / 31.959 ≈ 155

上界假设除线程栈外其余 Virt 完全相同。日志里还有更早的一段:多线程计算启动前、内存库刚加载完时,两机 Virt 已经差约 377 GB(A:Virt 252 GB / RSS 251 GB;B:Virt 629 GB / RSS 251 GB)。差额来源未完全确认,可能是分配器预留,也可能含少量早期线程。从总差里扣掉后再估:

1
N(下界)= (4967 − 377) / 31.959 ≈ 144

(N) 大约在 144~155。两种估法都指向同一方向:Virt 膨胀的主体,可以用 ulimit -s 造成的栈预留差解释。

根因

未调用 pthread_attr_setstacksize 时,pthread_create 默认取 RLIMIT_STACK(即 ulimit -s)。内核为每个线程预留对应大小的虚拟地址区间:

  1. 预留立刻计入进程 VSZ(日志里的 Virt)
  2. 不立即分配物理页,故不计入 RSS
  3. 线程真正写入栈时,才缺页映射物理页

按约 144 线程估算:

机器 每线程栈 线程栈合计计入 Virt
机器 A 42 MB ≈ 6 GB
机器 B 32 GB ≈ 4608 GB

两机任务量和线程数接近,Virt 差的主因是每线程栈预留差了约 785 倍,而不是「机器 B 多干了很多活」。RSS 可以几乎不变:真正触达的页才进 RSS;未触达的栈预留只抬高 Virt。

题外话:32 位地址空间下的差异

上面「Virt 很大但仍能正常跑完」主要适用于 64 位进程。x86_64 用户态地址空间约 128 TB,本文约 5 TB 的 Virt 远没顶到天花板。

32 位用户态通常约 3 GB(Linux 默认约 3 GB / 1 GB 用户/内核分割)。这时过大的 ulimit -s 会很快耗尽地址空间:每多一个线程就多占数百 MB 虚拟地址,线程稍多,pthread_create 就会返回 EAGAIN(errno=11,Resource temporarily unavailable),程序直接失败。

ulimit -v 3145728 把虚拟地址空间限制为 3 GB,模拟 32 位上限,实测如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 异常场景:ulimit -s 262144(256 MB/线程)
ulimit -v 3145728; ulimit -s 262144; ./thread-stack-32bit-demo

ulimit -s (stack/thread) = 256 MB
ulimit -v (virtual addr) = 3072 MB
理论最大线程数 ≈ 3072 / 256 = 12

[ 0 threads] Virt = 2 MB RSS = 1 MB
[ 1 threads] Virt = 258 MB RSS = 1 MB ← 每线程 +256 MB Virt
[ 2 threads] Virt = 514 MB RSS = 1 MB ← RSS 始终只有 1 MB
[ 3 threads] Virt = 770 MB RSS = 1 MB
...
[ 11 threads] Virt = 2818 MB RSS = 1 MB

pthread_create 在第 12 个线程时失败: Resource temporarily unavailable (errno=11, EAGAIN)
^^^^^^^^^^^^^^^^^^^^
虚拟地址空间耗尽,内核无法为新线程栈执行 mmap,返回 EAGAIN
成功创建线程数: 11

# 对照组:ulimit -s 8192(8 MB/线程,正常值)
ulimit -v 3145728; ulimit -s 8192; ./thread-stack-32bit-demo

[ 1 threads] Virt = 10 MB RSS = 1 MB ← 每线程仅 +8 MB Virt
[ 2 threads] Virt = 18 MB RSS = 1 MB
...(可正常创建 300+ 个线程)

两组对比很清楚:RSS 全程只有约 1 MB(栈几乎没真正用起来),Virt 却按 ulimit -s 的步长往上加,直到地址空间耗尽报错。对 32 位程序,过大的 ulimit -s 是实质故障;对 64 位程序,同类配置多半只让日志里的 Virt「看起来吓人」。复现程序:

title:thread-stack-32bit-demo.cview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
/*
* thread-stack-32bit-demo.c
*
* 演示:ulimit -s 过大时,有限虚拟地址空间内线程创建失败的过程。
*
* ============================================================
* 测试 32-bit 虚拟地址受限场景的两种方法
* ============================================================
*
* 【方法一】真正编译为 32-bit 二进制(最真实)
*
* 前提:需安装 32-bit 开发库
* RHEL/CentOS: sudo yum install glibc-devel.i686 libgcc.i686
* Ubuntu/Debian: sudo apt install gcc-multilib
*
* 编译:
* gcc -m32 -o thread-stack-32bit-demo thread-stack-32bit-demo.c -lpthread
*
* 运行:进程天然受 32-bit 用户态 ~3 GB 地址空间限制,无需 ulimit -v
* ulimit -s 262144 # 每线程栈 256 MB
* ./thread-stack-32bit-demo
*
* ------------------------------------------------------------
*
* 【方法二】64-bit 二进制 + ulimit -v 模拟(无需额外依赖)
*
* ulimit -v 限制进程的最大虚拟地址空间(RLIMIT_AS),
* 效果与 32-bit 地址空间受限完全等价:mmap 超出上限时同样返回 EAGAIN,
* pthread_create 因无法为新线程栈分配地址而失败。
*
* 编译:
* gcc -o thread-stack-32bit-demo thread-stack-32bit-demo.c -lpthread
*
* 运行:
* ulimit -v 3145728 # 限制虚拟地址空间为 3 GB(模拟 32-bit 用户态上限)
* ulimit -s 262144 # 每线程栈 256 MB
* ./thread-stack-32bit-demo
*
* ============================================================
* 对照组(正常 ulimit -s,两种方法均适用)
* ============================================================
* ulimit -v 3145728
* ulimit -s 8192 # 正常值:8 MB/线程,同样地址空间可创建 300+ 线程
* ./thread-stack-32bit-demo
*/

#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/resource.h>

static volatile int keep_running = 1;

/* 从 /proc/self/status 读取 VmSize 和 VmRSS,单位 MB */
static void print_mem(int n_threads) {
FILE *f = fopen("/proc/self/status", "r");
if (!f) return;
char line[256];
long vmsize_kb = -1, vmrss_kb = -1;
while (fgets(line, sizeof(line), f)) {
if (strncmp(line, "VmSize:", 7) == 0) sscanf(line + 7, "%ld", &vmsize_kb);
if (strncmp(line, "VmRSS:", 6) == 0) sscanf(line + 6, "%ld", &vmrss_kb);
}
fclose(f);
printf(" [%3d threads] Virt = %6ld MB RSS = %5ld MB\n",
n_threads, vmsize_kb / 1024, vmrss_kb / 1024);
fflush(stdout);
}

static void *thread_func(void *arg) {
(void)arg;
while (keep_running)
sleep(1);
return NULL;
}

int main(void) {
/* 读取当前 ulimit 设置 */
struct rlimit rl_stack, rl_as;
getrlimit(RLIMIT_STACK, &rl_stack);
getrlimit(RLIMIT_AS, &rl_as);

long stack_mb = (long)(rl_stack.rlim_cur / 1024 / 1024);
long as_mb = (rl_as.rlim_cur == RLIM_INFINITY)
? -1
: (long)(rl_as.rlim_cur / 1024 / 1024);

printf("========================================\n");
printf("ulimit -s (stack/thread) = %ld MB\n", stack_mb);
if (as_mb < 0)
printf("ulimit -v (virtual addr) = unlimited\n");
else
printf("ulimit -v (virtual addr) = %ld MB\n", as_mb);

if (as_mb > 0 && stack_mb > 0)
printf("理论最大线程数 ≈ %ld / %ld = %ld\n",
as_mb, stack_mb, as_mb / stack_mb);
printf("========================================\n\n");

print_mem(0); /* 基线 */

pthread_t threads[1024];
int i;
for (i = 0; i < 1024; i++) {
int ret = pthread_create(&threads[i], NULL, thread_func, NULL);
if (ret != 0) {
const char *meaning = (ret == EAGAIN) ? "EAGAIN: 虚拟地址空间耗尽,无法为线程栈执行 mmap"
: (ret == ENOMEM) ? "ENOMEM: 物理内存不足"
: "其他错误";
printf("\npthread_create 在第 %d 个线程时失败\n", i + 1);
printf(" errno = %d (%s)\n", ret, strerror(ret));
printf(" 含义 : %s\n", meaning);
printf("成功创建线程数: %d\n", i);
print_mem(i);
keep_running = 0;
for (int j = 0; j < i; j++)
pthread_join(threads[j], NULL);
return 1;
}
print_mem(i + 1);
}

printf("达到测试上限 1024 个线程,未触发失败。\n");
keep_running = 0;
for (int j = 0; j < i; j++)
pthread_join(threads[j], NULL);
return 0;
}

定位方法

这次是日志对比触发的个案。若以后再看到 Virt 远大于 RSS,仍建议先定位最大的映射段,再归因(文件 mmap、分配器预留、共享内存等都可能)。

1
2
cat /proc/<PID>/status | grep -E 'VmSize|VmRSS|VmSwap'
pmap -x <PID> | sort -k3 -rn | head -10

pmap -x 里:Kbytes 是段的虚拟大小(进 Virt),RSS 是驻留页,Mapping 标明类型([stack:TID][heap]、路径、[anon] 等)。

本次这类场景下,ulimit -s 为 32 GB 时,排序结果通常类似:

1
2
3
4
5
Address           Kbytes       RSS   Dirty Mode  Mapping
00007f1200000000 33554432 128 0 rw--- [stack:4312]
00007f2200000000 33554432 64 0 rw--- [stack:4313]
...(约 144 行 [stack:*])
0000000001a00000 524288 263144 263144 rw--- [heap]

单段 Kbytes = 33554432 即约 32 GB 虚拟预留,RSS 往往只有几十到几百 KB。

  • [stack:N] → 查 ulimit -s 和线程数
  • 大块 [anon] → 查分配器或显式 mmap
  • 具名路径 → 查映射文件大小
1
2
3
4
awk '/\[stack/{s=1} s && /^Size/{sum+=$2; s=0} END{print sum/1024 " MB"}' \
/proc/<PID>/smaps
ulimit -s
cat /proc/<PID>/status | grep Threads

结论

机器 A 机器 B
ulimit -s 42 MB 32 GB
线程数(默认栈) ~144 ~144
线程栈合计 Virt ~6 GB ~4608 GB
Peak RSS 473 GB 478 GB
Peak Virt 518 GB 5485 GB
运行是否正常

这次日志里的 Virt「异常」,不是泄漏,也不是跑挂了。 两机默认线程栈都绑在 RLIMIT_STACK 上,而 ulimit -s 差了约 785 倍,多线程一叠加,Virt 就被放大到近 10 倍;RSS 与正确性可以保持正常。

可落地的收尾:

场景 做法
程序内工作线程 pthread_attr_setstacksize 设明确栈大小(如 8 MB)
机器 / 集群默认 ulimit -s 建议 8~64 MB,避免对多线程负载设 unlimited
看日志 / 告警 多线程进程优先看 RSS(和 swap),不要单凭 Virt 判内存异常
跨机对比 同二进制 Virt 差很多时,先比双方 ulimit -a

P型与N型半导体的形成与导电原理(含PN结延伸)

一、 基本概念

  1. P型半导体(Positive-type):又称空穴型半导体。通过在本征半导体(如硅)中掺入三价元素形成,其主要依靠带正电的空穴导电。
  2. N型半导体(Negative-type):又称电子型半导体。通过在本征半导体(如硅)中掺入五价元素形成,其主要依靠带负电的自由电子导电。

二、 本征硅晶体的基本结构

  1. 价电子特征:纯净的硅(Si)原子最外层有 4个价电子
  2. 共价键结合:1个硅原子与周围 4个相邻硅原子 各共用一对电子,形成4个共价键,构成稳定的正四面体立体网状结构。

三、 P型半导体的形成原理(以掺硼为例)

  1. 原子替代:三价杂质硼(B)原子直接替代晶格中某些硅原子的位置。
  2. 价电子不匹配:硼原子最外层只有 3个价电子,比硅原子少1个。
  3. 空穴的产生
    • 硼原子与周围4个硅原子中的 3个 形成完整的共价键。
    • 与第4个硅原子结合时,由于缺少1个电子,无法形成完整的电子对,从而留下一个电子空位,即形成 “空穴”
  4. 受主原子(Acceptor):硼原子极易吸引并捕获邻近硅原子的价电子。接受电子后,硼原子自身变成带负电、无法移动的负离子
  5. 导电原理:邻近电子跳入该空穴时,会在原位留下新空穴。这种空穴的连续移动形成电流,其多数载流子(多子)为空穴

四、 N型半导体的形成原理(以掺磷为例)

  1. 原子替代:五价杂质磷(P)原子直接替代晶格中某些硅原子的位置。
  2. 价电子不匹配:磷原子最外层有 5个价电子,比硅原子多1个。
  3. 自由电子的产生
    • 磷原子的4个价电子与周围的 4个硅原子 形成完整的共价键。
    • 多出来的第5个价电子不受共价键的束缚,其能量极高,极易脱离磷原子核的束缚,在晶格中运动,成为自由电子
  4. 施主原子(Donor):磷原子因为能够向导带提供(施予)电子,被称为施主杂质。提供电子后,磷原子自身变成带正电、无法移动的正离子
  5. 导电原理:在晶格中存在大量的自由电子。在外加电场作用下,这些电子会发生定向移动形成电流,其多数载流子(多子)为自由电子

五、 P型与N型半导体核心参数对比

对比项目 P型半导体 N型半导体
掺杂元素 三价元素(如硼 B、铟 In、镓 Ga) 五价元素(如磷 P、砷 As、锑 Sb)
杂质类型 受主杂质(吸纳电子) 施主杂质(释放电子)
多数载流子(多子) 空穴(带正电) 自由电子(带负电)
少数载流子(少子) 自由电子(热激发产生) 空穴(热激发产生)
电离后的杂质离子 固定不动的负离子 固定不动的正离子
电中性 整体呈电中性 整体呈电中性

六、 PN结的形成与单向导电原理

1. 空间电荷区的形成(平衡态)

  • 载流子扩散:当P型和N型半导体接触时,由于交界面两侧存在浓度差,P区的空穴会向N区扩散,N区的自由电子会向P区扩散。
  • 复合与离子区:扩散过去的电子和空穴在交界面附近相遇并复合消失。
  • 内建电场
    • P区一侧失去空穴,留下带负电的杂质离子。
    • N区一侧失去电子,留下带正电的杂质离子。
    • 这部分没有自由载流子的区域称为空间电荷区(或阻挡层/耗尽层),它产生了一个由N区指向P区的内建电场,阻止载流子继续扩散,最终达到动态平衡。

2. 单向导电原理

  • 正向偏置(外加正向电压)
    • 接法:电源正极接P区,负极接N区。
    • 原理:外加电场与内建电场方向相反,削弱了内建电场,使空间电荷区变窄。P区多子(空穴)和N区多子(电子)能够轻易通过交界面,形成较大的正向电流(PN结导通)。
  • 反向偏置(外加反向电压)
    • 接法:电源正极接N区,负极接P区。
    • 原理:外加电场与内建电场方向相同,加强了内建电场,使空间电荷区变宽。多子无法通过,此时只有两区极少数的少子在外电场作用下形成微弱的漂移电流,称为反向饱和电流(PN结截止)。

一、现象

在两台 64 位 Linux 主机上运行同一段代码,结果截然不同:

命令 主机 A(RHEL 9.4,内核 5.14) 主机 B(RHEL 8.10,内核 4.18)
malloc(64 GiB) ./probe_malloc_failure mmap[1] 返回 NULLerrno = ENOMEM 返回有效指针,成功
物理内存 grep MemTotal /proc/meminfo 约 7.5 GiB 约 3.0 TiB
Swap grep SwapTotal /proc/meminfo 约 8.0 GiB 约 128 GiB
overcommit 模式 cat /proc/sys/vm/overcommit_memory mode 0(默认) mode 0(默认)

直觉上,64 位进程拥有约 128 TiB 的虚拟地址空间,为什么请求区区 64 GiB 会返回"内存不足"?


二、背景:Linux 的"先承诺、后兑现"内存模型

理解这个问题,需要先搞清楚 Linux 的内存分配不是"直接给你物理内存":

  • malloc 调用 mmap:请求的块超过约 128 KiB 时,glibc 内部不通过 brk 扩堆,而是调用 mmap 系统调用申请匿名内存。
  • 承诺(commit)在先,物理页在后mmap 成功时,内核只是"承诺"了一段虚拟地址区间,物理页面要等到程序真正读写时才分配(缺页异常触发)。
  • 承诺也有上限:内核在 mmap 时会检查"这次的承诺量是否合理",超过限制直接拒绝,返回 ENOMEM——即使此刻物理 RAM 还没用完。

因此 ENOMEM 不一定意味着"内存满了",可能只是内核在承诺阶段预判"将来可能兑现不了"而提前拒绝。


三、排查过程

3.1 确认失败发生在哪一层:malloc 还是 mmap

malloc 失败可能有两种情况:glibc 内部逻辑拒绝,或底层 mmap 系统调用已失败。先分层确认:

probe_malloc_failure.cpp:32-48view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// §3.1:确认失败发生在 mmap 层而非 malloc 层
static void probe_mmap_vs_malloc()
{
size_t sz = 64 * kGiB;
errno = 0;
void* mp = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("mmap(64 GiB): %s errno=%d (%s)\n",
mp == MAP_FAILED ? "FAIL" : "OK", errno, strerror(errno));
if (mp != MAP_FAILED) munmap(mp, sz); // 释放本次 mmap 申请的虚拟内存

errno = 0;
void* p = malloc(sz);
printf("malloc(64 GiB): %s errno=%d (%s)\n",
p ? "OK" : "NULL", errno, strerror(errno));
if (p) free(p); // 释放本次 malloc 申请的内存
}
1
2
3
# 配套测试程序见第六节,编译:
# g++ -O0 -o probe_malloc_failure probe_malloc_failure.cpp
./probe_malloc_failure mmap
调用 主机 A 主机 B
mmap(64 GiB)(默认标志) MAP_FAILED,errno=12 成功
malloc(64 GiB) NULL,errno=12 成功

主机 A 上 mmap 系统调用本身已失败,malloc 返回 NULL 只是下游表现。排查范围收缩至内核 mmap 路径。


3.2 假设:进程虚拟地址空间受到 RLIMIT_AS 限制

Linux 可以通过 RLIMIT_AS(address space limit,进程虚拟地址空间上限)限制单个进程可映射的虚拟地址总量。内核在 mmap_region() 中的 may_expand_vm() 检查:若当前已映射量 + 本次请求 > 上限,拒绝并返回 ENOMEM

查看方式:

1
2
ulimit -v
cat /proc/self/limits | grep -i 'max address'
主机 A 主机 B
RLIMIT_AS unlimited unlimited

两台主机均无限制,排除此假设


3.3 假设:内核 overcommit 检查拒绝了本次请求

Linux 允许全系统"承诺"的虚拟内存总量超过物理 RAM,这叫超量承诺(overcommit)。策略由 /proc/sys/vm/overcommit_memory 控制,共三种模式:

mode 行为
启发式(默认) 0OVERCOMMIT_GUESS 单次 mmap 请求的页数 > 物理 RAM 页数 + Swap 页数,则拒绝(Linux v5.14及以后)[2]
始终允许 1OVERCOMMIT_ALWAYS 无条件同意所有分配
严格限制 2OVERCOMMIT_NEVER 全系统累计承诺量 < 上限时才允许

两台主机均为默认的 mode 0。其核心判断逻辑(Linux v5.14,mm/util.c L884–891):

1
2
3
4
5
// __vm_enough_memory()
if (sysctl_overcommit_memory == OVERCOMMIT_GUESS) {
if (pages > totalram_pages() + total_swap_pages)
goto error; // 返回 -ENOMEM
}

在主机 A 上代入数字:

数据项
请求页数(64 GiB ÷ 4 KiB/页) 16,777,216 页
totalram_pages(物理 RAM) 1,967,521 页
total_swap_pages(Swap) 2,097,151 页
系统容量合计 4,064,672 页(约 15.5 GiB)
判断 16,777,216 > 4,064,672 → 拒绝

在主机 B 上代入数字:

数据项
系统容量合计(RAM + Swap) 约 825,719,106 页(约 3.1 TiB)
判断 16,777,216 825,719,106 → 通过

这直接解释了两台主机的差异。

用实验验证边界:

probe_malloc_failure.cpp:71-85view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// §3.3:探测 overcommit 检查的边界(RAM+Swap 附近逐步测试)
static void probe_threshold()
{
// 15360 MiB=15 GiB;15872 MiB=15.5 GiB;15880 MiB=15.5078125 GiB;
// 16384 MiB=16 GiB;65536 MiB=64 GiB
for (int mb : {15360, 15872, 15880, 16384, 65536}) {
size_t sz = static_cast<size_t>(mb) * 1024 * 1024;
errno = 0;
void* p = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("%5d MiB: %s errno=%d\n",
mb, p == MAP_FAILED ? "FAIL" : "OK", errno);
if (p != MAP_FAILED) munmap(p, sz); // 释放本次 mmap 申请的虚拟内存
}
}
1
./probe_malloc_failure threshold
1
2
3
4
5
15360 MiB: OK errno=0
15872 MiB: OK errno=0
15880 MiB: FAIL errno=12
16384 MiB: FAIL errno=12
65536 MiB: FAIL errno=12
请求大小 请求页数 与系统容量比较 实测结果
15,872 MiB 4,063,232 ≤ 4,064,672(通过) 成功
15,880 MiB 4,065,280 > 4,064,672(超限) 失败
64 GiB 16,777,216 > 4,064,672(超限) 失败

实测边界与公式完全吻合,确认 mode 0 overcommit 检查是根因


3.4 排除:mode 2 的累计承诺量限制

两台主机的 overcommit_memory 都是 0,本不该走到 mode 2。仍用实验确认当前失败不是「全系统累计承诺量超限」。

下面按 Linux v5.14 源码(mm/mmap.c accountable_mapping / mmap_regionmm/util.c __vm_enough_memory)画出承诺检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
mmap_region()

├─ accountable_mapping()? // 私有可写且无 VM_NORESERVE
│ ├─ 否 → 跳过 __vm_enough_memory(不进入承诺检查)
│ └─ 是 → security_vm_enough_memory_mm()
│ └─ __vm_enough_memory(pages)
│ │
│ ① vm_acct_memory(pages)
│ (先把本次 pages 计入 Committed_AS)
│ │
│ ② 按 overcommit_memory 三选一(互斥,只走一条)
│ ┌─────────────────────┼─────────────────────────┐
│ │ == 0 GUESS │ == 1 ALWAYS │ == 2 NEVER
│ │ (当前主机) │ │
│ ▼ ▼ ▼
│ pages > 直接通过 allowed = vm_commit_limit()
│ totalram + total_swap? return 0 (再减去 admin/user reserve)
│ │是 │否 (保留记账) │
│ │ │ ▼
│ │ │ vm_committed_as(已含本次)
│ │ │ < allowed ?
│ │ │ │否(超限) │是(未超)
│ ▼ │ ▼ │
│ goto error │ goto error │
│ │ │
│ └─────────────────────────┬──────────────────────────┘
│ ▼
│ return 0
│ (保留①的记账)
│ mmap 继续成功路径

│ goto error:
│ ③ vm_unacct_memory(pages) ← 仅失败路径
│ 回滚刚才的记账
│ return -ENOMEM

简化版(只看主干):

1
2
3
4
5
6
accountable_mapping?  // 私有可写且无 VM_NORESERVE
├─ 否 → 跳过承诺检查,mmap 继续(仍可能因其它原因失败)
└─ 是 → 先记账,再按 overcommit_memory:
├─ 0 → 单次 pages > RAM+Swap? 是→失败(回滚) / 否→检查通过
├─ 1 → 始终通过承诺检查
└─ 2 → Committed_AS 超限? 是→失败(回滚) / 否→检查通过
  1. mode 0 不检查 CommitLimit
  2. mode 2 不检查 pages > RAM + Swap
  3. 记账是「先加;仅失败才回滚」,故失败时 Committed_AS 净不变。

第一步:读出 CommitLimit

1
2
grep CommitLimit /proc/meminfo
# CommitLimit: 12323644 kB → 11.75 GiB

要排除 mode 2,需要选一个同时满足的请求大小:

  1. 大于 CommitLimit(11.75 GiB)——否则即使用 mode 2 也不会因这次请求触顶
  2. 不超过 mode 0 的拒绝条件——即请求 ≤ RAM + Swap(主机 A 为 4,064,672 页 / 15.5 GiB),否则会先被 mode 0 拦下,看不清 mode 2

§3.3 实测 15872 MiB(=15.5 GiB) 正好落在该区间内(> 11.75 GiB 且 ≤ 15.5 GiB),用作本实验的主探测点。另用 16 GiB(=16384 MiB)(> 15.5 GiB)作对照,确认超 mode 0 边界时拒绝且不记账。

probe_malloc_failure.cpp:87-121view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// §3.4:观察 Committed_AS 变化,排除 mode 2 累计限制
// 主探测:15872 MiB(> CommitLimit 且 ≤ RAM+Swap);对照:16 GiB(> RAM+Swap)
static void probe_committed()
{
auto kib_to_gib = [](long kib) {
return static_cast<double>(kib) / (1024.0 * 1024.0);
};
// Committed_AS:AS = Address Space(地址空间承诺量),随 mmap/munmap 等动态升降
auto committed = []() { return read_meminfo_kb("Committed_AS:"); };

long limit = read_meminfo_kb("CommitLimit:");
printf("CommitLimit: %ld KiB (%.2f GiB)\n", limit, kib_to_gib(limit));

// ① 15872 MiB = 15.5 GiB:> CommitLimit 且 ≤ RAM+Swap → 应通过;若 Committed_AS 超过 CommitLimit 仍成功 → 非 mode 2
long c0 = committed();
size_t sz_ok = kPassCapMiB * 1024ULL * 1024; // 15872 MiB = 15.5 GiB
void* p_ok = mmap(NULL, sz_ok, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
long c1 = committed();
printf("mmap(15872 MiB / 15.5 GiB): %s Committed_AS %ld KiB (%.2f GiB) -> %ld KiB (%.2f GiB)\n",
p_ok == MAP_FAILED ? "FAIL" : "OK",
c0, kib_to_gib(c0), c1, kib_to_gib(c1));
if (p_ok != MAP_FAILED) munmap(p_ok, sz_ok); // 释放本次 mmap 申请的虚拟内存

// ② 16 GiB = 16384 MiB:> RAM+Swap → mode 0 拒绝,且 Committed_AS 应不变
c0 = committed();
size_t sz_fail = kFailCapGiB * kGiB; // 16 GiB = 16384 MiB
void* p_fail = mmap(NULL, sz_fail, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
c1 = committed();
printf("mmap(16384 MiB / 16 GiB): %s Committed_AS %ld KiB (%.2f GiB) -> %ld KiB (%.2f GiB)\n",
p_fail == MAP_FAILED ? "FAIL" : "OK",
c0, kib_to_gib(c0), c1, kib_to_gib(c1));
if (p_fail != MAP_FAILED) munmap(p_fail, sz_fail); // 释放本次 mmap 申请的虚拟内存
}
1
./probe_malloc_failure commit
1
2
3
CommitLimit: 12323644 KiB (11.75 GiB)
mmap(15872 MiB / 15.5 GiB): OK Committed_AS 6953140 KiB (6.63 GiB) -> 23206068 KiB (22.13 GiB)
mmap(16384 MiB / 16 GiB): FAIL Committed_AS 6953140 KiB (6.63 GiB) -> 6953140 KiB (6.63 GiB)
步骤 请求 与条件的关系 结果 Committed_AS 说明
CommitLimit 上限 = 11.75 GiB 划定「须大于此值」
15872 MiB(=15.5 GiB) 11.75 < 15.5 ≤ 15.5(RAM+Swap) 成功 6.63 → 22.13 GiB 记账后已超过 CommitLimit 仍成功 → 不是 mode 2
16384 MiB(=16 GiB) 16 > 15.5(超 mode 0 边界) 失败 6.63 → 6.63 GiB(不变) 未记账即拒绝 → mode 0 单次页数检查

排除 mode 2 累计承诺量限制。


3.5 排除:地址空间碎片化,找不到连续区域

mmap(NULL, size) 要求内核在进程虚拟地址空间中找到一段连续的、长度为 size 的未映射区域。如果已有映射将地址空间切得过碎,即使未映射区域总量够,也可能找不到一段连续 64 GiB 的空闲区间。

区分方法MAP_NORESERVE 标志会让内核跳过 overcommit 承诺检查,但不会跳过"寻找连续区域"这一步。因此:

  • 加了 MAP_NORESERVEmmap 成功 → 连续区域是有的,失败只因承诺检查
  • 加了 MAP_NORESERVEmmap 仍失败 → 可能确实找不到连续区域
probe_malloc_failure.cpp:50-69view raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// §3.5:MAP_NORESERVE 绕过承诺检查,确认失败不是地址碎片化
static void probe_noreserve()
{
for (size_t gb : {16ULL, 64ULL}) {
size_t sz = gb * kGiB;
errno = 0;
void* d = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("%zu GiB default: %s errno=%d\n",
gb, d == MAP_FAILED ? "FAIL" : "OK", errno);
if (d != MAP_FAILED) munmap(d, sz); // 释放本次 mmap 申请的虚拟内存

errno = 0;
void* n = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
printf("%zu GiB NORESERVE: %s errno=%d\n",
gb, n == MAP_FAILED ? "FAIL" : "OK", errno);
if (n != MAP_FAILED) munmap(n, sz); // 释放本次 mmap 申请的虚拟内存
}
}
1
./probe_malloc_failure noreserve
请求大小 默认 mmap mmap + MAP_NORESERVE
16 GiB 失败,errno=12 成功
64 GiB 失败,errno=12 成功

加上 MAP_NORESERVE 后立即成功,说明连续未映射区域完全够用,排除地址空间碎片化


四、内核调用链全貌

下图展示 malloc(64 GiB) 到返回 NULL 的完整内核路径,以及各排查步骤对应的检查点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
                        malloc(64 GiB)

glibc: size > MMAP_THRESHOLD
→ 转为 mmap() 系统调用

┌─────▼─────┐
│ do_mmap() │
└─────┬─────┘

┌───────────────▼───────────────┐
│ 有 MAP_NORESERVE │
│ 且 overcommit_memory ≠ 2? │
└──────┬────────────────┬───────┘
是│ │否
▼ ▼
置 VM_NORESERVE vm_flags 不变
│ │
└────────┬───────┘

┌─────▼──────┐
│mmap_region │
└─────┬──────┘

┌───────────────▼────────────────┐
│ RLIMIT_AS 检查 │
│ total_vm + pages > 上限? │ §3.2 已排除
└──────┬─────────────────┬───────┘
超限│ │通过
▼ │
ENOMEM │
(VA 上限) │
┌─────────▼────────────┐
│ accountable_mapping()│
│ VM_NORESERVE 已置位? │
└──────┬────────────────┘
已置位(MAP_NORESERVE 路径)

┌──────────────────────┤
│ 未置位(默认路径) │已置位(跳过承诺检查)
▼ │
__vm_enough_memory() │
mode 0: │
pages > RAM + Swap? │
┌────┬───────────────────┐ │
│是 │否 │ │
▼ ▼ │ │
ENOMEM 通过 │ │
主机A ────────────────────┘ │
失败点 │
┌──────────────────────┘

get_unmapped_area() §3.5 已排除
查找连续未映射区域
┌────┬──────────────┐
│未找到 │找到
▼ ▼
ENOMEM 建立 VMA
(碎片化) malloc 返回有效指针

主机 A 的默认路径(止于 overcommit 检查):

1
2
3
4
5
6
7
8
malloc(64 GiB)
└─> glibc: mmap(64 GiB) ← 无 MAP_NORESERVE
└─> do_mmap() ← VM_NORESERVE 未置位
└─> mmap_region()
└─> accountable_mapping() == true
└─> __vm_enough_memory()
└─> 16,777,216 > 4,064,672 → ENOMEM
malloc 返回 NULL

MAP_NORESERVE 路径(绕过 overcommit 检查,主机 A 也成功):

1
2
3
4
5
6
mmap(64 GiB, MAP_NORESERVE)
└─> do_mmap() ← VM_NORESERVE 置位
└─> mmap_region()
└─> accountable_mapping() == false ← 跳过承诺检查
└─> get_unmapped_area() ← 找到连续区域
└─> 建立 VMA,返回地址

五、根因总结

项目 内容
直接原因 Linux mode 0 overcommit 检查:单次 mmap 请求页数 > 物理 RAM 页数 + Swap 页数
内核函数 __vm_enough_memory()mm/util.c,Linux v5.14 L885–886)
主机 A 64 GiB(16,777,216 页) > RAM + Swap(4,064,672 页,约 15.5 GiB) → ENOMEM
主机 B 64 GiB 仅占约 3.1 TiB 容量的 2%,检查通过
已排除 进程 VA 上限(RLIMIT_AS)、地址空间碎片化、mode 2 累计承诺量限制

六、测试程序

编译与运行

1
2
3
4
5
6
# 源码展示名:probe_malloc_failure.cpp
g++ -O0 -o probe_malloc_failure probe_malloc_failure.cpp
./probe_malloc_failure mmap # §3.1 定位失败层
./probe_malloc_failure threshold # §3.3 验证 overcommit 边界
./probe_malloc_failure commit # §3.4 排除 mode 2
./probe_malloc_failure noreserve # §3.5 排除地址碎片化

预期输出对照

子命令 主机 A 主机 B
mmap mmap FAIL errno=12;malloc NULL 均成功
threshold ≥15880 MiB 失败,≤15872 MiB 成功 均成功
commit 16 GiB 失败时 Committed_AS 不变;15872 MiB 成功后 Committed_ASCommitLimit 也不报错 均成功
noreserve 默认失败;加 MAP_NORESERVE 成功 均成功

源码

probe_malloc_failure.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string>
#include <sys/mman.h>
#include <unistd.h>

namespace {

constexpr size_t kGiB = 1024ULL * 1024 * 1024;
// §3.4 主探测点:> CommitLimit 且 ≤ RAM+Swap(mode 0 不拒绝)→ 15872 MiB = 15.5 GiB
constexpr size_t kPassCapMiB = 15872;
// 对照:> RAM+Swap,由 mode 0 拒绝
constexpr size_t kFailCapGiB = 16; // 16 GiB = 16384 MiB

static long read_meminfo_kb(const char* key)
{
std::ifstream f("/proc/meminfo");
std::string line;
while (std::getline(f, line)) {
if (line.find(key) == 0) {
long v = 0;
sscanf(line.c_str(), "%*s %ld", &v);
return v;
}
}
return -1;
}

// §3.1:确认失败发生在 mmap 层而非 malloc 层
static void probe_mmap_vs_malloc()
{
size_t sz = 64 * kGiB;
errno = 0;
void* mp = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("mmap(64 GiB): %s errno=%d (%s)\n",
mp == MAP_FAILED ? "FAIL" : "OK", errno, strerror(errno));
if (mp != MAP_FAILED) munmap(mp, sz); // 释放本次 mmap 申请的虚拟内存

errno = 0;
void* p = malloc(sz);
printf("malloc(64 GiB): %s errno=%d (%s)\n",
p ? "OK" : "NULL", errno, strerror(errno));
if (p) free(p); // 释放本次 malloc 申请的内存
}

// §3.5:MAP_NORESERVE 绕过承诺检查,确认失败不是地址碎片化
static void probe_noreserve()
{
for (size_t gb : {16ULL, 64ULL}) {
size_t sz = gb * kGiB;
errno = 0;
void* d = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("%zu GiB default: %s errno=%d\n",
gb, d == MAP_FAILED ? "FAIL" : "OK", errno);
if (d != MAP_FAILED) munmap(d, sz); // 释放本次 mmap 申请的虚拟内存

errno = 0;
void* n = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0);
printf("%zu GiB NORESERVE: %s errno=%d\n",
gb, n == MAP_FAILED ? "FAIL" : "OK", errno);
if (n != MAP_FAILED) munmap(n, sz); // 释放本次 mmap 申请的虚拟内存
}
}

// §3.3:探测 overcommit 检查的边界(RAM+Swap 附近逐步测试)
static void probe_threshold()
{
// 15360 MiB=15 GiB;15872 MiB=15.5 GiB;15880 MiB=15.5078125 GiB;
// 16384 MiB=16 GiB;65536 MiB=64 GiB
for (int mb : {15360, 15872, 15880, 16384, 65536}) {
size_t sz = static_cast<size_t>(mb) * 1024 * 1024;
errno = 0;
void* p = mmap(NULL, sz, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
printf("%5d MiB: %s errno=%d\n",
mb, p == MAP_FAILED ? "FAIL" : "OK", errno);
if (p != MAP_FAILED) munmap(p, sz); // 释放本次 mmap 申请的虚拟内存
}
}

// §3.4:观察 Committed_AS 变化,排除 mode 2 累计限制
// 主探测:15872 MiB(> CommitLimit 且 ≤ RAM+Swap);对照:16 GiB(> RAM+Swap)
static void probe_committed()
{
auto kib_to_gib = [](long kib) {
return static_cast<double>(kib) / (1024.0 * 1024.0);
};
// Committed_AS:AS = Address Space(地址空间承诺量),随 mmap/munmap 等动态升降
auto committed = []() { return read_meminfo_kb("Committed_AS:"); };

long limit = read_meminfo_kb("CommitLimit:");
printf("CommitLimit: %ld KiB (%.2f GiB)\n", limit, kib_to_gib(limit));

// ① 15872 MiB = 15.5 GiB:> CommitLimit 且 ≤ RAM+Swap → 应通过;若 Committed_AS 超过 CommitLimit 仍成功 → 非 mode 2
long c0 = committed();
size_t sz_ok = kPassCapMiB * 1024ULL * 1024; // 15872 MiB = 15.5 GiB
void* p_ok = mmap(NULL, sz_ok, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
long c1 = committed();
printf("mmap(15872 MiB / 15.5 GiB): %s Committed_AS %ld KiB (%.2f GiB) -> %ld KiB (%.2f GiB)\n",
p_ok == MAP_FAILED ? "FAIL" : "OK",
c0, kib_to_gib(c0), c1, kib_to_gib(c1));
if (p_ok != MAP_FAILED) munmap(p_ok, sz_ok); // 释放本次 mmap 申请的虚拟内存

// ② 16 GiB = 16384 MiB:> RAM+Swap → mode 0 拒绝,且 Committed_AS 应不变
c0 = committed();
size_t sz_fail = kFailCapGiB * kGiB; // 16 GiB = 16384 MiB
void* p_fail = mmap(NULL, sz_fail, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
c1 = committed();
printf("mmap(16384 MiB / 16 GiB): %s Committed_AS %ld KiB (%.2f GiB) -> %ld KiB (%.2f GiB)\n",
p_fail == MAP_FAILED ? "FAIL" : "OK",
c0, kib_to_gib(c0), c1, kib_to_gib(c1));
if (p_fail != MAP_FAILED) munmap(p_fail, sz_fail); // 释放本次 mmap 申请的虚拟内存
}

} // namespace

int main(int argc, char* argv[])
{
const char* mode = argc > 1 ? argv[1] : "all";
if (!strcmp(mode, "mmap") || !strcmp(mode, "all")) probe_mmap_vs_malloc();
if (!strcmp(mode, "threshold") || !strcmp(mode, "all")) probe_threshold();
if (!strcmp(mode, "commit") || !strcmp(mode, "all")) probe_committed();
if (!strcmp(mode, "noreserve") || !strcmp(mode, "all")) probe_noreserve();
return 0;
}

七、参考资料

资源 链接
__vm_enough_memoryvm_commit_limit(v5.14) https://github.com/torvalds/linux/blob/v5.14/mm/util.c
__vm_enough_memory(v4.18) https://github.com/torvalds/linux/blob/v4.18/mm/util.c
mmap_regionaccountable_mapping(v5.14) https://github.com/torvalds/linux/blob/v5.14/mm/mmap.c
overcommit 机制文档 https://github.com/torvalds/linux/blob/v5.14/Documentation/vm/overcommit-accounting.rst
proc(5) 手册 https://man7.org/linux/man-pages/man5/proc.5.html

  1. 配套测试程序 probe_malloc_failure.cpp第六节↩︎

  2. 此行为为 Linux 5.14 及以后的实现(mm/util.c L885–886, v5.14)。5.14 之前(如本文主机 B 的 4.18)mode 0 改用基于系统空闲页的启发式估算(mm/util.c, v4.18),公式不同,但对本次场景的结论一致。排查时须先用 uname -r 确认内核主版本号再对照源码。 ↩︎

Sand / 沙子 (SiO₂)
↓ High-temperature reduction with carbon in electric arc furnace / 矿热炉高温强力还原
Metallurgical Grade Silicon (MGS) / 冶金级硅
↓ Chlorination with HCl and fractional distillation / 通入 HCl 气体并反复精馏提纯
Trichlorosilane Gas (SiHCl₃) / 三氯氢硅气体
↓ Modified Siemens Process (CVD) / 改良西门子法(化学气相沉积)
Electronic Grade Silicon (EGS) / 电子级多晶硅(硅原料)
↓ Czochralski (CZ) crystal pulling method / 柴可拉斯基法(单晶旋转提拉)
Silicon Ingot (Silicon Crystal) / 单晶硅棒(硅晶体)
↓ Diamond wire sawing and CMP / 钻石线锯切片与化学机械抛光
Wafer (Bare Wafer) / 晶圆(裸晶圆)
↓ Photolithography, Etching, Ion Implantation / 光刻、刻蚀、离子注入(反复几十次)
Processed Wafer (Patterned Wafer) / 满布电路的晶圆(有图形晶圆)
↓ Wafer testing and Dicing / 晶圆测试与芯片切割
Die (Bare Die) / 管芯(裸片/晶粒)
↓ Wire bonding and Packaging / 金属打线连接与引脚封装保护
Chip (IC / Integrated Circuit) / 芯片(集成电路)

💡 同一个 Wafer 上的各个 Die 电路一样吗?

  • 量产时(绝大多数情况):完全一样。 像盖章一样复制同一套设计图纸(如批量生产某款特定 CPU 或内存),以实现规模化量产。
  • 研发时(极少数情况):可能不一样(MPW,多项目晶圆)。 俗称"拼车"或"班车",几家公司或实验室为了平摊昂贵的模具费,把各自不同的芯片设计拼在同一块 Wafer 上制造,此时的 Die 各不相同。

💡 晶圆非有效区域

  • 划片槽(Scribe Line): 切割 Die 的必需间隙,与省料无关。槽内刻测试图形(Test Structures)做工艺监控,反正划片时会变成粉末,不占良品面积。
  • 边缘残缺区(Edge Dice): Wafer 按网格划片切成方形 Die,但 Wafer 是圆的,最外圈凑不出完整 Die;边缘缺陷率也高,本就无法出货。降级刻对齐标记、测试图案等,榨干辅助价值后报废。

变量约定:

  • LIB:要解析的 *.so 绝对或相对路径(请换成你的文件)。
  • OFF:栈里 +0x... 的十六进制偏移;下文示例中 OFF = 0x8fa860(与 addr2line 所用一致)。
  • PC:栈里 [0x...]进程内虚拟地址;示例 PC = 0x15182684b860(会随每次运行/ASLR 变化
    ,仅为与下组 BASE 配套的一例)。
  • BASE:与上式满足 PC - BASE = OFF 的映射基址;示例 BASE = 0x151825f51000。具体取
    maps 中哪一段以实际进程为准。

场景

栈回溯里常见这种形式:

1
/path/to/libapp.so(+0x8fa860)[0x15182684b860]

含义:

  • libapp.so:出问题的共享库(例名,请替换为实际名)。
  • +0x8fa860:相对该次映射基址的偏移(示例中固定为上述 OFF)。
  • [0x15182684b860]:当时的绝对虚拟地址(示例;单独用时需配合 maps 中的 BASE)。

调试时通常先用 +0x... 偏移磁盘上的 .so 文件 做解析。


首选:addr2line

在带有 debug info-g,且未 strip 掉调试信息)的 .so 上:

1
addr2line -e /path/to/libapp.so -f -C 0x8fa860

形态示例(你本机实跑时会是你的函数与路径):

1
2
myapp::CommandHandler(int, char const**, long, char const*)
/path/to/src/cli/handler.cpp:42

addr2line 可能把路径记成编译时目录(如 out/../handler.cpp),以 DWARF 记录为准;若行号对不上
,多半是二进制与当前源码不是同一版构建。

选项 作用
-e FILE 指定 ELF(.so 或可执行文件)
-f 同时打印函数名
-C Demangle C++ 符号

从「PC + 映射基址」反算并调用(注意 64 位运算)

addr2line 需要的是相对该 ELF 文件布局的偏移,通常就是栈上 +0x...,此处即
0x8fa860。若你只有 PCBASE,应得到 OFF = PC - BASE(在 64 位下计算)。

Bash 的 $(( ... )) 对很大的十六进制字面量会溢出;大地址用 Python 等做减法后再喂给
addr2line

脚本形态(数值与上节 PC / OFF / BASE 一致):

1
2
3
4
5
6
7
8
9
python3 <<'PY'
import subprocess
so = "/path/to/libapp.so" # 换成你的 LIB
pc = 0x15182684b860
off = 0x8fa860 # 与栈中 +0x8fa860 相同
base = pc - off
print(f"BASE = 0x{pc:x} - 0x{off:x} = 0x{base:x}")
subprocess.run(["addr2line", "-e", so, "-f", "-C", hex(pc - base)], check=False)
PY

形态示例(BASE 行与上式为算术一致的一例;函数名/路径为占位):

1
2
3
BASE = 0x15182684b860 - 0x8fa860 = 0x151825f51000
myapp::CommandHandler(int, char const**, long, char const*)
/path/to/src/cli/handler.cpp:42

pc - base 与栈中 +0x8fa860 一致时,与直接 addr2line -e ... -C 0x8fa860 等价。


补充:nm / objdump(看落在哪个符号里)

无行号信息或想确认符号边界时,可按地址排序查看。全量往往很长,可只取前几行,或用 grep已知
串(来自 demangle 后的名字片段)。

1
nm -n --defined-only /path/to/libapp.so | head -5

形态示例:

1
2
3
4
5
0000000000000000 n _GLOBAL_OFFSET_TABLE_
000000000000a000 t _ZN3App6WidgetC1Ev
000000000000a030 t _ZN3App6WidgetD1Ev
000000000000a060 t _ZN3App4InitEi
000000000000a090 t _ZN3App3RunEv

粗判:在排序列表里找 地址不大于目标 0x8fa860 的最后一个对应函数符号。精确行号仍以 addr2line
为准。

符号条数可能很大,例如某次对同一类大 .so 的统计(仅作数量级参考):

1
nm -n --defined-only /path/to/libapp.so | wc -l

形态示例:

1
86817

objdump 在符号表里按短关键字辅助定位;下面地址与偏移 0x8fa860 配套(符号起点略小于该
PC,落在函数体内):

1
objdump -t /path/to/libapp.so | grep 'CommandHandler' | head -3

形态示例:

1
00000000008fa71a l     F .text	0000000000000abc              _ZN5myapp15CommandHandlerEiPPKclS1_

行首 0x8fa71a 为该符号的起点,大小 0xabc0x8fa860 落在区间 [0x8fa71a, 0x8fa71a+0xabc)
内。精确行号仍以 addr2line 为准。

readelf:查看 LOAD 段

下面来自与上述 0x8fa860 同一 ELF 样例的 readelf -l | head -20(段尺寸因库而异,此组与示
例偏移同时出现时便于对照;若你换库则整段以本机为准):

1
readelf -l /path/to/libapp.so | head -20

配套示例输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Elf file type is DYN (Shared object file)
Entry point 0x0
There are 9 program headers, starting at offset 64

Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x00000000007aa408 0x00000000007aa408 R 0x1000
LOAD 0x00000000007ab000 0x00000000007ab000 0x00000000007ab000
0x00000000007bf251 0x00000000007bf251 R E 0x1000
LOAD 0x0000000000f6b000 0x0000000000f6b000 0x0000000000f6b000
0x0000000000317c05 0x0000000000317c05 R 0x1000
LOAD 0x0000000001282e20 0x0000000001283e20 0x0000000001283e20
0x00000000000c67c8 0x00000000000e2b68 RW 0x1000
DYNAMIC 0x00000000012ec758 0x00000000012ed758 0x00000000012ed758
0x0000000000000530 0x0000000000000530 RW 0x8
NOTE 0x0000000000000238 0x0000000000000238 0x0000000000000238
0x0000000000000024 0x0000000000000024 R 0x4

不同 readelf 版本与不同 ELF 细节可能略有差异,以本机实跑为准。


C++ 符号 demangle

1
echo '_ZN5myapp15CommandHandlerEiPPKclS1_' | c++filt

形态示例:

1
myapp::CommandHandler(int, char const**, long, char const*)

(上式 mangled 名须换成你从 nm/objdump/栈上看到的真实字符串。)


文件是符号链时

1
file /path/to/libapp.so

形态示例:

1
/path/to/libapp.so: symbolic link to ../../build/obj/libapp.so

addr2line 应针对实际打开的那份 ELF(解析符号链后的目标或你确认参与调试的那份);若安装目录下链
到构建树,以 inode 与更新时间为准,避免对旧副本解析。


常见问题

  1. ?? 或明显不对

    • 二进制被 strip、或 debug 在单独文件且工具未找到 → 需带 debug 的构建物或 DEBUGINFOD 等。
  2. 行号与当前仓库不一致

    • 对应栈的二进制与当前源码版本/配置不同 → 以构建该库时的源码为准。
  3. 大地址用 Bash 做 PC - BASE

    • 易 32 位溢出;用 python3 或能表示 64 位无符号/有符号整数的工具计算后再调 addr2line

最小复现(变量 + 一次 addr2line

1
2
3
4
SO=/path/to/libapp.so
OFF=0x8fa860

addr2line -e "$SO" -f -C "$OFF"

形态示例:

1
2
myapp::CommandHandler(int, char const**, long, char const*)
/path/to/src/cli/handler.cpp:42

SO 换成你的库路径;偏移若与栈上一致,可继续用 0x8fa860 作对照。函数名、源路径与行号以
你本机 addr2line 输出为准(受调试信息与构建一致性约束)。

美式英语 44 音素(IPA)表

发音教学视频

一、元音 Vowels

1. 单元音 Monophthongs

IPA 示例词 描述
/ɑ/ father 低后不圆唇
/ɪ/ sit 近高前不圆唇
/iː/ beat 高前不圆唇
/e/ 或 /eɪ/ bait 中前不圆唇(美音多为 /eɪ/)
/ɛ/ bet 中前不圆唇
/æ/ bat 低前不圆唇
/ɔ/ thought 中后圆唇(部分美音保留)
/ʌ/ cup 中央不圆唇
/ə/ sofa 中央弱读元音
/u/ goose 高后圆唇
/ʊ/ book 近高后圆唇

2. 双元音 Diphthongs

IPA 示例词
/aɪ/ price
/aʊ/ mouth
/ɔɪ/ choice
/oʊ/ go
/eɪ/ face

二、辅音 Consonants

1. 爆破音 Stops

IPA 示例词
/p/ pat
/b/ bat
/t/ top
/d/ dog
/k/ cat
/g/ go

2. 摩擦音 Fricatives

IPA 示例词
/f/ fan
/v/ van
/θ/ think
/ð/ this
/s/ see
/z/ zoo
/ʃ/ she
/ʒ/ vision
/h/ hat

3. 破擦音 Affricates

IPA 示例词
/tʃ/ church
/dʒ/ judge

4. 鼻音 Nasals

IPA 示例词
/m/ man
/n/ no
/ŋ/ sing

5. 近音 Approximants

IPA 示例词
/l/ light
/r/ red
/j/ yes
/w/ we

CLI11 简介

CLI11 是一个用于处理命令行参数和选项的 C++ 库,旨在简化 C++ 应用程序的命令行界面开发。其主要特点包括:

  1. 简单易用:提供直观的 API,使开发者能够轻松定义和解析命令行选项
  2. 现代 C++ 支持:充分利用现代 C++ 特性,如类型推导和 lambda 表达式
  3. 丰富的选项支持:支持标志选项、位置参数、可选参数和必选参数等
  4. 类型安全:在解析和处理命令行参数时提供类型安全的机制
  5. 灵活的错误处理:提供多种错误处理方式,包括参数验证失败时的错误提示和帮助信息的自动生成
  6. 跨平台支持:可在主流操作系统上运行,包括 Windows、macOS 和各种 Linux 发行版

下载和安装

CLI11 是一个单头文件库,安装非常简单。有以下几种安装方式:

方式一:单文件头文件(推荐)

  1. CLI11 GitHub 仓库 下载最新的 CLI11.hpp 文件
  2. CLI11.hpp 复制到您的项目包含目录中
  3. 在代码中直接包含即可使用:
    1
    #include "CLI11.hpp"

方式二:使用 CMake 集成

如果您的项目使用 CMake,可以通过以下方式集成:

  1. 作为 Git 子模块

    1
    git submodule add https://github.com/CLIUtils/CLI11.git

    CMakeLists.txt 中:

    1
    2
    add_subdirectory(CLI11)
    target_link_libraries(your_target CLI11::CLI11)
  2. 使用 FetchContent(CMake 3.11+):

    1
    2
    3
    4
    5
    6
    7
    8
    include(FetchContent)
    FetchContent_Declare(
    CLI11
    GIT_REPOSITORY https://github.com/CLIUtils/CLI11.git
    GIT_TAG v2.4.1 # 使用最新版本标签
    )
    FetchContent_MakeAvailable(CLI11)
    target_link_libraries(your_target CLI11::CLI11)

方式三:包管理器安装

  • vcpkgvcpkg install cli11
  • Conanconan install CLI11/2.4.1@cliutils/stable
  • Homebrew(macOS):brew install cli11

方式四:全局安装

CLI11.hpp 复制到系统共享文件夹位置(如 /opt/CLI11/usr/local/include),然后在 CMake 中:

1
include_directories(/opt/CLI11)

注意CLI11.hpp 包含整个命令行解析库的核心功能。如果需要使用单独的实用工具(如 Timer、AutoTimer),需要单独复制相应的头文件。

CLI11 API 层级关系

CLI11 的核心数据结构

CLI11 有三个核心数据结构:

  1. App - 应用根对象

    • 所有选项和子命令的容器
    • 代表整个命令行应用程序
    • Subcommand 实际上就是 App 对象,不是独立的数据结构。
      • add_subcommand() 返回 App*,所以 Subcommand 可以无限嵌套。
  2. Option - 选项对象

    • 通过 add_flag()add_option() 创建
    • add_flag() → 返回 Option*
      • 布尔标志,不需要值
      • 示例: app.add_flag("-v", verbose)
    • add_option() → 返回 Option*
      • 需要值的选项
      • 示例: app.add_option("-f", file, "File path")
  3. Option_group - 选项组(继承自 App

    • 继承关系: class Option_group : public App
    • 本质上是 App 对象,可以使用 App 的所有方法
    • 用于组织相关选项
    • 通过 add_option_group() 创建,返回 Option_group*(可转换为 App*
    • 用于实现 Suboption 功能
    • 嵌套支持: 因为继承自 App,可以在 Option_group 中再创建 Option_group 实现多层嵌套

层级结构

层级 0: App(应用根对象)

1
CLI::App app("description");
  • 所有选项和子命令的顶层容器

层级 1: App 的直接子级(三种平级对象)

Option(选项) - 通过 add_flag()add_option() 添加

  • add_flag(): 布尔标志,不需要值
  • add_option(): 需要值的选项
  • 两者都返回 Option*,独立存在

Subcommand(子命令) - 通过 add_subcommand() 添加

  • 返回 App* 对象,可以继续调用 add_subcommand() 实现无限嵌套
  • 独立存在,本身也是 App 类型

Option_group(选项组) - 通过 add_option_group() 添加

  • 返回 Option_group*(继承自 App)
  • 用于组织选项,实现 Suboption 功能

层级 2: 依赖关系

Suboption(子选项) - 通过 Option_group + needs() 实现

1
2
3
4
app.add_flag("-add", add_flag);  // 父选项
CLI::Option_group *add_group = app.add_option_group("add_suboptions", "Sub-options for -add");
add_group->add_option("-file", file_path); // 子选项
add_group->needs(app.get_option("-add")); // 建立依赖
  • 使用: myprog -add -file path.txt(必须先有 -add
  • 多层嵌套: 在 Option_group 中再创建 Option_group

Subcommand 的 Option - 属于 Subcommand

1
2
CLI::App *start = app.add_subcommand("start");
start->add_option("-f", file_path); // 子命令的选项
  • 使用: myprog start -f file.txt

关键区别

类型 API 函数 返回类型 层级 独立性 示例
App CLI::App app("desc") App 0 根对象 应用根对象
Option (flag) app.add_flag() Option* 1 独立 myprog -v
Option (option) app.add_option() Option* 1 独立 myprog -f file.txt
Subcommand app.add_subcommand() App* 1 独立,可嵌套 myprog start
Suboption Option_group + needs() - 2 依赖父 Option myprog -add -file path.txt
Subcommand 的 Option subcmd->add_option() Option* 2 属于 Subcommand myprog start -f file.txt

一个 Suboption 的示例程序

以下是一个 Suboption 的示例程序,不支持子命令使用单横线作为长选项支持子选项
我们选择这个示例是因为 Subcommand 的实现比较简单(就是 App 的无限嵌套),所以我们写了一个支持子选项的示例程序来演示更复杂的用法。

示例程序的层级结构

示例程序演示了三级层级结构:

1
2
3
4
5
6
7
8
9
10
App (myprog)
├── Option (-add) ← Level 1: 顶级选项 (flag)
├── Option (-del) ← Level 1: 顶级选项 (flag, 与 -add 平级)
├── Option (-force) ← Level 1: 顶级选项 (flag, 与 -add 平级)
└── Option_group (add_suboptions) ← Level 2: 子选项组
├── Option (-file) ← Level 2: -add 的子选项 (需要值)
├── Option (-recursive)← Level 2: -add 的子选项 (flag, 不需要值)
└── Option_group (file_suboptions) ← Level 3: -file 的子选项组
├── Option (-encoding) ← Level 3: -file 的子选项 (需要值)
└── Option (-overwrite) ← Level 3: -file 的子选项 (flag, 不需要值)

依赖关系通过以下方式建立:

  • add_group->needs(add_option) - 使选项组要求 -add 选项必须存在
  • file_group->needs(file_option) - 使文件子选项组要求 -file 选项必须存在

示例命令行:

  • 仅 Level 1: myprog -add -del -force
  • Level 1 + 2: myprog -add -file path/to/file.txt -recursive -del -force
  • Level 1 + 2 + 3: myprog -add -file path/to/file.txt -encoding utf8 -overwrite -del -force

源文件说明

cli11-usage-01.cpp - 示例程序

cli11-usage-01.cpp 是一个可执行的示例程序,用于演示子选项功能。可以直接运行并测试功能。

cli11-usage-01.cppview raw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// Copyright (c) 2017-2025, University of Cincinnati, developed by Henry Schreiner
// under NSF AWARD 1414736 and by the respective contributors.
// All rights reserved.
//
// SPDX-License-Identifier: BSD-3-Clause

// 示例程序:演示子选项功能(三级层级)
// 编译: g++ -std=c++11 example.cpp -I../../include -o example
// 运行示例:
// ./example -add -file path/to/file.txt -recursive -del -force
// ./example -add -file path/to/file.txt -encoding utf8 -overwrite -del -force

#ifdef CLI11_SINGLE_FILE
#include "CLI11.hpp"
#else
#include "CLI/CLI.hpp"
#endif

#include <iostream>
#include <string>

int main(int argc, char **argv) {
CLI::App app{"SubOption Example Program"};

bool add_flag = false;
bool del_flag = false;
bool force_flag = false;
std::string file_path; // Level 2: 需要值的选项
bool recursive_flag = false; // Level 2: 标志选项
std::string encoding; // Level 3: 需要值的选项
bool overwrite_flag = false; // Level 3: 标志选项

// 允许非标准选项名(单破折号后面跟多个字符)
app.allow_non_standard_option_names();

// 平级选项:-add, -del, -force
app.add_flag("-add", add_flag, "Add option");
app.add_flag("-del", del_flag, "Delete option");
app.add_flag("-force", force_flag, "Force option");

// -add 的子选项:-file (需要值) 和 -recursive (标志)
// 使用选项组来实现子选项功能
CLI::Option_group *add_group = app.add_option_group("add_suboptions", "Sub-options for -add");
add_group->allow_non_standard_option_names(); // 选项组也需要启用非标准选项名
add_group->add_option("-file", file_path, "File path (requires a value)");
add_group->add_flag("-recursive", recursive_flag, "Process recursively (flag, no value needed)");

// 子选项需要 -add 选项存在
CLI::Option *add_option = app.get_option("-add");
add_group->needs(add_option);

// 第三级层级:-file 的子选项
// 在 -file 选项组下再创建一个选项组
CLI::Option_group *file_group = add_group->add_option_group("file_suboptions", "Sub-options for -file");
file_group->allow_non_standard_option_names();
file_group->add_option("-encoding", encoding, "File encoding (requires a value, e.g., utf8, gbk)");
file_group->add_flag("-overwrite", overwrite_flag, "Overwrite existing file (flag, no value needed)");

// 子子选项需要 -file 选项存在
auto *file_option = add_group->get_option("-file");
file_group->needs(file_option);

// 解析命令行参数
try {
app.parse(argc, argv);
} catch(const CLI::ParseError &e) {
return app.exit(e);
}

// 输出结果
std::cout << "解析结果:\n";
std::cout << " -add: " << (add_flag ? "true" : "false") << "\n";
std::cout << " -del: " << (del_flag ? "true" : "false") << "\n";
std::cout << " -force: " << (force_flag ? "true" : "false") << "\n";

if(add_flag) {
std::cout << " -file: " << (file_path.empty() ? "(未设置)" : file_path) << "\n";
std::cout << " -recursive: " << (recursive_flag ? "true" : "false") << "\n";

if(!file_path.empty()) {
std::cout << " -encoding: " << (encoding.empty() ? "(未设置)" : encoding) << "\n";
std::cout << " -overwrite: " << (overwrite_flag ? "true" : "false") << "\n";
}
}

return 0;
}
构建和运行示例程序
1
2
3
4
5
g++ -std=c++11 cli11-usage-01.cpp -I/path/to/CLI11/include -o cli11-usage-01
# Run examples
./cli11-usage-01 --help
./cli11-usage-01 -add
./cli11-usage-01 -add -file path/to/file.txt -encoding utf8 -overwrite -del -force

实现细节

测试使用 CLI11 的 Option_group 功能和 needs() 方法,确保子选项只有在父选项存在时才有效。

关键实现细节

  1. 非标准选项名:

    • 使用 app.allow_non_standard_option_names() 允许单破折号后面跟多个字符的选项(例如 -add 而不是 --add
  2. 选项组创建:

    1
    2
    CLI::Option_group *file_group = add_group->add_option_group("file_suboptions", "Sub-options for -file");
    file_group->allow_non_standard_option_names();
  3. 依赖关系:

    1
    2
    CLI::Option *add_option = app.get_option("-add");
    add_group->needs(add_option); // 子选项需要 -add 选项存在
  4. 添加子选项:

    1
    2
    3
    4
    // 注意:CLI11 没有 add_suboption() 函数
    // 子选项通过在 Option_group 中使用 add_option() 或 add_flag() 实现
    add_group->add_option("-file", file_path, "File path (requires a value)");
    add_group->add_flag("-recursive", recursive_flag, "Process recursively (flag, no value needed)");