搞InfiniBand的人应该都有印象:SMP(Subnet Management Packet,子网管理包)是子网管理器(SM,Subnet Manager)用来发现拓扑、分配LID、配置交换机转发表的“控制信令”。但绝大多数人在读内核代码时,都会被定向路由(Directed Route)这一支线绕晕——尤其是那几个看似简单、实则暗藏玄机的字段:HopCount、HopPointer,以及路径数组里一长串LID。正好前阵子帮人排查一个集群里“SM能起来但定向路由SMP总是超时”的问题,把Linux内核里从接收、识别、逐跳转发到驱动侧最终下发的整条链路重新捋了一遍。这篇就围绕这个主题,把定向路由SMP在内核中的处理机制拆开讲透,重点说清楚每个关键判断背后的原因,以及实际调试点。
这篇文章适合三类人:一是正在读内核源码、想搞明白ib_mad和ib_smi这套机制的人;二是做IB集群运维、经常面对SM日志和定向路由报错的工程师;三是做驱动或协议栈开发、需要理解SMP在HCA内部的传递路径的人。纯应用层用户也可以看个大概,但核心章节我会尽量在讲清楚原理的前提下展开,尽量做到不靠猜。
1. 为什么是SMP:先搞懂定向路由在子网管理中的位置
1.1 LID路由与定向路由的分工
IB子网里有两套寻址方式:LID路由和定向路由。LID路由很好理解——每个端节点和交换机都被分配了一个16位LID,交换机查转发表,看DLID(目的LID)决定从哪个端口出去。这套机制高效,但它有一个前提:子网必须已经被SM配置过了。也就是说,在子网初始化、SM刚启动、LID还没分配完的时候,LID路由根本用不上。
定向路由就是为这个阶段准备的。它不依赖DLID,而是由SM在SMP包里直接写一串路径数组,路径数组里每一个字段指定从当前交换机哪个端口往下走。每一跳交换机收到SMP之后,检查HopPointer,如果还没走到头,就把HopPointer加一,然后按路径数组指定的端口号继续转发;如果HopPointer已经到了HopCount,说明包到达了目的地,交给本地的SMA(Subnet Management Agent)处理。
用大白话说:LID路由像快递按门牌号送件,定向路由像快递手里攥着一张“下一个路口左转、第三个路口右转”的纸条。内核里都支持,但读取路径完全不同。定向路由SMP一旦进入ib_mad模块,会直接走smi_handle_dr_smp_recv这条分支,不再走普通的LID查找逻辑。
1.2 从一次“SM起不来的现场”说起
我之前遇到过一个真实场景:某高性能计算集群,开源SM软件在管理机上反复重启,日志里报DR SMP timeout。这句话的字面意思是——SM发出去了一个定向路由SMP,但一直没有等到响应。
乍一看可能是网络不通,但真正可疑的是另一件事:普通的LID路由SMP是通的,SM能够发现相邻交换机,只是某些远端交换机一直发现不了。这就说明物理链路大概率没问题,问题出在定向路由SMP在某个中间节点上的处理环节——要么是路径数组算错了,要么是某个交换机在转发DR SMP时没有正确处理。
当时第一反应是打开内核的动态调试,但这里有个现实问题:很多发行版默认不带CONFIG_DYNAMIC_DEBUG,或者ib_mad模块根本没编译调试信息。后来在实验环境自己编了一版内核,才把这套路径完整看清。后面会专门用一章讲调试手段,这里先按下不表。
2. 内核接收侧:SMP从端口到ib_mad的完整旅程
2.1 mad_agent与QP0注册机制
SMP在内核里有非常特殊的待遇:它只能走QP0(Queue Pair 0),也就是每个HCA上那个专门用于子网管理的“管理队列对”。普通数据走QP1(GSI,General Service Interface),而QP0对驱动来说是完全独立的另一套上下文。
在内核里,QP0的接收处理被ib_mad模块接管。模块初始化时会为每个HCA设备创建一个ib_mad_port_private结构,里面维护了一组分端口的管理代理列表。这里的“管理代理”在概念上就是mad_agent,用户态SM通过/dev/infiniband/umad打开字符设备后,内核会创建一个对应的ib_umad文件对象,进而在ib_mad核心里注册一个mad_agent。
关键点在于:QP0上的SMP并不是默认有代理在听。只有当SM进程通过uMAD设备注册之后,内核的接收路径上才会有一个匹配的mad_agent接管SMP。如果没有任何注册的代理,SMP会被静默丢弃——这在排查问题的时候是一个容易被忽略的盲区:你以为内核收到了SMP,实际上接收侧根本没有注册者。
顺带一提,QP0还有一个特点:它不支持RMPP(Reliable Multi-Packet Protocol),因为SMP的包大小限制很严格,不需要分片重组。RMPP相关代码在ib_mad里占据了很大篇幅,但基本与SMP的QP0路径无关。如果你看到有人试图在SM路径上开RMPP,那大概率是对协议理解有偏差。
2.2 recv_mad到smi_check_local_smp的过滤逻辑
当一个定向路由SMP从端口进入HCA,驱动把它拿到QP0的接收队列,ib_mad模块的ib_mad_recv_done_handler会被触发。这个函数会先解析MAD头,判断class——SMP的Management Class是IB_MGMT_CLASS_SUBN_LID_ROUTED(0x01)或IB_MGMT_CLASS_SUBN_DIRECTED_ROUTE(0x81)。注意,定向路由SMP的class与LID路由SMP不同,内核在接收时就根据这个class区分后续处理路径。
class等于0x81的包会被标记为定向路由SMP,进入smi_check_local_smp做“是否是发给本节点”的判断。这个函数干的事是:比较HopPointer和HopCount。
- 如果HopPointer等于HopCount,说明路径已走完,SMP的目的就是本节点,交给本地处理;
- 如果HopPointer小于HopCount,说明本节点只是一个中间跳,应当继续转发。
在继续转发的情况下,smi_handle_dr_smp_recv会被调用。这个函数的返回值描述包是应该被转发、还是应该被丢弃、还是应该本地处理。这里最容易产生误解的地方是:“本地处理”并不代表SMP已经结束。对于交换机来说,即使这个SMP是发给自己的(比如读交换机端口属性),它也可能需要生成一个响应包,通过同样的定向路径原路返回。而如果只是途经本交换机,那就更简单——改一下HopPointer,塞回发送队列即可。
2.3 M_Key校验与RMPP在SM路径上的特殊性
SMP的安全性依赖M_Key(Management Key)。M_Key是16字节的密钥,SM在发送SMP时把它放在SM MAD头里,接收端会校验这个密钥是否与本地配置一致。定向路由SMP的校验有一个很重要的细节:中间交换机在转发DR SMP时通常不做M_Key校验,只有路径终点才校验。这就带来一个安全隐患:如果路径中间有一台交换机做了严格校验,它可能会把不是发给自己的SMP直接丢掉。
内核代码在转发路径上并不会强制做M_Key匹配。如果一台交换机的固件或内核驱动在转发光路条件之外额外增加了这一层校验,定向路由的探测就会莫名失败。我们在实测中确实遇到过某一型号交换机在特定固件版本下出现“途经SMP被当作终点校验”的行为,日志上毫无线索,只能通过拔线、换端口、看SM路径计算来排除。这类问题不能光靠读内核代码解决,还需要对硬件行为有足够敏感度。
此外,ib_mad模块里还有recv_mad_ib层在做“是否支持该class”的判断,以及针对非SMP的Generic MAD走RMPP处理。前面说了,SMP不走RMPP,但有一个例外:部分SM实现在发送长响应时会利用多个SMP完成多包响应,这是SM软件层面的协议行为,与内核RMPP机制无关。内核只负责单包收发。
3. 定向路由的核心:smi_handle_dr_smp_recv的逐跳语义
3.1 Hop Pointer与Hop Count的角色反转
刚才提到HopPointer和HopCount,这里要把它们的语义彻底理清。HopCount表示路径总跳数,HopPointer表示当前已经走过的跳数。SM发出的SMP初始时通常HopPointer=0,HopCount=n。每一跳交换机收到后,先看HopPointer:
- 若HopPointer == HopCount,本机是终点,交给SMA处理;
- 若HopPointer < HopCount,取出路径数组中
HopPointer位置的端口号,将HopPointer加1,把包从指定端口发出。
这里容易混淆的点是:路径数组里的元素到底是对端端口还是本端端口?答案是本端端口号。SM在计算路径时,会在路径数组里按顺序写下每一跳交换机“应该从哪个端口出发前往下一跳”。所以对中间交换机来说,它不需要查转发表,只要看自己路径数组对应位置的那个数字就行。
有些初读代码的人会疑惑:为什么不是说“从哪个端口进来的”,而是“从哪个端口出去”?因为在定向路由的设计里,SM已经把整条路算好了,每个中间节点只需要执行一条最简单的指令。这样做的优势是转发逻辑极其简单——不需要拓扑理解、不需要查找表,几个字段自解释。
3.2 响应包如何沿反向路径折返
当定向路由SMP到达终点,终点节点要把响应通过同样的路径返回给SM。这里就有意思了:响应包的路径数组并不是原封不动地复制,而是要做反向处理。
在smi_handle_dr_smp_send中,内核会根据原SMP的方向构造响应SMP的路径数组,把路径反转。具体做法是:让每个中间节点在收到响应后,从路径数组的相应位置取反向端口继续转发。HopPointer在响应路径上同样起着标记作用,只不过方向跟请求路径相反。
一个经验性的总结:请求包的路径数组是SM到目标的路径;响应包的路径数组是目标到SM的路径。两点之间路径可能不对称,所以不能想当然地认为“原路返回就等于数组反转”。如果响应SMP的路由不对称,SM必须重新计算一条从终点到自身的路径并写进响应定向字段。内核代码中对响应SMP的路径数组处理相对保守——它把SM Header里的DR字段保留,由上层SM软件决定如何填,内核只按HopPointer执行。
3.3 被多数人忽略的端口号/属性修饰符细节
看完转发逻辑,再看一个很多人忽略的字段:SMP MAD头里的attr_mod(属性修饰符)和port_num。定向路由SMP在终点被处理时,往往还要携带一个端口号,指示操作目标是交换机的哪个物理端口。例如SubnGet(PortInfo)要读取端口属性,就必须在属性修饰符里带上端口号。
问题在于:中间交换机在转发DR SMP时不会修改这些字段,只有终点交换机在本地处理时才读取。如果SM构造的端口号与目标交换机内部的端口编号体系不一致——比如SM从1开始编号,而交换机内部从0开始——就会导致SMP最终被处理时报错。这类问题极难排查,因为路径转发本身没问题,但最终结果不对。
我们当时遇到的那个集群问题,最后定位下来就是这一类:SM计算出的路径端口号比交换机实际端口号多1。交换机的SMA在处理时认为这个端口号越界,返回了一个错误状态;SM读不懂这个错误,报超时。这种“号码偏移”问题在混合厂商环境中特别常见,因为不同厂商对外部端口编号的起点约定不一致。读内核代码能帮你理解“字段在哪个环节被消费”,但“消费方”的固件行为往往只能靠实测摸索。
4. 驱动侧与硬件QP0:定向路由SMP在HCA内部的落地
4.1 为什么一定要走QP0
QP0是IB规范里专门为子网管理保留的队列对。它和QP1的区别不只是接口不同:QP0没有GRH(Global Route Header),不用RoCE,不参与普通数据面的拥塞控制。它更像是一条带外管理通道。
HCA驱动在初始化时会创建QP0和QP1两个特殊队列对,其中QP0完全由ib_mad模块独占。驱动层收到的QP0包会直接送给ib_mad模块的回调,而ib_mad模块发送SMP时会向驱动提交一个WQE(Work Queue Element),驱动再把包塞进QP0发送队列。定向路由SMP本质上和其他SMP一样走QP0,区别只在MAD头里的class和定向字段。
在实际调参的时候,这个区分是有意义的:你可以通过ibv_devinfo看QP0状态,但必须记住,QP0的发送队列满没满、有没有背压,和普通数据路径完全不同。如果你的SM软件疯狂发送SMP导致QP0发送队列堆积,从数据面是看不出来的,只能看/sys/class/infiniband/<hca>/ports/<port>/下的计数器。
4.2 ib_send_mad沿路径发送时的DR包构造
内核中,发送SMP的入口是ib_send_mad。它接收一个ib_mad_send_wr_private结构,里面已经构造好了要发送的MAD包。对定向路由SMP来说,发送路径的关键是:send_buf->mad里的SM MAD头必须由上层填好路径数组、HopCount、HopPointer。内核本身不负责计算定向路径,它只是一个“邮差”。
有一个值得一提的点:ib_send_mad会检查发送代理的mad_agent注册信息,确认发送方是否有权在该QP上发送。用户态SM通过uMAD发送时,实际上绕过了大部分检查——uMAD到内核后直接构造send WR交给驱动,权限由/dev/infiniband/umad的文件权限和设备的CAP_SYS_ADMIN检查控制。所以内核态代码里看到的校验相对严格,但用户态出口反而更开放。
4.3 多端口HCA上的定向路由坑
多端口HCA是定向路由问题的重灾区。因为路径数组里的端口号是“交换机视角”的端口号,物理交换机还好理解,但HCA本身也有多个端口——DR SMP如果目标是HCA的某个端口,路径数组最后一项就要指向HCA的那个物理端口。
很多SM软件在计算到HCA的路径时会出现偏差:它把HCA当作一个“有两个端口的交换机”,写出来的路径数组在最后一跳指向了错误的端口号。这种情况下,HCA端口的SMA收到SMP,发现attr_mod里的端口号不是自己的,会返回错误状态。SM可能只看到超时。
另外要注意:同一个HCA的多个端口可能是独立子网端口,也可能在SM看来属于同一个子网。跨端口转发SMP的行为在不同驱动里处理方式不同。某些驱动对从QP0收到的SMP按端口号直接排队;某些驱动则会做一次“本机跨越”处理,把SMP从另一个端口发出去。内核ib_mad模块在这块没有统一逻辑,完全依赖HCA驱动如何实现QP0的收包分发。所以读代码时要结合具体驱动看,不能只读core层。
5. 实战排查:DR SMP超时问题的一次完整复盘
5.1 现场现象与初步定位
那次问题的现场环境是这样的:一个两层的IB子网,核心交换机下挂了十几台计算节点,管理机上跑开源SM。现象是SM能发现核心交换机和少数几个计算节点,但绝大部分节点处于“未知”状态,日志满屏都是DR SMP timeout。
刚开始怀疑是物理链路问题,但拔插光纤、换过端口之后没有任何改善。后来在SM日志里看到它尝试探索某台交换机时一直超时,于是怀疑方向转到中间交换机是否丢包。但此时单纯靠SM日志很难区分“包根本没到”和“响应没回来”。这时候就要靠内核侧的观测手段。
先做了两步:
- 在管理机上查看
ibstat,确认SM所在的HCA端口状态正常,QP0没有异常计数增长; - 在核心交换机上看日志,确认是否有SMP被异常丢弃的记录。
都没有发现异常。于是决定从内核下手。
5.2 开启内核动态调试后的关键日志
在一台同型号的测试机上,重新编译了内核,开启CONFIG_DYNAMIC_DEBUG和CONFIG_INFINIBAND_DEBUG,然后加载ib_mad模块时附上动态调试参数:
modprobe ib_mad dyndbg=+p再配合ftrace跟踪关键函数,例如:
echo 'smi_handle_dr_smp_recv' > /sys/kernel/debug/tracing/set_ftrace_filter echo function > /sys/kernel/debug/tracing/current_tracer cat /sys/kernel/debug/tracing/trace_pipe这时复现SM探索流程,能看到内核在smi_handle_dr_smp_recv里的逐跳处理记录。日志里重现的规律是:SMP在核心交换机的某个端口上被处理时,返回的状态一直是IB_SMP_Unsupported。这个状态通常意味着接收方认为请求的属性或修饰符非法,而不是路径不通。
于是把注意力放到终点节点对attr_mod端口号的解析上。通过对比SM软件配置,发现它对交换机端口号的编号是从1开始的,SM认为核心交换机的第5口就是物理面板的第5口,但该交换机固件内部对SMP类请求的端口号定义也是从1开始——问题不再这里。
进一步查发现,路径数组里中间一层的交换机把SMP转到了错误端口。核心交换机把包转给下一层交换机时,路径数组指定的出端口是7,但下一层交换机实际连接计算节点的端口是8。SM软件到底层链路的拓扑发现阶段,习惯性把“上行端口”当成了下一跳端口,这个逻辑在其他品牌交换机上没问题,到这台交换机上就不适用。
5.3 修复手段与验证
最后修复并不复杂:在SM软件的拓扑发现配置里,关闭基于“端口角色猜测”的路径优化,强制使用逐跳真实发现结果来构建定向路径。改完配置后重启SM,整个子网在几十秒内完成发现,不再有DR SMP超时。
这个案例最值得记录的一点是:问题不在Linux内核,却必须靠Linux内核的观测手段才能定位。内核smi_handle_dr_smp_recv不会帮你判断路径数组计算得对不对,但你把它的逐跳行为记录下来之后,就能看出SMP是在哪一跳偏离了预期的。
如果你遇到的DR SMP超时问题,根本连smi_handle_dr_smp_recv都没有被调用——那说明包没有到达ib_mad模块,可能是HCA驱动层或物理链路问题。如果函数被调用但转发方向错了,那问题出在路径数组本身或交换机固件。这两个方向的定位策略完全不同,先分清是哪一类,能省掉大量无效操作。
6. 内核调试技巧:看清DR SMP的两板斧
6.1 dynamic_debug与ftrace的组合用法
现在总结一下实际调试DR SMP问题最实用的两板斧:dynamic_debug和ftrace。
dynamic_debug能打开ib_mad.c里的pr_debug信息,例如:
echo 'file drivers/infiniband/core/mad.c +p' > /sys/kernel/debug/dynamic_debug/control echo 'file drivers/infiniband/core/smi.c +p' > /sys/kernel/debug/dynamic_debug/control这一步能让你看到ib_mad_recv_done_handler是否被触发、接收到的class是什么、包是否进入了smi_handle_dr_smp_recv。
ftrace可以跟踪特定函数的调用频率和参数值。比如:
echo 'smi_handle_dr_smp_recv' > /sys/kernel/debug/tracing/set_ftrace_filter echo 1 > /sys/kernel/debug/tracing/events/ib_mad/enable # 如果内核编译了对应事件 echo 1 > /sys/kernel/debug/tracing/tracing_on如果内核没有ib_mad跟踪点,可以用kprobe_events动态挂载。比如:
echo 'p:smi_recv smi_handle_dr_smp_recv $arg1' > /sys/kernel/debug/tracing/kprobe_events echo 1 > /sys/kernel/debug/tracing/events/kprobes/smi_recv/enable这样能直接拿到函数第一个参数(struct ib_smp *)的地址,再用trace-cmd record -e kprobes采集现场。对没有动态调试的发行版内核,这是最可靠的路径。
6.2 常见误判和惯例
在调试实践中,有几个“惯例”值得分享给第一次碰DR SMP的人:
不要只看SM日志里的timeout。timeout是一个汇总症状,它掩盖了中间过程的复杂性:可能是请求没到、响应没回、响应回了但SM不认、或者SM在等待下一次轮询。必须靠内核侧确认包的实际走向。
区分“交换机转发时丢”和“终点处理时报错”。前者经常表现为某个中间跳的函数完全不出现;后者表现为函数被调用但返回错误码。内核里可以从返回值看出状态,但用户态工具很难感知这个差异。
路径数组端口号偏移是混合环境里的高频坑。任何一次“换了一个牌子交换机后DR SMP开始超时”的报障,都建议第一时间排查路径数组里端口号与目标交换机端口的对应关系。
HCA多端口机器上,先确认SMP是发给HCA哪个端口的。有时候SM软件把端口号0和1搞混,导致一切看起来正常但就是拿不到响应。这种问题用内核日志也能看出来,因为终点SMA的响应状态码会带有端口无效的暗示。
真正做到对定向路由SMP的“驾驭”,不是背住几个函数名,而是能在实际报障时快速判断问题属于哪一层:是包没进内核、是包在内核里被错误转发、还是包到了终点但处理失败。上面这套逐层定位的方法,基本覆盖了绝大多数真实场景。我后来遇到类似问题时,已经习惯性地先开动态调试,再根据日志走向做二分定位——这套流程在多个环境里都稳定有效。