RLC协议状态机与定时器:LTE可靠传输的底层逻辑
简介:面向LTE无线接入网研发、协议栈实现与测试岗位工程师的学习资料,聚焦TD-LTE数字蜂窝移动通信网Uu接口中的RLC协议。内容依据中国通信标准化协会技术报告,系统规定E-UTRA RLC子层结构、RLC实体、上下层业务关系、功能划分,以及与对等层通信的协议数据元、格式和参数,并说明未知或错误协议数据的处理方式,同时覆盖确认模式等关键机制,可帮助读者理解RLC在数据传输与差错校正中的职责边界。资源包共1个doc文件,约1.74MB,为中文协议文本,适合作为协议查阅与实现对照的案头文档。目前已有493人学习,适合需要梳理Uu接口协议分层、准备协议栈开发或测试用例设计的读者参考使用。
1. RLC 协议:为什么 LTE 的“可靠传输”不靠重传次数堆出来
很多人第一次翻 LTE 协议栈,会觉得 RLC 这一层“没什么存在感”——上有 PDCP 做加密和头压缩,下有 MAC 做调度和 HARQ,RLC 夹在中间,好像只是把数据切一切、编个号。但真正在链路层调过参数的人会告诉你,掉话、速率上不去、切换后业务卡顿,最后追下去,十有八九落在 RLC 的状态变量和定时器上。RLC 协议真正解决的问题不是“重传”,而是在一个底层 HARQ 已经会重传、上层又要求按序交付的环境里,如何用最少的反馈把“哪些收到了、哪些没收到、什么时候该等、什么时候该放弃”讲清楚。这份 TD-LTE Uu 接口技术要求第 7 部分,把 TM、UM、AM 三种实体、发送/接收窗口、t_Reordering、t_PollRetransmit、t_StatusProhibit 这些机制一条条写成了可实现的规范。搞懂它,你才能明白为什么 LTE 的可靠传输不是靠“多传几遍”堆出来的,而是靠窗口和定时器把不确定性收敛掉。
2. 三种模式的分工:TM、UM、AM 到底在取舍什么
2.1 TM 实体:为什么它连 RLC 头都不给
透明模式是三种模式里最“反直觉”的一个。按规范描述,发送 TM RLC 实体从上层收到 RLC SDU 后,不做分段、不做级联,也不在 TMD PDU 里加任何 RLC 头,直接往下层交。接收侧同样,收到 TMD PDU 就原样往上分发。这意味着 TM 实体根本不提供序列号,也就没有重排序、没有重复检测、没有重传。
那它存在的意义是什么?看它绑定的逻辑信道就明白了:BCCH、DL/UL CCCH、PCCH。这些信道传的是广播信息、公共控制信息和寻呼信息,特点是“一次性、面向小区内所有终端、对时延敏感但对个别丢失不敏感”。如果给寻呼消息加一个 RLC 头再编个 SN,接收侧还得维护窗口状态,纯属给终端增加无谓的状态机负担。TM 的设计动机就是:当上层业务本身不需要 RLC 级别的可靠性保证时,把这一层的开销压到零。
这里有个容易踩的细节:TM 不分段,意味着上层交下来的 SDU 必须能塞进下层通知的传输机会里。实际实现中,TM 实体通常只处理固定或受控大小的消息,不会出现“SDU 太大塞不下”的情况。如果你在实现里给 TM 加了分段逻辑,那已经不是规范里的 TM 了。
2.2 UM 实体:单向可靠性的边界在哪里
UM 实体绑定在 DL/UL DTCH 上,做分段、级联、加 RLC 头、编 SN,接收侧做重复检测、重排序、丢失检测,但不请求重传。这是它和 AM 最本质的区别:UM 承认“有些 PDU 就是会丢”,它只负责把收到的按序整理好、把等不到的东西在合理时间内放过去,而不是死等。
规范里 UM 接收侧维护一个以 VR(UH) 为基准的重排序窗口,窗口范围是(VR(UH) – UM_Window_Size) <= SN < VR(UH)。收到一个 UMD PDU 时,先判断它是否落在窗口内、是否重复,再决定丢弃还是放进接收缓存。放进去之后,如果这个 SN 落在窗口外,就推进 VR(UH),把窗口外能重组的 SDU 按 SN 升序往上交;如果 VR(UR) 落在窗口外,就把 VR(UR) 拉回VR(UH) – UM_Window_Size。这一套状态推进的逻辑,核心目的只有一个:不让一个丢失的 PDU 把整个接收缓存无限期卡住。
t_Reordering 就是那个“后悔药”。当 VR(UH) > VR(UR),说明有空洞,启动定时器;超时后直接把 VR(UR) 推到>= VR(UX)的第一个未收到 SN,把空洞之前能交的都交上去。这样即使中间丢了一个包,后续数据也不会被永久阻塞。UM 的取舍很清晰:用“可能丢一点”换“不会因为一个包卡死整条链路”,适合语音、流媒体这类对时延敏感、对少量丢包可容忍的业务。
2.3 AM 实体:ARQ 和 HARQ 为什么要叠两层
AM 实体是三种模式里状态最复杂的,发送部分和接收部分各自维护窗口和一组状态变量。发送侧窗口由 VT(A) 和 VT(MS) 界定,VT(A) <= SN < VT(MS)内的 PDU 才允许下发;接收侧窗口由 VR(R) 和 VR(MR) 界定。AM 同时支持分段、级联、重分段、ARQ 重传、轮询和状态报告。
一个常见疑问是:MAC 层已经有 HARQ 了,为什么 RLC 还要做 ARQ?因为两者解决的问题不在一个时间尺度上。HARQ 是快速反馈、基于传输块的,重传次数有限,失败后就把“这个块没救回来”的事实往上报;RLC ARQ 是慢速反馈、基于 PDU 序列号的,它负责把 HARQ 漏掉的那些洞补上。规范里 AM 发送部分明确写了“传输 RLC 控制 PDU 优先于数据 PDU,重传数据 PDU 优先于新数据 PDU”,这个优先级顺序就是为 ARQ 服务的:状态报告先走,欠的账先还,新数据往后排。
AM 的重分段能力也值得单独说。当一个待重传的 AMD PDU 塞不进当前传输机会时,发送部分可以把它重新分段成多个 AMD PDU 段,而且规范明确写了“重分段的分段数量没有限制”。这给了实现很大的灵活性:底层给的授权忽大忽小,RLC 不用死等一个刚好合适的传输机会,而是把大 PDU 拆小分批送。代价是接收侧要按“字节段序号”来管理重复检测,比 UM 的整 PDU 粒度复杂得多。
3. 状态变量与窗口:RLC 的“账本”是怎么记的
3.1 UM 接收侧的四个变量和一次收包决策
UM 接收侧的核心状态变量是 VR(UR)、VR(UH)、VR(UX)。VR(UR) 是“下一个期望按序收到的 SN”,VR(UH) 是“已收到的最高 SN 加一”,VR(UX) 是 t_Reordering 启动时记下的快照。收到 SN = x 的 UMD PDU 时,决策顺序大致如下:
收到 SN = x 的 UMD PDU: 若 VR(UR) < x < VR(UH) 且 x 之前已收到 -> 丢弃(重复) 若 (VR(UH) - UM_Window_Size) <= x < VR(UR) -> 丢弃(窗口外旧包) 否则 -> 放入接收缓存 放入缓存后: 若 x 落在重排序窗口之外: VR(UH) = x + 1 重组并向上分发窗口外可交付的 SDU 若 VR(UR) 落在窗口之外: VR(UR) = VR(UH) - UM_Window_Size 若缓存中存在 SN = VR(UR) 的 PDU: VR(UR) 推进到下一个未收到的 SN 重组并分发 SN < 新 VR(UR) 的 SDU 若 t_Reordering 在运行且满足停止条件: 停止并重置 t_Reordering 若 t_Reordering 未运行且 VR(UH) > VR(UR): 启动 t_Reordering,VR(UX) = VR(UH)这段伪代码对应规范 5.1.2.2.2 和 5.1.2.2.3 两节。几个参数的含义需要说清楚:UM_Window_Size决定了接收侧最多能容忍多大的乱序范围,窗口越大抗乱序能力越强,但接收缓存和状态维护成本越高;VR(UX)的作用是给 t_Reordering 超时后的推进设一个下界,避免超时后把还没等到的包直接跳过太多。t_Reordering 超时时,VR(UR) 被推到“SN >= VR(UX) 的第一个未收到 PDU”,然后如果还有空洞就重启定时器。这套逻辑的本质是:用定时器把“等”这件事量化,等多久由 VR(UX) 和窗口共同决定,而不是无限等。
3.2 AM 发送窗口:VT(A)、VT(S)、VT(MS) 三者的关系
AM 发送侧的状态变量更多,但核心就三个:VT(A) 是“最早未被肯定应答的 SN”,VT(S) 是“下一个新 PDU 要用的 SN”,VT(MS) 是“发送窗口上界”,满足VT(MS) = VT(A) + AM_Window_Size。发送窗口的有效范围是VT(A) <= SN < VT(MS),任何 SN 落在窗口外的数据 PDU 都不允许下发。
这个窗口机制解决的是“发送方不能无限往前跑”的问题。如果没有窗口,发送方可以一直发新数据,接收方的缓存和状态变量会无限增长,而且一旦某个早期 PDU 丢了,后面所有 PDU 都得在接收侧等着,重排序压力全压在接收端。窗口把“在途未确认数据”的数量限制住,让收发双方的状态空间有界。
收到肯定应答时,如果应答的 SN 等于 VT(A),发送部分就把 VT(A) 推进到“VT(A) 到 VT(S) 范围内最小的未被肯定应答的 SN”。这里有个容易忽略的点:规范说的是“最小的未被肯定应答的 SN”,不是简单地 VT(A)+1。因为 STATUS PDU 可能一次性确认多个 PDU,也可能有空洞,VT(A) 必须跳到第一个真正没被确认的位置。如果某个 RLC SDU 的所有相关 AMD PDU 都被确认了,还要向上层发“发送成功指示”——这就是 AM 模式对 PDCP 的交付保证。
3.3 AM 接收窗口与字节段粒度
AM 接收侧的窗口由 VR(R) 和 VR(MR) 界定,VR(R) <= SN < VR(MR)为窗口内。和 UM 不同的是,AM 的重复检测粒度是字节段:一个 AMD PDU 可能被重分段成多个段,接收侧要记录每个 SN 下哪些字节段已经收到。规范 5.1.3.2.2 写得很明确:收到包含 SN=x 的 AMD PDU 字节段 y 到 z 的 PDU 时,如果 x 在窗口外或 y 到 z 之前已收到,丢弃;否则放入缓存,并把其中重复的字节段丢掉。
这个设计直接来自 AM 的重分段能力。如果只按整 PDU 做重复检测,重分段后的段就没法正确合并。字节段粒度让接收侧可以“拼图”:同一个 SN 下,不同段可能从不同传输机会到达,只要所有字节段凑齐,这个 AMD PDU 就算完整收到。VR(MS) 就是用来跟踪“哪些 SN 还没凑齐所有字节段”的,t_Reordering 超时后 VR(MS) 被更新为“SN >= VR(X) 的第一个未凑齐的 PDU”,触发状态报告把缺口告诉发送方。
4. ARQ 与轮询:STATUS 报告什么时候发、发给谁
4.1 轮询的触发条件:不是想发就发
AM 发送部分通过 P 域(poll 位)触发对等实体回 STATUS 报告。规范给了几类触发条件:PDU_WITHOUT_POLL >= pollPDU、BYTE_WITHOUT_POLL >= pollByte、发送后发送缓存和重传缓存都空、或者没有新数据能发(窗口延迟)。这几个条件覆盖了不同场景:前两个是“发够了就问问”,后两个是“没得发了,赶紧确认一下好推进窗口”。
轮询不是免费的。每发一个带 P 域的 PDU,就要启动或重启 t_PollRetransmit;如果对等实体没回 STATUS,定时器超时后要重传并再次轮询。所以 pollPDU 和 pollByte 设得太小,STATUS 报告会过于频繁,反向链路开销上去;设得太大,窗口推进慢,吞吐受影响。这是典型的参数调优取舍,没有放之四海皆准的值。
4.2 STATUS 报告的触发与禁止
接收侧触发 STATUS 报告的来源有三类:收到 P 域为 1 的 PDU、t_Reordering 超时检测到接收失败、以及规范里提到的其他触发。但触发不等于立刻发,还要过 t_StatusProhibit 这一关:如果 t_StatusProhibit 没在运行,就在下一个传输机会构造并下发 STATUS PDU;如果在运行,就等它超时后的第一个传输机会再发,而且期间多次触发也只发一个。
这个“状态禁止定时器”的设计动机是防止 STATUS 风暴。设想接收侧连续收到多个带 P 域的 PDU,如果每个都立刻回一个 STATUS,反向链路会被状态报告淹没,而且这些报告内容高度重叠。t_StatusProhibit 把反馈合并、限频,用一点反馈延迟换反向链路效率。规范里还特别注了一句:P 域为 1 的 PDU 如果 x < VR(MS) 或 x >= VR(MR),触发 STATUS;否则延迟触发,直到条件满足——这是为了保证状态报告在 HARQ 重排序之后发,避免基于乱序到达的中间状态做出错误判断。
4.3 重传计数与 maxRetxThreshold
AM 发送部分对每个 AMD PDU 维护 RETX_COUNT。首次重传时置 0,之后每次“该 PDU 或它的部分不再等待重传”时加一。当 RETX_COUNT 达到 maxRetxThreshold,向上层指示“已达到最大重传”。这个机制是 RLC 的“止损线”:ARQ 不能无限重传,否则一个永远收不到的 PDU 会把整个承载卡死。达到阈值后往上报告,由上层决定是重建、释放还是别的处理。
这里有个实现上的坑:RETX_COUNT 的加一条件是“不再等待重传”,而不是“每次重传就加一”。因为一个 PDU 可能被重分段成多个段,分多次发送,规范用“不再等待重传”这个状态来界定一次完整的重传尝试,避免分段导致计数虚高。
5. 避坑与排查:RLC 实现里最容易翻车的几件事
5.1 现象:切换后业务卡住,STATUS 报告一直不发
原因通常是 t_StatusProhibit 配置过大,或者状态报告触发条件判断有误。切换过程中接收侧可能积压了多个触发事件,如果 t_StatusProhibit 没超时,这些触发会被合并延迟。排查时先看接收侧是否在 t_StatusProhibit 运行期间持续触发但从未下发 STATUS PDU,再看 VR(MS) 是否长时间不更新。解决方式是核对 t_StatusProhibit 的配置值和触发逻辑,确认“延迟触发直到 x < VR(MS) 或 x >= VR(MR)”这个条件没有被实现成永久等待。
5.2 现象:UM 业务偶发大面积乱序交付
原因多半是 t_Reordering 超时处理里 VR(UR) 推进逻辑写错。规范要求超时后 VR(UR) 更新为“SN >= VR(UX) 的第一个未收到 PDU 的 SN”,如果实现里误写成“VR(UX) 本身”或“VR(UH)”,就会把还没等到的包直接跳过,造成上层看到空洞。排查时抓接收侧状态变量日志,对比超时前后 VR(UR)、VR(UX)、VR(UH) 的变化是否符合规范 5.1.2.2.4 节。解决是按规范逐条核对状态推进顺序,特别注意“先更新 VR(UR),再重组分发,再判断是否重启定时器”这个次序。
5.3 现象:AM 重传次数异常增长,RETX_COUNT 很快打满
常见原因是重分段后 RETX_COUNT 的加一条件判断错误。如果实现里每发一个分段就加一,一个 PDU 被拆成多段重传几次就会虚高到阈值。规范说的是“该 PDU 或它的部分不再等待重传”时才加一。排查时看 RETX_COUNT 的增长是否和“完整重传尝试次数”一致,而不是和“分段发送次数”一致。解决是把计数点放在“一次重传尝试结束”的位置,而不是每次下发分段时。
5.4 现象:窗口延迟导致吞吐上不去
发送侧 VT(MS) 长时间不推进,新数据发不出去,表现为“有数据但发不了”。根因可能是 STATUS 报告丢失或轮询没触发。检查 pollPDU、pollByte 是否设得过大导致长时间不轮询,以及 t_PollRetransmit 超时后是否正确重传并重新轮询。另一个可能是接收侧 VR(MS) 更新逻辑有误,导致状态报告里漏报缺口。排查时同时看发送侧 VT(A)/VT(S)/VT(MS) 和接收侧 VR(R)/VR(MS) 的日志,确认缺口是否被正确报告和确认。
5.5 现象:TM 承载上出现“SDU 太大塞不下”
TM 不分段,如果上层交下来的 SDU 超过下层传输机会能承载的大小,实现里没有合法处理路径。这通常不是 RLC 本身的问题,而是上层对 TM 承载的使用方式不对,或者传输机会通知的大小计算有误。排查时确认该承载是否真的应该用 TM,以及下层通知的 RLC PDU 总大小是否包含了必要的开销。解决要么调整上层数据大小,要么重新评估该业务是否适合 TM。
6. 设计取舍:RLC 为什么把复杂度放在状态机上
回头看这份规范,RLC 最值得琢磨的地方不是某个具体过程,而是它把“可靠性”拆成了几层不同时间尺度的机制:HARQ 管快速重传,RLC ARQ 管慢速补洞,t_Reordering 管乱序容忍,t_StatusProhibit 管反馈限频,窗口管状态有界。每一层都在做一个取舍——用多少状态、多少延迟、多少反馈开销,换多少可靠性。
一个有意思的对比是 UM 和 AM 的接收侧。UM 只有 VR(UR)、VR(UH)、VR(UX) 三个变量,重复检测是整 PDU 粒度;AM 多了 VR(R)、VR(MR)、VR(MS)、VR(H)、VR(X),重复检测细到字节段。这个复杂度差异不是随意的,而是由“是否支持重传和重分段”决定的:一旦允许重传和重分段,接收侧就必须能表达“这个 SN 下哪些字节收到了、哪些没收到”,状态空间自然膨胀。规范没有试图用一套统一的状态机覆盖三种模式,而是让 TM 极简、UM 中等、AM 完整,各取所需。
另一个取舍是定时器的角色。t_Reordering、t_PollRetransmit、t_StatusProhibit 三个定时器分别对应“等乱序”“等状态”“限反馈”,它们都不是为了“保证正确”而存在,而是为了“在不确定的情况下做出可接受的决策”。协议不能假设底层永远不丢包、不能假设对等实体永远及时回复,所以用定时器把“等多久”变成一个可配置、可验证的量。这也解释了为什么 RLC 的参数调优这么依赖场景:定时器值本质上是把业务特征(时延容忍度、丢包率、往返时间)编码进协议行为里。
我自己的习惯是,每次碰到 RLC 相关的异常,先把收发两侧的状态变量按时间轴拉出来,对照规范里状态推进的条件逐条核对,而不是先去调定时器参数。因为大多数“玄学”问题,最后都能在某个状态变量的推进条件里找到根因——定时器只是把问题暴露出来的那个触发器。希望这份规范里的状态机和过程描述,能帮你在下次排查链路层问题时少走一点弯路。
本文还有配套的精品资源,点击获取