4. MAC层协议实现:以太网MAC帧结构、CSMA/CD机制、MAC地址过滤与FCS校验

MAC层,说白了就是数据链路层的下半部分。它负责把数据装进帧里,通过物理介质发出去,再收回来。我做了这么多年协议栈,发现很多工程师对MAC层的理解停留在“会调API就行”,结果一遇到丢包、错帧、冲突问题就抓瞎。这一章,咱们把MAC层掰开揉碎了讲清楚。

4.1 以太网MAC帧结构——你得知道每一比特在干嘛

以太网帧的结构,IEEE 802.3标准里写得明明白白。但实际项目中,我见过不少人把前导码和帧起始定界符搞混,或者把FCS校验范围弄错。咱们先看标准帧格式:

字段 长度(字节) 说明
前导码(Preamble) 7 10101010... 交替码型,用于接收端时钟同步
帧起始定界符(SFD) 1 10101011,标志帧正式开始
目的MAC地址 6 接收方MAC,可以是单播/多播/广播
源MAC地址 6 发送方MAC
长度/类型(L/T) 2 ≤1500表示长度,≥1536表示上层协议类型
数据(Payload) 46~1500 上层协议数据,不足46字节需填充
帧校验序列(FCS) 4 CRC-32校验,覆盖目的MAC到数据字段
我的经验:前导码和SFD是物理层的事,MAC层芯片通常自动处理。但如果你在做FPGA实现,这两个字段必须自己拼。我曾经在SFD上栽过跟头——少了一个比特的跳变沿,接收端死活锁不住帧头。

这里有个关键点:最小帧长64字节(从目的MAC到FCS)。为什么是64?因为要保证在CSMA/CD机制下,冲突信号能在发送结束前传回来。后面会细说。

4.2 CSMA/CD机制——听、说、撞、退

CSMA/CD,全称是“载波监听多路访问/冲突检测”。你想想看,在早期的同轴电缆以太网里,所有设备共享一根线。谁想发数据,就得先听听线上有没有人在发。没人发,你就发;有人发,你就等。

但问题来了:如果两个设备同时监听,发现没人发,同时开始发送,那就撞上了。CSMA/CD就是处理这个的。它的工作流程可以概括为四个字:听、说、撞、退。

  1. 听(载波监听):发送前先监听信道。如果空闲,等待一个帧间间隔(IFG,96比特时间)后开始发送。
  2. 说(发送):开始发送数据,同时持续监听信道。
  3. 撞(冲突检测):如果发送过程中检测到冲突(信号电平异常),立即停止发送,并发送一个32比特的阻塞信号(Jamming Signal),通知所有节点。
  4. 退(退避重发):执行截断二进制指数退避算法,等待随机时间后重新尝试。

截断二进制指数退避算法

重传次数为n时,退避时间 = 随机数 × 512比特时间,其中随机数范围是 [0, 2k - 1],k = min(n, 10)。

重传超过16次,丢弃该帧并上报上层错误。

我在调试一个工业以太网项目时遇到过一个问题:网络里设备多了,冲突率飙升,吞吐量掉得厉害。查了半天,发现是退避算法的随机数种子没初始化好,导致多个设备退避时间高度重合。嗯,这种坑,你光看协议文档是看不出来的。

4.3 MAC地址过滤——不是你的帧,别收

MAC地址是48位的,前24位是OUI(组织唯一标识符),后24位是设备分配的序列号。但MAC层收到一个帧,怎么判断该不该收?

标准做法是:接收端把帧的目的MAC地址和自己的MAC地址做比较。匹配就收,不匹配就丢。但实际实现中,有几种过滤模式:

  • 单播过滤:只接收目的MAC等于本机MAC的帧。
  • 多播过滤:接收目的MAC是多播地址的帧,通常配合哈希表实现。
  • 广播过滤:目的MAC全F(FF:FF:FF:FF:FF:FF)的帧,一般都要收。
  • 混杂模式:所有帧都收,不管目的MAC是谁。抓包工具就是靠这个工作的。
注意:MAC地址过滤是在硬件层面完成的,不经过CPU。如果你在驱动里看到“所有帧都中断CPU”,那性能一定好不了。我曾经优化过一个网卡驱动,把混杂模式改成硬件过滤,CPU占用率直接从80%降到了15%。

多播过滤的实现,我建议用哈希表。把48位MAC地址哈希成6位或8位索引,查表判断是否接收。但哈希会有冲突,所以上层协议(比如IGMP)还要做二次过滤。

4.4 FCS校验——CRC-32的硬核实现

FCS用的是CRC-32,生成多项式是:

G(x) = x³² + x²⁶ + x²³ + x²² + x¹⁶ + x¹² + x¹¹ + x¹⁰ + x⁸ + x⁷ + x⁵ + x⁴ + x² + x + 1

这个多项式对应的十六进制是 0x04C11DB7。但实际计算时,通常用反射后的值 0xEDB88320。

CRC-32的计算过程:

  1. 初始化CRC寄存器为全1(0xFFFFFFFF)。
  2. 逐字节处理数据,每字节从LSB开始计算。
  3. 处理完所有数据后,取反得到最终的FCS值。

下面是一个软件实现的CRC-32查表法示例:

// CRC-32查表法实现
uint32_t crc32_table[256];

void crc32_init_table() {
    for (uint32_t i = 0; i < 256; i++) {
        uint32_t crc = i;
        for (int j = 0; j < 8; j++) {
            if (crc & 1)
                crc = (crc >> 1) ^ 0xEDB88320;
            else
                crc >>= 1;
        }
        crc32_table[i] = crc;
    }
}

uint32_t crc32_calc(uint8_t *data, uint32_t len) {
    uint32_t crc = 0xFFFFFFFF;
    for (uint32_t i = 0; i < len; i++) {
        uint8_t index = (crc ^ data[i]) & 0xFF;
        crc = (crc >> 8) ^ crc32_table[index];
    }
    return ~crc;
}
硬件实现建议:如果MAC层有硬件CRC引擎,尽量用硬件。软件算CRC-32,1Gbps线速下CPU根本扛不住。我在一个FPGA项目里用LFSR(线性反馈移位寄存器)实现了硬件CRC,一个时钟周期算4比特,线速处理毫无压力。

校验时,接收端对目的MAC到数据字段重新计算CRC,然后与帧尾的FCS比较。如果一致,帧有效;不一致,直接丢弃。这里有个细节:FCS本身不参与校验,它只是结果。

4.5 知识结构总览

下面这张图把MAC层的核心知识点串起来了。我建议你把它打印出来贴在工位上,调试的时候一眼就能定位问题。

MAC层协议实现知识结构 MAC层核心功能 以太网帧结构 前导码 / SFD 目的MAC / 源MAC 长度/类型 / 数据 FCS (CRC-32) CSMA/CD机制 载波监听 → 空闲发送 冲突检测 → 阻塞信号 退避算法 → 重传 最大重传16次 MAC地址过滤 单播 / 多播 / 广播 混杂模式 硬件过滤 vs 软件过滤 哈希表多播过滤 FCS校验 CRC-32多项式 查表法 / LFSR硬件实现 校验范围:目的MAC~数据 校验失败 → 丢弃帧 核心原则:先听后发、边发边听、冲突退避、校验收尾 最小帧长64字节保证冲突检测有效性

4.6 实战中的几个坑

最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路:

  • 最小帧长问题:如果你在调试时发现某些短帧被接收端丢弃,先检查数据长度。小于46字节的Payload必须填充到46字节,否则接收端会认为帧不完整。
  • FCS计算范围:CRC只覆盖目的MAC到数据字段,不包括前导码、SFD和FCS本身。我见过有人把前导码也算进去,结果校验永远不过。
  • 多播哈希冲突:硬件多播过滤用的是哈希表,不同多播地址可能映射到同一个哈希桶。这时候上层协议(如IGMP)必须做二次过滤,否则你会收到不该收的帧。
  • 退避时间精度:CSMA/CD的退避时间单位是512比特时间。在10Mbps下是51.2μs,在100Mbps下是5.12μs。如果你的定时器精度不够,退避时间会偏差很大,导致冲突率上升。

一句话总结:MAC层是通信栈的“交通警察”。帧结构是规则,CSMA/CD是调度算法,地址过滤是门禁,FCS是安检。四个环节环环相扣,任何一个出问题,数据都到不了上层。