一次 macOS smd 内存泄漏排查¶
这次遇到的问题是:机器长期不重启,活动监视器里看到一个 root 启动的 smd 进程占用了接近 2GB 内存。因为这台机器需要长期运行,所以目标不是简单重启,而是弄清楚:
smd到底是什么服务- 2GB 内存具体是什么
- 是谁在持续触发它
- 有没有不用重启整机的处理办法
smd 是什么¶
先看进程:
能看到它由 launchd 启动,路径是:
再看 launchd 里的服务定义:
关键信息是:
path = /System/Library/LaunchDaemons/com.apple.xpc.smd.plist
program = /usr/libexec/smd
managed service = com.apple.xpc.smd
man smd 的说明很短:
它是 ServiceManagement framework daemon,负责替系统处理 ServiceManagement 相关操作。简单说,登录项、后台项、LaunchAgent、LaunchDaemon 的注册和状态查询,都可能走到它。
也可以确认它确实是 Apple 签名:
如果签名链是 Apple 的,就说明这不是第三方伪装进程。
先确认 2GB 是不是误读¶
一开始比较迷惑的是,不同工具显示不一致。
top 里看到:
输出类似:
但 ps 的 RSS 可能只有几 MB:
这种时候不能只看一个指标。要看 vmmap:
这一步很关键。摘要里能看到:
Physical footprint: 2.0G
Physical footprint (peak): 2.0G
Writable regions: Total=2.1G written=2.0G resident=4466K swapped_out=2.0G
MALLOC_SMALL:
SIZE=2.0G
RESIDENT=2448K
SWAPPED=2.0G
DefaultMallocZone:
ALLOCATION COUNT=6570996
BYTES ALLOCATED=2.0G
这说明 2GB 不是普通文件缓存,也不是库映射,而是 smd 自己的 malloc 堆里有大量小对象。很多页已经被系统换出或压缩,所以 RSS 看起来不大,但进程的 footprint 和脏内存账本确实涨到了 2GB。
用 heap 和 leaks 确认泄漏形态¶
继续看堆:
heap 的关键信息:
All zones: 6571421 nodes (2102158480 bytes)
COUNT BYTES AVG CLASS_NAME
6569135 2101983344 320 non-object
leaks 的关键信息:
Process 345: 6571471 nodes malloced for 2052905 KB
Process 345: 6567675 leaks for 2101654656 total leaked bytes.
6567675 (2004M) << TOTAL >>
1 (320 bytes) ROOT LEAK: ...
1 (320 bytes) ROOT LEAK: ...
...
这就很明确了:smd 泄漏了大约 656 万个 320 字节的小块,合计约 2GB。
如果没有提前开启 MallocStackLogging,heap 无法告诉我们这些 non-object 小块具体来自哪个函数。但泄漏形态已经很清楚:不是正常缓存,是大量同尺寸小块没有释放。
找到是谁在触发 smd¶
下一步是看 smd 在忙什么。
先看最近日志:
/usr/bin/log show --last 10m \
--predicate 'process == "smd" AND eventMessage CONTAINS[c] "getEffectiveDisposition"' \
--style compact
可以看到大量类似内容:
这说明 smd 在频繁处理后台项状态查询。
为了找到调用方,可以实时抓 smd.peer[PID],然后立刻查这个短命 PID:
/usr/bin/log stream --style compact \
--predicate 'process == "smd" AND eventMessage CONTAINS "activating connection"' |
while IFS= read -r line; do
pid=$(printf '%s\n' "$line" | sed -n 's/.*peer\[\([0-9][0-9]*\)\].*/\1/p')
if [ -n "$pid" ]; then
echo "--- smd peer pid=$pid ---"
echo "$line"
ps -p "$pid" -o pid,ppid,user,etime,command
fi
done
这次抓到的调用方是 Karabiner-Elements 的两个 helper:
Karabiner-Elements Privileged Daemons v2 core-daemons-enabled
Karabiner-Elements Non-Privileged Agents v2 core-agents-enabled
它们大约每 3 秒查询一次后台服务和 agent 是否启用。退出 Karabiner 后,日志停止;重新启动 Karabiner 后,smd 又出现并继续被触发。
这说明触发链路是:
Karabiner 定期查询 agent/daemon 状态
-> ServiceManagement / BackgroundTaskManagement
-> smd
-> smd 在这个路径上泄漏 320 字节小块
需要注意:Karabiner 查询自己的后台组件状态,本身是合理需求。真正泄漏的是 smd。Karabiner 只是一个稳定复现源。
不重启整机的处理办法¶
方案一:让 smd 自动退出¶
smd 支持 idle exit。没有客户端使用它时,系统可能自动回收它。
如果你能接受在空闲时退出触发源,比如退出 Karabiner 一段时间,smd 可能会自己消失。之后重新启动 Karabiner,smd 会被重新拉起,PID 也会变化。
验证:
方案二:直接重启 smd¶
如果不想影响 Karabiner,可以直接重启 smd:
如果前后 PID 变化,说明重启成功。
也可以看内存是否下降:
这个办法不需要重启整机,也不需要退出 Karabiner。对于长期运行的机器,可以用 root LaunchDaemon 定期执行。
示例脚本:
sudo mkdir -p /usr/local/sbin
sudo tee /usr/local/sbin/restart-smd.sh >/dev/null <<'EOF'
#!/bin/zsh
set -euo pipefail
before="$(/usr/bin/pgrep -x smd || true)"
echo "$(date '+%Y-%m-%d %H:%M:%S') before=${before:-none}"
/bin/launchctl kickstart -k system/com.apple.xpc.smd
/bin/sleep 2
after="$(/usr/bin/pgrep -x smd || true)"
echo "$(date '+%Y-%m-%d %H:%M:%S') after=${after:-none}"
EOF
sudo chmod 755 /usr/local/sbin/restart-smd.sh
每天凌晨 0 点执行的 LaunchDaemon:
sudo tee /Library/LaunchDaemons/local.restart-smd.daily.plist >/dev/null <<'EOF'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>local.restart-smd.daily</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/sbin/restart-smd.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>0</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/var/log/restart-smd.log</string>
<key>StandardErrorPath</key>
<string>/var/log/restart-smd.err</string>
</dict>
</plist>
EOF
sudo launchctl bootstrap system /Library/LaunchDaemons/local.restart-smd.daily.plist
验证:
方案三:重置 BTM 数据库¶
如果怀疑 Background Task Management 数据库里有脏记录或重复记录,可以考虑:
这个操作会重置后台项数据库,系统设置里的“登录项与后台活动”授权状态可能需要重新确认。它不适合作为第一选择,但在后台项状态明显异常时值得尝试。
总结¶
这次问题的关键不是“看到一个 2GB 进程就杀掉”,而是按层次拆开:
- 用
launchctl和codesign确认smd是 Apple 系统服务。 - 用
vmmap确认 2GB 落在MALLOC_SMALL,不是普通缓存或文件映射。 - 用
heap和leaks确认是 656 万个 320 字节小块泄漏。 - 用
log stream + ps把短命 peer PID 对应到触发源。 - 区分触发源和泄漏责任:Karabiner 查询是正常业务,
smd泄漏才是问题。 - 对长期运行机器,优先考虑定期重启
smd,而不是重启整机。
这类问题最容易误判成某个第三方软件“占了 2GB”。实际上第三方软件可能只是触发了系统服务的某条泄漏路径。排查时要尽量拿到 vmmap、heap、leaks 和日志证据,再决定是停触发源、重启系统服务,还是等待系统修复。