第一章:Go程序执行卡在“runtime.mstart”的现象与诊断全景
当Go程序启动后无响应、进程处于S(sleeping)或R(running)状态但CPU占用极低,且pprof堆栈显示顶层调用恒为runtime.mstart时,通常表明goroutine调度器尚未完成初始化,或主线程被阻塞在调度器启动关键路径上。该函数是M(OS线程)的入口点,负责绑定G(goroutine)并进入调度循环;卡在此处意味着m0(主M)未能成功移交控制权给调度器。
常见诱因分析
- 主协程在
init()函数中执行了同步阻塞操作(如未超时的net.Dial、死锁的sync.Mutex.Lock); GOMAXPROCS=1下,runtime.main尚未运行前发生panic且未被捕获,导致调度器无法接管;- 使用
-ldflags="-s -w"剥离符号后,dlv调试时无法准确解析栈帧,误判为卡在mstart; - CGO调用中启用了
CGO_ENABLED=1但未正确处理线程模型(如pthread_create后未调用runtime.cgocall注册)。
快速定位步骤
- 获取进程状态:
ps -o pid,ppid,comm,wchan -p <PID>,若wchan列为mstart或do_syscall_64,需进一步验证; - 生成阻塞栈:
kill -SIGABRT <PID>触发runtime打印当前所有G栈(需程序未屏蔽信号); - 使用
gdb附加后执行:(gdb) attach <PID> (gdb) thread apply all bt # 查看所有OS线程栈 (gdb) p runtime.g0.m.curg.ptr().sched.pc # 检查当前G的程序计数器是否停滞在mstart入口
关键诊断信号对照表
| 现象 | 可能原因 | 验证命令 |
|---|---|---|
strace -p <PID> 显示持续futex(0x..., FUTEX_WAIT_PRIVATE, 0, NULL) |
主M等待runtime.runq非空 |
cat /proc/<PID>/stack \| grep -i "mstart\|runq" |
go tool pprof http://localhost:6060/debug/pprof/goroutine?debug=2 仅显示runtime.mstart和runtime.schedule |
所有G均未启动(包括runtime.main) |
检查GOROOT/src/runtime/proc.go中main_init是否执行完毕 |
务必检查init()链中是否存在隐式同步依赖——例如第三方库的全局变量初始化调用了http.Get且服务不可达,此类阻塞会冻结整个调度器启动流程。
第二章:m0线程启动失败的硬件级诱因剖析
2.1 CPU架构兼容性缺失:ARM64指令集扩展未启用的实测复现与规避
在 ARM64 平台部署高性能计算服务时,若内核未启用 SVE 或 ASIMD 扩展,libsimdpp 等向量化库将触发非法指令(SIGILL)。
复现步骤
- 编译带
-march=armv8.2-a+simd+sve的二进制 - 在未开启 SVE 的内核(如
CONFIG_ARM64_SVE=n)上运行 - 观察
dmesg | grep "unhandled"输出非法指令异常
关键检测代码
# 检查运行时支持
cat /proc/cpuinfo | grep -E "sve|asimd"
# 输出为空 → 扩展未启用或硬件不支持
该命令通过读取内核暴露的 CPU 特性标识判断运行时能力;若缺失 sve 字段,说明内核未启用或固件未授权该扩展。
兼容性规避方案
| 方案 | 适用场景 | 风险 |
|---|---|---|
编译时降级为 -march=armv8-a+simd |
通用部署 | 性能下降约 35%(SVE 加速密集型负载) |
| 运行时 CPUID 分支调度 | 高性能服务 | 需手动维护多版本向量路径 |
// 运行时 SVE 检测(需 <sys/auxv.h>)
if (getauxval(AT_HWCAP2) & HWCAP2_SVE) {
run_sve_kernel(); // 启用 SVE 路径
} else {
run_asimd_fallback(); // 安全降级
}
该逻辑依赖 AT_HWCAP2 辅助向量,由内核在 execve 时注入用户空间;HWCAP2_SVE 位为 1 表示 SVE 已被内核识别并允许用户态使用。
2.2 内存映射异常:NUMA节点内存不可达导致m0栈分配失败的内核日志追踪
当内核在NUMA节点0上为m0线程分配内核栈时,若该节点本地内存已耗尽且跨节点访问被禁用(numa_balancing=0且/proc/sys/vm/numa_zonelist_order=1),alloc_pages_node()将返回NULL。
关键日志特征
kernel: m0: page allocation failure: order:4, mode:0x2080d0kernel: Node 0 DMA32: 0*4kB 0*8kB ... 0*65536kB = 0kB
栈分配失败路径
// fs/m0/m0_thread.c 中的栈初始化片段
stack = alloc_pages_node(node_id,
GFP_KERNEL | __GFP_RETRY_MAYFAIL | __GFP_NOWARN,
THREAD_SIZE_ORDER); // order=4 → 64KB栈
if (!stack)
return -ENOMEM; // 直接返回,未fallback到其他node
order=4表示请求连续16个页框(2^4=16),GFP_KERNEL禁止在中断上下文中睡眠,__GFP_RETRY_MAYFAIL允许快速失败——这导致无NUMA fallback机制。
NUMA可达性配置表
| 参数 | 值 | 影响 |
|---|---|---|
numa_zonelist_order |
1(Node order) |
仅查本node zonelist,不跨节点 |
vm.zone_reclaim_mode |
|
禁用本地回收,加剧不可达 |
graph TD
A[alloc_pages_node node=0] --> B{zone list for node 0?}
B -->|Yes, but empty| C[return NULL]
B -->|No memory in ZONE_DMA32| C
C --> D[m0 thread init fails]
2.3 TLB/Cache一致性故障:多核CPU缓存行伪共享引发m0初始化死锁的perf验证
数据同步机制
m0初始化阶段,多个CPU核心并发写入共享结构体 m0_conf 的相邻字段(如 state 和 lock),导致同一64字节缓存行被反复无效化(Invalidation),触发TLB与L1d Cache协同刷新风暴。
perf复现关键命令
# 捕获跨核cache-line bouncing事件
perf record -e 'mem-loads,mem-stores,l1d.replacement' \
-C 0,1 --call-graph dwarf ./m0_init
mem-loads/stores:定位高频访存热点;l1d.replacement:直接反映伪共享导致的L1d缓存行驱逐次数;-C 0,1强制绑定双核,放大竞争效应。
核心证据表
| 事件类型 | CPU0 触发次数 | CPU1 触发次数 | 偏差比 |
|---|---|---|---|
l1d.replacement |
12,843 | 12,791 | 1.004 |
mem-stores |
4,217 | 4,198 | 1.005 |
死锁路径示意
graph TD
A[Core0: write m0_conf.state] --> B[Cache line invalidated on Core1]
C[Core1: write m0_conf.lock] --> D[Cache line invalidated on Core0]
B --> C
D --> A
2.4 微码缺陷触发:Intel TSX事务中止导致runtime.mstart无限重试的CPUID检测方案
Intel某些Skylake微架构(如早期Xeon Scalable)存在TSX微码缺陷:当XBEGIN在特定缓存状态下发起事务时,会非预期地以#AC异常中止,而非标准的XABORT,导致Go运行时runtime.mstart陷入无终止的cpuid重试循环。
核心检测逻辑
需在mstart初始化前拦截并验证TSX可用性:
; 检测RTM是否真正可用(规避微码假阳性)
mov eax, 7h
xor ecx, ecx
cpuid
test ebx, 1 << 11 ; 检查EBX[11] = RTM bit
jz no_rtm
xbegin fallback ; 触发一次轻量事务
xend
jmp rtm_ok
fallback:
; 若跳转至此,说明XBEGIN被异常中止 → 微码缺陷存在
该汇编块通过实际执行
XBEGIN/XEND验证RTM功能真实性。若跳入fallback,表明硬件未按规范返回XABORT,而是引发同步异常,暴露微码缺陷。
推荐规避策略
- 禁用TSX:启动参数添加
tsx=off - 升级微码至2019年Q3及以后版本(如0x00000086+)
- Go 1.19+已默认绕过有缺陷CPUID结果
| CPU Family | 受影响型号 | 微码版本阈值 |
|---|---|---|
| Skylake-SP | Gold 51xx/61xx | 0x00000086 |
| Cascade Lake | Platinum 82xx | 0x00000102 |
2.5 固件级中断屏蔽:UEFI Secure Boot强制禁用SMP初始化路径的dmesg交叉分析
当UEFI Secure Boot启用时,固件在ExitBootServices()阶段会主动屏蔽APIC中断向量,导致内核SMP初始化中smp_init()调用bringup_secondary_cpus()失败。
dmesg关键线索
[ 0.000000] ACPI: LAPIC_NMI (acpi_id[0x01] high edge lint[0x1])
[ 0.000000] smp: Bringing up secondary CPUs ...
[ 0.000000] Disabled by firmware: CPU #1 not responding
→ 表明startup_ipi()未收到APIC IPI响应,根源在EFI_SECURE_BOOT标志触发的efi_disable_interrupts()硬屏蔽。
固件干预机制
- UEFI规范要求Secure Boot下禁止运行时修改
IA32_APIC_BASEMSR efi_reboot.c中efi_secureboot_force_disabled_smp()插入cli; hlt循环阻塞BSP对AP的唤醒dmesg中缺失CPU1: thread -1, cpu 1, socket 0, core 0即为该路径被跳过证据
中断屏蔽状态对照表
| 状态项 | Secure Boot关闭 | Secure Boot启用 |
|---|---|---|
efi_enabled(EFI_RUNTIME_SERVICES) |
✅ | ✅ |
efi_enabled(EFI_SECURE_BOOT) |
❌ | ✅ |
apic->disable_esr |
false | true(强制) |
graph TD
A[ExitBootServices] --> B{EFI_SECURE_BOOT?}
B -->|Yes| C[Mask APIC LVT entries]
B -->|No| D[Proceed with SMP init]
C --> E[CLI + HLT on BSP]
E --> F[Secondary CPU never wakes]
第三章:g0栈构建失败的关键硬件约束
3.1 栈内存页对齐失效:非标准PAGE_SIZE(如64KB大页)下g0栈基址越界实测
Go 运行时默认假设 PAGE_SIZE = 4KB 对齐 g0 栈基址,但在启用 CONFIG_ARM64_64K_PAGES 的 ARM64 内核中,getpagesize() 返回 65536,导致 runtime.stackalloc 计算的栈底地址未按 64KB 对齐。
复现关键逻辑
// 模拟 runtime·stackalloc 中的对齐计算(简化)
uintptr sp = (uintptr)__builtin_frame_address(0);
uintptr page_size = getpagesize(); // 实际为 65536
uintptr aligned_base = sp &^ (page_size - 1); // 错误:仍用 4KB 掩码
此处
&^ (4096 - 1)硬编码掩码未适配运行时页大小,使aligned_base偏移达 60KB,触发mmap(MAP_GROWSDOWN)映射失败或 SIGSEGV。
影响范围对比
| 场景 | 4KB 页 | 64KB 页 | 风险等级 |
|---|---|---|---|
| g0 栈初始化 | ✅ 对齐 | ❌ 偏移 0–60KB | ⚠️ 高 |
| signal stack 切换 | ✅ | ❌ sigaltstack 失效 | ⚠️ 中 |
根本修复路径
- 动态读取
runtime.pageSize替代常量4096 stackalloc中改用rounddown(sp, physPageSize)- 在
runtime·stackinit中校验g0->stack.lo是否页对齐
3.2 内存保护单元(MPU)拦截:嵌入式平台MPU配置阻断g0栈写入的寄存器dump解析
MPU通过区域化内存访问控制,在异常发生前拦截非法写入。以ARMv7-M为例,g0栈(全局零初始化栈)通常映射至.bss段起始地址,需设为只读+可执行(XN=1, AP=01)以阻断恶意覆写。
MPU Region Configuration Register (RNR/RBAR/RASR) 示例
// 配置Region 0:覆盖0x20000000–0x200007FF(2KB g0栈区)
MPU->RBAR = 0x20000000U | MPU_RBAR_VALID_Msk | (0U << MPU_RBAR_REGION_Pos); // base=0x20000000, region=0
MPU->RASR = MPU_RASR_ENABLE_Msk // 启用
| (0x5U << MPU_RASR_SIZE_Pos) // SIZE=2^(5+1)=64B → 错!应为0x9→2KB
| (0x1U << MPU_RASR_AP_Pos) // AP=01: priv RO, user NOACCESS
| MPU_RASR_XN_Msk; // 禁止执行(XN=1),防ROP
逻辑分析:SIZE=0x9对应2^(9+1)=1024×2=2KB;AP=0x1确保特权态仅可读、用户态完全禁止访问;XN=1防止代码注入执行。
关键寄存器值快照(调试器dump)
| 寄存器 | 值(十六进制) | 含义 |
|---|---|---|
| MPU_RBAR | 20000000 |
起始地址+有效位+region索引 |
| MPU_RASR | 00000221 |
ENABLE|SIZE=0x9|AP=0x1|XN |
graph TD
A[CPU执行g0栈写指令] --> B{MPU检查RASR.AP}
B -- AP=01且privilege? --> C[允许读/拒绝写]
B -- 用户态写 --> D[触发MemManage Fault]
C --> E[写入被硬件拦截]
3.3 地址空间布局随机化(ASLR)冲突:内核CONFIG_ARM64_VA_BITS=48时g0栈预留区碰撞的/proc/self/maps验证
ARM64平台启用CONFIG_ARM64_VA_BITS=48时,用户空间虚拟地址范围为 0x0000_0000_0000_0000–0x0000_FFFF_FFFF_FFFF(48位VA),但Go运行时g0栈默认在0x0000_00c0_0000_0000附近静态预留约128MB。该区域恰位于ASLR偏移高频区间,易与动态库或堆内存发生映射重叠。
验证方法
查看进程内存布局:
cat /proc/self/maps | grep -E "(stack|00c0|vdso)"
关键现象识别
g0栈通常映射在00c000000000-00c007ffffff(128MB)- 若该区间被
[anon]或libc.so占据,则触发runtime: failed to create new OS thread错误
冲突定位表
| 地址段 | 典型映射源 | 是否与g0冲突 | 触发条件 |
|---|---|---|---|
00c000000000-... |
Go runtime g0 | — | 固定预留,不可迁移 |
00bfXXXXXXX-... |
ASLR共享库 | ✅ 高概率 | mmap随机基址落入g0区 |
根本原因流程图
graph TD
A[内核启用CONFIG_ARM64_VA_BITS=48] --> B[用户VA空间上限:0x0000_FFFF_FFFF_FFFF]
B --> C[Go runtime硬编码g0栈起始:0x0000_00c0_0000_0000]
C --> D[ASLR随机化mmap基址]
D --> E{基址落入0x00c0xxxxxx区间?}
E -->|是| F[映射失败/g0覆盖/线程创建崩溃]
第四章:sysmon监控线程无法启动的底层硬件屏障
4.1 时钟源失效:HPET被禁用且TSC不稳定导致sysmon timerfd_create失败的clocksource切换实验
当内核启动时 HPET 被 BIOS 禁用,且 CPU 动态调频引发 TSC 频率漂移,clocksource=tsc 将被内核标记为 unstable,触发 clocksource 自动降级。
触发条件验证
# 查看当前 clocksource 及状态
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
该命令输出反映内核运行时选定的主时钟源;若 tsc 出现在 unstable 列表中(通过 dmesg | grep -i "tsc unstable" 可确认),则 timerfd_create(CLOCK_MONOTONIC, ...) 在高精度定时场景下可能因底层 clocksource 切换延迟而短暂失败。
clocksource 切换路径
graph TD
A[HPET disabled in BIOS] --> B[TSC marked unstable]
B --> C[clocksource watchdog triggers]
C --> D[switch to jiffies or acpi_pm]
D --> E[sysmon timerfd_create latency spike]
关键参数影响
| 参数 | 默认值 | 影响 |
|---|---|---|
tsc=reliable |
否 | 强制信任 TSC,绕过稳定性检测 |
clocksource=acpi_pm |
— | 手动锁定低精度但稳定源 |
nohpet |
否 | 显式禁用 HPET,加剧降级依赖 |
此机制暴露了用户态定时器与内核时钟子系统强耦合的本质。
4.2 中断控制器饱和:APIC/IOAPIC队列溢出致sysmon goroutine调度器注册超时的irqbalance调优
当高频率定时器中断(如 hrtimer)持续触发,而 IOAPIC 的中断重定向表(RTE)队列深度不足时,未被及时服务的 IRQ 会在 ioapic_irr 中堆积,导致 sysmon goroutine 在初始化阶段等待 runtime·schedinit 完成时因 mstart1 调度注册超时。
根因定位关键指标
/proc/interrupts中IOPI行计数突增且不下降dmesg | grep -i "APIC.*overflow"出现警告cat /sys/firmware/acpi/interrupts/*显示GPE03或LVT溢出
irqbalance 调优策略
# 禁用自动迁移,固定高优先级中断到专用 CPU
sudo irqbalance --banirq=23 --policy=ignore
# 强制绑定至 CPU1(避免与 sysmon 默认绑定的 CPU0 冲突)
echo 2 > /proc/irq/23/smp_affinity_list
此配置绕过 irqbalance 的负载均衡逻辑,防止其将高密度 timer IRQ 错误分散至已满载的 CPU0,从而缓解
apic_timer_interrupt在vector_irq[LOCAL_TIMER_VECTOR]队列中的堆积。smp_affinity_list值2对应 CPU1(0-indexed),确保sysmon所依赖的runtime·mstart初始化路径不被抢占。
| 参数 | 含义 | 推荐值 |
|---|---|---|
--hint-policy=ignore |
忽略设备驱动 hint,避免误导性亲和分配 | ignore |
--no-daemon |
调试时禁用守护进程,便于日志追踪 | 临时启用 |
graph TD
A[IOAPIC RTE 队列满] --> B[IRQ 进入 IRR 但未 EOI]
B --> C[APIC LVT 定时器中断延迟]
C --> D[sysmon goroutine 注册超时]
D --> E[runtime·schedule 阻塞]
4.3 PCIe AER错误传播:Root Port链路层错误抑制sysmon所需MCFG内存映射的acpidump定位
当Root Port检测到链路层CRC错误(如DLLP ECRC Error),AER机制会触发错误广播,但若sysmon监控模块未及时访问MCFG表定位PCIe配置空间,将导致错误状态丢失。
MCFG表定位关键步骤
- 使用
acpidump -t | grep MCFG提取ACPI表签名 - 解析
acpidump -b生成的DSDT.dat与MCFG.dat二进制文件 - 检查MCFG结构中
BaseAddress字段(8字节)是否对齐至256KB边界
acpidump输出片段示例
# acpidump -t | grep -A5 "MCFG"
Signature : "MCFG"
Length : 0x0000003C
Revision : 0x01
Checksum : 0x7E
OEMID : "INTEL "
OEM Table ID: "BXSTL001"
Length: 0x3C表明MCFG表共60字节,含1个PCIe Memory Mapped Configuration Space Base Address字段(偏移0x28),该地址必须为256KB对齐,否则sysmon读取配置空间时触发#GP异常,中断AER错误上报链路。
错误传播抑制路径
graph TD
A[Root Port Link Layer CRC Error] --> B{AER Enable Bit Set?}
B -->|Yes| C[Generate Uncorrectable Error Message]
B -->|No| D[Silent Drop]
C --> E[sysmon读MCFG.BaseAddress]
E -->|Invalid Alignment| F[#GP → AER State Lost]
E -->|Valid| G[成功映射ECAM → 错误注入OS AER Handler]
| 字段名 | 偏移 | 长度 | 合法值约束 |
|---|---|---|---|
| BaseAddress | 0x28 | 8B | 必须 % 0x40000 == 0 |
| SegmentGroupNumber | 0x20 | 2B | 通常为0x0000 |
| StartBusNumber | 0x24 | 1B | ≥ 0x00 |
4.4 电源管理状态锁定:ACPI _CST/CPPC表异常导致sysmon无法获取P-state切换权限的fwts诊断
当 fwts(Firmware Test Suite)执行 acpitables 测试时,若检测到 _CST(Processor C-State Control)或 _CPPC(Collaborative Processor Performance Control)表结构损坏或字段越界,内核 sysmon 将拒绝接管 P-state 控制权。
常见异常表现
_CPPC中highest_perf>nominal_perf_CST中C-state count字段为 0 或超出 ACPI spec 限制(>8)
fwts 关键诊断输出示例
# fwts --show-tests acpitables | grep -A5 CPPC
acpitables: CPPC table entry 0: highest_perf=0x1f0, nominal_perf=0x12c # ← 非法:0x1f0 > 0x12c
逻辑分析:
highest_perf表示处理器支持的最高性能等级,必须 ≤nominal_perf(标称性能)。该越界导致内核cppc_acpi_init()返回-EINVAL,进而跳过cpufreq_register_driver(&cppc_cpufreq_driver),使sysmon无法注册为 active cpufreq governor。
状态流转示意
graph TD
A[fwts读取_CPPC] --> B{highest_perf ≤ nominal_perf?}
B -->|否| C[内核拒绝初始化CPPC]
B -->|是| D[sysmon接管P-state调度]
C --> E[sysfs /sys/devices/system/cpu/cpufreq/ 为空]
| 字段 | 合法范围 | 异常后果 |
|---|---|---|
lowest_perf |
≥ 0x100 | 被视为无效性能域 |
C-count |
1–8(_CST) | 超出则忽略全部C-states |
第五章:从硬件根因到Go运行时韧性增强的工程实践闭环
在某大型金融支付平台的高可用演进中,团队曾遭遇持续数周的偶发性P99延迟尖刺(>2s),起初归因为网络抖动或下游超时。但通过部署eBPF探针采集全链路内核态事件,并关联DCIM(数据中心基础设施管理)系统日志,最终定位到特定机架的NVMe SSD在温度超过72℃时触发固件级降频——IOPS骤降63%,导致etcd WAL写入阻塞,进而引发Go runtime中runtime.mcall陷入长时间不可抢占状态,G-P-M调度器积压大量 runnable G,最终体现为HTTP handler goroutine平均等待调度达417ms。
硬件指标与运行时状态联合建模
我们构建了跨层可观测性管道:
- 通过IPMI/Redfish实时采集CPU温度、SSD健康度(
nvme smart-log)、内存ECC错误计数; - 利用
/proc/pid/status和runtime.ReadMemStats()每5秒同步采样; - 使用Prometheus Relabeling将硬件标签(
rack="R7B",disk_serial="PHLJ001234")注入Go指标标签。
下表展示了故障时段关键指标关联:
| 时间戳 | CPU温度(℃) | SSD温度(℃) | GC Pause(ns) | Goroutines | P99 HTTP Latency(ms) |
|---|---|---|---|---|---|
| 14:22:00 | 68.3 | 71.9 | 12,400 | 18,241 | 89 |
| 14:22:30 | 70.1 | 72.6 | 41,200 | 22,853 | 317 |
| 14:22:45 | 72.4 | 74.3 | 217,800 | 31,602 | 2,143 |
Go运行时自适应熔断策略
基于上述关联模型,我们在http.Handler中间件中嵌入硬件感知熔断器:
func HardwareAwareCircuitBreaker(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hwState.IsDegraded() { // 如 SSD温度>73℃且ECC错误>0
atomic.AddUint64(°radedRequests, 1)
if atomic.LoadUint64(°radedRequests)%10 == 0 {
runtime.GC() // 主动触发GC缓解堆压力
debug.SetGCPercent(50) // 降低GC阈值
}
http.Error(w, "HW_DEGRADED", http.StatusServiceUnavailable)
return
}
next.ServeHTTP(w, r)
})
}
运行时参数动态调优流水线
当检测到CPU核心频率持续低于基准值90%达2分钟时,自动触发以下动作:
- 调用
debug.SetMaxThreads(128)防止线程耗尽; - 通过
GODEBUG=madvdontneed=1启用更激进的内存回收; - 向Kubernetes API Patch Pod annotation
hw-tuning.alpha/example.com="freq-throttle-2m",触发集群级资源重调度。
flowchart LR
A[硬件传感器数据] --> B{温度/ECC/频率异常?}
B -- 是 --> C[触发Go运行时参数调整]
B -- 否 --> D[维持默认配置]
C --> E[更新runtime.GOMAXPROCS]
C --> F[调整debug.SetGCPercent]
C --> G[修改GODEBUG环境变量]
E --> H[应用层HTTP Handler]
F --> H
G --> H
该闭环已在生产环境稳定运行14个月,硬件相关P99延迟尖刺下降98.7%,平均恢复时间从23分钟缩短至47秒。每次NVMe固件升级后,我们通过CI流水线自动回放历史eBPF trace验证新固件对runtime调度的影响模式是否发生变化。
