JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

抓住錯誤:奇偶校驗、檢查碼與 CRC

在第一篇指南裡,你學會了如何在位元周圍畫線,把每一塊稱為一個框幀。但導線是嘈雜的,位元可能在途中翻轉。這篇指南會說明接收端如何看出有東西到達時出了錯——以及為什麼偵測錯誤遠比修正錯誤容易。

位元為何會出錯,以及我們為何必須察覺

從實體層那個階梯,你已經知道一個不愉快的事實:媒介是類比且嘈雜的,接收端三不五時會把送出的 1 誤認為 0,或者反過來。這種事多常發生,由位元錯誤率來刻畫——比方說,每一千萬個位元裡翻轉一個。聽起來微不足道,直到你想起單一個網頁可能就有數千萬個位元。一個你從未察覺的翻轉,可能把銀行餘額、一個像素,或一行程式碼悄悄變成胡言亂語。

資料鏈結層就坐在導線正上方,所以它是第一個能對此做點什麼的地方。在第一篇指南裡,你學到它如何把位元包裝成一個邊界清楚的框幀。接下來自然的問題是:一旦我有了一個框幀,怎麼知道它是否完好地撐過了旅程?錯誤偵測的全部重點,就是讓接收端在從未見過原件的情況下回答這個問題。

訣竅是隨著資料一起送出一點額外的東西:一段從位元算出來的簡短摘要,放在框幀的尾端。傳送端計算這段摘要,接收端從它實際收到的位元重新計算,然後比對兩者。如果它們不一致,途中就有東西變了。這份額外的東西稱為冗餘——它不攜帶任何新資訊,只提供一種方法去核對你已經擁有的資訊。

奇偶校驗:一個位元抓一次翻轉

最簡單的偵測器多加一個位元,也就是奇偶校驗位元。挑一個規則——比方說偶校驗——並設定這個額外位元,讓 1 的總數(連校驗位元本身一起算)永遠是偶數。要送出 1011001,數一數 1:有四個,已經是偶數,所以校驗位元是 0,你送出 10110010。若資料改成 1011000(三個 1,奇數),校驗位元就會是 1,得到 10110001。接收端只要數一數它收到的所有東西裡有幾個 1,檢查是否為偶數即可。

現在假設途中有一個位元翻轉。1 的計數正好變動一個,於是偶數總和變成奇數,接收端立刻知道這個框幀壞了。漂亮地簡單——但留意這道裂縫:如果有兩個位元翻轉,計數變動兩個、仍維持偶數,於是錯誤就神不知鬼不覺地溜過去了。單一位元的奇偶校驗能抓到任何奇數次的翻轉,卻對任何偶數次的翻轉視而不見。它是最便宜的檢查,也是最脆弱的。

你可以做得更好:把位元排成一個格子,為每一列與每一行都加上校驗位元——也就是二維奇偶校驗。現在單一個翻轉會正好破壞一個列檢查和一個行檢查,它們的交會點就指出那個有罪的位元,於是你甚至可以修復它。從偵測跨到定位的這一跳,正是錯誤更正的種子,我們會在最後遇到它。但二維奇偶校驗仍會被某些成簇的翻轉騙過,所以對真正的連結,我們想要更強得多的東西。

檢查碼:把它全部加起來

檢查碼從另一個角度切入。它不數 1,而是把資料當成一串數字加總起來,然後把這個總和(或它整理過的版本)一起送出。舉例來說,IP、TCP 與 UDP 所用的網際網路檢查碼,會把資料切成 16 位元的字組,用一種特別的回繞進位把它們加總,再送出取反後的總和。接收端把所有東西連同檢查碼一起相加;若沒有任何變動,結果會是一個已知的固定樣式,也就是全部都是 1。

檢查碼比單一個奇偶校驗位元更強,因為每一個資料位元都會以一種透過進位層層擴散的方式貢獻給總和。但它有眾所周知的盲點:由於加法不在乎順序,把兩個字組對調並不會改變總和,而某些成對的翻轉也能彼此抵消。當年選用網際網路檢查碼,是因為它在 1970 年代緩慢的機器上以軟體計算既快又容易,而不是因為它是最強的偵測器——這是一個誠實的工程取捨,而非當時能拿到的最佳數學。

CRC:用一個神奇的數字去除

幾乎每一條真實連結——乙太網路、Wi-Fi、磁碟機——的主力,都是循環冗餘檢查,也就是 CRC。它的想法美妙地具體:把整個框幀的位元當成一個巨大的二進位數字,然後用一個固定、雙方議定好的數字(稱為生成多項式)去除它。我們不留商;我們留餘數。那個餘數就是 CRC,它會被附加到框幀的尾端。

這裡是優雅之處。傳送端先補上幾個零位元、做除法,然後把那些零換成它算出的餘數。現在整個框幀加上 CRC 正好能被生成多項式整除,沒有餘數。接收端只做一件事:用同一個生成多項式去除它收到的位元。如果餘數是零,這個框幀幾乎肯定是乾淨的;如果餘數不是零,就有一個錯誤在對你大喊。接收端從不需要知道原件——零餘數就是證明。

SENDER                                RECEIVER
  data:        1 1 0 1 0 1 1            received: 1101011 . 100
  generator:   1 0 0 1   (degree 3)             divide by 1001
  append 3 zeros -> 1101011000                  remainder = 000  -> OK
  divide by 1001 (XOR, no borrows)
  remainder    -> 1 0 0                  but if a bit flipped:
  send: 1101011 . 100  (data + CRC)             remainder != 000 -> DROP
迷你版的 CRC:補零、做除法、送出餘數;接收端做除法並期望得到零。

為什麼 CRC 比檢查碼好那麼多?因為這不是普通的算術——這個除法用的是不帶進位的逐位元 XOR,行為像是多項式上的代數,而數學家精挑了具有可證明威力的生成多項式。一個好的標準生成多項式,保證能抓到每一個單位元與雙位元錯誤、每一種奇數個錯誤,以及任何短於 CRC 本身的成簇翻轉——正是真實導線會產生的那種叢發雜訊。它在硬體裡也很快:幾個移位暫存器加 XOR 閘,就能以線速把一個框幀逐位元推過去。

偵測不是修正:談談更正

到目前為止的一切都只是偵測。當 CRC 失敗時,框幀就被丟掉,必須由更上層的某個機制安排重送它。那套重試機器就是自動重送請求,也就是下一篇指南的主題,在那裡傳送端會重送任何接收端無法確認的東西。偵測加上重送,是有線網際網路上最主流的配方,因為當連結又快又少出錯時,重送很便宜。

有時重送太慢或根本不可能——一艘深空探測器、一段現場廣播串流、一張你無法再問一次的刮傷磁碟。在那些場合,接收端必須靠自己修復資料,使用前向錯誤更正。代價是更多冗餘:你送出的不是一小段 CRC,而是經過精心安排的額外位元,好讓最可能出現的錯誤樣式能被逆轉回來。漢明碼是經典的教學範例——透過讓數個奇偶校驗彼此重疊,它不僅偵測到單一個翻轉的位元,還能精確指出是哪一個位元翻了,於是接收端能把它翻回去。

所以這裡有一個真正的取捨,而非明確的贏家。偵測很便宜,但需要一套重試的辦法;更正不需要回程,卻一直在頻寬上付出代價,即使什麼都沒出錯。真實系統會把兩者混用:Wi-Fi 與行動網路倚重前向錯誤更正,因為它們的連結嘈雜且往返很慢,而資料中心裡一條安靜的光纖,則樂於用一個 CRC 加上偶爾的重送。正確的選擇永遠取決於連結有多嘈雜,以及再問一次有多昂貴。