0.1. 场景
用户在 SONiC 设备上执行 sudo config interface mtu eth0 2000,把 eth0 端口的 MTU 改为 2000。
本文从 CLI 一路追踪到 ASIC 硬件寄存器写入,覆盖每一层涉及的进程、Redis 数据库变化、消息通道、关键代码与时间顺序。
本文中所有代码引用均基于本机 /Users/martinzhang/git/sonic/ 目录下的真实代码。
0.2. 一、整体架构与角色 0.2.1. 1.1 涉及的 Redis 数据库
DB 编号
名称
作用
DB 4
CONFIG_DB
用户的目标配置(CLI/REST/gNMI 写入此处)
DB 0
APPL_DB
各 mgrd 写入的”期望态”,供 orchagent 消费
DB 1
ASIC_DB
SAI 对象的下发态,供 syncd 消费
DB 6
STATE_DB
实际运行态(端口 oper_status、能力等)
0.2.2. 1.2 涉及的进程 / 容器
容器
进程
角色
database
redis-server
持有 6 个逻辑 DB
host
config(Python click) → portconfig(Python)
CLI 入口
swss
portmgrd
订阅 CONFIG_DB.PORT,写 kernel + APPL_DB
swss
orchagent
订阅 APPL_DB.PORT_TABLE,写 ASIC_DB
syncd-bfn(TF 平台对应容器)
syncd
订阅 ASIC_DB,调用 libsai_tofino.so
同 syncd 容器
bf_switchd
Tofino SDE 主进程,被 libsai 通过 IPC/直链调用
0.2.3. 1.3 整体数据流 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 用户 ──CLI──▶ config/main.py ──exec──▶ portconfig 脚本 │ (Python 写 CONFIG_DB) ▼ ┌──────────────────────────────────────────────────────────┐ │ Redis 实例(多 DB) │ │ DB 4 = CONFIG_DB DB 0 = APPL_DB DB 1 = ASIC_DB │ │ DB 6 = STATE_DB │ └──────────────┬───────────────────────────────────────────┘ │ keyspace notify "PORT|eth0" ▼ portmgrd (swss 容器) - ip link set dev eth0 mtu 2000 → Linux kernel - 写 APPL_DB:_PORT_TABLE:eth0 mtu=2000 │ │ ProducerStateTable channel ▼ APPL_DB 的 _PORT_TABLE: 临时表 + channel 通知 │ ▼ ┌─────────── 上半部:orchagent 进程(厂商无关) ───────────┐ │ │ │ PortsOrch ::doPortTask () │ │ - 调用 sai_port_api->set_port_attribute (...) │ │ (SAI 标准接口,函数指针由 libsairedis 填充) │ │ - 实际跳到 RedisRemoteSaiInterface ::set │ │ - 序列化属性 → 写 ASIC_DB → PUBLISH 通知 │ │ │ └────────────────────────┬────────────────────────────────┘ ▼ ASIC_DB: ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0xXXXX mtu=2022 │ (channel REDIS_ASIC_STATE_COMMAND_SET) │ ★ 进程间通过 Redis pub/sub 解耦 ▼ ┌─────────── 下半部:syncd 进程(厂商相关) ──────────────┐ │ │ │ Syncd ::processQuadEvent → processOidSet │ │ - VID → RID 翻译 │ │ - 调用 m_vendorSai->set (objectType, rid, attr) │ │ (VendorSai 类成员方法,不是 sai_port_api 调用) │ │ - VendorSai 内部按 objectType 派发到厂商 SAI 表 │ │ m_apis.port_api->set_port_attribute (rid, attr) │ │ (函数指针由厂商 libsai_xxx.so 填充) │ │ │ │ Tofino SDE (bf_switchd) │ │ - bf_pal_port_mtu_set () │ │ - 通过 PCIe 写 Tofino TM/EBuf 寄存器 │ │ │ └────────────────────────┬────────────────────────────────┘ ▼ ASIC 硬件 MTU 立即生效
0.3. 二、时间轴(t0 → t12)
0.4. 四、时间轴一览(压缩版) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 t0 CLI: config interface mtu eth0 2000 t1 config/main.py.mtu( ) → exec portconfig -p eth0 -m 2000 t2 portconfig.set_mtu( ) → CONFIG_DB: HSET PORT|eth0 mtu 2000 t3 Redis pub keyspace event → portmgrd 被唤醒 t4 PortMgr::d oTask 取出 ( eth0, SET, mtu=2000 ) t5 PortMgr:: setPortMtu: ├─ exec "ip link set dev eth0 mtu 2000" → Linux netdev MTU=2000 └─ writeConfigToAppDb( eth0, mtu, 2000 ) → APPL_DB._PORT_TABLE t6 ProducerStateTable channel → ConsumerStateTable in orchagent 临时表合并到 PORT_TABLE,事件入 m_toSync t7 PortsOrch::d oPortTask 处理 mtu 字段(如有 RIF 联动 setRouterIntfsMtu) t8 PortsOrch:: setPortMtu ( orchagent 进程) : mtu+22 = 2022 调 sai_port_api->set_port_attribute ← SAI 标准接口(指向 sairedis 实现) t9 RedisRemoteSaiInterface:: set ( orchagent 进程,厂商无关) : → ASIC_DB: ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:... SAI_PORT_ATTR_MTU=2022 → PUBLISH "set" 到 ASIC_STATE_CHANNEL ★ 上半部到此结束,进程间通过 Redis 解耦 t10 syncd 进程 ( 厂商相关) : processEvent → processSingleEvent → processQuadEvent → processOidSet → VID→RID 翻译 → m_vendorSai->set( ) ← VendorSai 类成员方法(不是 sai_port_api) → 内部派发到 m_apis.port_api->set_port_attribute(厂商 libsai_tofino.so 填充) t11 libsai_tofino → bf_switchd → bf_pal_port_mtu_set → 写 Tofino TM 寄存器 max_frame_size → 硬件 MTU 立即生效 t12 syncd 把 SAI 返回值通过 GETRESPONSE 通道回给 orchagent orchagent 把日志 "Set port eth0 MTU to 2000" 写入 syslog
0.4.1. t0 — 用户敲命令 1 sudo config interface mtu eth0 2000
0.4.2. t1 — sonic-utilities 入口 文件: sonic-utilities/config/main.py | 行号: 5728-5750 | 函数: mtu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @interface.command() @click.pass_context @click.argument('interface_name' , metavar='<interface_name>' , required=True ) @click.argument('interface_mtu' , metavar='<interface_mtu>' , required=True , type =click.IntRange(68 , 9216 ) )@click.option('-v' , '--verbose' , is_flag=True , help ="Enable verbose output" ) def mtu (ctx, interface_name, interface_mtu, verbose ): """Set interface mtu""" config_db = ctx.obj['config_db' ] if clicommon.get_interface_naming_mode() == "alias" : interface_name = interface_alias_to_name(config_db, interface_name) if interface_name is None : ctx.fail("'interface_name' is None!" ) portchannel_member_table = config_db.get_table('PORTCHANNEL_MEMBER' ) if interface_is_in_portchannel(portchannel_member_table, interface_name): ctx.fail("'interface_name' is in portchannel!" ) if ctx.obj['namespace' ] is DEFAULT_NAMESPACE: command = ['portconfig' , '-p' , str (interface_name), '-m' , str (interface_mtu)] else : command = ['portconfig' , '-p' , str (interface_name), '-m' , str (interface_mtu), '-n' , str (ctx.obj['namespace' ])] ... clicommon.run_command(command, display_cmd=verbose)
关键动作 :
校验 MTU 范围 [68, 9216]
校验该 port 不在 PortChannel 成员里
fork 子进程 portconfig -p eth0 -m 2000
0.4.3. t2 — portconfig 脚本写入 CONFIG_DB 文件: sonic-utilities/scripts/portconfig | 行号: 152-158 | 函数: portconfig.set_mtu
1 2 3 4 5 6 def set_mtu (self, port, mtu ): if self .is_lag: raise Exception("Invalid port %s" % (port)) if self .verbose: print ("Setting mtu %s on port %s" % (mtu, port)) self .db.mod_entry(PORT_TABLE_NAME, port, {PORT_MTU_CONFIG_FIELD_NAME: mtu})
mod_entry('PORT', 'eth0', {'mtu': '2000'}) 等价于 Redis 命令:
1 2 HSET "PORT|eth0" mtu 2000 ( CONFIG_DB / DB4) PUBLISH __keyspace@4 __:PORT|eth0 hset ( Redis 自动发出)
此时 CONFIG_DB 状态 :
1 2 3 4 5 6 127.0.0.1 :6379 [4 ]> HGETALL "PORT|eth0" 1 ) "admin_status" 2 ) "up" 3 ) "alias" 4 ) "eth0" 5 ) "lanes" 6 ) "65,66,67,68" 7 ) "mtu" 8 ) "2000" ← 新值9 ) "speed" 10 ) "100000"
0.4.4. t3 — Redis keyspace 通知触发 portmgrd portmgrd 启动时订阅了 CONFIG_DB 的 PORT 表:
文件: sonic-swss/cfgmgr/portmgrd.cpp | 行号: 27-40 | 函数: main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 vector<string> cfg_port_tables = { CFG_PORT_TABLE_NAME, CFG_SEND_TO_INGRESS_PORT_TABLE_NAME, };DBConnector cfgDb ("CONFIG_DB" , 0 ) ;DBConnector appDb ("APPL_DB" , 0 ) ;DBConnector stateDb ("STATE_DB" , 0 ) ;PortMgr portmgr (&cfgDb, &appDb, &stateDb, cfg_port_tables) ; vector<Orch *> cfgOrchList = {&portmgr}; swss::Select s;for (Orch *o : cfgOrchList) { s.addSelectables (o->getSelectables ()); }
PortMgr 内部创建 SubscriberStateTable("PORT"),挂在 epoll 上。Redis 一发布 keyspace event,select() 立刻返回,调到 PortMgr::doTask。
0.4.5. t4 — PortMgr::doTask 取出变更 文件: sonic-swss/cfgmgr/portmgr.cpp | 行号: 134-225 | 函数: PortMgr::doTask
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 void PortMgr::doTask (Consumer &consumer) { ... while (it != consumer.m_toSync.end ()) { KeyOpFieldsValuesTuple t = it->second; string alias = kfvKey (t); string op = kfvOp (t); ... bool portOk = isPortStateOk (alias); ... for (auto i : kfvFieldsValues (t)) { if (fvField (i) == "mtu" ) { mtu = fvValue (i); } else if (fvField (i) == "admin_status" ) { ... } else { field_values.emplace_back (i); } } ... if (field_values.size ()) writeConfigToAppDb (alias, field_values); ... if (!mtu.empty ()) { setPortMtu (alias, mtu); SWSS_LOG_NOTICE ("Configure %s MTU to %s" , alias.c_str (), mtu.c_str ()); } if (!admin_status.empty ()) { setPortAdminStatus (alias, admin_status == "up" ); ... } } }
isPortStateOk 要求 STATE_DB.PORT_TABLE:eth0.state = ok,即 portsyncd 已经创建对应的 netdev 并标记完成。
0.4.6. t5 — setPortMtu:先内核,再 APPL_DB 文件: sonic-swss/cfgmgr/portmgr.cpp | 行号: 25-58 | 函数: PortMgr::setPortMtu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 bool PortMgr::setPortMtu (const string &alias, const string &mtu) { stringstream cmd; string res, cmd_str; cmd << IP_CMD << " link set dev " << shellquote (alias) << " mtu " << shellquote (mtu); cmd_str = cmd.str (); int ret = swss::exec (cmd_str, res); if (!ret) { return writeConfigToAppDb (alias, "mtu" , mtu); } ... }
这一步串行做两件事 :
同步配置到 Linux 内核 (/sbin/ip link set dev eth0 mtu 2000)
底层是 RTNETLINK,更新该 netdev 的 dev->mtu
影响内核协议栈的 ARP/IPv4/IPv6/ICMP/socket 分片决策
这就是 host 网络(BGP、SSH、ping、tcpdump)能立刻”看到”新 MTU 的原因
如果 ip 命令成功 ,再写 APPL_DB:1 2 SET "_PORT_TABLE:eth0" mtu 2000 (DB 0 )PUBLISH PORT_TABLE_CHANNEL@0 ...
背后是 ProducerStateTable ,写到的是带下划线前缀的”临时表”,并发通道事件给 orchagent。
关于”是否要先 down 再 up” :不需要。从 iproute2(ip/iplink.c:939-946,addattr_l(IFLA_MTU, ...) 透传)到内核核心层(net/core/dev.c:9892,netif_set_mtu_ext 不检查 IFF_UP),任何一层都不强制要求接口先 down。是否需要 down 完全是各驱动 ndo_change_mtu 的私有决定。SONiC 的 eth0 是 SAI hostif 创建的虚拟 netdev(类似 tun/veth),没有 ring buffer,ndo_change_mtu 直接 dev->mtu = new_mtu,零副作用。
0.4.7. t6 — ProducerStateTable / ConsumerStateTable 机制 ProducerStateTable.set() 写入大致做:
1 2 1 ) 把 fields 临时写到 _PORT_TABLE:eth02 ) PUBLISH 一个 op="set" 到 PORT_TABLE_CHANNEL
orchagent 的 ConsumerStateTable("PORT_TABLE") 收到 channel 事件后:
1 2 3 1 ) 用 lua 脚本把 _PORT_TABLE:eth0 字段合并到 PORT_TABLE:eth02 ) 把 ( key , op, fvs) 推到 m_toSync 队列3 ) 回到 epoll 主循环,触发 Orch::d oTask
这套机制保证了 多次写入合并、orchagent 看到的是一致的字段全集 ,避免普通 hash + keyspace notify 的”丢事件/丢字段”问题。
0.4.8. t7 — PortsOrch::doPortTask 处理 mtu 字段 文件: sonic-swss/orchagent/portsorch.cpp | 行号: 5375-5407 | 函数: PortsOrch::doPortTask(mtu 分支)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 if (pCfg.mtu.is_set) { if (p.m_mtu != pCfg.mtu.value) { if (!setPortMtu (p, pCfg.mtu.value)) { SWSS_LOG_ERROR ("Failed to set port %s MTU to %u" , p.m_alias.c_str (), pCfg.mtu.value); it++; continue ; } p.m_mtu = pCfg.mtu.value; m_portList[p.m_alias] = p; if (p.m_rif_id) { gIntfsOrch->setRouterIntfsMtu (p); } if (gP4Orch) { gP4Orch->setRouterIntfsMtu (p.m_alias, p.m_mtu); } updateChildPortsMtu (p, pCfg.mtu.value); SWSS_LOG_NOTICE ("Set port %s MTU to %u" , p.m_alias.c_str (), pCfg.mtu.value); } }
如果 eth0 已经配过 IP(即有 RIF),还会调 IntfsOrch::setRouterIntfsMtu 联动更新三层接口 MTU:
文件: sonic-swss/orchagent/intfsorch.cpp | 行号: 222-246 | 函数: IntfsOrch::setRouterIntfsMtu
1 2 3 4 5 6 7 8 9 10 11 12 bool IntfsOrch::setRouterIntfsMtu (const Port &port) { SWSS_LOG_ENTER (); sai_attribute_t attr; attr.id = SAI_ROUTER_INTERFACE_ATTR_MTU; attr.value.u32 = port.m_mtu; sai_status_t status = sai_router_intfs_api-> set_router_interface_attribute (port.m_rif_id, &attr); ... }
这条会再走一次 sairedis → ASIC_DB → syncd → libsai_tofino.so,把 RIF 对象的 MTU 也改成 2000,影响三层转发的 too-big drop / ICMP unreachable 行为。
0.4.9. t8 — PortsOrch::setPortMtu:调 SAI 文件: sonic-swss/orchagent/portsorch.cpp | 行号: 2385-2419 | 函数: PortsOrch::setPortMtu
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 bool PortsOrch::setPortMtu (const Port& port, sai_uint32_t mtu) { SWSS_LOG_ENTER (); sai_attribute_t attr; attr.id = SAI_PORT_ATTR_MTU; mtu += (uint32_t )(sizeof (struct ether_header) + FCS_LEN + VLAN_TAG_LEN); attr.value.u32 = mtu; if (isMACsecPort (port.m_port_id)) attr.value.u32 += MAX_MACSEC_SECTAG_SIZE; sai_status_t status = sai_port_api->set_port_attribute (port.m_port_id, &attr); ... }
⚠️ 单位差异 :用户配 mtu 2000 是 L3 payload 长度 ,但下到 ASIC 的是 L2 帧总长 (要再加 14 字节 Ethernet header + 4 字节 FCS + 4 字节 VLAN tag = 22 字节)。所以 ASIC 实际写的是 2022 。MACsec 口还要再加 32 字节 SecTag。
在 redis-cli -n 1 看到 ASIC_DB MTU = 2022 不是 bug。
0.4.10. t9 — sairedis 把 SAI 调用编码进 ASIC_DB 文件: sonic-sairedis/lib/RedisRemoteSaiInterface.cpp | 行号: 994-1022 | 函数: RedisRemoteSaiInterface::set
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 sai_status_t RedisRemoteSaiInterface::set ( _In_ sai_object_type_t objectType, _In_ const std::string &serializedObjectId, _In_ const sai_attribute_t *attr) { SWSS_LOG_ENTER (); auto entry = SaiAttributeList::serialize_attr_list ( objectType, 1 , attr, false ); auto serializedObjectType = sai_serialize_object_type (objectType); std::string key = serializedObjectType + ":" + serializedObjectId; SWSS_LOG_DEBUG ("generic set key: %s, fields: %lu" , key.c_str (), entry.size ()); m_recorder->recordGenericSet (key, entry); m_communicationChannel->set (key, entry, REDIS_ASIC_STATE_COMMAND_SET); auto status = waitForResponse (SAI_COMMON_API_SET); ... }
m_communicationChannel(默认是 RedisChannel)会:
1 2 3 1 ) 在 ASIC_DB 写 key "ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x1000000000016" 字段 SAI_PORT_ATTR_MTU = 2022 2 ) PUBLISH 一条 "set" 消息到 ASIC_STATE_CHANNEL
此时 ASIC_DB 状态 :
1 2 127.0.0.1:6379 [1] > HGETALL "ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x1000000000016" "SAI_PORT_ATTR_MTU" "2022"
注意这里的 oid:0x1000000000016 是 VID(Virtual ID) ,由 orchagent 侧的 sairedis 生成;它和真正给到 SDK 的硬件 RID(Real ID)不是同一个,需要 syncd 来翻译。
0.4.11. t10 — syncd 接收事件并调真正的 vendor SAI 0.4.11.1. 10.1 syncd 启动时订阅 ASIC_STATE channel 文件: sonic-sairedis/syncd/Syncd.cpp | 行号: 163-176 | 函数: Syncd::Syncd(构造函数)
1 2 3 4 5 6 m_selectableChannel = std::make_shared <sairedis::RedisSelectableChannel>( m_dbAsic, ASIC_STATE_TABLE, REDIS_TABLE_GETRESPONSE, TEMP_PREFIX, modifyRedis);
文件: sonic-sairedis/meta/RedisSelectableChannel.cpp | 行号: 7-32 | 函数: RedisSelectableChannel::RedisSelectableChannel
1 m_asicState = std::make_shared <swss::ConsumerTable>(m_dbAsic.get (), asicStateTable);
文件: sonic-swss-common/common/consumertable.cpp | 行号: 17-39 | 函数: ConsumerTable::ConsumerTable
1 subscribe (m_db, getChannelName (m_db->getDbId ()));
构造期就发出了 Redis SUBSCRIBE ASIC_STATE_CHANNEL@1 命令。
0.4.11.2. 10.2 主循环 select 触发 文件: sonic-sairedis/syncd/Syncd.cpp | 行号: 6694-6697 | 函数: Syncd::run
1 2 3 4 s->addSelectable (m_selectableChannel.get ()); s->addSelectable (m_restartQuery.get ()); s->addSelectable (m_flexCounter.get ()); s->addSelectable (m_flexCounterGroup.get ());
文件: sonic-sairedis/syncd/Syncd.cpp | 行号: 6724-6824 | 函数: Syncd::run(主循环)
1 2 3 4 5 6 7 8 9 10 11 while (runMainLoop) { swss::Selectable *sel = NULL ; int result = s->select (&sel, 1000 ); ... else if (sel == m_selectableChannel.get ()) { processEvent (*m_selectableChannel.get ()); } ... }
0.4.11.3. 10.3 processEvent → processSingleEvent → processQuadEvent 文件: sonic-sairedis/syncd/Syncd.cpp | 行号: 377-399 | 函数: Syncd::processEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 void Syncd::processEvent (_In_ sairedis::SelectableChannel& consumer) { SWSS_LOG_ENTER (); std::lock_guard<std::mutex> lock (m_mutex) ; do { swss::KeyOpFieldsValuesTuple kco; consumer.pop (kco, isInitViewMode ()); processSingleEvent (kco); } while (!consumer.empty ()); }
文件: sonic-sairedis/syncd/Syncd.cpp | 行号: 434-462 | 函数: Syncd::processSingleEvent
1 2 3 4 5 6 7 8 9 10 11 if (op == REDIS_ASIC_STATE_COMMAND_CREATE) return processQuadEvent (SAI_COMMON_API_CREATE, kco);if (op == REDIS_ASIC_STATE_COMMAND_REMOVE) return processQuadEvent (SAI_COMMON_API_REMOVE, kco);if (op == REDIS_ASIC_STATE_COMMAND_SET) return processQuadEvent (SAI_COMMON_API_SET, kco);if (op == REDIS_ASIC_STATE_COMMAND_GET) return processQuadEvent (SAI_COMMON_API_GET, kco);
0.4.11.4. 10.4 processOidSet:VID→RID + 调 vendor SAI 文件: sonic-sairedis/syncd/Syncd.cpp | 行号: 4520-4540 | 函数: Syncd::processOid
1 2 3 4 5 6 7 8 9 10 11 12 switch (api) { case SAI_COMMON_API_CREATE: return processOidCreate (objectType, strObjectId, attr_count, attr_list); case SAI_COMMON_API_REMOVE: return processOidRemove (objectType, strObjectId); case SAI_COMMON_API_SET: return processOidSet (objectType, strObjectId, attr_list); case SAI_COMMON_API_GET: return processOidGet (objectType, strObjectId, attr_count, attr_list); ... }
文件: sonic-sairedis/syncd/Syncd.cpp | 行号: 4702-4723 | 函数: Syncd::processOidSet
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 sai_status_t Syncd::processOidSet ( _In_ sai_object_type_t objectType, _In_ const std::string &strObjectId, _In_ sai_attribute_t *attr) { SWSS_LOG_ENTER (); sai_object_id_t objectVid; sai_deserialize_object_id (strObjectId, objectVid); sai_object_id_t rid = m_translator->translateVidToRid (objectVid); sai_status_t status = m_vendorSai->set (objectType, rid, attr); if (Workaround::isSetAttributeWorkaround (objectType, attr->id, status)) return SAI_STATUS_SUCCESS; return status; }
关键澄清 :syncd 进程没有调用 sai_port_api->set_port_attribute(),它调用的是 VendorSai 类的成员方法 m_vendorSai->set(objectType, rid, attr)。
VendorSai::set 内部根据 objectType 派发到对应的厂商 SAI 函数表——对于 PORT 对象,等价于 m_apis.port_api->set_port_attribute(rid, attr)。这里的 m_apis.port_api 是 syncd 启动时通过厂商 libsai_xxx.so 的 sai_api_query() 拿到的函数指针表(在 Tofino 平台是 libsai_tofino.so)。这一步才真正进入厂商代码 。
0.4.12. t11 — Tofino 这一段:libsai_tofino → bf_switchd → ASIC 到这里就进入厂商私有代码 ,公开仓库里没有完整源码(在 Intel/Barefoot 的 SDE 里,构建成 libsai_tofino.so 和 bf_switchd)。但内部走向是公开的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 libsai_tofino.so : sai_port_api_t::set_port_attribute (rid, SAI_PORT_ATTR_MTU) │ ▼ 通过 thrift / 直链 IPC 跨到同一容器中的 bf_switchd bf_switchd (TF SDE 主进程) │ ▼bf_pal_port_mtu_set (dev_id, dev_port, mtu) │ ├──▶ MAC block 配置(TM/EBuf 的 max_frame_size 寄存器) │ 通过 PCIe BAR0 写到 Tofino MAU/TM 寄存器 │ 具体寄存器: │ Tofino-1 : tm_top.tm_ebuf .epb_disp_port_regs │ .epb_pipe_port_regs [N] .max_frame_size │ Tofino-2 : 类似命名 ▼ ASIC 硬件 - 入端口超过 MTU 的帧 → drop(counter: PORT_OVER_MTU_DISCARD) - 出端口 TM 计数 → 不再切分
说明 :Tofino 不像内核那样做 IP 分片——这不是 ASIC 的工作;超长帧直接计入 PORT_OVER_MTU_DISCARD 计数器丢弃。MTU 是 hot change,不需要 disable port。
0.4.13. t12 — 返回响应 + syslog 记录 syncd 把 SAI 返回值通过 GETRESPONSE 通道回送给 orchagent:
同步模式 (SAI_REDIS_COMMUNICATION_MODE_REDIS_SYNC):orchagent 在 RedisRemoteSaiInterface::waitForResponse 阻塞等待。
异步模式 (默认):orchagent 不等响应。
orchagent 把日志写入 syslog:
1 NOTICE swss#orchagent: :- setPortMtu: Set port eth0 MTU to 2000
0.5. 三、Redis 三个数据库的最终状态 0.5.1. CONFIG_DB (DB 4) 1 2 3 HGETALL "PORT|eth0" ... mtu 2000
0.5.2. APPL_DB (DB 0) 1 2 3 HGETALL "PORT_TABLE:eth0" ... mtu 2000
0.5.3. ASIC_DB (DB 1) 1 2 HGETALL "ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x1000000000016" SAI_PORT_ATTR_MTU 2022 # 加了 22 字节 L2 头开销
如果端口配过 IP(有 RIF)还会有:
1 2 HGETALL "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x6000000000093" SAI_ROUTER_INTERFACE_ATTR_MTU 2000
0.5.4. STATE_DB (DB 6) 1 2 HGETALL "PORT_TABLE|eth0" state ok
0.6. 五、几个值得关注的设计细节 0.6.1. 5.1 portmgrd 是”双写者” 同时写内核和 APPL_DB,但顺序是 先内核,后 APPL_DB 。如果内核失败,根本不会写 APPL_DB,因此永远不会出现”硬件配了但内核没配” 。这就是 SONiC 选择”先内核再 ASIC”的核心原因之一。
0.6.2. 5.2 MTU 单位换算只在一处 PortsOrch::setPortMtu 是唯一加 22 字节(+ 32 字节 MACsec 可选)的位置。Linux 内核里 dev->mtu = 2000 表示 IP payload 长度,到 ASIC 是帧长。这点和 SAI 抽象层”语义不一致需要 SONiC 来翻译”对得上。
0.6.3. 5.3 ProducerStateTable 保证字段完整性 _PORT_TABLE 临时表 + channel 通知机制保证了 orchagent 看到的是合并后的字段全集 ,不会一次只看到 mtu 而看不到 admin_status。
0.6.4. 5.4 VID↔RID 隔离 orchagent 完全不需要知道真实硬件对象编号——orchagent 只面向”逻辑 OID”,真实硬件 OID 由 syncd 启动时通过 objectTypeQuery / SaiSwitch 探测得到。
0.6.5. 5.5 TF 平台的特殊性 bf_switchd 习惯独占进程跑 SDE,所以 SONiC 的 Barefoot syncd 容器里 syncd 进程会通过 thrift/IPC 与 bf_switchd 通信;这与 Broadcom(直接 dlopen libsai_xgs.so 跑在 syncd 同进程)不同——但对 ASIC_DB 之上的所有逻辑都是透明的 。这正是 SAI 抽象的价值。
0.6.6. 5.6 上半部 vs 下半部:两段平行,不是包含关系 这是理解 SONiC 架构最容易出错的点。orchagent 和 syncd 不是上下游调用关系,而是平行的两段独立处理 ,通过 ASIC_DB 解耦:
维度
上半部(orchagent 进程)
下半部(syncd 进程)
处理阶段
APPL_DB → ASIC_DB
ASIC_DB → 硬件
调用入口
sai_port_api->set_port_attribute(oid, &attr)
m_vendorSai->set(objectType, rid, &attr)
接口性质
SAI 标准接口(全局函数指针表)
VendorSai 类的成员方法
指针来源
libsairedis 填充
厂商 libsai_xxx.so 通过 sai_api_query 填充
实际跳到
RedisRemoteSaiInterface::set
厂商私有函数(如 bf_pal_port_mtu_set)
作用
写 ASIC_DB + PUBLISH
调 SDK 写硬件寄存器
厂商相关性
❌ 完全无关
🔴 紧密相关
链接的库
libsairedis.so
libsai_tofino.so / libsai_brcm.so / …
所在容器
swss(通用,编一次跑遍所有 ASIC)
syncd-bfn / syncd-brcm / …(按厂商打包)
关键点 :
sai_port_api->set_port_attribute() 只在 orchagent 进程中被调用 ,整个过程与厂商无关。它是 SAI 标准接口,但在这里的实现是 sairedis 提供的 “假 SAI”——把调用序列化为 ASIC_DB 操作,并不真正写硬件。
syncd 进程根本不调用 sai_port_api->set_port_attribute() 。syncd 调用的是 m_vendorSai->set()(VendorSai 类的成员方法),由 VendorSai 内部派发到厂商 SAI 表,最终进入厂商 libsai_xxx.so 实现。
两段通过 ASIC_DB 完全解耦 :可以独立重启(warm reboot)、独立替换(换通信方式只动上半部,换厂商只动下半部)、独立调试(saiplayer 录制回放)。
ASIC_DB 作为”标准化交换格式”,所有厂商内容完全一致 ——这是这种解耦设计能成立的基础。
0.6.7. 5.7 改 MTU 不需要 down 接口
iproute2 用户态 :ip/iplink.c:939-946 仅 addattr_l(IFLA_MTU, ...) 透传,不检查 IFF_UP。
内核 netlink 入口 :net/core/rtnetlink.c:3143 不检查 IFF_UP。
内核核心层 :net/core/dev.c:9892 的 netif_set_mtu_ext 仅做范围校验和 netif_device_present 校验。
驱动层 :是否需要 down 由具体驱动决定(如 e1000_change_mtu 在 netif_running 时会内部 e1000_down/e1000_up,但 IFF_UP flag 不变,对协议栈透明)。
SONiC 的 eth0 是 SAI hostif 虚拟 netdev (类似 tun/veth),没有 ring buffer,零副作用。
0.7. 六、关键代码索引
步骤
文件
行号
函数
t1
sonic-utilities/config/main.py
5728-5750
mtu
t2
sonic-utilities/scripts/portconfig
152-158
portconfig.set_mtu
t3
sonic-swss/cfgmgr/portmgrd.cpp
27-40
main
t4
sonic-swss/cfgmgr/portmgr.cpp
134-225
PortMgr::doTask
t5
sonic-swss/cfgmgr/portmgr.cpp
25-58
PortMgr::setPortMtu
t7
sonic-swss/orchagent/portsorch.cpp
5375-5407
PortsOrch::doPortTask
t7
sonic-swss/orchagent/intfsorch.cpp
222-246
IntfsOrch::setRouterIntfsMtu
t8
sonic-swss/orchagent/portsorch.cpp
2385-2419
PortsOrch::setPortMtu
t9
sonic-sairedis/lib/RedisRemoteSaiInterface.cpp
994-1022
RedisRemoteSaiInterface::set
t10
sonic-sairedis/syncd/Syncd.cpp
163-176
Syncd::Syncd(构造)
t10
sonic-sairedis/meta/RedisSelectableChannel.cpp
7-32
RedisSelectableChannel::RedisSelectableChannel
t10
sonic-swss-common/common/consumertable.cpp
17-39
ConsumerTable::ConsumerTable
t10
sonic-sairedis/syncd/Syncd.cpp
6694-6697
Syncd::run(addSelectable)
t10
sonic-sairedis/syncd/Syncd.cpp
377-399
Syncd::processEvent
t10
sonic-sairedis/syncd/Syncd.cpp
434-462
Syncd::processSingleEvent
t10
sonic-sairedis/syncd/Syncd.cpp
4702-4723
Syncd::processOidSet
0.8. 七、调试与验证 0.8.1. 7.1 命令行实操 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 sudo config interface mtu eth0 2000 redis-cli -n 4 HGETALL "PORT|eth0" redis-cli -n 0 HGETALL "PORT_TABLE:eth0" redis-cli -n 1 KEYS "ASIC_STATE:SAI_OBJECT_TYPE_PORT:*" redis-cli -n 1 HGETALL "ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x1000000000016" ip link show eth0 redis-cli -n 6 HGETALL "PORT_TABLE|eth0" sudo grep -E "(portmgrd|orchagent|syncd)" /var/log/syslog | tail -50
0.8.2. 7.2 验证 hot change 不影响协议 1 2 3 4 5 6 7 8 9 10 ip -s link show eth0 show ip bgp summarysudo config interface mtu eth0 9100 ip -s link show eth0 show ip bgp summary dmesg | tail
0.9. 八、扩展阅读
对比 intfmgrd(IP 地址)和 portmgrd(mtu/admin_status)的双写差异
看 platform/barefoot/ 里 SONiC 怎么打包 TF SDE,理解 docker-syncd-bfn 容器内 bf_switchd 与 syncd 的协作(supervisord.conf)
看 consumer_table_pops.lua:理解 ConsumerTable 用 lua 脚本”批量+原子+把临时 hash 搬到正式 hash”的设计
看 Syncd::processQuadEvent 4338-4540:OID 对象 vs Entry 对象(route_entry / fdb_entry)的处理分歧