第一章:Go+RTOS混合编程的工业级意义与挑战
在高可靠性工业嵌入式系统中,实时性、内存确定性与开发效率长期处于结构性张力之中。RTOS(如FreeRTOS、Zephyr)保障微秒级中断响应与可预测调度,而Go语言凭借其并发模型、内存安全与快速迭代能力,在边缘网关、设备管理中间件等上层控制逻辑中日益普及。两者的混合编程并非简单共存,而是构建“分层可信计算栈”的关键实践:RTOS固件层直接驱动传感器、执行器与通信外设;Go运行时则作为轻量级协处理器代理,处理协议解析(如Modbus TCP/OPC UA)、本地AI推理结果聚合、OTA元数据校验等非硬实时但高复杂度任务。
实时性与GC停顿的冲突本质
Go默认GC采用三色标记清除算法,即使启用GOGC=off并手动触发runtime.GC(),STW(Stop-The-World)时间仍可能达数百微秒——远超典型工业PLC的100μs周期约束。解决方案需硬件协同:将Go进程绑定至独立CPU核心(如ARM Cortex-M7双核中的非RTOS核),并通过内存隔离区(MPU配置)禁止GC访问RTOS关键段。
跨层通信的确定性保障
推荐采用零拷贝共享内存+自旋锁机制替代传统IPC:
// RTOS侧(FreeRTOS,C)
volatile uint32_t go_cmd_flag __attribute__((section(".shared_ram"))); // MPU映射为Non-cacheable
uint8_t sensor_data[256] __attribute__((section(".shared_ram")));
// Go侧(通过cgo调用)
/*
#include "shared_mem.h"
*/
import "C"
func ReadSensor() []byte {
C.__sync_synchronize() // 内存屏障确保读序
if *C.go_cmd_flag == 1 {
return C.GoBytes(unsafe.Pointer(&C.sensor_data), 256)
}
return nil
}
工业部署的验证清单
- ✅ RTOS中断服务程序(ISR)禁用所有Go runtime调用(包括
printf、malloc) - ✅ Go交叉编译目标为
armv7-unknown-linux-musleabihf,静态链接避免动态库依赖 - ✅ 共享内存区域在链接脚本中显式分配,且经
mmap(MAP_SHARED | MAP_LOCKED)锁定物理页
这种混合架构已在风电变流器远程诊断模块中落地:RTOS以50μs精度采样IGBT温度,Go服务每200ms聚合10通道数据并执行异常检测,整体系统MTBF提升40%。
第二章:STM32H7平台上的实时执行环境构建
2.1 Cortex-M7双核架构与内存映射的确定性约束分析
Cortex-M7双核(如STM32H745/755)采用紧密耦合双核(CPU1+CPU2),但非对称共享内存架构带来关键确定性约束。
内存映射硬性分区
- AXI SRAM(0x20000000–0x2007FFFF):双核可缓存访问,需手动同步
- TCM RAM(ITCM/DTCM):仅绑定至对应核心,不可跨核直读
- Flash(0x08000000):支持双核取指,但写入需停核或使用专用指令序列
数据同步机制
// 双核共享变量强制内存屏障与缓存清理
volatile uint32_t shared_flag __attribute__((section(".shared_data")));
void signal_to_core2(void) {
__DSB(); // 数据同步屏障:确保之前写操作完成
SCB_CleanDCache_by_Addr((uint32_t)&shared_flag, 4); // 清理D-Cache行
__DSB();
__SEV(); // 触发事件,唤醒休眠中的Core2
}
__DSB() 确保写操作在内存系统中全局可见;SCB_CleanDCache_by_Addr 防止Core1修改后Core2仍读取脏缓存;__SEV 是双核通信最小开销唤醒原语。
确定性约束汇总
| 约束类型 | 典型影响 | 解决路径 |
|---|---|---|
| 缓存一致性 | D-Cache未清理导致读旧值 | 显式Clean/Invalidate |
| TCM独占访问 | Core2无法访问Core1的ITCM代码段 | 代码/数据重定位至AXI SRAM |
| 中断向量重映射 | 双核共用NVIC但向量表基址独立 | 各自配置VTOR寄存器 |
graph TD
A[Core1写共享内存] --> B{是否执行DSB?}
B -->|否| C[Core2可能读到陈旧值]
B -->|是| D[执行SCB_CleanDCache_by_Addr]
D --> E[Core2读取前需Invalidate]
E --> F[最终一致]
2.2 FreeRTOS内核裁剪与中断响应路径优化实践
关键裁剪宏配置
通过 FreeRTOSConfig.h 精准关闭非必要功能:
configUSE_TIMERS→ 设为(禁用软件定时器)configUSE_MUTEXES→ 设为(若无优先级继承需求)configUSE_COUNTING_SEMAPHORES→ 设为
中断响应路径精简
// 在 port.c 中重写 PendSV_Handler,移除上下文保存冗余判断
__attribute__((naked)) void PendSV_Handler( void )
{
__asm volatile
(
"ldr r0, =pxCurrentTCB \n" // 获取当前任务控制块地址
"ldr r0, [r0] \n" // 解引用获取 TCB 指针
"stmia r0!, {r4-r11} \n" // 直接压栈寄存器组(跳过条件检查)
"bx lr \n"
::: "r0"
);
}
逻辑分析:裸函数绕过编译器栈帧管理;stmia 批量压栈替代逐条 push,节省约 8 个周期;r4–r11 为 FreeRTOS 定义的被调用者保存寄存器,确保上下文完整性。
裁剪效果对比
| 配置项 | 默认大小 (KB) | 裁剪后 (KB) | 减少 |
|---|---|---|---|
| 内核代码段 | 12.3 | 7.1 | 42% |
| 最大中断延迟(us) | 1.82 | 1.15 | ↓37% |
graph TD
A[中断触发] --> B[硬件自动压入 xPSR/PC/LR/R12/R0-R3]
B --> C[手动压入 r4-r11]
C --> D[调用 vTaskSwitchContext]
D --> E[恢复下一任务 r4-r11]
E --> F[硬件自动弹出基础寄存器]
2.3 Go运行时(gc runtime)交叉编译适配与栈帧对齐改造
Go运行时在交叉编译场景下面临目标平台ABI差异带来的栈帧对齐挑战,尤其在ARM64与RISC-V等非x86架构上,runtime.stackalloc需动态适配GOARCH感知的最小对齐粒度。
栈帧对齐关键参数
stackMin: 最小栈大小(默认2KB),但需按arch.stackAlign向上取整stackGuard: 栈保护页偏移,依赖arch.stackGuardMultiplier校准
交叉编译适配核心逻辑
// src/runtime/stack.go
func stackalloc(n uint32) *g {
n = alignUp(n, _StackAlign) // _StackAlign由buildmode+GOARCH决定
// ...
}
_StackAlign 在 src/runtime/internal/goarch/ 中通过 //go:build 构建标签生成,例如 RISC-V64 定义为 16 字节,确保 CALL 指令前 SP 满足 SP % 16 == 0。
对齐策略对比表
| 架构 | 默认栈对齐 | 触发条件 | 运行时检查方式 |
|---|---|---|---|
| amd64 | 16 | GOOS=linux GOARCH=amd64 |
runtime.checkASMStack |
| arm64 | 16 | CGO_ENABLED=0 |
stackmap.align |
| riscv64 | 16 | GOEXPERIMENT=riscv |
arch.stackAlign() |
graph TD
A[交叉编译启动] --> B{GOARCH识别}
B -->|arm64| C[加载arch_arm64.go]
B -->|riscv64| D[加载arch_riscv64.go]
C & D --> E[设置_stackAlign/stackGuardMultiplier]
E --> F[stackalloc时动态对齐]
2.4 硬件抽象层(HAL)与Go绑定接口的零拷贝通信设计
零拷贝通信通过共享内存页与原子描述符队列,绕过内核缓冲区复制,显著降低HAL与Go运行时间的数据搬运开销。
共享环形缓冲区结构
type RingBuffer struct {
Data *C.uint8_t // mmap映射的物理连续页
ProdIdx *C.uint32_t // 生产者索引(volatile)
ConsIdx *C.uint32_t // 消费者索引(volatile)
Capacity uint32 // 2的幂次,支持位运算取模
}
Data指向预分配的DMA-able内存;ProdIdx/ConsIdx使用atomic.Load/StoreUint32同步,避免锁竞争;Capacity隐式提供无锁模运算能力(idx & (cap-1))。
零拷贝数据流
graph TD
A[HAL驱动] -->|直接写入Data[ConsIdx]| B[共享RingBuffer]
B -->|原子递增ConsIdx| C[Go goroutine]
C -->|读取ProdIdx确认就绪| D[按索引偏移解析结构体]
关键约束对比
| 维度 | 传统Syscall路径 | HAL+Go零拷贝路径 |
|---|---|---|
| 内存拷贝次数 | ≥2(用户↔内核↔用户) | 0 |
| 上下文切换 | 2次(syscall进入/返回) | 0(纯用户态轮询) |
| 延迟抖动 | 高(受调度器影响) |
2.5 启动流程重构:从Reset Handler到goroutine主调度器接管
ARM64平台启动时,固件跳转至_start后执行reset_handler,完成MMU初始化与栈切换,随后调用runtime·rt0_go进入Go运行时世界。
关键跳转点
rt0_go→runtime·schedinit(初始化全局调度器)schedinit→runtime·newproc1(启动main.maingoroutine)- 最终由
mstart触发schedule()循环,交由P/M/G模型自主调度
初始化参数对比
| 阶段 | 栈基址来源 | 调度器状态 | 是否启用抢占 |
|---|---|---|---|
| Reset Handler | SP_EL3(固件提供) | 未初始化 | ❌ |
schedinit 返回后 |
g0.stack.hi(分配自boot allocator) |
sched结构体就绪 |
✅(基于sysmon信号) |
// arch/arm64/runtime/asm.s 中的 reset_handler 片段
reset_handler:
mov x0, #0x1 // 初始化SCTLR_EL1位域
msr sctlr_el1, x0
isb
b runtime·rt0_go(SB) // 跳入Go运行时入口
该跳转彻底放弃裸机上下文,将控制权移交runtime·newosproc创建的首个M,由其调用schedule()启动goroutine主调度循环。
第三章:确定性goroutine调度器的设计与实现
3.1 基于SCHED_FIFO语义的抢占式G-P-M模型重定义
传统G-P-M模型中,M(OS线程)对P(处理器上下文)的绑定是协作式调度,无法响应实时优先级变化。引入SCHED_FIFO语义后,每个M在内核态注册为实时线程,其调度权交由Linux实时调度器直接管理。
核心变更点
- P不再由G(goroutine)主动让出,而是由M的实时优先级触发强制抢占
- G的就绪队列按优先级分层,高优先级G可中断低优先级M的P绑定
调度状态迁移(mermaid)
graph TD
A[New G] -->|high-prio| B{P occupied?}
B -->|yes, low-prio M| C[Preempt M via sched_setscheduler]
B -->|no| D[Bind to idle P]
C --> E[Save M context → RT queue]
关键系统调用封装
// 将当前M线程升级为SCHED_FIFO,优先级90
struct sched_param param = {.sched_priority = 90};
sched_setscheduler(0, SCHED_FIFO, ¶m); // 0表示调用线程自身
sched_setscheduler()使M获得硬实时抢占能力;SCHED_FIFO确保同优先级FIFO排队,无时间片轮转,契合G-P-M中“一个P服务一个高优G直到完成”的语义。
| 字段 | 含义 | G-P-M对应实体 |
|---|---|---|
sched_priority |
实时优先级(1–99) | G的runtime.g.priority映射 |
SCHED_FIFO |
无抢占退出的实时策略 | 替代原SCHED_OTHER协作模型 |
3.2 时间片量子与硬件定时器(TIM1/HTIM)的硬同步机制
数据同步机制
时间片量子由 HTIM 的重载寄存器(ARR)与预分频器(PSC)共同决定:
htim1.Instance = TIM1;
htim1.Init.Prescaler = 71; // 分频72 → 基准时钟 = 1MHz (假设系统时钟72MHz)
htim1.Init.Period = 999; // 自动重载值 → 定时周期 = 1000 × 1μs = 1ms
HAL_TIM_Base_Init(&htim1);
HAL_TIM_Base_Start_IT(&htim1); // 启用中断,实现硬同步触发点
该配置使 TIM1 每毫秒产生一次更新事件(UEV),作为所有任务调度器的时间锚点。
同步信号链路
- UEV 事件可直连 DAC、ADC 触发输入,实现外设级硬同步
- 多定时器可通过 TIMx_CR2 中的 MMS(主模式选择)广播同步脉冲
| 寄存器 | 功能 | 典型值 |
|---|---|---|
TIM1_PSC |
预分频系数 | 71 |
TIM1_ARR |
自动重载值(决定量子) | 999 |
TIM1_CR2.MMS |
主模式输出信号选择 | 0b100(UEV) |
graph TD
A[CPU Clock 72MHz] --> B[TIM1 Prescaler: /72]
B --> C[TIM1 Counter: 0→999]
C --> D[Update Event UEV]
D --> E[Task Scheduler Tick]
D --> F[ADC Start Conversion]
D --> G[DAC Waveform Sync]
3.3 全局可剥夺点注入:在SysTick与PendSV中嵌入调度钩子
RTOS 的确定性调度依赖于可控、可预测的抢占时机。SysTick 和 PendSV 是 Cortex-M 架构中唯二被内核(如 FreeRTOS、RT-Thread)授权用于触发上下文切换的异常,二者天然具备“全局可剥夺点”属性。
调度钩子的注入位置对比
| 异常类型 | 触发时机 | 可剥夺性 | 典型用途 |
|---|---|---|---|
| SysTick | 定时周期到达 | ✅ 强制 | 时间片轮转、超时检测 |
| PendSV | 延迟至最低优先级执行 | ✅ 无栈冲突 | 任务切换、yield、队列唤醒后 |
在 SysTick 中注入调度检查
void SysTick_Handler(void) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 关键:在中断中安全调用调度器入口
xHigherPriorityTaskWoken = xTaskIncrementTick();
if (xHigherPriorityTaskWoken != pdFALSE) {
portYIELD_FROM_ISR(xHigherPriorityTaskWoken); // 触发 PendSV 挂起
}
}
逻辑分析:
xTaskIncrementTick()执行时间片更新与就绪态迁移;返回非零表示高优先级任务就绪。portYIELD_FROM_ISR不直接切换,而是置位 PendSV 异常请求,确保切换发生在 PendSV 处理器中——避免在 SysTick 栈帧内修改 MSP/PSP,保障栈安全。
PendSV 的原子切换保障
graph TD
A[PendSV Entry] --> B[保存当前任务上下文到其TCB]
B --> C[读取pxCurrentTCB指向的新任务]
C --> D[加载新任务上下文到CPU寄存器]
D --> E[更新MSP/PSP并返回新任务SP]
- PendSV 由硬件保证不可被同级或更低优先级异常打断;
- 所有上下文保存/恢复均通过
__set_PSP()/__get_MSP()精确控制; - 钩子注入不修改异常向量表,仅复用标准异常处理流程,零侵入式增强可剥夺性。
第四章:混合调度时序验证与内核补丁交付
4.1 使用Logic Analyzer捕获goroutine切换关键路径时序图
要精准定位调度延迟,需在 runtime.schedule()、gopark() 和 goready() 关键函数入口插入硬件触发点(如 ARM CoreSight ETM 或 x86 Last Branch Record 配合逻辑分析仪探针)。
触发信号配置示例
// 在 src/runtime/proc.go 中添加(仅调试构建)
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer, reason waitReason, traceEv byte, traceskip int) {
// ▶️ 插入 GPIO 脉冲:低电平表示 park 开始(逻辑分析仪通道0)
runtime·triggerProbe(0, 1) // 参数1:通道号;参数2:脉冲宽度(ns级精度)
...
}
runtime·triggerProbe 是汇编封装的内存映射 I/O 写操作,确保无分支、无缓存干扰,时序抖动
关键事件时序对照表
| 信号通道 | 事件 | 对应源码位置 |
|---|---|---|
| CH0 | gopark() 开始 |
proc.go:3210 |
| CH1 | findrunnable() 返回 |
proc.go:2588 |
| CH2 | schedule() 切换 G |
proc.go:3125 |
调度状态跃迁(简化模型)
graph TD
A[CH0: gopark] --> B[CH1: findrunnable]
B --> C[CH2: schedule → new G]
C --> D[CH0: next gopark]
4.2 内核补丁diff详解:runtime/schedule.go与portasm_arm.s双端修改
协同修改的必要性
Go运行时调度器在ARM平台需同时保障Go层面的逻辑正确性与底层汇编的寄存器语义一致性。单改schedule.go会导致goroutine切换时PC/SP未同步更新;仅修portasm_arm.s则无法适配新引入的g0.stackguard0校验逻辑。
关键代码变更
// runtime/schedule.go(节选)
func schedule() {
gp := getg()
if gp.m.locks != 0 { // 新增锁状态快照
throw("lock held during schedule")
}
gogo(gp.sched) // 调用汇编入口
}
gp.m.locks快照避免抢占时破坏临界区;gogo不再内联,确保调用约定与ARM汇编严格匹配。
// portasm_arm.s(节选)
TEXT gogo(SB), NOSPLIT, $0-4
MOVW g+0(FP), R0 // 加载g指针
LDR R1, (R0, #g_sched+gobuf_sp) // 取新SP
MOVW R1, SP // 切换栈
B gosave_return // 跳转至新goroutine PC
gobuf_sp偏移量由#28修正为#32(因g结构体新增stackguard0字段),确保栈指针精准复位。
修改影响对照表
| 维度 | 仅改 Go 文件 | 仅改汇编文件 | 双端协同修改 |
|---|---|---|---|
| 栈切换可靠性 | ❌ SP偏移错位崩溃 | ❌ 无锁检查导致死锁 | ✅ 全链路一致 |
| 抢占安全性 | ✅ | ❌ 寄存器未保存 | ✅ |
graph TD
A[goroutine阻塞] --> B[schedule()检查locks]
B --> C[gogo调用汇编]
C --> D[portasm_arm.s加载gobuf_sp]
D --> E[SP精确切换至目标栈]
4.3 最坏执行时间(WCET)实测对比:纯RTOS vs Go+RTOS混合模式
在 Cortex-M7 平台(主频 600 MHz,关闭 L1 cache 预取)上,对关键控制任务(PID 调节周期 1 ms)进行 WCET 压力测试:
| 模式 | 最高观测 WCET | 波动范围 | 中断延迟抖动 |
|---|---|---|---|
| FreeRTOS(C) | 842 ns | ±12 ns | |
| Go+FreeRTOS(cgo桥接) | 2.13 μs | ±310 ns | 180–420 ns |
数据同步机制
Go 协程通过 runtime.LockOSThread() 绑定至专用 RTOS 任务,共享环形缓冲区:
// rtos_task.c —— RTOS侧写入(无锁SPSC)
static uint32_t tx_head = 0;
void send_to_go(uint32_t val) {
ring_buf[tx_head & RING_MASK] = val; // RING_MASK = 0xFF
__DMB(); // 内存屏障确保顺序写入
tx_head++; // 原子递增(单生产者)
}
__DMB() 强制刷新写缓冲,避免编译器/CPU 重排;RING_MASK 保证 O(1) 索引,消除分支预测开销。
执行路径差异
graph TD
A[中断触发] --> B{纯RTOS}
B --> C[直接执行PID C函数]
A --> D{Go+RTOS}
D --> E[cgo调用栈切换]
E --> F[Go runtime调度检查]
F --> G[最终调用C封装函数]
实测表明:Go 运行时元开销使 WCET 增长约 154%,且抖动显著放大。
4.4 中断延迟抖动压测报告与Jitter Budget分配策略
压测环境配置
采用 Linux RT 内核(5.10.126-rt77)+ Xilinx ZynqMP SoC,关闭 CPU 频率调节与非关键中断(如 irq/32-eth0),仅保留定时器与自定义 GPIO 中断源。
Jitter Budget 分配原则
- 总预算:≤ 5 μs(满足工业伺服控制闭环周期 100 μs 下的 5% 抖动容限)
- 拆分策略:
- 硬件传播延迟:≤ 1.2 μs(PHY→INTC 走线+触发器采样)
- 内核中断入口开销:≤ 2.0 μs(
do_IRQ到handle_irq_event) - 驱动 handler 执行上限:≤ 1.8 μs(含临界区保护)
关键测量代码片段
// 在中断 handler 开头插入高精度时间戳(ARM CNTPCT_EL0)
u64 ts_start;
asm volatile("mrs %0, cntpct_el0" : "=r"(ts_start));
// ... 驱动处理逻辑 ...
u64 ts_end;
asm volatile("mrs %0, cntpct_el0" : "=r"(ts_end));
u64 delta = (ts_end - ts_start) * 1000 / CNTFRQ; // 转纳秒,CNTFRQ=50MHz → 20ns/tick
该代码利用 ARM 通用计数器直接读取硬件 tick,规避 ktime_get_ns() 的调度路径开销;CNTFRQ 必须在启动时通过 /sys/devices/system/clocksource/clocksource0/current_clocksource 校准,误差 >0.1% 将导致抖动误判。
抖动分布统计(10万次采样)
| 百分位 | 延迟(μs) | 含义 |
|---|---|---|
| P50 | 2.3 | 典型响应延迟 |
| P99 | 4.1 | 可接受上限 |
| P99.99 | 5.7 | 超预算,触发告警 |
graph TD
A[GPIO边沿触发] --> B[INTC仲裁延迟]
B --> C[CPU IRQ入口保存上下文]
C --> D[RT调度器抢占判断]
D --> E[handler执行]
E --> F[中断返回恢复]
第五章:工业场景落地边界与演进路线图
落地边界的三重硬约束
在某汽车零部件智能工厂的实际部署中,边缘AI质检系统遭遇了明确的物理边界:现场工业相机帧率上限为60fps,而模型推理延迟必须压至≤35ms才能满足节拍要求;PLC通信协议强制采用OPC UA over TSN,导致非标模型中间件无法直连;更关键的是,产线环境EMI等级达IEC 61000-6-4 Class A,商用GPU服务器需加装双层屏蔽机柜并重新认证,单台改造成本增加27万元。这些并非技术选型偏差,而是不可绕行的工程红线。
典型场景成熟度矩阵
| 场景类型 | 当前可用性 | 数据闭环周期 | 人因干预频次(/班次) | 商业ROI阈值 |
|---|---|---|---|---|
| 焊缝X光缺陷识别 | 已量产 | ≤3次 | ≥18个月 | |
| 设备振动预测性维护 | 小批量验证 | 48小时 | 12次 | 未达标 |
| AGV集群动态路径规划 | PoC阶段 | >7天 | 持续人工接管 | 不适用 |
演进路线的关键拐点
某钢铁集团冷轧产线分三期推进数字孪生:第一阶段(2022Q3–2023Q1)仅接入DCS历史数据,构建静态工艺拓扑图;第二阶段(2023Q2–2024Q1)通过加装217个IEPE加速度传感器与时间敏感网络(TSN)交换机,实现轧机轴承温度-振动-电流的毫秒级同步采集,但数据丢包率仍达0.8%;第三阶段(2024Q3起)将部署国产化实时操作系统+确定性AI推理框架,在西门子S7-1500PLC中嵌入轻量化LSTM模块,直接输出辊缝补偿量——该方案跳过SCADA层,使控制指令端到端延迟从420ms压缩至68ms。
flowchart LR
A[现场设备层] -->|Modbus TCP/RTU| B(边缘网关)
B --> C{数据分流}
C -->|高频时序数据| D[TSN交换机 → 实时分析节点]
C -->|低频图像数据| E[千兆以太网 → 视觉分析节点]
D --> F[PLC硬接线输出补偿信号]
E --> G[MES系统告警工单]
style D stroke:#2E8B57,stroke-width:2px
style F stroke:#DC143C,stroke-width:2px
人机协同的临界规模
在宁德时代某电芯装配线验证中,当AI质检系统覆盖≥83%的外观检测工位时,质检员工作模式发生质变:从逐帧判读转为抽检复核,但必须保留3个物理工位用于处理模型置信度
边缘算力的能效悖论
实测数据显示:在-10℃~60℃宽温环境中,NVIDIA Jetson AGX Orin模组持续运行12小时后,GPU频率自动降频32%,导致YOLOv8s模型mAP下降5.2个百分点;而采用寒武纪MLU220-M.2加速卡的定制边缘盒,在同等散热条件下保持满频运行,但其驱动层需针对宝信软件iPlat平台深度适配,开发周期延长11周。
工业现场的每一次算法迭代都必须经过产线停机窗口验证,某光伏硅片厂规定所有模型更新仅允许在每周二凌晨02:00–04:00执行,且需提前72小时提交FMEA分析报告。
