ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

3GPP 37.324 SDAP协议详解:QoS映射原理、开源实现与排错指南

3GPP 37.324 SDAP协议详解:QoS映射原理、开源实现与排错指南 简介本资源为3GPP TS 37.324协议文档聚焦5G NR中SDAPService Data Adaptation Protocol子层的架构与流程面向从事5G协议栈开发、测试及网络优化的工程师与通信专业学习者。文档系统梳理了SDAP子层结构、SDAP实体、上下行数据传输、QoS流到DRB映射及反射QoS映射等核心内容并给出SDAP实体建立与释放、UL/DL SDAP数据PDU构造、末端标记控制PDU处理等过程细节可帮助读者理解QoS流与数据无线承载之间的映射机制及RQI处理逻辑。资源包共1个docx文件约242KB内容为协议原文节选涵盖架构、服务、功能与过程等章节适合作为协议查阅与实现对照的参考材料。目前已有584人学习下载便于快速定位SDAP关键条款并配合38.300、38.331等协议交叉阅读。1. 从 3GPP 37.324 g20 看 SDAP一个被低估的 QoS 映射层如果你正在做 5G 用户面排障大概率遇到过这种场景gNB 侧抓包看到 DRB 上跑着好几条 QoS Flow但终端侧应用却抱怨时延忽高忽低排查半天发现是 SDAP 头里的 QFI 映射和核心网下发的 QoS Rule 对不上。3GPP 37.324 就是专门定义 SDAPService Data Adaptation Protocol的协议文档g20 对应的是该规范在 Release 17 阶段的版本。SDAP 处在 PDCP 之上、NAS 之下的位置核心职责只有两件事把核心网下发的 QoS Flow 映射到空口的 DRB 上以及在上下行数据包里打上 QFI 标记。听起来简单但实际部署中 QoS 映射错配、反射式 QoS 不生效、SDAP 头压缩配置冲突这三类问题占了用户面异常的一半以上。这篇文章面向做 5G 基站协议栈开发、核心网对接测试、终端射频一致性验证的工程师从协议结构拆到代码实现再到参数配置和排错把 37.324 里那些文档上写得清楚但落地时容易翻车的地方讲透。2. SDAP 在 5G 用户面协议栈里的位置与 QoS 映射逻辑2.1 为什么 PDCP 之上还需要一个 SDAP4G 时代用户面协议栈是 IP → PDCP → RLC → MAC → PHY到了 5G 之所以要在 PDCP 和 IP 之间硬塞一个 SDAP根本原因是 5G 的 QoS 模型从 bearer-centric 变成了 flow-centric。4G 里 EPS Bearer 和 DRB 基本是一一对应的QoS 参数绑在承载上核心网建几个承载空口就建几个 DRB。5G 改成了 QoS Flow 模型核心网 SMF 可以下发几十条 QoS Flow每条 Flow 有自己的 5QI、GBR/MBR、ARP但空口不可能给每条 Flow 都建一个 DRB否则调度器直接崩掉。SDAP 就是解决这个多对一映射问题的多条 QoS Flow 可以复用同一个 DRBSDAP 负责在发送时把每条 Flow 的数据打上 QFI 标签接收时根据 QFI 把数据分发回对应的 Flow。这个设计带来的直接好处是空口资源利用率上去了但代价是映射关系的维护复杂度转移到了 SDAP 层。37.324 里定义了两种映射方式显式配置和反射式 QoS。显式配置就是 RRC 通过 SDAP-Config IE 直接告诉终端哪条 QoS Flow 映射到哪个 DRB反射式 QoS 则是终端从下行包的 SDAP 头里学习 QFI 到 DRB 的映射关系然后自动应用到上行。反射式 QoS 看起来省信令但实际测试中经常出现终端学错了映射导致上行数据走错 DRB 的情况后面避坑章节会细说。2.2 SDAP PDU 格式与 QFI 字段的编码规则37.324 定义的 SDAP PDU 分两种带 SDAP 头的 Data PDU 和不带头的 Data PDU。带头的格式如下---------------------------------------------------------------- | D/C | R | R | R | QFI (6 bits) | ---------------------------------------------------------------- | SDAP SDU (payload) | ---------------------------------------------------------------------D/C 是 1 bit0 表示 Control PDU1 表示 Data PDU。QFI 占 6 bit取值范围 0 到 63其中 0 保留不用实际可用 1 到 63。R 是保留位发送时置 0接收时忽略。注意 SDAP 头不是必须的当一条 DRB 上只映射了一条 QoS Flow 且不需要反射式 QoS 时可以配置为不带头省 1 个字节的开销。这个配置由 RRC 的 sdap-HeaderDL 和 sdap-HeaderUL 参数控制。Control PDU 目前 37.324 只定义了一种DL SDAP Control PDU with Reflective QoS flow to DRB mapping格式是 D/C0后面跟 QFI 和 DRB ID。终端收到这个 Control PDU 后要更新本地的 QFI-to-DRB 映射表。实际实现中很多协议栈偷懒不处理 Control PDU只靠 Data PDU 的 QFI 来学习这在大多数场景下能跑通但遇到核心网主动发 Control PDU 做映射更新时就会出问题。2.3 QoS Flow 到 DRB 的映射决策由谁做这是新手最容易搞混的地方。QoS Flow 到 DRB 的映射决策权在 gNB 的 RRC 层不在核心网。核心网 SMF 只负责下发 QoS Rule 和 QoS Flow 描述告诉 gNB 有哪些 Flow、每个 Flow 的 QoS 参数是什么。gNB 收到后根据调度策略、DRB 容量、Flow 的 5QI 等决定怎么打包。具体来说gNB 内部有一个映射表维护 QFI 到 DRB ID 的对应关系这个表在 RRC 重配时更新通过 SDAP-Config IE 下发给终端。SDAP-Config IE 的关键字段包括pdu-Session 标识 PDU Session IDsdap-HeaderDL 和 sdap-HeaderUL 控制是否带头defaultDRB 指示是否是默认 DRBmappedQoS-FlowsToAdd 和 mappedQoS-FlowsToRelease 是映射关系的增删列表。终端收到后更新本地映射表后续上行数据根据这个表选择 DRB 并打 QFI。一个常见的误解是认为核心网直接控制 DRB 映射。实际上核心网只到 QoS Flow 这一层DRB 是空口概念核心网不感知。所以当核心网工程师说“我把这条 Flow 绑到那个 DRB 上了”他实际说的是“我下发了 QoS RulegNB 应该会把它映射到合适的 DRB”中间隔着一层 gNB 的决策逻辑。3. 用开源协议栈跑通 SDAP 映射的最小实现3.1 环境准备与依赖确认要验证 SDAP 行为最直接的方式是用开源 5G 协议栈搭一个端到端环境。常见的选择是 srsRAN 加 Open5GSsrsRAN 负责 gNB 和 UE 侧的空口协议栈Open5GS 负责核心网。SDAP 在 srsRAN 里的实现位于lib/sdap目录下核心文件是sdap_entity.cc和sdap_entity.h。编译前确认系统有 cmake 3.14 以上、gcc 9 以上、libfftw3-dev、libmbedtls-dev 这些依赖。# 安装 srsRAN 编译依赖 sudo apt update sudo apt install -y cmake gcc g libfftw3-dev libmbedtls-dev \ libboost-program-options-dev libconfig-dev libsctp-dev # 克隆 srsRAN假设已有仓库访问权限 git clone https://github.com/srsran/srsRAN_Project.git cd srsRAN_Project mkdir build cd build cmake ../ -DENABLE_EXPORTON -DENABLE_ZEROMQON make -j$(nproc)这里-DENABLE_ZEROMQON是为了用 ZeroMQ 做射频仿真不需要真实 USRP 硬件就能跑通协议栈。-DENABLE_EXPORTON导出一些内部头文件方便调试。编译完成后在build/apps/gnb和build/apps/ue下会有可执行文件。3.2 配置 gNB 侧的 SDAP 参数srsRAN 的 gNB 配置文件在configs/gnb.ymlSDAP 相关配置在pdcp和sdap段落下。关键参数如下# gnb.yml 中 SDAP 相关配置片段 sdap: # 是否在下行数据包中携带 SDAP 头 header_dl: true # 是否在上行数据包中携带 SDAP 头 header_ul: true # 默认 DRB 的 QFI 映射格式为 QFI:DRB_ID default_drb_map: - qfi: 1 drb_id: 1 - qfi: 2 drb_id: 1 - qfi: 5 drb_id: 2 - qfi: 9 drb_id: 2header_dl和header_ul设为 true 表示所有数据包都带 SDAP 头方便抓包分析 QFI。生产环境如果一条 DRB 只映射一条 Flow可以设为 false 省开销。default_drb_map定义了初始的 QFI 到 DRB 映射关系QFI 1 和 2 映射到 DRB 1通常是默认承载走互联网流量QFI 5 和 9 映射到 DRB 2通常是专用承载走 VoNR 或低时延业务。配置改完后启动 gNBsudo ./build/apps/gnb/gnb -c configs/gnb.yml启动日志里会打印 SDAP entity 的创建信息和映射表初始化结果确认没有报错再继续。3.3 用 Open5GS 下发 QoS Flow 并验证映射Open5GS 侧需要配置 SMF 下发多条 QoS Flow。在open5gs-smf.yaml里找到session段落添加 QoS Flow 定义session: - subnet: 10.45.0.0/16 gateway: 10.45.0.1 qos_flows: - qfi: 1 five_qi: 9 arp: 8 gbr: false - qfi: 5 five_qi: 1 arp: 2 gbr: true mbr_ul: 100000000 mbr_dl: 100000000 - qfi: 9 five_qi: 6 arp: 4 gbr: false这里 QFI 1 对应 5QI 9默认互联网业务QFI 5 对应 5QI 1 conversational voiceGBR 业务QFI 9 对应 5QI 6视频流非 GBR。启动 Open5GS 后UE 发起 PDU Session 建立请求SMF 会把这些 QoS Flow 通过 N2 接口的 PDU Session Resource Setup Request 发给 gNB。验证映射是否生效的方法是在 gNB 侧抓包看 SDAP 头。用 tcpdump 抓 gNB 的 N3 接口GTP-U 隧道sudo tcpdump -i any -w sdap_capture.pcap udp port 2152然后在 UE 侧发起不同 QoS 的业务比如用 iperf3 打流走 QFI 1用 SIP 呼叫走 QFI 5。抓完包用 Wireshark 打开过滤gtp后看内层 IP 包的 SDAP 头确认 QFI 字段和预期一致。如果 QFI 对不上说明 gNB 的映射表配置有误需要检查default_drb_map和 Open5GS 下发的 QoS Flow 是否匹配。3.4 反射式 QoS 的触发条件与代码路径反射式 QoS 在 37.324 里的定义是当 gNB 在下行 SDAP Data PDU 中携带 QFI且该 DRB 配置了反射式 QoS 时UE 自动学习 QFI 到 DRB 的映射并用于上行。srsRAN 里反射式 QoS 的处理在sdap_entity.cc的handle_sdu和handle_pdu函数中。关键逻辑是收到下行 PDU 时如果reflective_qos_enabled为 true则更新本地的qfi_to_drb_map发送上行 SDU 时根据目的 QFI 查表选择 DRB。// sdap_entity.cc 中反射式 QoS 映射更新的简化逻辑 void sdap_entity::handle_pdu(byte_buffer_t* pdu) { sdap_header header parse_sdap_header(pdu); if (header.dc SDAP_DC_DATA reflective_qos_enabled) { // 从下行包中学习 QFI 到 DRB 的映射 uint8_t qfi header.qfi; uint8_t drb_id current_drb_id; if (qfi_to_drb_map.find(qfi) qfi_to_drb_map.end()) { qfi_to_drb_map[qfi] drb_id; log_info(Reflective QoS: learned QFI %d - DRB %d, qfi, drb_id); } } // 继续处理 payload deliver_to_upper_layer(pdu); }这段代码的逻辑是只在 Data PDU 且反射式 QoS 开启时才学习映射且只在新 QFI 首次出现时更新避免频繁覆盖。reflective_qos_enabled由 RRC 的 SDAP-Config IE 中的reflectiveQoS字段控制。实际测试中要注意反射式 QoS 要求下行包必须带 SDAP 头如果header_dl设为 false终端学不到任何映射上行只能走默认 DRB。4. SDAP 参数配置的边界条件与性能取舍4.1 SDAP 头开关对吞吐量的实际影响SDAP 头占 1 个字节看起来不多但在小包场景下开销比例很高。以 VoNR 为例AMR-WB 编码的语音包 payload 大约 60 到 80 字节加 1 字节 SDAP 头相当于增加 1.5% 左右的开销。如果同时开了 PDCP 头2 到 3 字节、RLC 头、MAC 头总开销能到 10% 以上。所以在一条 DRB 只映射一条 QoS Flow 且不需要反射式 QoS 的场景下关掉 SDAP 头是合理的优化。但关掉头之后有个隐患如果后续 RRC 重配往这条 DRB 上加了新的 QoS Flow而 SDAP 头还是关的新 Flow 的数据就没法打 QFI 标签接收端无法区分。所以关头的条件是“该 DRB 的 QoS Flow 映射关系在会话期间不变”。实际部署中默认 DRB 通常关头因为只跑互联网业务QFI 固定为 1专用 DRB 开头因为可能承载多条 Flow 且需要反射式 QoS。4.2 QFI 取值范围的限制与保留值QFI 是 6 bit理论范围 0 到 63。37.324 明确规定 QFI 0 保留实际可用 1 到 63。但 23.501 里又规定了一些特定 5QI 到 QFI 的默认映射关系比如 5QI 9 通常映射到 QFI 15QI 5 映射到 QFI 5 等。这些不是强制的但核心网和 gNB 实现通常遵循这些惯例。需要注意的是 QFI 和 5QI 不是一回事。5QI 是核心网用来描述 QoS 特征的标量时延、丢包率、GBR 等QFI 是空口用来标识 QoS Flow 的标签。一条 QFI 对应的 5QI 在会话期间可以不变但 QFI 本身可以重新映射到不同的 DRB。实际配置中常见错误是把 QFI 和 5QI 设成一样的值然后以为它们等价结果在核心网改 5QI 参数时发现空口行为没变因为 QFI 没动。4.3 多 PDU Session 场景下的 SDAP 实体管理一个 UE 可以同时建立多个 PDU Session每个 PDU Session 有独立的 SDAP 实体。srsRAN 里每个 PDU Session 对应一个sdap_entity实例通过pdu_session_id区分。当 UE 发起新的 PDU Session 建立时gNB 的 RRC 层会创建新的 SDAP 实体并配置映射表。多 Session 场景下容易出的问题是 QFI 冲突。不同 PDU Session 的 QFI 是独立编号的QFI 1 在 Session 1 里可能映射到 DRB 1在 Session 2 里可能映射到 DRB 3。如果实现时用全局的 QFI 到 DRB 映射表而不带 Session ID就会串。正确的做法是映射表的 key 用(pdu_session_id, qfi)二元组。// 正确的多 Session 映射表结构 struct qfi_drb_key { uint8_t pdu_session_id; uint8_t qfi; bool operator(const qfi_drb_key other) const { return std::tie(pdu_session_id, qfi) std::tie(other.pdu_session_id, other.qfi); } }; std::mapqfi_drb_key, uint8_t qfi_to_drb_map;这个结构在 srsRAN 的sdap_entity里已经实现但自己写协议栈时很容易忽略 Session ID 这一维。测试时可以用两个 PDU Session 分别跑不同 QFI 的业务抓包确认 QFI 没有串到错误的 DRB 上。5. SDAP 落地避坑从抓包异常到映射错配的排查清单5.1 现象上行数据全部走默认 DRB专用 DRB 空载原因反射式 QoS 没生效终端没有从下行包学到 QFI 到 DRB 的映射。常见触发条件是 gNB 侧header_dl设为 false或者 RRC 的 SDAP-Config IE 里reflectiveQoS字段没置位。另一个可能是下行包虽然带了 SDAP 头但 QFI 字段填的是 0保留值终端直接忽略。解决先确认 gNB 配置里header_dl: true且reflective_qos: true。然后在 gNB 侧抓 N3 接口的包用 Wireshark 看下行 GTP-U 内层包的 SDAP 头确认 QFI 非 0 且和核心网下发的 QoS Flow 一致。如果 QFI 正确但终端还是不走专用 DRB检查终端的 SDAP 实现是否处理了反射式 QoS有些低端模组为了省电会关掉这个功能。5.2 现象SDAP 头解析失败PDU 被丢弃原因SDAP 头的 D/C 位和 QFI 位的字节序处理错了。37.324 定义 SDAP 头是网络字节序大端第一个字节的 bit 7 是 D/Cbit 6 到 bit 1 是 QFIbit 0 是 R。有些实现按小端解析导致 D/C 和 QFI 都读错。另一个常见原因是没区分带头的 Data PDU 和不带头的 Data PDU对不带头的包也去读第一个字节当 SDAP 头结果把 payload 的第一个字节当成了 QFI。解决在解析函数入口先判断当前 DRB 是否配置了 SDAP 头。如果header_dl或header_ul为 false直接跳过 SDAP 头解析把整个 PDU 当 SDU 处理。如果带头按大端解析第一个字节提取 D/C 和 QFI。建议在代码里加断言QFI 为 0 时打 warning 日志方便定位。5.3 现象QoS Flow 重配后旧映射没清除数据走错 DRB原因RRC 重配时mappedQoS-FlowsToRelease列表里的 QFI 没有被正确从映射表里删除。37.324 规定终端收到 release 列表后要删除对应的映射关系但有些实现只处理 add 不处理 release导致旧映射残留。当同一个 QFI 被重新映射到新 DRB 时旧表项和新表项冲突数据可能走旧 DRB。解决在 SDAP 实体的配置更新函数里先处理 release 列表删除旧表项再处理 add 列表插入新表项。删除时要按(pdu_session_id, qfi)精确匹配不要只按 QFI 删否则会误删其他 Session 的同号 QFI。测试时可以用 RRC 重配消息强制触发映射变更抓包确认数据走了新 DRB。5.4 现象SDAP Control PDU 被当成 Data PDU 处理payload 解析异常原因D/C 位判断逻辑写反了或者 Control PDU 的处理分支缺失。37.324 里 D/C0 是 Control PDUD/C1 是 Data PDU。有些实现把 0 当成 Data导致 Control PDU 被送到上层协议解析payload 格式不对直接崩。解决在handle_pdu入口先读 D/C 位如果是 0 则走 Control PDU 处理分支解析 QFI 和 DRB ID 更新映射表不往上层送。如果是 1 则走 Data PDU 分支。Control PDU 目前只有一种类型处理逻辑简单但不能不处理否则核心网主动更新映射时终端不响应。5.5 现象多 PDU Session 下 QFI 串号Session 1 的数据跑到 Session 2 的 DRB原因映射表的 key 只用了 QFI 没有带 PDU Session ID。当两个 Session 都有 QFI 1 时后建立的 Session 覆盖了先建立的映射导致数据串号。这个坑在单 Session 测试时发现不了一到多 Session 场景就翻车。解决映射表 key 改成(pdu_session_id, qfi)二元组所有查表、插入、删除操作都带 Session ID。srsRAN 的sdap_entity已经这么做了但自己实现时容易图省事只用 QFI。测试时至少建两个 PDU Session每个 Session 配相同的 QFI 但映射到不同 DRB抓包确认数据没串。6. 用 Wireshark 插件解析 SDAP 头并做自动化验证Wireshark 原生支持 GTP-U 内层包的 SDAP 解析但需要手动开启。默认情况下 Wireshark 把 GTP-U 内层包当普通 IP 包解析看不到 SDAP 头。开启方法是打开 Wireshark 的 Preferences → Protocols → GTPv1勾选 “Dissect inner IP packet” 和 “Dissect SDAP”。如果版本较老没有 SDAP 选项可以自己写一个 Lua 插件。-- sdap_dissector.lua -- 自定义 SDAP 头解析插件适用于 Wireshark 3.6 以上 local sdap_proto Proto(sdap, SDAP Protocol) local f_dc ProtoField.uint8(sdap.dc, D/C, base.DEC, {[0]Control, [1]Data}) local f_qfi ProtoField.uint8(sdap.qfi, QFI, base.DEC) local f_r ProtoField.uint8(sdap.r, R, base.DEC) sdap_proto.fields {f_dc, f_qfi, f_r} function sdap_proto.dissector(buffer, pinfo, tree) local len buffer:len() if len 1 then return end local first_byte buffer(0,1):uint() local dc bit.rshift(bit.band(first_byte, 0x80), 7) local qfi bit.rshift(bit.band(first_byte, 0x7E), 1) local r bit.band(first_byte, 0x01) pinfo.cols.protocol SDAP local subtree tree:add(sdap_proto, buffer(), SDAP Header) subtree:add(f_dc, buffer(0,1), dc) subtree:add(f_qfi, buffer(0,1), qfi) subtree:add(f_r, buffer(0,1), r) -- 剩余部分作为 payload 交给上层 local payload buffer(1):tvb() Dissector.get(ip):call(payload, pinfo, tree) end -- 注册到 GTP-U 内层协议 local gtp_port DissectorTable.get(gtp.user) gtp_port:add(2152, sdap_proto)这个 Lua 插件的逻辑是从 GTP-U 内层包的第一个字节提取 D/C、QFI、R 三个字段然后在协议树里展示剩余部分交给 IP 解析器。bit.rshift和bit.band是 Lua 的位操作函数Wireshark 内置支持。注册到gtp.user表的 2152 端口后所有 GTP-U 内层包都会先经过这个解析器。把插件放到 Wireshark 的插件目录Linux 下是~/.local/lib/wireshark/plugins/重启 Wireshark 后抓包过滤sdap就能看到 SDAP 头字段。自动化验证的思路是用 tshark 命令行导出所有 SDAP 头的 QFI 和对应的 DRB ID然后和预期映射表做 diff。# 用 tshark 导出 SDAP 头的 QFI 和 GTP-U TEID tshark -r sdap_capture.pcap -Y sdap -T fields \ -e sdap.qfi -e gtp.teid -e ip.src -e ip.dst sdap_flows.txt # 统计每个 TEID 下出现的 QFI 集合 awk {print $2, $1} sdap_flows.txt | sort | uniq -c | sort -rn第一条命令从抓包文件里过滤出所有带 SDAP 头的包导出 QFI、GTP-U TEID、源 IP、目的 IP 四个字段。第二条命令按 TEID 分组统计 QFI 分布正常情况下每个 TEID对应一个 DRB下应该只出现预期的 QFI 集合。如果某个 TEID 下出现了不该有的 QFI说明映射错配需要回去检查 gNB 的default_drb_map和核心网的 QoS Flow 配置。我自己做 SDAP 验证时养成的习惯是每次改完映射配置先跑一遍这个 tshark 统计确认 QFI 分布和预期一致再往下做业务测试。这个步骤花不了两分钟但能省掉后面抓空口 log 排查半天的功夫。另外反射式 QoS 的验证要单独做因为它的映射是终端动态学的抓包时要注意看上行包的 QFI 是不是跟着下行包变了。希望帮到你。本文还有配套的精品资源点击获取
返回列表