一、struct net_device
与header_ops
的作用
Linux内核中,struct net_device
是网络设备的核心抽象,其header_ops
成员定义了链路层头部操作函数,用于处理数据包的封装与解析。这一机制使得不同硬件(如以太网卡、InfiniBand设备)能够适配统一的协议栈。
-
header_ops
的功能-
create
:构建链路层头部(如以太网头或IPoIB头)。 -
parse
:解析接收到的数据包头部,提取协议类型(如IPv4/IPv6)。 -
cache
:缓存邻居的链路层地址(如ARP缓存)。
例如,以太网设备使用eth_header_ops
,而IPoIB设备通过ipoib_header_ops
实现InfiniBand特有的头部操作。
-
-
daddr
参数的传递
在header_ops->create
函数中,目标地址(daddr
)的获取依赖于路由和邻居子系统:-
路由决策:通过
fib_lookup
确定下一跳IP地址。 -
邻居解析:使用ARP(IPv4)或ND(IPv6)将IP地址转换为链路层地址(如MAC或GID)。
代码路径示例:
ip_finish_output2() → neigh_resolve_output() → dev_hard_header() → header_ops->create()
-
二、IPoIB驱动的链路层地址管理
IPoIB(IP over InfiniBand)通过InfiniBand的GID(Global Identifier)作为链路层地址,其实现与传统以太网有显著差异。
-
本地GID的初始化与更新
-
set_base_guid
函数的作用
该函数用于更新IPoIB设备及其子接口的本地GID,并同步到net_device->dev_addr
。关键操作包括:memcpy(&priv->local_gid, gid, sizeof(*gid)); // 更新私有数据结构 memcpy(dev->dev_addr + 4, &priv->local_gid, 16); // 写入设备地址(后16字节为GID) list_for_each_entry(child_priv, ...) // 递归更新子接口
-
GID的组成
IPoIB的dev_addr
为20字节结构:-
前4字节:固定标识(如队列对号)。
-
后16字节:InfiniBand端口的GID(包含子网前缀和GUID)。
-
-
-
对端GID的动态解析
-
ARP/ND机制适配
IPoIB通过扩展的ARP协议实现IP到GID的映射:-
多播ARP请求:通过InfiniBand多播组广播查询目标IP的GID。
-
邻居缓存:解析结果缓存在
struct neighbour
中,可通过ip neigh
查看。
-
-
示例邻居表项
192.168.1.3 dev ib0 lladdr 80:00:04:04:fe:80:00:00:00:00:00:00:00:02:c9:02:00:21:70:d1
-
三、IPoIB的通信模式与地址处理
IPoIB支持两种通信模式,其对地址管理的需求不同:
-
Connected模式
-
特点:通过QP(Queue Pair)直接通信,减少头部开销。
-
地址管理:使用QP号作为链路层地址的一部分,无需完整GID。
-
-
Datagram模式
-
特点:兼容标准IPoIB头部,支持广播/组播。
-
地址管理:依赖完整的GID封装,需严格遵循MTU限制。
-
四、调试与问题排查
-
关键调试命令
-
查看本地GID:
cat /sys/class/infiniband/mlx5_0/ports/1/gids/0
-
查看邻居缓存:
bash
复制
ip -d neigh show dev ib0
-
-
常见问题
-
GID变更未同步:若InfiniBand端口状态变化(如热插拔)未触发
set_base_guid
,可能导致通信中断。 -
ARP缓存失效:未及时更新邻居表时,需手动刷新:
ip neigh flush dev ib0
-
五、总结
IPoIB通过GID替代传统MAC地址,实现了InfiniBand网络与Linux协议栈的无缝集成。其核心机制包括:
-
地址配置:通过
set_base_guid
同步本地GID,维护设备与子接口的一致性。 -
动态解析:扩展ARP/ND协议实现IP到GID的映射,依赖邻居子系统缓存结果。
-
模式适配:Connected模式优化性能,Datagram模式确保兼容性。
这一设计在保持InfiniBand高性能的同时,充分复用Linux网络协议栈的能力,为高性能计算和存储网络提供了坚实基础。
在Linux内核中,struct net_device
的header_ops
字段并非总是被强制赋值,其存在与否取决于网络设备的类型和驱动程序的具体实现。以下是关键分析:
1. header_ops
的作用
-
header_ops
定义了链路层头部操作(如创建、解析、缓存),通常用于处理数据包的二层头部(如以太网头部)。 -
包含函数指针如
create
、parse
、cache
等,用于构建和解析链路层头部。
2. 何时必须赋值?
-
以太网类设备(如真实网卡、TAP设备)通常需要设置
header_ops
,例如使用eth_header_ops
来管理以太网头部。 -
依赖链路层协议的设备(如桥接、VLAN)必须设置该字段以确保正确封装/解封装数据。
3. 何时可能为NULL
?
-
无需链路层头部的设备:例如点对点的TUN设备(非TAP模式)直接处理IP层数据,无需二层头部,因此可能不设置
header_ops
。 -
特殊用途设备:如某些虚拟或实验性设备,若其数据传输不依赖传统链路层头部,驱动可能选择不赋值。
-
内核默认处理:某些情况下,内核可能通过其他机制(如
hard_header
旧接口)处理头部,但现代驱动通常优先使用header_ops
。
4. 代码实例验证
-
环回设备(loopback):在
drivers/net/loopback.c
中显式设置dev->header_ops = ð_header_ops
,尽管其无需物理链路层。 -
TUN/TAP设备:
-
TAP模式(模拟以太网设备)设置
header_ops = &tap_header_ops
。 -
TUN模式(纯IP层)不设置
header_ops
,因为无需处理二层头部。
-
5. 未赋值的风险
-
若设备需要链路层操作但未设置
header_ops
,可能导致内核异常(如空指针解引用)。 -
驱动需根据设备类型谨慎处理:必需时赋值,否则留空。
结论
header_ops
字段不是强制要求赋值的。其赋值与否取决于设备是否需要链路层头部处理:
-
必须赋值:依赖二层协议的网络设备(如以太网卡、TAP)。
-
可能为
NULL
:无需链路层封装的设备(如TUN、某些虚拟设备)。
开发者需根据设备功能决定是否实现header_ops
,内核本身对此无绝对约束。
在Linux内核中,struct net_device
结构体确实包含 header_ops
成员,但其存在性和具体行为与内核版本及配置相关。以下是详细分析:
1. header_ops
成员的存在性
-
现代内核版本(如4.x/5.x及以上):
struct net_device
明确包含header_ops
成员,定义在include/linux/netdevice.h
中。例如:struct net_device { // ... const struct header_ops *header_ops; // ... };
该成员用于处理链路层头部操作(如以太网头部),是网络设备驱动的重要部分。
-
旧版本内核(如2.6.x早期版本):
在较旧内核中,链路层头部操作可能通过其他字段(如hard_header
函数指针)实现,而非header_ops
结构体。此时header_ops
可能不存在。
2. header_ops
的依赖条件
header_ops
的存在性可能受内核配置影响:
-
网络协议栈支持:
若内核启用了网络协议栈(CONFIG_NET
),struct net_device
的定义会包含完整的网络设备相关成员,包括header_ops
。 -
特定驱动需求:
部分虚拟设备(如TUN/TAP)或特殊硬件可能需要显式操作链路层头部,因此依赖header_ops
成员。
3. 代码示例与验证
以 Linux 5.15 内核源码为例,struct net_device
的定义明确包含 header_ops
:
// include/linux/netdevice.h struct net_device { // ... const struct header_ops *header_ops; // 链路层头部操作 // ... };
典型赋值场景(如以太网驱动):
// drivers/net/ethernet/example/example_driver.c dev->header_ops = ð_header_ops; // 指向以太网头部操作函数
4. 成员未定义的风险
若在代码中直接访问 header_ops
:
-
内核版本兼容性问题:
在旧版本内核中,直接引用header_ops
会导致编译错误(成员不存在)。 -
条件编译处理:
若需跨版本兼容,可通过内核版本宏(如LINUX_VERSION_CODE
)或检查成员存在性(如IS_ENABLED
)进行条件处理。
5. 结论
-
存在性:
在现代Linux内核(4.x/5.x及以上)中,struct net_device
必定包含header_ops
成员。 -
旧内核差异:
在2.6.x及更早版本中可能不存在该成员,链路层操作通过其他方式实现。 -
实践建议:
开发内核模块时,应基于目标内核版本验证结构体定义,或通过条件编译确保兼容性。
在Linux内核中,struct net_device
的header_ops
成员用于定义链路层头部的操作函数(如创建、解析、缓存等)。其调用时机与网络数据包的发送、接收及协议栈处理密切相关。以下是具体的调用场景及作用分析:
1. 数据包发送时:构建链路层头部
当网络协议栈需要发送数据包时,需构造链路层头部(如以太网头部),此时会调用header_ops->create
方法。该函数负责填充目标MAC地址、源MAC地址及协议类型(如IPv4/IPv6)等字段。
-
调用路径示例:
数据包通过dev_queue_xmit()
进入发送队列,最终由网络设备驱动处理。在封装数据包头部时,内核会调用dev_hard_header()
,进而触发header_ops->create
10。 -
典型场景:
发送IP数据包时,通过arp_find()
获取目标MAC地址后,调用header_ops->create
生成以太网头部。
2. 数据包接收时:解析链路层头部
当网络设备接收到数据帧时,内核需解析链路层头部以确定协议类型(如IPv4/IPv6),此时调用header_ops->parse
方法。
-
调用路径示例:
数据包通过netif_receive_skb()
进入协议栈处理流程,eth_type_trans()
函数会调用header_ops->parse
解析头部,提取协议类型并设置skb->protocol
字段1011。 -
作用:
区分数据包所属协议,确保后续协议栈(如IP层)能正确处理。
3. 邻居子系统交互:缓存与更新链路层地址
在ARP或NDP(IPv6邻居发现)过程中,当需要缓存或更新邻居的链路层地址(如MAC地址)时,会调用header_ops->cache
方法。
-
调用路径示例:
邻居子系统通过neigh_update()
更新邻居表项时,若需要缓存地址,会调用header_ops->cache
。例如,ARP响应处理完成后,将目标MAC地址缓存以供后续数据包直接使用1011。 -
作用:
避免频繁触发ARP请求,提升网络效率。
4. 头部重建:路径或配置变更时
当网络路径变化(如网关更新)或需要重新生成链路层头部时,可能调用header_ops->rebuild
方法。
-
典型场景:
若ARP缓存失效或路由变更,需重新构建头部信息。例如,通过neigh_forced_gc()
清理过期邻居表项后,重新发送数据包时会触发头部重建10。
5. 特殊设备类型的定制操作
某些网络设备(如虚拟网卡、隧道设备)可能需要定制链路层头部处理逻辑:
-
TAP设备:模拟以太网设备,需通过
header_ops
处理完整的以太网头部。 -
TUN设备:仅处理IP层数据,可能不设置
header_ops
,因其无需链路层封装210。
总结
header_ops
的调用时机主要集中在以下场景:
-
发送数据包:通过
create
构建链路层头部。 -
接收数据包:通过
parse
解析协议类型。 -
邻居交互:通过
cache
缓存或更新链路层地址。 -
头部重建:通过
rebuild
应对路径或配置变更。 -
设备定制:虚拟设备根据功能需求选择性实现。
若驱动未正确实现header_ops
,可能导致数据包封装错误、协议解析失败或邻居子系统异常,甚至触发内核Oops13。因此,需根据设备类型(如以太网卡需完整实现)合理设置该字段。
在Linux内核中,header_ops->create
函数的第四个地址参数(通常是目标链路层地址,如MAC地址)的传递过程与网络协议栈的路由和邻居子系统密切相关。以下是详细的步骤分析:
1. header_ops->create
的函数原型
首先,明确header_ops->create
的函数定义。在Linux内核中,其原型通常如下:
int (*create)(struct sk_buff *skb, struct net_device *dev, unsigned short type, const void *daddr, const void *saddr, unsigned int len);
参数含义:
-
skb
: 待发送的数据包缓冲区(struct sk_buff
)。 -
dev
: 发送数据的网络设备(struct net_device
)。 -
type
: 协议类型(如ETH_P_IP
表示IPv4)。 -
daddr
: 目标链路层地址(如目标MAC地址)。 -
saddr
: 源链路层地址(通常从dev->dev_addr
获取)。 -
len
: 数据包长度。
用户问题的核心是 daddr
(第四个参数)的来源。
2. daddr
的传递路径
daddr
的生成和传递涉及以下内核组件:
(1) 路由子系统确定下一跳IP地址
当上层协议(如IP层)需要发送数据包时,首先通过路由子系统确定下一跳的IP地址:
-
调用
ip_route_output_flow()
或fib_lookup()
查询路由表。 -
路由结果中会包含下一跳的IP地址(
struct flowi4
或struct flowi6
)。
(2) 邻居子系统解析链路层地址
通过邻居子系统(ARP或IPv6 ND)将下一跳的IP地址转换为链路层地址(MAC地址):
-
IPv4:通过
arp_resolve()
触发ARP请求,获取目标IP对应的MAC地址。 -
IPv6:通过
ndisc_resolve()
触发邻居发现(ND)。
解析后的链路层地址会缓存在邻居表(struct neighbour
)中。
(3) 数据包发送时填充daddr
在数据包发送路径中,内核调用链路层头部构建函数时,从邻居表或路由结果中获取daddr
:
-
调用链:
ip_finish_output2() -> neigh_resolve_output() -> dev_hard_header() -> header_ops->create()
-
关键代码逻辑(以IPv4为例):
// net/ipv4/ip_output.c int ip_finish_output2(struct net *net, struct sock *sk, struct sk_buff *skb) { struct dst_entry *dst = skb_dst(skb); struct rtable *rt = (struct rtable *)dst; struct neighbour *neigh; // 获取下一跳的邻居信息 neigh = dst_neigh_lookup_skb(dst, skb); if (!neigh) return -ENODEV; // 解析邻居地址(可能触发ARP请求) int rc = neigh_resolve_output(neigh, skb); neigh_release(neigh); return rc; }
在
neigh_resolve_output()
中,如果邻居地址未解析(NUD_INCOMPLETE
状态),会触发ARP请求并等待响应。一旦地址解析完成,daddr
会被填充到skb
或直接传递给dev_hard_header()
。
3. 具体场景分析
(1) 单播流量
-
目标地址为单播IP地址:
-
通过路由子系统确定下一跳IP地址。
-
通过邻居子系统(ARP/ND)解析为对应的MAC地址。
-
daddr
参数为解析后的目标MAC地址。
-
(2) 广播/多播流量
-
目标地址为广播(如
255.255.255.255
)或多播(如IPv4组播地址):-
路由子系统直接返回广播或多播地址。
-
daddr
参数为预定义的广播MAC地址(FF:FF:FF:FF:FF:FF
)或组播MAC地址(如IPv4组播映射为01:00:5E:xx:xx:xx
)。 -
无需触发ARP/ND解析,直接填充固定地址。
-
(3) 点对点链路(如PPP)
-
无需目标链路层地址:
-
daddr
可能为NULL
或固定值(如全零)。 -
链路层头部可能不包含目标地址字段(如PPP协议)。
-
4. 代码验证
以以太网设备为例,查看eth_header()
函数的实现(位于net/ethernet/eth.c
):
int eth_header(struct sk_buff *skb, struct net_device *dev, unsigned short type, const void *daddr, const void *saddr, unsigned int len) { struct ethhdr *eth = skb_push(skb, ETH_HLEN); // 填充目标MAC地址(daddr) if (daddr) ether_addr_copy(eth->h_dest, daddr); else memset(eth->h_dest, 0, ETH_ALEN); // 填充源MAC地址(saddr,通常来自dev->dev_addr) if (saddr) ether_addr_copy(eth->h_source, saddr); else ether_addr_copy(eth->h_source, dev->dev_addr); eth->h_proto = htons(type); return ETH_HLEN; }
-
daddr
的来源:由调用者(如dev_hard_header()
)通过路由和邻居子系统传递。
5. 总结
header_ops->create
的第四个参数(daddr
)的传递机制如下:
-
路由决策:确定下一跳的IP地址。
-
邻居解析:通过ARP(IPv4)或ND(IPv6)将IP地址转换为链路层地址。
-
地址注入:将解析后的地址(或广播/组播地址)传递给
header_ops->create
,构建链路层头部。
若邻居地址未解析(如首次发送数据包),会触发ARP请求并暂停数据包发送,直到地址解析完成。这一过程确保了链路层头部的正确性和网络通信的可靠性。
在Linux内核的IPoIB(IP over InfiniBand)实现中,本地链路层地址(如InfiniBand的GID或LID)的使用主要集中在以下几个关键模块和流程中:
1. 设备初始化与地址配置
IPoIB驱动在初始化网络设备(struct net_device
)时,会从InfiniBand硬件获取本地端口的GID(Global Identifier)作为链路层地址,并赋值给dev->dev_addr
字段。例如:
-
GID的获取:通过InfiniBand Verbs接口(如
ib_query_gid()
)查询端口的GID,并将其格式化为类似MAC地址的结构(长度为16字节)。 -
地址赋值:将GID写入
dev_addr
,供协议栈后续使用。代码示例如下:dev->dev_addr_len = INFINIBAND_ALEN; // 通常为16字节 memcpy(dev->dev_addr, gid.raw, dev->dev_addr_len);
这一步骤使得IPoIB接口在逻辑上兼容标准的网络接口操作57。
2. 链路层头部构建(header_ops->create
)
在封装IP数据包时,IPoIB的header_ops->create
函数(如ipoib_header()
)会填充InfiniBand链路层头部,其中:
-
源地址:从
dev->dev_addr
中获取本地GID。 -
目标地址:通过邻居子系统解析目标IP对应的远端GID(类似于以太网ARP机制)。
代码逻辑简化如下:struct ipoib_header *hdr = skb_push(skb, sizeof(*hdr)); memcpy(hdr->saddr, dev->dev_addr, INFINIBAND_ALEN); // 本地GID memcpy(hdr->daddr, daddr, INFINIBAND_ALEN); // 目标GID hdr->proto_type = htons(type); // 协议类型(如IPv4/IPv6)
这一过程使得InfiniBand网络能够像以太网一样承载IP数据包58。
3. 邻居子系统与地址解析(ARP/ND)
IPoIB通过ARP(IPv4)或ND(IPv6)机制将目标IP地址映射到GID:
-
ARP请求:当发送数据包时,若邻居表中无目标IP的GID缓存,IPoIB会触发多播ARP请求,通过InfiniBand网络广播查询目标IP对应的GID。
-
ARP响应:目标节点返回包含其GID的响应,邻居子系统将GID缓存为链路层地址。
例如,ip neigh
命令显示的lladdr
字段即为GID87。
4. 路由与邻居缓存更新
在路由决策后,IPoIB通过邻居子系统(如neigh_resolve_output()
)获取下一跳的GID,并将其传递给链路层头部构建函数。若GID缓存失效,会触发ARP重新解析8。
5. 特殊模式下的地址处理
-
Connected模式:允许大报文传输,直接使用QP(Queue Pair)的地址,减少头部开销。
-
Datagram模式:需严格遵循MTU限制,头部包含完整的GID信息27。
6. 用户空间工具与调试接口
通过系统文件(如/sys/class/net/ib0/address
)可查看IPoIB接口的GID,或使用ibstat
、ibaddr
等工具直接查询InfiniBand端口信息78。
总结
IPoIB源码中本地链路层地址(GID)的核心应用场景包括:
-
设备初始化:从硬件获取GID并配置到
net_device
。 -
数据包封装:在链路层头部填充源/目标GID。
-
地址解析:通过ARP/ND机制动态映射IP到GID。
-
邻居缓存:维护GID与IP的映射关系以优化性能。
这些设计使得IPoIB在保持InfiniBand高性能的同时,无缝集成到Linux网络协议栈中578。
static void set_base_guid(struct ipoib_dev_priv *priv, union ib_gid *gid)
{
struct ipoib_dev_priv *child_priv;
struct net_device *netdev = priv->dev;
netif_addr_lock_bh(netdev);
memcpy(&priv->local_gid.global.interface_id,
&gid->global.interface_id,
sizeof(gid->global.interface_id));
memcpy(netdev->dev_addr + 4, &priv->local_gid, sizeof(priv->local_gid));
clear_bit(IPOIB_FLAG_DEV_ADDR_SET, &priv->flags);
netif_addr_unlock_bh(netdev);
if (!test_bit(IPOIB_FLAG_SUBINTERFACE, &priv->flags)) {
down_read(&priv->vlan_rwsem);
list_for_each_entry(child_priv, &priv->child_intfs, list)
set_base_guid(child_priv, gid);
up_read(&priv->vlan_rwsem);
}
}
set_base_guid
函数在 IPoIB(IP over InfiniBand)驱动 中用于更新设备的 基础GUID(Global Unique Identifier),即 InfiniBand 网络中的链路层标识符。以下是该函数的分步解析及其核心作用:
1. 函数功能概述
该函数的主要目的是 更新 IPoIB 设备(包括其子接口)的本地 GID,并同步修改网络设备的链路层地址(dev_addr
),确保协议栈能正确识别设备的 InfiniBand 链路层身份。
2. 关键代码分析
(1) 锁定网络设备地址
netif_addr_lock_bh(netdev);
-
作用:通过自旋锁(spinlock)锁定设备的地址配置,防止并发修改导致数据竞争。
(2) 更新本地 GID
memcpy(&priv->local_gid.global.interface_id, &gid->global.interface_id, sizeof(gid->global.interface_id));
-
操作:将传入的 GID(
gid->global.interface_id
)复制到设备的私有数据结构priv->local_gid
中。 -
意义:GID 是 InfiniBand 网络中设备的唯一标识符,用于链路层通信。
(3) 更新网络设备地址
memcpy(netdev->dev_addr + 4, &priv->local_gid, sizeof(priv->local_gid));
-
目标地址:
netdev->dev_addr
是网络设备的链路层地址(如以太网的 MAC 地址)。在 IPoIB 中,它被设计为 20 字节 的结构:-
前 4 字节:固定头(
0x00 0x00 0x00 0x00
或类型标识)。 -
后 16 字节:存储 GID。
-
-
操作:将
priv->local_gid
(16 字节)复制到dev_addr + 4
的位置,覆盖原有的 GID 部分。 -
示例:
// dev_addr 结构示意 // [0-3] [4-19] // 固定头 | GID
(4) 清除设备地址标志
clear_bit(IPOIB_FLAG_DEV_ADDR_SET, &priv->flags);
-
作用:标记设备的链路层地址(
dev_addr
)需要重新配置。后续流程(如ipoib_set_mac
)会检查此标志,确保地址正确初始化。
(5) 递归更新子接口
if (!test_bit(IPOIB_FLAG_SUBINTERFACE, &priv->flags)) { down_read(&priv->vlan_rwsem); list_for_each_entry(child_priv, &priv->child_intfs, list) set_base_guid(child_priv, gid); up_read(&priv->vlan_rwsem); }
-
条件:当前设备是 父接口(非子接口)。
-
操作:遍历所有子接口(VLAN 或子设备),递归调用
set_base_guid
,确保所有子接口继承相同的基 GID。 -
同步机制:通过读写锁(
vlan_rwsem
)保护子接口列表,避免遍历时列表被修改。
3. 调用场景
该函数通常在以下情况下被调用:
-
设备初始化:在 IPoIB 设备启动时,从 InfiniBand 硬件获取初始 GID。
-
GID 变更事件:当 InfiniBand 端口状态变化(如热插拔、重新连接)导致 GID 更新时。
-
用户配置操作:通过管理工具强制更新设备的 GID。
4. 示例场景
假设 InfiniBand 交换机的端口重新配置,导致设备的 GID 发生变化。此时:
-
驱动通过 InfiniBand 事件(如
IB_EVENT_PORT_ACTIVE
)检测到 GID 变更。 -
调用
set_base_guid
更新本地 GID 和dev_addr
。 -
所有子接口(如
ib0.100
)同步更新,确保整个 IPoIB 逻辑网络的链路层标识一致。
5. 总结
set_base_guid
的核心作用是:
-
同步 GID:确保 IPoIB 设备的链路层地址与 InfiniBand 硬件实际地址一致。
-
维护子接口一致性:递归更新所有子接口,保持网络拓扑的完整性。
-
协议栈兼容性:通过更新
dev_addr
,使 Linux 网络协议栈能正确处理 InfiniBand 链路层封装。
在 IPoIB(IP over Infiniband)中,本地 GID(Global Identifier)通过 set_base_guid
函数从 InfiniBand 硬件获取并设置到网络设备的 dev_addr
字段中。而对端 GID 或 MAC 地址的获取机制则依赖于协议栈的 地址解析(ARP/ND) 和 邻居子系统,具体过程如下:
1. 对端 GID 的获取机制
(1) ARP/ND 地址解析
IPoIB 通过 ARP(IPv4)或 ND(IPv6 邻居发现)协议动态解析目标 IP 对应的 GID:
-
触发条件:当发送数据包时,若邻居表中无目标 IP 的 GID 缓存,IPoIB 驱动会触发 多播 ARP 请求,通过 InfiniBand 网络广播查询目标 IP 的 GID。
-
响应处理:目标节点收到请求后,通过 ARP 响应返回其 GID。邻居子系统将 GID 缓存为链路层地址(
lladdr
),供后续数据包直接使用。
(2) 邻居子系统缓存
-
缓存结构:GID 与 IP 的映射关系存储在
struct neighbour
中,可通过ip neigh
命令查看,例如:# ip neigh 192.168.1.3 dev ib0 lladdr 80:00:04:04:fe:80:00:00:00:00:00:00:00:02:c9:02:00:21:70:d1 REACHABLE
其中
lladdr
的后 16 字节为目标 GID。
(3) GID 的组成
IPoIB 的链路层地址(dev_addr
)为 20 字节,结构如下:
-
前 4 字节:固定标识(如队列对号)。
-
中间 8 字节:子网前缀(用于全局路由)。
-
后 8 字节:端口的 GUID(Global Unique Identifier)。
对端 GID 的解析需包含子网前缀和 GUID,这些信息由 ARP 响应提供。
2. MAC 地址的特殊性
在 IPoIB 中,不直接使用传统 MAC 地址,而是以 GID 作为链路层标识:
-
MAC 地址字段的兼容性:IPoIB 的
dev_addr
被设计为 20 字节的扩展格式,与传统以太网的 6 字节 MAC 地址不兼容。通过ip link
或ip neigh
命令显示的lladdr
字段实际为 GID 的编码形式。 -
协议栈适配:Linux 网络协议栈将 GID 视为链路层地址处理,确保 IPoIB 接口与标准网络接口(如以太网)行为一致。
3. 特殊场景下的地址获取
(1) 广播/组播通信
-
广播地址:目标 GID 为全 1(
FF:FF:FF:FF:FF:FF:FF:FF
),直接发送到所有节点。 -
组播地址:通过 IPv4/IPv6 组播地址映射到 InfiniBand 组播 GID,无需 ARP 解析。
(2) 非 IPoIB 模式(纯 Verbs 编程)
若绕过 IPoIB 直接使用 InfiniBand Verbs,需通过 带外机制(如 SSH、共享文件)手动交换地址信息:
-
LID(Local ID):通过
ibv_query_port()
获取本地端口的 16 位 LID。 -
QP 地址:队列对(Queue Pair)的地址需显式传递给通信对端。
4. 调试与验证
-
查看本地 GID:通过
/sys/class/infiniband/<设备>/ports/<端口号>/gids/0
文件读取36。 -
查看邻居缓存:使用
ip neigh
或arp -n
命令验证 GID 解析结果。
总结
对端 GID 的获取主要依赖 动态 ARP/ND 解析,由协议栈自动完成;而传统 MAC 地址在 IPoIB 中无实际意义,被扩展的 GID 取代。若需直接操作 InfiniBand 地址(如 LID 或 QP),则需结合带外通信和 Verbs API。