linux switchdev 介绍 & 源码
云网络发展趋势
云网络发展早期,很多厂商的虚拟化网路方案基于内核模块来实现,这时的带宽通常从千兆到万兆,一般情况下还能够满足要求(SDWAN厂商华夏创新的网络加速就是在内核的PREROUTING上做的,性能还可以)。再后来,进入到25G时代了,基于内核的一些实现已经不能满足业务对网络吞吐的性能要求了,大约在2013~2018左右,很多厂商转向用DPDK实现,着实火了一段时间。到现在逐渐进入100G时代,会发现,无论大厂小厂,都逐渐开始转向智能网卡,网络特性offload到硬件,基本是当前和今后一段时间的主流。
switchdev介绍
通常情况下,交换芯片厂商会提供用户态软件开发工具包(SDK)来实现与硬件的接口。要用交换芯片设计一个交换机或路由器产品,设备商需要开发一个网络操作系统(NOS)或者移植像SONiC一样的开源产品。这并不能够有效地满足不断变化的市场需求。为了满足网络扩展和应用需求的增长,硬件设备商需要不断开发新技术和新协议,这就导致变更的成本高昂,大部分情况下客户都会抱怨响应速度太慢。
Switchdev的出现就帮助厂商解决了这一难题,因为它利用大家熟知的Linux开源框架,用户利用Linux环境和工具就可以打破厂商的禁锢,这种灵活性和自由度可以更好地满足客户的需求。
如图所示,Switchdev位于Linux内核层,它可以将内核的数据转发平面卸载到交换机的ASIC芯片上。通过这种方式,就可以用标准开放的Linux接口取代专有的SDK和NOS接口。Switchdev还规划了一个统一的接口,简化了集成、配置和支持的过程。与SONiC等NOS相比,Switchdev驱动的网络系统更加轻量级。
智能网卡厂商也可以利用开放的Linux原生接口来实现对硬件的控制。Switchdev可以用来管理服务器端的网卡,配置物理端口和虚拟端口之间的通信。

switchdev框架是从Linux 4.0引入的,它代表一类拥有“交换”能力芯片的多网口设备的抽象。其中每一个网口就是一个port,在switchdev框架中被注册成一个net_device。除此之外,内核中自带了一个rocker driver,演示了一个实际的设备驱动的实现。

利用Switchdev,除了常见的Linux内核数据面能够卸载到硬件,也可以直接将流表注入到设备中,从而指导设备直接进行数据包交换,如mellanox的一些智能网卡的做法。采用了硬件交换模块的Linux BOX和原来的截然不同了,它更像是一个高端的专业网络设备,类似Cisco那样的。它看起来就是下面的样子:

相关源码
rocker dirver
它是内核中自带了一个rocker driver,演示了一个实际的设备驱动的实现,最先支持 switchdev 的就是是 QEMU 的 Rocker 软件交换机。后来 Mellanox 和 Broadcom 等公司均提供了支持 switchdev 的交换机器。它就是一个pci dirver,对接了switchdev框架将kernel数据面下发的模拟的硬件。
static struct pci_driver rocker_pci_driver = {
.name = rocker_driver_name,
.id_table = rocker_pci_id_table,
.probe = rocker_probe,
.remove = rocker_remove,
};
Rocker 是一个模拟网络交换机平台,旨在加速内核网络交换机驱动程序模型的开发。 Rocker 有两个部分:一个带有 PCI 主机接口的 62 端口交换机芯片的 Qemu 仿真和一个 Linux 设备驱动程序。 目标是模拟数据中心/企业中使用的当代网络交换机 ASIC 的功能,以便社区可以在内核中开发交换机设备驱动程序接口。 最初的目标功能是 L2 桥接功能卸载和 L3 路由功能卸载。 在这两种情况下,转发(数据)平面都被卸载到交换机设备,但控制和管理平面仍保留在 Linux 中。 L2overL3 隧道、L2 绑定、ACL 支持和基于流的网络等其他功能正在计划中或正在进行中。

根据官方的说法,Rocker 背后的动机是加速开发用于网络交换机的 Linux 内核设备驱动程序模型,在没有供应商提供的开源驱动程序的情况下,Rocker 被创建为网络交换机设备的仿真,其功能集接近于现实世界的供应商交换机 ASIC。使用 Rocker 设备,我们可以创建设备驱动程序来开发和测试 switchdev 驱动程序模型,而无需依赖供应商的 SDK。期望一旦 switchdev 达到一定的成熟度,供应商或社区提供的用于现实世界 ASIC 的设备驱动程序将会出现,并且对 Rocker 的需求将随着时间的推移而减少。也就是说是一个演示性质的实现,为大家开发支持switchdev的设备驱动提供参考。
代码 (kernel 5.15)
从第一张图,我们可以看到能够通过switchdev框架下发的硬件的转发面信息可能有下面一些:
- port信息,包括接口的属性、状态等;
- 二层fdb表项,包括bridge fdb、vxlan fdb表项等;
- 三层转发表项;
- 通过tc下发的流控表项,openflow流表也是可以通过tc下发的。
下面简单看看一下这部分内核代码,简单过了一下,可能有错,仅供参考……_。
1、二层fdb表项
- bridge fdb表项:
无论是静态配置fdb表项或者是动态学习fdb表项,内核创建fdb表项成功后,会调用fdb_notify 函数,如:
void br_fdb_update(struct net_bridge *br, struct net_bridge_port *source,
const unsigned char *addr, u16 vid, unsigned long flags)
{
struct net_bridge_fdb_entry *fdb;
/* some users want to always flood. */
if (hold_time(br) == 0)
return;
fdb = fdb_find_rcu(&br->fdb_hash_tbl, addr, vid);
if (likely(fdb)) {
/* attempt to update an entry for a local interface */
if (unlikely(test_bit(BR_FDB_LOCAL, &fdb->flags))) {
if (net_ratelimit())
br_warn(br, "received packet on %s with own address as source address (addr:%pM, vlan:%u)\n",
source->dev->name, addr, vid);
} else {
unsigned long now = jiffies;
bool fdb_modified = false;
if (now != fdb->updated) {
fdb->updated = now;
fdb_modified = __fdb_mark_active(fdb);
}
/* fastpath: update of existing entry */
if (unlikely(source != READ_ONCE(fdb->dst) &&
!test_bit(BR_FDB_STICKY, &fdb->flags))) {
br_switchdev_fdb_notify(br, fdb, RTM_DELNEIGH);
WRITE_ONCE(fdb->dst, source);
fdb_modified = true;
/* Take over HW learned entry */
if (unlikely(test_bit(BR_FDB_ADDED_BY_EXT_LEARN,
&fdb->flags)))
clear_bit(BR_FDB_ADDED_BY_EXT_LEARN,
&fdb->flags);
}
if (unlikely(test_bit(BR_FDB_ADDED_BY_USER, &flags)))
set_bit(BR_FDB_ADDED_BY_USER, &fdb->flags);
if (unlikely(fdb_modified)) {
trace_br_fdb_update(br, source, addr, vid, flags);
fdb_notify(br, fdb, RTM_NEWNEIGH, true);
}
}
} else {
spin_lock(&br->hash_lock);
fdb = fdb_create(br, source, addr, vid, flags);
if (fdb) {
trace_br_fdb_update(br, source, addr, vid, flags);
fdb_notify(br, fdb, RTM_NEWNEIGH, true);
}
/* else we lose race and someone else inserts
* it first, don't bother updating
*/
spin_unlock(&br->hash_lock);
}
}
fdb_notify做两个事情,1)调用br_switchdev_fdb_notify 进switchdev框架,offload fdb表项到硬件;2)通过netilink 发布fdb表项变化事件(RTNLGRP_NEIGH),用户态内核态都可以监听以获取fdb数据变化。
static void fdb_notify(struct net_bridge *br,
const struct net_bridge_fdb_entry *fdb, int type,
bool swdev_notify)
{
struct net *net = dev_net(br->dev);
struct sk_buff *skb;
int err = -ENOBUFS;
if (swdev_notify)
br_switchdev_fdb_notify(br, fdb, type);
skb = nlmsg_new(fdb_nlmsg_size(), GFP_ATOMIC);
if (skb == NULL)
goto errout;
err = fdb_fill_info(skb, br, fdb, 0, 0, type, 0);
if (err < 0) {
/* -EMSGSIZE implies BUG in fdb_nlmsg_size() */
WARN_ON(err == -EMSGSIZE);
kfree_skb(skb);
goto errout;
}
rtnl_notify(skb, net, 0, RTNLGRP_NEIGH, NULL, GFP_ATOMIC);
return;
errout:
rtnl_set_sk_err(net, RTNLGRP_NEIGH, err);
}
void
br_switchdev_fdb_notify(struct net_bridge *br,
const struct net_bridge_fdb_entry *fdb, int type)
{
const struct net_bridge_port *dst = READ_ONCE(fdb->dst);
struct switchdev_notifier_fdb_info info = {
.addr = fdb->key.addr.addr,
.vid = fdb->key.vlan_id,
.added_by_user = test_bit(BR_FDB_ADDED_BY_USER, &fdb->flags),
.is_local = test_bit(BR_FDB_LOCAL, &fdb->flags),
.offloaded = test_bit(BR_FDB_OFFLOADED, &fdb->flags),
};
struct net_device *dev = (!dst || info.is_local) ? br->dev : dst->dev;
switch (type) {
case RTM_DELNEIGH:
call_switchdev_notifiers(SWITCHDEV_FDB_DEL_TO_DEVICE,
dev, &info.info, NULL);
break;
case RTM_NEWNEIGH:
call_switchdev_notifiers(SWITCHDEV_FDB_ADD_TO_DEVICE,
dev, &info.info, NULL);
break;
}
}
br_switchdev_fdb_notify函数通过内核通知链,发出 fdb 添加到设备(SWITCHDEV_FDB_ADD_TO_DEVICE)、从设备删除(SWITCHDEV_FDB_DEL_TO_DEVICE)通知,如果设备支持switchdev框架,设备驱动中会注册并相应这两个通知的内核通知链,假如还是rocker dirver。
Rocker 在 rocker_switchdev_event 中处理这两个通知,通过queue_work 最终调用 rocker_switchdev_event_work 函数处理这两个通知,调用rocker的wops->port_obj_fdb_add 添加fdb,挂载的是ofdpa_port_obj_fdb_add 函数,一直往下看,在ofdpa_flow_tbl_bridge 函数中将fdb数据封装成ofdpa_flow_tbl_entry,统一成了流表数据,其实rocker中,无论是fdb 还是 fib,或者是tc下发的转发规则包括ovs流表,最终都转化为ofdpa_flow_tbl_entry 。
最后的流程如下,ofdpa_cmd_flow_tbl_add 将ofdpa_flow_tbl_entry 再加工成 struct rocker_desc_info *desc_info,是qemu virio driver识别的数据了,再执行rocker_desc_head_set(rocker, &rocker->cmd_ring, desc_info); 放置到驱动的ring中等待取走。
static int ofdpa_flow_tbl_add(struct ofdpa_port *ofdpa_port,
int flags, struct ofdpa_flow_tbl_entry *match)
{
struct ofdpa *ofdpa = ofdpa_port->ofdpa;
struct ofdpa_flow_tbl_entry *found;
size_t key_len = match->key_len ? match->key_len : sizeof(found->key);
unsigned long lock_flags;
match->key_crc32 = crc32(~0, &match->key, key_len);
spin_lock_irqsave(&ofdpa->flow_tbl_lock, lock_flags);
found = ofdpa_flow_tbl_find(ofdpa, match);
if (found) {
match->cookie = found->cookie;
hash_del(&found->entry);
kfree(found);
found = match;
found->cmd = ROCKER_TLV_CMD_TYPE_OF_DPA_FLOW_MOD;
} else {
found = match;
found->cookie = ofdpa->flow_tbl_next_cookie++;
found->cmd = ROCKER_TLV_CMD_TYPE_OF_DPA_FLOW_ADD;
}
hash_add(ofdpa->flow_tbl, &found->entry, found->key_crc32);
spin_unlock_irqrestore(&ofdpa->flow_tbl_lock, lock_flags);
return rocker_cmd_exec(ofdpa_port->rocker_port,
ofdpa_flags_nowait(flags),
ofdpa_cmd_flow_tbl_add,
found, NULL, NULL);
}
int rocker_cmd_exec(struct rocker_port *rocker_port, bool nowait,
rocker_cmd_prep_cb_t prepare, void *prepare_priv,
rocker_cmd_proc_cb_t process, void *process_priv)
{
struct rocker *rocker = rocker_port->rocker;
struct rocker_desc_info *desc_info;
struct rocker_wait *wait;
unsigned long lock_flags;
int err;
spin_lock_irqsave(&rocker->cmd_ring_lock, lock_flags);
desc_info = rocker_desc_head_get(&rocker->cmd_ring);
if (!desc_info) {
spin_unlock_irqrestore(&rocker->cmd_ring_lock, lock_flags);
return -EAGAIN;
}
wait = rocker_desc_cookie_ptr_get(desc_info);
rocker_wait_init(wait);
wait->nowait = nowait;
err = prepare(rocker_port, desc_info, prepare_priv);
if (err) {
spin_unlock_irqrestore(&rocker->cmd_ring_lock, lock_flags);
return err;
}
rocker_desc_head_set(rocker, &rocker->cmd_ring, desc_info);
spin_unlock_irqrestore(&rocker->cmd_ring_lock, lock_flags);
if (nowait)
return 0;
if (!rocker_wait_event_timeout(wait, HZ / 10))
return -EIO;
err = rocker_desc_err(desc_info);
if (err)
return err;
if (process)
err = process(rocker_port, desc_info, process_priv);
rocker_desc_gen_clear(desc_info);
return err;
}
2、三层转发表项
- 内核添加fib表项,调用 fib_table_insert 函数,添加成功后,发出一个 FIB_EVENT_ENTRY_ADD 类型的notifier call,注册过内核通知链的模块将处理该通知。
fib_table_insert:
call_fib_entry_notifiers(net, FIB_EVENT_ENTRY_ADD,
key, plen, fi,
new_fa->fa_tos, cfg->fc_type,
tb->tb_id, cfg->fc_nlflags);
- rocker驱动中注册了这个事件,还有其他路有事件都在这里处理。
static int rocker_router_fib_event(struct notifier_block *nb,
unsigned long event, void *ptr)
{
struct rocker *rocker = container_of(nb, struct rocker, fib_nb);
struct fib_entry_notifier_info *fen_info = ptr;
int err;
switch (event) {
case FIB_EVENT_ENTRY_ADD:
err = rocker_world_fib4_add(rocker, fen_info);
if (err)
rocker_world_fib4_abort(rocker);
else
break;
case FIB_EVENT_ENTRY_DEL:
rocker_world_fib4_del(rocker, fen_info);
break;
case FIB_EVENT_RULE_ADD: /* fall through */
case FIB_EVENT_RULE_DEL:
rocker_world_fib4_abort(rocker);
break;
}
return NOTIFY_DONE;
}
static int rocker_world_fib4_add(struct rocker *rocker,
const struct fib_entry_notifier_info *fen_info)
{
struct rocker_world_ops *wops = rocker->wops;
if (!wops->fib4_add)
return 0;
return wops->fib4_add(rocker, fen_info);
}
ops 挂的 ofdpa_fib4_add 函数。
struct rocker_world_ops rocker_ofdpa_ops = {
.kind = "ofdpa",
.priv_size = sizeof(struct ofdpa),
.port_priv_size = sizeof(struct ofdpa_port),
.mode = ROCKER_PORT_MODE_OF_DPA,
.init = ofdpa_init,
.fini = ofdpa_fini,
.port_pre_init = ofdpa_port_pre_init,
.port_init = ofdpa_port_init,
.port_fini = ofdpa_port_fini,
.port_open = ofdpa_port_open,
.port_stop = ofdpa_port_stop,
.port_attr_stp_state_set = ofdpa_port_attr_stp_state_set,
.port_attr_bridge_flags_set = ofdpa_port_attr_bridge_flags_set,
.port_attr_bridge_flags_get = ofdpa_port_attr_bridge_flags_get,
.port_attr_bridge_ageing_time_set = ofdpa_port_attr_bridge_ageing_time_set,
.port_obj_vlan_add = ofdpa_port_obj_vlan_add,
.port_obj_vlan_del = ofdpa_port_obj_vlan_del,
.port_obj_vlan_dump = ofdpa_port_obj_vlan_dump,
.port_obj_fdb_add = ofdpa_port_obj_fdb_add,
.port_obj_fdb_del = ofdpa_port_obj_fdb_del,
.port_obj_fdb_dump = ofdpa_port_obj_fdb_dump,
.port_master_linked = ofdpa_port_master_linked,
.port_master_unlinked = ofdpa_port_master_unlinked,
.port_neigh_update = ofdpa_port_neigh_update,
.port_neigh_destroy = ofdpa_port_neigh_destroy,
.port_ev_mac_vlan_seen = ofdpa_port_ev_mac_vlan_seen,
.fib4_add = ofdpa_fib4_add,
.fib4_del = ofdpa_fib4_del,
.fib4_abort = ofdpa_fib4_abort,
};
最终调用ofdpa_flow_tbl_add,这个函数中调用rocker_cmd_exec函数先将ofdpa_flow_tbl_entry 转化为rocker_desc_info( ofdpa_cmd_flow_tbl_add 函数完成),再将rocker_desc_info set到驱动的 dma ring中等待硬件读取,流程结束。
static int ofdpa_flow_tbl_add(struct ofdpa_port *ofdpa_port,
struct switchdev_trans *trans, int flags,
struct ofdpa_flow_tbl_entry *match)
{
struct ofdpa *ofdpa = ofdpa_port->ofdpa;
struct ofdpa_flow_tbl_entry *found;
size_t key_len = match->key_len ? match->key_len : sizeof(found->key);
unsigned long lock_flags;
match->key_crc32 = crc32(~0, &match->key, key_len);
spin_lock_irqsave(&ofdpa->flow_tbl_lock, lock_flags);
found = ofdpa_flow_tbl_find(ofdpa, match);
if (found) {
match->cookie = found->cookie;
if (!switchdev_trans_ph_prepare(trans))
hash_del(&found->entry);
ofdpa_kfree(trans, found);
found = match;
found->cmd = ROCKER_TLV_CMD_TYPE_OF_DPA_FLOW_MOD;
} else {
found = match;
found->cookie = ofdpa->flow_tbl_next_cookie++;
found->cmd = ROCKER_TLV_CMD_TYPE_OF_DPA_FLOW_ADD;
}
if (!switchdev_trans_ph_prepare(trans))
hash_add(ofdpa->flow_tbl, &found->entry, found->key_crc32);
spin_unlock_irqrestore(&ofdpa->flow_tbl_lock, lock_flags);
if (!switchdev_trans_ph_prepare(trans))
return rocker_cmd_exec(ofdpa_port->rocker_port,
ofdpa_flags_nowait(flags),
ofdpa_cmd_flow_tbl_add,
found, NULL, NULL);
return 0;
}
int rocker_cmd_exec(struct rocker_port *rocker_port, bool nowait,
rocker_cmd_prep_cb_t prepare, void *prepare_priv,
rocker_cmd_proc_cb_t process, void *process_priv)
{
struct rocker *rocker = rocker_port->rocker;
struct rocker_desc_info *desc_info;
struct rocker_wait *wait;
unsigned long lock_flags;
int err;
spin_lock_irqsave(&rocker->cmd_ring_lock, lock_flags);
desc_info = rocker_desc_head_get(&rocker->cmd_ring);
if (!desc_info) {
spin_unlock_irqrestore(&rocker->cmd_ring_lock, lock_flags);
return -EAGAIN;
}
wait = rocker_desc_cookie_ptr_get(desc_info);
rocker_wait_init(wait);
wait->nowait = nowait;
err = prepare(rocker_port, desc_info, prepare_priv);
if (err) {
spin_unlock_irqrestore(&rocker->cmd_ring_lock, lock_flags);
return err;
}
rocker_desc_head_set(rocker, &rocker->cmd_ring, desc_info);
spin_unlock_irqrestore(&rocker->cmd_ring_lock, lock_flags);
if (nowait)
return 0;
if (!rocker_wait_event_timeout(wait, HZ / 10))
return -EIO;
err = rocker_desc_err(desc_info);
if (err)
return err;
if (process)
err = process(rocker_port, desc_info, process_priv);
rocker_desc_gen_clear(desc_info);
return err;
}
3、port 属性、状态
也是通过内核通知链完成的,同上,入口 switchdev_port_obj_add,调用的地方不多,可以自己看。