云网络发展早期,很多厂商的虚拟化网路方案基于内核模块来实现,这时的带宽通常从千兆到万兆,一般情况下还能够满足要求(SDWAN厂商华夏创新的网络加速就是在内核的PREROUTING上做的,性能还可以)。再后来,进入到25G时代了,基于内核的一些实现已经不能满足业务对网络吞吐的性能要求了,大约在2013~2018左右,很多厂商转向用DPDK实现,着实火了一段时间。到现在逐渐进入100G时代,会发现,无论大厂小厂,都逐渐开始转向智能网卡,网络特性offload到硬件,基本是当前和今后一段时间的主流。
通常情况下,交换芯片厂商会提供用户态软件开发工具包(SDK)来实现与硬件的接口。要用交换芯片设计一个交换机或路由器产品,设备商需要开发一个网络操作系统(NOS)或者移植像SONiC一样的开源产品。这并不能够有效地满足不断变化的市场需求。为了满足网络扩展和应用需求的增长,硬件设备商需要不断开发新技术和新协议,这就导致变更的成本高昂,大部分情况下客户都会抱怨响应速度太慢。
Switchdev的出现就帮助厂商解决了这一难题,因为它利用大家熟知的Linux开源框架,用户利用Linux环境和工具就可以打破厂商的禁锢,这种灵活性和自由度可以更好地满足客户的需求。
如下图所示,Switchdev位于Linux内核层,它可以将内核的数据转发平面卸载到交换机的ASIC芯片上。通过这种方式,就可以用标准开放的Linux接口取代专有的SDK和NOS接口。Switchdev还规划了一个统一的接口,简化了集成、配置和支持的过程。与SONiC等NOS相比,Switchdev驱动的网络系统更加轻量级。
智能网卡厂商也可以利用开放的Linux原生接口来实现对硬件的控制。Switchdev可以用来管理服务器端的网卡,配置物理端口和虚拟端口之间的通信。
Switchdev项目由Linux内核(具体是netdev)托管,这是Linux基金会旗下的一个开源项目社区。随着Linux内核的变化,无论是增加新的功能还是关键问题的修复,通过社区就可以确保Switchdev基础架构持续向前演进。 Linux有众多的应用程序来实现各种网络协议,主要的愿景就是让这些协议能够利用到交换机的硬件能力,或者说利用到交换芯片来卸载流量处理,这样就不会浪费通用CPU资源。为了将基础级的Switchdev转化为白盒设备所需的全面商用级NOS,Linux基金会正在围绕Switchdev建立一个应用生态系统DENT,创始成员包括亚马逊和一些网络硬件厂商。
DENT操作系统架构图 DENT是基于Ubuntu的Linux发行版,它封装了交换机硬件(风扇、温度传感器、ASIC等)的驱动程序以及开源的FRRouting路由协议套件,其中包括BGP、IS-IS、LDP、OSPF、PIM和RIP的协议守护进程。FRRouting软件使用Linux netlink API对Linux内核的数据包转发进行编程,在硬件交换机平台上,数据包转发由switchdev驱动卸载到ASIC进行线速转发。 DENT采用Linux作为网络操作系统的一个主要优势是,用于配置、管理和监控Linux服务器的工具也可以用来管理网络交换机。此外,DENT虚拟机的运行方式与在物理交换机上运行的DENT完全相同,这样就可以保证配置在进入生产网络之前在虚拟环境中得到充分验证。
本案例利用开源的Host sFlow代理从Linux主机收集标准的sFlow遥测数据。在基于switchdev的网络交换机上,通过标准的Linux API就可以收集硬件计数器数据用于监控网络接口。Host sFlow 代理也支持 Linux psample 和 drop_monitor API,可以提供数据流和丢包的可视性。在硬件交换机上,switchdev驱动程序将测量交由交换机ASIC完成,从而可以实现数据包线速转发的可视性。 在基于DENT OS的交换机上安装Host sFlow代理,交换芯片通过标准sFlow硬件模块可以将网络遥测数据流传输到sFlow采集器,例如sFlow-RT,从而可以获得网络自动化所需的实时全网性能视图。
流量采集案例图
目前高端网络设备市场已经开始普及白盒产品,中低端商用交换机和路由器等细分市场如果想要打开白盒交换机的想象空间,就必须有一套开放、灵活和标准的解决方案,Switchdev的目标就是为客户提供一个全新的、简化的网络设备开发环境。 通过采用Switchdev作为开源基础架构框架,就可以利用到开放的Linux工具,设备厂商省去开发专有NOS或网管系统的商务成本,同时减少整合商业交换芯片所需的繁琐开发流程。统一的Linux界面也降低了客户的使用成本,简化了学习曲线。智能网卡采用Switchdev可以方便地与数据中心或网络编排系统(OpenStack、Kubernetes、XCloud等)集成。 开发Switchdev需要包括Linux内核、驱动和应用方面的相关知识,软件工程师要有底层网络和嵌入式软件开发能力,包括对所选交换芯片SDK的了解和平台集成经验。芯片和设备厂商在为其产品开发Switchdev驱动时,在遵循开源社区的开发原则同时,建议将相应产品的Switchdev驱动向Linux内核提交。因为Switchdev尚在发展中,设备厂商在为产品适配Switchdev时,相关平台代码以及移植问题上传到开源社区,就可以确保在未来的Linux发行版中得到支持。
switchdev框架是从Linux 4.0引入的,它代表一类拥有“交换”能力芯片的多网口设备的抽象。其中每一个网口就是一个port,在switchdev框架中被注册成一个net_device。除此之外,内核中自带了一个rocker driver,演示了一个实际的设备驱动的实现。
利用Switchdev,除了常见的Linux内核数据面能够卸载到硬件,也可以直接将流表注入到设备中,从而指导设备直接进行数据包交换,如mellanox的一些智能网卡的做法。采用了硬件交换模块的Linux BOX和原来的截然不同了,它更像是一个高端的专业网络设备,类似Cisco那样的。它看起来就是下面的样子:
它是内核中自带了一个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的设备驱动提供参考。
从第一张图,我们可以看到能够通过switchdev框架下发的硬件的转发面信息可能有下面一些:
1、二层fdb表项
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做两个事情,
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_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);
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,调用的地方不多,可以自己看。
linux switchdev 介绍 & 源码 - 简书
Switchdev:释放开源Linux的网络力量_mob604756f52321的技术博客_51CTO博客
Linux switchdev | Houmin