CRC(循环冗余校验)原理
一句话讲透:把数据看成一个 GF(2) 上的多项式,用一个约定好的生成多项式 做「模 2 长除法」,把余数当校验值;接收方对「数据 + 校验值」做同样的除法, 余数为 0 就认为没出错。
本文只讲逻辑:它在算什么、为什么这样算就能发现错误、工程上如何把 「逐位长除法」变成一次查表。目的不是给你一份实现,而是让你能在白纸上把 这个算法重新推一遍。
它到底解决什么问题
数据在链路、磁盘、内存里传输存储时,比特会翻转(噪声、硬件故障、半写)。 我们希望额外附加一个短、算得快、纯软件可实现的冗余量,用来 高概率地发现「收到的数据和发出的不一样」。
CRC 的定位必须先划清:
| 能力 | CRC | 密码学哈希(SHA-256 / HMAC) |
|---|---|---|
| 发现随机比特翻转 | ✅ 强 | ✅ 更强 |
| 纠错 | ❌ | ❌ |
| 防篡改 / 抗伪造 | ❌ 无密钥,可构造碰撞 | ✅ 有密钥时(HMAC)或抗碰撞(哈希) |
| 速度 | 纳秒级 / GB/s | 慢几十倍 |
| 长度 | 8 / 16 / 32 / 64 bit | 128 / 256 / 512 bit |
所以:CRC 是「错误检测」,不是「加密」,也不是「防篡改」。拿 CRC 当 完整性签名(攻击者能让改过的数据保持同一 CRC)是经典误用。
同类手段的能力排序(同样长度的冗余):
| 手段 | 能力 | 成本 |
|---|---|---|
| 奇偶校验(parity) | 只能发现奇数个比特错误,漏检率高 | 几乎为零 |
| 加法校验和(sum) | 对字节顺序、零字节不敏感;整段移位/交换常常无感 | 很便宜 |
| Fletcher / Adler-32 | 位置敏感,比 sum 强很多(ZFS 元数据用过 Fletcher) | 便宜 |
| CRC | 对突发错误(burst)有确定的检出保证,位级错误概率可控 | 一次查表 / 一条指令 |
| 密码学哈希 | 抗碰撞,但为防篡改设计,代价高 | 慢 |
数学基础:一串比特就是一个多项式
CRC 的全部魔法来自一个换视角的动作:把比特串看成系数只有 0/1 的多项式, 最高位对应最高次项。
在 GF(2)(系数只有 0/1 的有限域)上做运算,性质和我们熟悉的多项式略有不同:
- 加法 = 减法 = 按位异或(XOR)。没有进位、没有借位:
1 + 1 = 0。 这一步是 CRC 高效的全部原因 —— 除法里的"减法"从带借位的整数减法变成了 一条 XOR 指令。 - 乘法 = 无进位乘法(后来 CLMUL 这类"carry-less multiply"指令就是干这个的)。
- 最高位固定为 1 的多项式可以用剩余低位唯一表示(所以
0x04C11DB7这样的 参数其实省略了x³²那一项)。 - 关键性质 1:A XOR A = 0。
- 关键性质 2:若 A 能被 G 整除,且 C 能被 G 整除,则 A XOR C 也能被 G 整除 (线性的来源,后面讲分段/并行全靠它)。
核心运算:模 2 长除法
和小学竖式除法一样,只是每一步的"减"换成 XOR,而且"够不够减"只看当前最 高位是不是 1(是 1 才 XOR 除数,等价于商的这一位取 1;是 0 就跳过)。
1 0 1 ← 商(实际只关心余数)
┌──────────
1 0 1 1 │ 1 1 0 1 0 1
1 0 1 1 ← 对齐被除数的最高位 1,按位 XOR
─────────
0 1 1 0 0 1
1 0 1 1 ← 最高位仍为 1,继续 XOR
─────────
0 0 1 1 1 1
1 0 1 1
─────────
0 0 0 1 0 0 ← 余数 100
两个值得记住的结论:
- 余数的宽度固定 = 除数位数 − 1(除数最高位恒为 1,所以 r 位余数 ⇔ r+1 位除数)。CRC-32 就是"除数是 33 位多项式、余数是 32 位"。
- 整个过程只需要:一次
<<、一次比较最高位、一次 XOR。逐位实现天然就是 这个形状。
CRC 的定义
约定一个 r+1 位生成多项式 G(x)(最高位为 1,如 1011 即 x³+x+1), 对数据 M(x) 做:
① 数据左移 r 位:M(x) · x^r —— 即数据后补 r 个 0,给校验值腾位置
② 求余数: CRC = (M·x^r) mod G
③ 组帧: 帧 = 数据 ‖ CRC (CRC 占 r 位,高位补 0 到 r 位)
④ 校验: 帧 mod G == 0 ? —— 接收方把整帧直接除以 G,余数为 0 即通过
为什么 ④ 会成立(这就是 CRC 那句"余数为 0 就对"的来历):
M·x^r 除以 G ⇒ M·x^r = Q·G XOR CRC
两边同时 XOR CRC: M·x^r XOR CRC = Q·G
而 帧 = M·x^r XOR CRC(拼接恰好等价于异或,因为低 r 位原来是 0)
∴ 帧 一定能被 G 整除
注意这里不是"数据能被整除",而是"数据左移 r 位后补上余数、整体能被 整除"。所以 CRC 值本身没有任何独立含义,它就是"余数"。
一个小例子(G = 1011,即 x³+x+1,r = 3;数据 M = 11010):
M 11010
M · x³ 11010000 (补 3 个 0)
÷ 1011 取余 010 (按上面的长除法)
帧 11010 010 = 11010010
验证:11010010 ÷ 1011 → 余数 000 ✅
为什么这样一个余数能发现错误
把传输/存储中发生的比特翻转统一建模成叠加了一个错误多项式 E(x):
因为"帧"本身能被 G 整除,所以:
余数是否非零,只取决于 E 是不是 G 的倍数。于是 CRC 的检出能力完全由 生成多项式 G 的因式结构决定,一条条都能证明:
| 错误模式 | 一定能检出? | 原因 |
|---|---|---|
| 任意 1 个比特翻转 | ✅ | E = x^k;标准 G 的常数项为 1,所以 x^k 不可能被 G 整除 |
| 任意 2 个比特翻转 | 对所有 j < 帧长,G 不整除 (1 + x^j) 时 ✅ | E = x^a(1 + x^j);G 不含 x 因子 → 等价于要求 G 不整除 (1 + x^j) |
| 任意奇数个比特翻转 | G 含 (x + 1) 因子时 ✅ | 奇数个错误 ⇔ E(1) = 1;若 (x+1) \| G 且 G \| E,则 E(1) = 0,矛盾 |
| 突发错误,长度 ≤ r | ✅ | E = x^a·B(x) 且 deg B ≤ r−1 < deg G,B 不可能是 G 的倍数 |
| 突发错误,长度 = r+1 | 漏检概率 2^-(r-1) | deg B = r,只有 B = G 这一种情况会被整除,而 B 的中间 r−1 位是任意的 |
| 突发错误,长度 > r+1 | 漏检概率 2^-r | 长度为 L 时能整除的 B 有 2^(L-2-r) 种,总共有 2^(L-2) 种 |
| 任意随机错误 | 漏检概率 2^-r | 除非 E 恰好是 G 的倍数 |
读表可以得出几个实用结论:
- CRC 从不承诺"绝不漏检",只承诺"特定模式一定检出 + 其余模式概率极小"。 CRC-32 的漏检概率约
2^-32,而"突发错误 ≤ 32 bit 必检出"是它的硬保证 —— 这正是网络/磁盘关心的模式(噪声通常是一小段连续比特)。 - 突发(burst)能力是 CRC 相对 sum/Adler 的核心优势,也是它被选进以太网、 磁盘扇区、KV 引擎 WAL 的原因。
(x + 1)因子换来"奇数个比特错误必检",所以好用的标准多项式(如 CRC-32 的0x04C11DB7)都刻意含(x+1)。- 还有一个常被引用的指标是汉明距离(HD):HD = 4 表示"任意 ≤ 3 个比特 翻转都能发现"。例如 CRC-32/ISO-HDLC 在数据长度 ≤ 916 bit 时 HD = 4。 想要"两比特错误一定检出"的适用范围更宽,就得换多项式(这就是 CRC-32C / Castagnoli 在存储领域流行的原因之一)。具体数值可以查 Koopman 的 CRC 多项式目录,不必自己推。
参数模型:一个多项式远远不够
标准里的 CRC 由一组参数完全确定(Rocksoft 参数模型):
| 参数 | 含义 | 为什么存在 |
|---|---|---|
width | 余数位数(8/16/32/64) | 检错能力与开销的取舍 |
poly | 生成多项式(省略最高位) | 各标准/厂商的约定 |
init | 寄存器初值 | 非零初值能覆盖"前导 0 丢失"等场景,也能区分"同一数据的重复帧" |
refin | 输入是否按位反转(LSB-first) | 串行电路(以太网)天然先收到字节的最低比特,"反射"让位序与硬件一致 |
refout | 输出是否反转 | 与 refin/实现配套;两者常同时为 true |
xorout | 结果再 XOR 一个常量 | 常见 0xFFFFFFFF,让"全 0 数据 + 全 0 CRC"这类边界情况也表现为非零 |
"反射"到底反射了什么
最难理解的是 refin/refout,建议这样记:
- 反射(reflected)是一种等价的改写:把多项式的系数/移位方向整体左右翻转, 于是"从高位读入、左移"变成"从低位读入、右移"。数学上仍然在做同一个模 2 除法,只是走位顺序不同。
- 反射版实现常见写法是右移 + 反射多项式(CRC-32 的
0x04C11DB7反射后是0xEDB88320)。 - 同一个多项式可以有两种位序写法,所以看到 "CRC-32/ISO-HDLC" 和 "以太网 FCS" 时不要以为有两个算法:以太网 FCS 是同一多项式按 MSB-first 逐位算出的结果,与 zlib/ZIP 的 CRC-32 互为位反转。
- 最常见的复现坑:拿到标准参数表(如
init=0xFFFFFFFF, refin=true)时, 初值要先按反射规则放进寄存器(等价于对初值做位反转),否则算出来的值对 不上任何一组已知测试向量。
常见标准一览
| 名称 | width | poly | init | refin/refout | xorout | 典型用途 |
|---|---|---|---|---|---|---|
| CRC-8 | 8 | 0x07 | 0x00 | 否 | 0x00 | 轻量协议(如部分传感器/固件) |
| CRC-16/ARC(IBM) | 16 | 0x8005 | 0x0000 | 是 | 0x0000 | Modbus、旧串行协议 |
| CRC-16/CCITT-FALSE | 16 | 0x1021 | 0xFFFF | 否 | 0x0000 | 各种通信/遥测协议 |
| CRC-32/ISO-HDLC | 32 | 0x04C11DB7 | 0xFFFFFFFF | 是 | 0xFFFFFFFF | ZIP、PNG、gzip、以太网(位序不同) |
| CRC-32C(Castagnoli) | 32 | 0x1EDC6F41 | 0xFFFFFFFF | 是 | 0xFFFFFFFF | iSCSI、SCTP、RocksDB/LevelDB、Kafka v2、SSE4.2 硬件指令 |
| CRC-64/XZ | 64 | 0x42F0E1EBA9EA3693 | 0xFFFFFFFFFFFFFFFF | 是 | 0xFFFFFFFFFFFFFFFF | xz、部分文件系统 |
存储/分布式系统的现状值得单独记一下:CRC-32C 是"现代默认选型" —— 有 x86 SSE4.2 与 ARMv8 的硬件指令、有成熟的合并(combine)算法,所以 RocksDB、 LevelDB、Kafka、iSCSI、HDFS(新版本)都选它;而 CRC-32/ISO-HDLC 因为历史 包袱几乎无处不在(ZIP/PNG/gzip/以太网)。
工程实现:从逐位到一条指令
① 逐位实现(对应长除法)
寄存器宽度 = r,每次把数据的一个比特移进来,看移出的那一位是不是 1:
这就是长除法的直接翻译:每处理 1 bit 迭代一次,8 bit 数据就要 8 轮。
② 查表实现(byte-at-a-time)
关键观察:模 2 除法是线性的 —— 每个比特对最终余数的贡献互相独立、可以用 XOR 叠加。所以"某 8 位块在最高位位置上的贡献"可以预先把 256 种情况全算好:
表 T[256]:T[i] = 把字节 i 放在最高位做 8 轮逐位除法得到的余数
随后每个字节的处理变成:
crc = (crc >> 8) XOR T[(crc XOR byte) & 0xFF] ← 反射版(LSB-first)
crc = (crc << 8) XOR T[(crc >> 24) XOR byte] ← 非反射版(MSB-first)
flowchart LR
B["输入字节 byte"] --> X["(crc XOR byte) & 0xFF"]
X --> L["查表 T[·]"]
C["当前 crc"] --> S["crc >> 8"]
L --> O["XOR"]
S --> O
O --> N["新 crc"]
要点:
- 表大小只有 256 项 × r bit(CRC-32 约 1 KB),生成一次即可复用,生成 逻辑就是"对 i 做 8 次逐位除法"。
- 查表和 XOR 与数据的每个字节一一对应,所以 CRC 天然支持流式/增量计算: 读一块算一块,不需要把整份数据放内存。
③ 更快的做法
| 做法 | 思路 | 代价/收益 |
|---|---|---|
| slicing-by-4 / -8 / -16 | 一次吃 4/8/16 字节,维护多张表,打断"查表→移位→再查表"的串行依赖链 | 表变大(KB~MB);能逼近内存带宽 |
| PCLMULQDQ 折叠(fold) | 把 CRC 看成 GF(2) 多项式乘法,用 carry-less multiply 指令并行处理 16 字节 | 实现复杂;软件实现里最快(zlib-ng、Chromium 等在用) |
| 硬件 CRC 指令 | x86 SSE4.2 crc32、ARMv8 crc32*:一条指令算一个字节的 CRC-32C | 只支持特定多项式(如 CRC-32C),跨平台需 fallback |
这也解释了为什么"换多项式"不是免费的:硬件指令绑定了多项式(CRC-32C), 所以选多项式时除了纠错能力,还要看有没有硬件加速。
④ 合并(combine):为什么能并行/分片
由线性性质可推出:crc(A ‖ B) 可以由 crc(A)、crc(B) 和 len(B) 直接 算出(本质是"把 A 的余数再左移 len(B) 位后与 B 的余数叠加")。因此:
- 大文件校验可以切块并行,最后合并;
- 分布式系统可以按块校验再组合(不必重读全量数据);
- 也说明 CRC 是可结合的,跨语言/跨实现只要参数一致,结果一致。
常见误区与工程实践
- 不要用 CRC 做防篡改/去重安全判断。无密钥 → 可以构造两份 CRC 相同的 不同数据;同理不能替代 SHA-256。
- CRC 不携带长度信息:协议里必须同时传输长度(或把长度纳入 CRC 覆盖 范围),否则"尾部少了字节且恰好仍能整除"(概率约
2^-r)无法排除。 换句话说,CRC 检的是"比特被翻转",不是"字节被丢弃"。 - 覆盖范围要显式约定:CRC 只能保护参与计算的那部分字节。以太网 FCS 覆盖目的/源 MAC + 类型/长度 + 载荷,但不含前导码与 SFD;协议头被改而没 纳入 CRC,检不出来。
- 注意 CRC 值本身是否被"再保护":有些格式把 CRC 放在头部并覆盖头部 自身(计算时该字段先填 0),有些格式只覆盖载荷 —— 复现别人的值之前先 确认这一点。
- 校验失败的处理策略要有层级:重传(网络)、读副本 / 修复(存储)、 报错并丢弃(日志)。CRC 只负责"报告坏了"。
- 跨平台一致性:同一份数据在 x86 与 ARM、反射版与非反射版实现之间, 值可能不同 —— 选参数时优先用现成标准(参数表里有测试向量可对), 并写测试固定住"空数据 / 单字节 / 长数据"的期望值。
一页速记
数据 = GF(2) 多项式(1 = 有这一项)
加/减 = XOR(无进位) ← 高效的全部原因
CRC = (数据 · x^r) mod G(x) ← 数据补 r 个 0 后取余数
校验 = (帧) mod G == 0 ← 因为 帧 = M·x^r XOR CRC ≡ 0 (mod G)
能力 = 由 G 的因式结构决定:突发错误 ≤ r 必检、奇数个错误必检(含 x+1)、
其余以约 2^-r 概率漏检
实现 = 逐位长除法 → 查表(8 位/轮)→ slicing / CLMUL / 硬件指令
参数 = width + poly + init + refin + refout + xorout(缺一项就复现不出来)
定位 = 快速错误检测,不是纠错、不是加密、不防篡改
Backlinks (1)
flowchart LR
n0["CRC(循环冗余校验)原理"]
n1["Algorithms"]
n2["Database"]
n3["Bitcask 存储模型(日志结构哈希表)"]
n4["KV 数据库对比"]
n5["KV 数据库"]
n6["PostgreSQL 活跃会话诊断(Active Sessions)"]
n7["PostgreSQL 磁盘占用查看(Table Size / Datafile)"]
n8["PostgreSQL"]
n9["PostgreSQL 内存占用查看(Memory / Buffers / Connections)"]
n10["PostgreSQL 查看慢查询(Slow Query)"]
n11["PostgreSQL WAL 与 LSN"]
n12["Knowledge"]
n13["Cloud"]
n14["Object Storage"]
n15["Infrastructure"]
n1 --> n0
n2 --> n5
n2 --> n8
n4 --> n3
n5 --> n3
n5 --> n4
n8 --> n6
n8 --> n7
n8 --> n9
n8 --> n10
n8 --> n11
n9 --> n6
n9 --> n7
n10 --> n6
n12 --> n1
n12 --> n2
n12 --> n15
n13 --> n14
n15 --> n13
click n0 "./" "CRC(循环冗余校验)原理"
click n1 "../" "Algorithms"
click n2 "../../database/" "Database"
click n3 "../../database/kv/bitcask/" "Bitcask 存储模型(日志结构哈希表)"
click n4 "../../database/kv/comparison/" "KV 数据库对比"
click n5 "../../database/kv/" "KV 数据库"
click n6 "../../database/postgresql/active-sessions/" "PostgreSQL 活跃会话诊断(Active Sessions)"
click n7 "../../database/postgresql/disk-usage/" "PostgreSQL 磁盘占用查看(Table Size / Datafile)"
click n8 "../../database/postgresql/" "PostgreSQL"
click n9 "../../database/postgresql/memory-usage/" "PostgreSQL 内存占用查看(Memory / Buffers / Connections)"
click n10 "../../database/postgresql/slow-queries/" "PostgreSQL 查看慢查询(Slow Query)"
click n11 "../../database/postgresql/wal-lsn/" "PostgreSQL WAL 与 LSN"
click n12 "../../" "Knowledge"
click n13 "../../infrastructure/cloud/" "Cloud"
click n14 "../../infrastructure/cloud/object-storage/" "Object Storage"
click n15 "../../infrastructure/" "Infrastructure"