跳转至

一次 macOS smd 内存泄漏排查

这次遇到的问题是:机器长期不重启,活动监视器里看到一个 root 启动的 smd 进程占用了接近 2GB 内存。因为这台机器需要长期运行,所以目标不是简单重启,而是弄清楚:

  • smd 到底是什么服务
  • 2GB 内存具体是什么
  • 是谁在持续触发它
  • 有没有不用重启整机的处理办法

smd 是什么

先看进程:

ps -p 345 -o pid,ppid,user,stat,etime,%cpu,%mem,rss,vsz,lstart,command

能看到它由 launchd 启动,路径是:

/usr/libexec/smd

再看 launchd 里的服务定义:

launchctl print system/com.apple.xpc.smd

关键信息是:

path = /System/Library/LaunchDaemons/com.apple.xpc.smd.plist
program = /usr/libexec/smd
managed service = com.apple.xpc.smd

man smd 的说明很短:

man smd

它是 ServiceManagement framework daemon,负责替系统处理 ServiceManagement 相关操作。简单说,登录项、后台项、LaunchAgent、LaunchDaemon 的注册和状态查询,都可能走到它。

也可以确认它确实是 Apple 签名:

codesign -dv --verbose=4 /usr/libexec/smd

如果签名链是 Apple 的,就说明这不是第三方伪装进程。

先确认 2GB 是不是误读

一开始比较迷惑的是,不同工具显示不一致。

top 里看到:

top -l 1 -pid 345 -stats pid,command,cpu,mem,threads,ports,state,time

输出类似:

PID  COMMAND %CPU MEM    #TH #PORTS STATE
345  smd     0.0  2024M  3   53     sleeping

ps 的 RSS 可能只有几 MB:

RSS 约 6MB

这种时候不能只看一个指标。要看 vmmap

sudo vmmap -summary 345 > vmmap-smd.txt

这一步很关键。摘要里能看到:

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 确认泄漏形态

继续看堆:

sudo heap 345 > heap-smd.txt
sudo leaks 345 > leaks-smd.txt

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。

如果没有提前开启 MallocStackLoggingheap 无法告诉我们这些 non-object 小块具体来自哪个函数。但泄漏形态已经很清楚:不是正常缓存,是大量同尺寸小块没有释放。

找到是谁在触发 smd

下一步是看 smd 在忙什么。

先看最近日志:

/usr/bin/log show --last 10m \
  --predicate 'process == "smd" AND eventMessage CONTAINS[c] "getEffectiveDisposition"' \
  --style compact

可以看到大量类似内容:

BackgroundTaskManagement
BTMManager.getEffectiveDisposition
SecKeyVerifySignature
Found status: ...

这说明 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 也会变化。

验证:

pgrep -x smd
# 退出触发源,等待几分钟
pgrep -x smd
# 重新启动触发源
pgrep -x smd

方案二:直接重启 smd

如果不想影响 Karabiner,可以直接重启 smd

pgrep -x smd
sudo launchctl kickstart -k system/com.apple.xpc.smd
sleep 2
pgrep -x smd

如果前后 PID 变化,说明重启成功。

也可以看内存是否下降:

top -l 1 -pid "$(pgrep -x smd)"

这个办法不需要重启整机,也不需要退出 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

验证:

sudo launchctl print system/local.restart-smd.daily
sudo /usr/local/sbin/restart-smd.sh

方案三:重置 BTM 数据库

如果怀疑 Background Task Management 数据库里有脏记录或重复记录,可以考虑:

sfltool dumpbtm > ~/Desktop/btm-before-reset.txt
sfltool resetbtm

这个操作会重置后台项数据库,系统设置里的“登录项与后台活动”授权状态可能需要重新确认。它不适合作为第一选择,但在后台项状态明显异常时值得尝试。

总结

这次问题的关键不是“看到一个 2GB 进程就杀掉”,而是按层次拆开:

  1. launchctlcodesign 确认 smd 是 Apple 系统服务。
  2. vmmap 确认 2GB 落在 MALLOC_SMALL,不是普通缓存或文件映射。
  3. heapleaks 确认是 656 万个 320 字节小块泄漏。
  4. log stream + ps 把短命 peer PID 对应到触发源。
  5. 区分触发源和泄漏责任:Karabiner 查询是正常业务,smd 泄漏才是问题。
  6. 对长期运行机器,优先考虑定期重启 smd,而不是重启整机。

这类问题最容易误判成某个第三方软件“占了 2GB”。实际上第三方软件可能只是触发了系统服务的某条泄漏路径。排查时要尽量拿到 vmmapheapleaks 和日志证据,再决定是停触发源、重启系统服务,还是等待系统修复。