SONiC Port MTU 更新流程详解(以 Tofino 芯片为例)

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 2000Linux 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() │
│ - 通过 PCIeTofino 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::doTask 取出 (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::doPortTask 处理 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_DBPORT 表:

文件: 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); // "eth0"
string op = kfvOp(t); // "SET"
...
bool portOk = isPortStateOk(alias); // 检查 STATE_DB:PORT_TABLE:eth0
...
for (auto i : kfvFieldsValues(t))
{
if (fvField(i) == "mtu")
{
mtu = fvValue(i); // "2000"
}
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;

// ip link set dev <port_name> mtu <mtu>
cmd << IP_CMD << " link set dev " << shellquote(alias) << " mtu " << shellquote(mtu);
cmd_str = cmd.str();
int ret = swss::exec(cmd_str, res);
if (!ret)
{
// Set the port MTU in application database to update both
// the port MTU and possibly the port based router interface MTU
return writeConfigToAppDb(alias, "mtu", mtu);
}
...
}

这一步串行做两件事

  1. 同步配置到 Linux 内核/sbin/ip link set dev eth0 mtu 2000
    • 底层是 RTNETLINK,更新该 netdev 的 dev->mtu
    • 影响内核协议栈的 ARP/IPv4/IPv6/ICMP/socket 分片决策
    • 这就是 host 网络(BGP、SSH、ping、tcpdump)能立刻”看到”新 MTU 的原因
  2. 如果 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-946addattr_l(IFLA_MTU, ...) 透传)到内核核心层(net/core/dev.c:9892netif_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:eth0
2) PUBLISH 一个 op="set" 到 PORT_TABLE_CHANNEL

orchagentConsumerStateTable("PORT_TABLE") 收到 channel 事件后:

1
2
3
1) 用 lua 脚本把 _PORT_TABLE:eth0 字段合并到 PORT_TABLE:eth0
2)(key, op, fvs) 推到 m_toSync 队列
3) 回到 epoll 主循环,触发 Orch::doTask

这套机制保证了 多次写入合并、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); // 联动 RIF MTU
}
if (gP4Orch)
{
gP4Orch->setRouterIntfsMtu(p.m_alias, p.m_mtu);
}
// 子接口(如 eth0.10)继承父口
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 + 14 + 4 + 4 = 22 bytes */
mtu += (uint32_t)(sizeof(struct ether_header) + FCS_LEN + VLAN_TAG_LEN);
attr.value.u32 = mtu; // 2000 + 22 = 2022

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 2000L3 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;
// 例如: "SAI_OBJECT_TYPE_PORT:oid:0x1000000000016"

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);
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// 字符串 "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:0x1000000000016VID(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, // ★ "ASIC_STATE"
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

构造期就发出了 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());     // ★ 加入 epoll
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()); // ★ 收到 ASIC_STATE 事件
}
...
}

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()); // ★ 从 channel 取一条
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) // ★ "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); // 解析 VID

sai_object_id_t rid = m_translator->translateVidToRid(objectVid); // VID→RID

sai_status_t status = m_vendorSai->set(objectType, rid, attr); // ★ 调厂商 SAI

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.sosai_api_query() 拿到的函数指针表(在 Tofino 平台是 libsai_tofino.so)。这一步才真正进入厂商代码


0.4.12. t11 — Tofino 这一段:libsai_tofino → bf_switchd → ASIC

到这里就进入厂商私有代码,公开仓库里没有完整源码(在 Intel/Barefoot 的 SDE 里,构建成 libsai_tofino.sobf_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 # 仅记录 oper 状态,不会因 mtu 变化主动更新

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 / …(按厂商打包)

关键点

  1. sai_port_api->set_port_attribute() 只在 orchagent 进程中被调用,整个过程与厂商无关。它是 SAI 标准接口,但在这里的实现是 sairedis 提供的 “假 SAI”——把调用序列化为 ASIC_DB 操作,并不真正写硬件。

  2. syncd 进程根本不调用 sai_port_api->set_port_attribute()。syncd 调用的是 m_vendorSai->set()VendorSai 类的成员方法),由 VendorSai 内部派发到厂商 SAI 表,最终进入厂商 libsai_xxx.so 实现。

  3. 两段通过 ASIC_DB 完全解耦:可以独立重启(warm reboot)、独立替换(换通信方式只动上半部,换厂商只动下半部)、独立调试(saiplayer 录制回放)。

  4. ASIC_DB 作为”标准化交换格式”,所有厂商内容完全一致——这是这种解耦设计能成立的基础。

0.6.7. 5.7 改 MTU 不需要 down 接口

  • iproute2 用户态ip/iplink.c:939-946addattr_l(IFLA_MTU, ...) 透传,不检查 IFF_UP
  • 内核 netlink 入口net/core/rtnetlink.c:3143 不检查 IFF_UP
  • 内核核心层net/core/dev.c:9892netif_set_mtu_ext 仅做范围校验和 netif_device_present 校验。
  • 驱动层:是否需要 down 由具体驱动决定(如 e1000_change_mtunetif_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
# 1. 改 MTU
sudo config interface mtu eth0 2000

# 2. 看 CONFIG_DB
redis-cli -n 4 HGETALL "PORT|eth0"

# 3. 看 APPL_DB
redis-cli -n 0 HGETALL "PORT_TABLE:eth0"

# 4. 看 ASIC_DB(注意 oid 是动态生成的)
redis-cli -n 1 KEYS "ASIC_STATE:SAI_OBJECT_TYPE_PORT:*"
redis-cli -n 1 HGETALL "ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x1000000000016"

# 5. 看内核 netdev
ip link show eth0

# 6. 看 STATE_DB
redis-cli -n 6 HGETALL "PORT_TABLE|eth0"

# 7. 看 syslog
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
# 改 MTU 前看接口和 BGP 邻居状态
ip -s link show eth0
show ip bgp summary

sudo config interface mtu eth0 9100

# 改完后再看:oper_status 不会变 DOWN,BGP 不会闪
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_switchdsyncd 的协作(supervisord.conf
  • consumer_table_pops.lua:理解 ConsumerTable 用 lua 脚本”批量+原子+把临时 hash 搬到正式 hash”的设计
  • Syncd::processQuadEvent 4338-4540:OID 对象 vs Entry 对象(route_entry / fdb_entry)的处理分歧

SONiC Port MTU 更新流程详解(以 Tofino 芯片为例)
https://martinbj2008.github.io/2026/06/30/2026-06-30-sonic-port-mtu-flow.ai/
Author
Martinbj2008
Posted on
June 30, 2026
Licensed under