☰
LTE RLC协议:TM/UM/AM模式、状态变量与定时器机制解析
2026/10/28 13:28:20 网站建设 项目流程

简介:《3GPP LTE RLC中文协议》是一份面向LTE无线接入网研发、协议栈实现与测试人员的中文技术资料,对应TD-LTE数字蜂窝移动通信网Uu接口技术要求第7部分,系统规定RLC协议的功能、子层结构、实体、上下层业务、协议数据单元及异常数据处理等内容,适合作为协议学习、代码实现和问题排查的案头参考。压缩包内为1个doc文档,大小约1.74MB,内容涵盖范围、规范性引用、术语与缩略语,并涉及确认模式(AM)等关键概念,便于按章节查阅。该资料已有496人学习,说明在LTE协议栈相关人群中具有一定参考价值。读者可据此理解RLC数据传输、分段与重组、错误检测和纠正机制,把握实现架构与测试要点,并可与3GPP TS 36.331等规范对照使用,提升协议理解与工程落地效率。

1. RLC 为什么在 MAC 和 PDCP 之间:一套看起来多余的可靠层

翻 LTE 空口协议栈框图,很多人的第一反应是:MAC 已经有 HARQ 了,上面为什么还要再压一层 RLC 做 ARQ?两层都在重传,这不是重复劳动吗。真跑过链路的人会告诉你,HARQ 的职责是把误块率从一成压到百分之一量级,剩下的那点残留它不管;RLC 要接手的恰恰是这百分之一,以及 HARQ 根本没资格碰的事——切换瞬间的缓存怎么迁移、重传时传输块尺寸变了怎么办、上层一个几 KB 的包怎么塞进一个只有几百字节的授权里。所以 RLC 的关键不在"它也重传",而在于它是整条栈里唯一知道上层数据流边界的那一层。搞明白它的状态变量和定时器怎么咬合,空口可靠传输的下半场就算通了。

这份 RLC 协议中文规范文本的价值也在这里:散在英文规范各章节里的状态变量、定时器、过程分支,被收拢到一处,能顺着一条数据包的路径从头读到尾。

2. TM、UM、AM 的分野:三种模式各砍掉了哪一层机制

2.1 传输机会反向驱动:PDU 不是想造就造

协议里有一句很容易被跳过的约束:仅当下层通知了一个传输机会时,才形成 RLC PDU,然后分发给下层。这一句话决定了整个 RLC 的设计风格。

RLC 不是先把 SDU 切好排队、等着被取走,而是等 MAC 说"这次给你 N 字节",才现场决定怎么切、怎么拼、头部怎么填。原因很朴素:MAC 每个 TTI 的传输块尺寸由调度器、MCS、天线层数共同决定,随时在变。RLC 如果预先切好,切出来的尺寸大概率对不上——要么授权没用满浪费吞吐,要么 PDU 塞不进去直接卡死。

所以 RLC 的分段和级联本质上是事后适配,把一个不定长的字节流动态塞进一个每次都不同的坑。三种模式的区别,说到底就是"在这个坑里填什么、填完留不留记号、留了记号对方回不回话"。这个视角比背三条定义有用得多。

顺手说一句踩坑经验:很多人第一次实现 RLC,习惯把 SDU 队列按固定大小切片缓存,跑单测没问题,一上真实调度就翻车,因为授权尺寸是抖的。

2.2 TM:把 SAP 当直通管,用零状态换零开销

TM 实体做的动作少到几乎没有:收到 SDU 不切、不分、不拼,不加任何 RLC 头,直接往下递;收到 PDU 也不解析,整条往上递。能跑的逻辑信道只有 BCCH、PCCH 和上下行 CCCH 这几类。

为什么能这么"裸"?两个原因。一是跑在这几条信道上的消息——系统广播、寻呼、接入期的公共控制——尺寸在协议里已经被约束到足够小,能整个装进一个传输块,不需要分段机制兜底。二是接收方在还没建立任何上下文的时候就得能解析这些消息,加 SN、加长度指示、加分段标志,每一项都在增加解析负担,而收益是零。

TM 真正的价值是零状态:RLC 重建时它不需要复位任何变量、不需要清任何缓存、不需要停任何定时器。公共控制类消息至今还挂在 TM 上,靠的就是这个特性。

2.3 UM:加了 SN 和重排序,但不回头

UM 走 DTCH,加 RLC 头,接收侧做五件事:重复检测、乱序重排序、下层丢包检测、重组、把凑不齐的 SDU 丢掉。注意它没有回执——发送侧发完就忘,不会因为对方没收到而重发。

这个取舍是冲着业务特性去的。语音、视频这类周期性数据,晚到的包价值已经归零,重传等于用宝贵的空口资源搬运一份已经过期的数据。但完全不管也不行:下层 HARQ 会导致乱序到达,如果直接往上递,上层看到的是乱序流;如果无限期等缺口补齐,延迟又失控。

UM 的解法是给重排序加一个时限:等不到就主动放行,宁可丢也不拖。这个时限由一个定时器承担,后面第 3 章要专门拆它。

2.4 AM:加回执,代价是整套状态机

AM 走 DCCH 和 DTCH,收发两侧各维护一个窗口,靠 STATUS PDU 交换确认信息,重传次数由 RETX_COUNT 计,超过 maxRetxThreshold 就向上层上报。它还独占两个能力:把待重传的 PDU 重分段(段数没有上限),以及协议错误检测。

为什么 AM 必须要窗口?SN 位数是有限的,10 位只有 1024 个取值。如果不划窗口,一个迟到很久的旧 PDU 会被误认为新 PDU,数据直接串号。窗口的作用就是用"只接受某个区间内的 SN"消除这种回绕歧义,代价是在途数据量被窗口大小卡死——窗口大小直接决定了 AM 的峰值吞吐,这是配置 AM 时第一个要想清楚的参数。

维度TMUMAM
典型逻辑信道BCCH / PCCH / CCCHDTCHDCCH / DTCH
RLC 头无有(SN + 分段级联指示)有(SN + 分段指示 + P 域)
分段 / 级联不做做做,且支持重分段
重排序无有有
重复检测无有有
重传无无有(ARQ)
发送成功指示无无有
RLC 重建时需复位状态否是是

这张表里最容易被忽略的是最后一行。面试里问"为什么 TM 简单",答"因为不加头"只对了一半,答"因为重建时无需复位状态"才是踩到点上的。

3. 接收侧窗口:VR(UH)、VR(UR)、VR(R)、VR(MR) 与 t_Reordering 的配合

3.1 UM 的三个变量撑起一个重排序窗口

UM 接收侧只靠三个状态变量就把乱序、重复、丢包三件事一起管住了:

  • VR(UH):已接收到的最高 SN 加一,本质上就是窗口上沿的水位线
  • VR(UR):还没收到的最老 SN,窗口下沿
  • VR(UX):启动 t_Reordering 那一刻记下的 VR(UH) 快照,专门用来判断"当初要别人等的那个理由还在不在"

窗口的判定条件写出来是一行:

窗口内 <=> (VR(UH) - UM_Window_Size) <= SN < VR(UH)

一个 UMD PDU 到达后,落到哪个分支只有三种可能,实现上直接翻译成分支判断最直观:

/* UM 接收侧:收到 SN = x 的 UMD PDU */ void um_rx_on_pdu(uint16_t x, uint8_t *data, uint16_t len) { /* 分支1:窗口内但之前已收过 —— 重复包,直接丢 */ if (VR_UR < x && x < VR_UH && already_received(x)) { discard(x); return; } /* 分支2:落在窗口下沿之外 —— 太老,重排序已放弃它,丢 */ if ((VR_UH - UM_WINDOW_SIZE) <= x && x < VR_UR) { discard(x); return; } /* 分支3:窗口内且未收过 —— 入缓存,走状态更新 */ put_into_rx_buffer(x, data, len); um_rx_on_buffered(x); }

UM_WINDOW_SIZE由 RRC 配置,常见取值是 SN 空间的一半,这样窗口内外的 SN 不会因为回绕而重叠。三个分支的顺序有讲究:先判重复再判过老,是因为重复包的判定条件依赖 VR_UR,而 VR_UR 会随着状态更新往前跑,顺序反了会把本该算重复的包当成新包塞进缓存。

3.2 t_Reordering 的启动、提前停表与超时放行

入缓存之后的状态更新才是 UM 的精华所在,四步按序走:

第一步,扩窗口上沿。如果 x 落在重排序窗口之外(也就是比 VR(UH) 还新),先把 VR(UH) 更新成 x+1,然后把所有落在窗口外的 PDU 解头重组、按 SN 升序往上递。这一步保证了"新包一到,之前被卡的旧包就有机会出队"。

第二步,推窗口下沿。如果 VR(UR) 已经落后到窗口外,说明缺口太多、窗口得强制前移,把它置为 VR(UH) 减去窗口大小。然后检查缓存里有没有 SN 等于 VR(UR) 的包,有就一路往前找到第一个真正缺的 SN,把小于新 VR(UR) 的包全部重组上交。

第三步,决定要不要停表。这是 VR(UX) 唯一派上用场的地方。停止并重置 t_Reordering 的条件是二者之一:VR(UX) <= VR(UR),或者 VR(UX) 已经落在窗口外且不等于 VR(UH)。前者说明当初启动定时器的那个缺口被填上了;后者说明窗口整体已经滑过那个水位,再等没意义。

第四步,没表在跑就起表。起表的前提是 VR(UH) > VR(UR),也就是还有缺口。起表的同时把 VR(UX) 置为 VR(UH),记下这次要等的水位。

超时后的动作最能体现设计意图:把 VR(UR) 直接跳到"SN 大于等于 VR(UX) 的第一个未收到的位置",然后把小于新 VR(UR) 的包全部重组上交。翻译成人话就是——不等了,缺口我认了,能交的赶紧交。这样重排序延迟就有了一个硬上界,等于一个 t_Reordering 周期。

提示:t_Reordering 的配置不是随便填的。它必须覆盖下层 HARQ 的残留重传时间,否则 UM 会在 HARQ 还在努力的时候提前放行,上层看到的就是"明明能救回来却被丢了"。

3.3 AM 接收侧:从"按 PDU"变成"按字节段"

AM 的接收逻辑和 UM 形似而神不似,根子在于重传时 PDU 可能被重分段。一条 AMD PDU 可能分几次到达,每次带一部分字节段,所以接收侧不能按 PDU 粒度记"收没收到",必须细化到字节段序号区间。

变量也多出两个:

  • VR(R):接收窗口下沿,第一个还没完整收下来的 SN
  • VR(MR):等于 VR(R) 加 AM_Window_Size,窗口上沿
  • VR(H):收到过的最高 SN 加一
  • VR(MS):所有字节段都被收齐的最高 SN 加一,这是能向对端做确定性应答的上界
  • VR(X):启动 t_Reordering 那一刻的 VR(H) 快照

VR(MS) 这个变量是理解 AM 的钥匙。UM 只需要知道"我按序能交到哪",AM 还要额外回答一个问题:"我能给对端保证收到哪一段?"答错了就会给对端发假 ACK,对端把数据从缓存里删掉,这段数据就永久丢了。所以 STATUS 报告里报的是 VR(R) 到 VR(MS) 这个区间内的收包情况,区间外的一律不表态。

放包流程按下面这个顺序走:

/* AM 接收侧:收到 SN = x、字节段 [y, z] 的 RLC 数据 PDU */ void am_rx_on_pdu(uint16_t x, uint16_t y, uint16_t z) { /* 窗口外,或该字节段区间之前已完整收过 —— 丢弃 */ if (x < VR_R || x >= VR_MR || segment_received(x, y, z)) { discard(); return; } put_into_rx_buffer(x, y, z); drop_duplicate_segments(x); /* 区间内已收部分要剔掉 */ /* 若 VR(MS) 那条 PDU 的字节段齐了,VR(MS) 前移 */ if (all_segments_received(VR_MS)) VR_MS = next_incomplete_sn_after(VR_MS); /* 只有当 x 正好是窗口下沿时才允许按序上交 */ if (x == VR_R && all_segments_received(VR_R)) { VR_R = next_incomplete_sn_after(VR_R); VR_MR = VR_R + AM_WINDOW_SIZE; reassemble_and_deliver_below(VR_R); } if (x >= VR_H) VR_H = x + 1; am_rx_update_reordering_timer(); /* 起停判断同 UM,但用 VR(X)/VR(H)/VR(MS) */ }

注意if (x == VR_R && ...)这个条件的双重要求:SN 必须正好压在窗口下沿,且这条 PDU 的所有字节段都到齐了。少一个条件就会出现"半条 PDU 被上交"的串数据问题,这也是实现里最高频的翻车点之一。

3.4 两种窗口的共同点与分野

共同点其实只有三条,但每条都很关键:都用"一段 SN 区间"消除回绕歧义;都用 t_Reordering 给重排序加时限;窗口外的包一律直接丢,不做任何补偿。

分野在于交付时机和应答粒度。UM 只要确定顺序就可以重组上交,因为它压根没有部分接收的概念;AM 必须等窗口下沿前移才能按序交付,而且交付的判断要细到字节段。UM 用一个 VR(UH) 就驱动了整个窗口,AM 需要 VR(MS) 额外承担"能报 ACK 到哪"的职责——多出来的这个变量,本质上是"部分接收"这个能力带来的复杂度代价。

4. 发送侧状态机:AM 的发送窗口、ARQ 重传与无限重分段

4.1 VT(A)、VT(S)、VT(MS):三个变量管住发送窗口

AM 发送侧三个变量分工明确:

  • VT(A):最早的、还没被确认的 SN,窗口下沿
  • VT(MS):等于 VT(A) 加 AM_Window_Size,窗口上沿
  • VT(S):下一个新 PDU 要用的 SN

发一个新 PDU 时,SN 取 VT(S),然后 VT(S) 加一。窗口判定是VT(A) <= SN < VT(MS),落在窗口外的 PDU不允许下发——这条禁止不是建议,是硬约束,因为窗口外用 SN 已经和对端的接收窗口错位了。

收到肯定应答后 VT(A) 怎么动,协议写得很精确:当 SN 等于 VT(A) 的那条 PDU 被确认,把 VT(A) 置为"在 VT(A) <= SN <= VT(S) 范围内、肯定应答尚未收到的 SN 最小的那条"。注意上界用的是 VT(S) 而不是 VT(MS)——因为只有已经发出去的才谈得上被确认,拿 VT(MS) 当界会把一堆没发过的 SN 卷进来。

还有一个容易漏掉的语义:一个 RLC SDU 什么时候算发送成功?答案是它映射到的所有 AMD PDU 都收到肯定应答。这时才向上层发一条成功指示。这是 AM 向上层提供的、TM 和 UM 都没有的东西,也是 PDCP 判断何时可以丢弃缓存副本的依据。

4.2 ARQ 重传与 RETX_COUNT 的计数时机

重传的入口是 STATUS PDU 里的否定应答。收到之后先判一件事:对应 SN 是不是落在VT(A) <= SN < VT(S)范围内。不在范围内就无视——要么已经被确认过了,要么压根没发过。

落在范围内,就标记为"待重传"。接下来 RETX_COUNT 的计数规则是这段逻辑里最容易写错的地方:

若这是该 PDU 的首次重传 -> RETX_COUNT = 0 否则,若它(或它的某个部分)已不再等待重传 -> RETX_COUNT += 1 若 RETX_COUNT == maxRetxThreshold -> 向上层指示已达到最大重传

"已不再等待重传"这个前提条件看着绕,作用是防止一次重传周期内被重复计数。对端可能连着发好几个 STATUS,每次都把这个 SN 标成 NACK,如果每收到一次就加一,RETX_COUNT 会疯长,几下就撞到阈值上报了。正确的理解是按状态迁移计数:从"不在等待重传"迁到"在等待重传"才算一次。

撞到 maxRetxThreshold 之后 RLC 只做一件事——向上层指示。它不自己决定断链、不自己重建,因为这个决策涉及切换、无线质量、业务类型,远超 RLC 的视野。分层职责在这里划得很干净。

4.3 重分段:一个 PDU 能被切成几段

这是 AM 相对 UM 最实质的增强。要重传的 AMD PDU 如果塞不进当前传输机会,就把它切开,切出来的每一段是一个新的 AMD PDU 段,分发下去。协议明确写了重分段的分段数量没有限制,一条 PDU 可以被切成任意多段陆续重传。

形成新段时有两条硬规定:只把原 AMD PDU 的数据域映射到新段的数据域,头部按新段的实际情况重新构造。也就是说数据部分原样搬运,头部完全重写——这一点和直觉相反,很多人第一反应是把原头也搬过去再改几个字段,结果分段偏移和末段标志全错。

/* 重传一条 AMD PDU,按当前授权尺寸决定是否重分段 */ void am_tx_retransmit(pdu_t *p, uint16_t grant_bytes) { if (pdu_size(p) <= grant_bytes) { /* 整条发得下:除 P 域按轮询规则重设外,原样下发 */ set_poll_field(p, should_poll()); submit_to_mac(p); } else { /* 发不下:切一段刚好塞满授权的,头部重新构造 */ seg_t *s = alloc_segment(grant_bytes); memcpy(s->data, p->data + p->tx_offset, grant_bytes); s->sn = p->sn; s->so = p->tx_offset; /* 段偏移 = 已发字节数 */ s->lsf = is_last_segment(p, grant_bytes); p->tx_offset += grant_bytes; set_poll_field(s, should_poll()); submit_to_mac(s); } }

so(段偏移)和lsf(末段标志)是接收侧重组时的两个锚点:前者告诉对端这段数据在原 PDU 里的位置,后者告诉对端"这条 PDU 到此为止,后面没有了"。两者任一算错,接收侧要么永远等不到齐,要么提前判定收齐,两种都是静默错误,日志里看不出来,只有对比上下行抓包才能定位。

4.4 发送优先级与"可用数据"的统计口径

发送侧有两条明确的优先级:控制 PDU 优先于数据 PDU,重传数据 PDU 优先于新数据 PDU。

第一条的理由是 STATUS PDU 决定了对端窗口能不能往前滑,它被数据挤掉,对端就卡住,卡住又导致本端收不到新数据,形成环路。第二条的理由是重传清的是窗口下沿,下沿不动窗口就没法前移,新数据发得再多也只是把窗口填满然后停摆。

顺带说一个和 MAC 对接的细节:UE 报给网络的缓存状态里,"RLC 层可用于传输的数据"包含三项——还没被装进任何 PDU 的 SDU 及其分段、等待重传的 PDU 及其部分、以及已触发且 t_StatusProhibit 没在跑(或已超时)时预估的那条 STATUS PDU 的大小。第三项最容易被漏,漏了会导致网络低估上行需求,STATUS 发不出去,对端窗口跟着卡。

5. 轮询与状态报告:POLL_SN、t_PollRetransmit、t_StatusProhibit 的时序闭环

5.1 接收侧不会主动说话:轮询存在的理由

AM 接收侧触发 STATUS 报告的场景很有限,主要是两个:被对端轮询,以及 t_Reordering 超时。它不会因为"我收到新数据了"就主动汇报。

这个设计带来一个必然结果:如果发送侧不主动要,最后几条 PDU 的确认可能永远回不来。发送侧缓存空了、没新数据可发了,VT(A) 卡在原地不动,窗口就那么大,然后就僵住了。这种僵局在现场表现为"业务停了但链路没断",非常难查。

轮询就是给这个僵局准备的解药。

5.2 三类轮询触发条件与被忽略的"缓存空"分支

轮询有三类触发条件,前两类是计数型,第三类不是:

  • PDU_WITHOUT_POLL >= pollPDU:发了够多 PDU 还没轮询过
  • BYTE_WITHOUT_POLL >= pollByte:发出去的字节数够多了
  • 发完这条 PDU 后,发送缓存和重传缓存都空了(不算等待确认的那些),或者已经没有新 PDU 能发(比如窗口停摆)

第三类最容易被实现者忽略,因为它不按计数走,而是按状态走。它的语义是"我没东西可发了,赶紧要个回执别僵着"。很多实现只做了前两个计数器,跑短包业务时看不出来,一跑长连接空闲前的最后几个包就卡。

触发之后的动作是固定的一套:把 PDU 的 P 域置 1,两个计数器清零,把 POLL_SN 置为 VT(S) 减一(也就是刚刚发出去的这条的 SN),然后启动或重启 t_PollRetransmit。

5.3 POLL_SN 与 t_PollRetransmit 的配对

POLL_SN 和 t_PollRetransmit 是一对,拆开看任何一个都没意义。

发送侧收到 STATUS 报告后,检查里面有没有针对 SN 等于 POLL_SN 的确认或否认应答——有就停表并重置。为什么"否认"也算?因为否认同样证明对端收到了轮询、生成了报告,轮询的目的已经达成,剩下的重传是 ARQ 的事,和轮询定时器无关。

超时则说明轮询本身丢了,或者对端生成的 STATUS 丢了。这时如果缓存空或窗口停摆,就认为 SN 等于 VT(S) 减一的那条 PDU 需要重传(或者任何还没被确认的 PDU 需要重传),重传时再带一次轮询,重新起表。这个循环会一直持续到拿到回执为止。

注意:POLL_SN 记的是"最近一次带轮询的那条 PDU 的 SN",不是"最早的未确认 SN"。这两个值在窗口滑动后可能差很远,混用会导致停表条件永远不满足,表一直重启,STATUS 越积越多。

5.4 t_StatusProhibit:STATUS 的合流与节流

接收侧这边有个对称的节流机制。STATUS 报告被触发后:

  • 如果 t_StatusProhibit 没在跑,就在下层指示的首个传输机会构造一条 STATUS PDU 发下去,然后启动它
  • 如果它在跑,就等它超时后的首个传输机会再发;期间无论触发多少次,最终只构造一条

第二条是合流。为什么必须合流?因为 STATUS PDU 的位图大小和窗口宽度成正比,一条报告可能占掉不少上行资源,频繁发会直接挤占数据。而且报告本身有时效性,相隔几毫秒的两条报告,增量信息几乎为零。

另外还有一条时序上的讲究:轮询到达时,如果那条 PDU 本身要被丢弃(比如落在接收窗口外),或者它的 SN 还没到 VR(MS) 之前,STATUS 的触发要延迟,等到 SN 落进可以表态的区间再说。协议在这里给的理由是确保状态报告发生在 HARQ 重排序之后——否则你报的"没收到",可能只是 HARQ 还在重传路上。

定时器 / 参数归属作用配小了会怎样配大了会怎样
t_Reordering接收侧(UM/AM)限制重排序等待时长误丢还能救回的包端到端延迟变大
t_PollRetransmit发送侧(AM)轮询丢失后的兜底重发无谓重传,浪费授权窗口停摆时间变长
t_StatusProhibit接收侧(AM)STATUS 报告的发送间隔STATUS 过密挤占资源对端确认滞后
pollPDU / pollByte发送侧(AM)计数型轮询触发门限轮询过频长时间不要回执
maxRetxThreshold发送侧(AM)重传次数上限过早放弃死磕一条包

这张表里的五个参数不是独立的。t_PollRetransmit 必须大于一个来回的 STATUS 往返时间,否则每次轮询都会假超时;pollPDU 和窗口大小的比例决定了窗口能不能在一个轮询周期内滑满;t_StatusProhibit 要小于 t_PollRetransmit,不然对端超时重发的时候本端还在禁止期,白重传一次。

6. 避坑与排查:RLC 状态变量与定时器的五类翻车现场

6.1 吞吐突然归零,窗口纹丝不动

现象:AM 业务跑着跑着速率掉到零,链路没断,MAC 还在给授权,但 RLC 没有新 PDU 下发。抓状态变量一看,VT(A) 和 VT(MS) 贴在一起,窗口满了。

原因:轮询没触发。常见有两种:pollPDU 和 pollByte 配得比窗口还大,一个窗口周期内根本凑不够计数;或者只实现了两个计数型条件,漏了"缓存空/窗口停摆"这第三条。结果就是最后几条 PDU 的确认永远等不到。

解决:先算比例——窗口大小除以 pollPDU 应该是个位数,保证窗口滑满之前至少轮询一次;然后把第三条触发条件补上,它是空闲前唯一能救场的那条路。

6.2 UM 语音偶发"卡一下再继续"

现象:UM 承载的实时业务周期性出现短暂停顿,停顿后数据继续,但中间少了一小段。

原因:t_Reordering 配得比下层 HARQ 的最大重传时长还短。HARQ 正在重传那个缺口,UM 的定时器已经超时,把 VR(UR) 强行跳过去,缺口后面已收到的包全交上去了。等 HARQ 真把包补回来,UM 一看 SN 落在窗口下沿之外,直接丢弃。

解决:t_Reordering 的取值要覆盖 HARQ 残留重传的最坏情况,通常需要按 HARQ 进程数和最大重传次数反推。这个值配小了不会报错,只会静默丢包,靠 KPI 里的小段丢包率才能看出来。

6.3 重建之后上层收到重复数据

现象:RLC 重建完成后,上层立刻收到几条之前已经交付过的 SDU。

原因:重建时只复位了发送侧的状态变量和定时器,接收侧缓存里的乱序 PDU 没按规则处理。协议对重建的要求是:能重组的先重组上交,不能重组的直接丢,然后复位变量、停定时器。三步缺任何一步,缓存里的残留就会在重建后和新数据混在一起。

解决:把重建流程写成两侧对称的检查清单,发送侧复位 VT(A)/VT(S)/VT(MS) 和 t_PollRetransmit,接收侧按"重组—丢弃—复位"三步走完再恢复。

6.4 STATUS 里报了 ACK,但对端说没收到

现象:本端 STATUS 明确确认了某个 SN,对端据此把数据从缓存删了,可上层最终没拿到这段数据。

原因:AM 是按字节段接收的。实现里把"收到过这个 SN 的任何一个字节段"当成了"完整接收",于是给了一个不该给的 ACK。真相是这条 PDU 还有后半段没到。

解决:收包跟踪必须细化到字节段序号区间,判断"完全接收"的条件是所有区间拼起来覆盖了整条 PDU。VR(MS) 的前移条件也必须是"该 SN 所有字节段收齐",少一个字都不行。这类错误在日志里完全看不出来,只能靠上下行抓包对齐 SN 和段偏移才能定位。

6.5 RETX_COUNT 涨得飞快,很快上报最大重传

现象:链路质量明明还可以,RETX_COUNT 却在几个来回内就撞到 maxRetxThreshold,频繁上报。

原因:把"每收到一次 NACK 就加一"当成了规则,忽略了"该 PDU 已不再等待重传"这个前提。对端连着发几条 STATUS,每条都把这个 SN 标成 NACK,计数器就一路飙。

解决:按状态迁移计数——只有从"不在等待重传"迁到"在等待重传"时才加一。实现上给每个 PDU 加一个 pending_retx 标志,置位时才计数,清位后才有资格再计。

7. 设计取舍:为什么 RLC 宁可变复杂,也不做成一刀切

把整份规范读下来,最值得琢磨的不是某个定时器怎么配,而是几个贯穿全局的取舍。

第一个取舍:可靠性做成三档可选,而不是统一保证。TM 零状态、UM 有时限但不回头、AM 全套 ARQ,三套状态机共存于同一个子层。代价是规范厚度翻了几倍,实现要维护三种实体;收益是同一套协议能同时服务系统广播、实时语音和文件传输这三类需求差异极大的业务。如果只做 AM,广播消息要白白背上 SN 和窗口开销;如果只做 UM,文件传输的丢包就只能靠上层应用重试,效率惨不忍睹。

第二个取舍:RLC 不做调度、不做拥塞控制。它只回答一个问题——在给定的字节数里怎么塞满。传输块给多大、什么时候给、给谁,全是 MAC 调度器的事。这让 RLC 成了一根"带状态机的哑管道":它不懂信道质量,不懂优先级,只懂自己的窗口和定时器。复杂度被推给了 MAC,换来了 RLC 在各制式之间的高度可复用。

第三个取舍:窗口大小卡在 SN 空间的一半附近。SN 位数越长,头开销越大,但能允许的在途数据越多。10 位 SN 配 512 的窗口是常见的折中——头只占几个比特,窗口又能撑起足够高的峰值速率。这个数不是协议写死的,是留给 RRC 配置的旋钮,配大了浪费头开销,配小了直接限制吞吐上限。

验证这套机制有没有实现对,我的习惯是抓状态快照而不是读代码。每一次异常复现,先 dump 一组量:发送侧的 VT(A)、VT(S)、VT(MS) 和 t_PollRetransmit 的剩余时间,接收侧的 VR(R)、VR(H)、VR(MS) 和 t_Reordering 的剩余时间,再加上最近十条 STATUS 的 SN 区间。这十几个数摆在一起,窗口卡在哪、哪个定时器在空转、哪条 ACK 报早了,基本一眼就能看出来。从那以后我每次接 RLC 相关的排查,都强制先要这组快照,不看代码先看数——比顺着几万行状态机读要快一个数量级。希望帮到你。

这份中文协议文本把散在各章节的状态变量定义、定时器行为和过程分支收在了一起,配合前面这几张窗口和定时器的对照关系读,会比直接啃原文快得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询