<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Troubleshooting on Tarragon</title><link>https://tarrragon.github.io/blog/tags/troubleshooting/</link><description>Recent content in Troubleshooting on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 02 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/troubleshooting/index.xml" rel="self" type="application/rss+xml"/><item><title>Linux 桌面的故障隔離模型</title><link>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/fault-isolation-model/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/fault-isolation-model/</guid><description>&lt;p>Linux 桌面環境的故障隔離建立在一個結構性的設計決策上：&lt;strong>顯示合成器（compositor）是 userspace process，不是 kernel 的一部分&lt;/strong>。這意味著 Hyprland 掛了只是桌面消失，作業系統核心還在正常運作。&lt;/p>
&lt;p>本文用「桌面環境」泛指使用者看到的圖形介面整體。技術上，Hyprland 是 Wayland compositor——負責視窗合成和輸入管理，不含完整桌面環境（DE）的套件管理、設定面板等元件。GNOME、KDE Plasma 才是完整的 DE。但在故障隔離的討論中，關鍵區分是 kernel space vs userspace，compositor 和 DE 都在 userspace 這一側。&lt;/p>
&lt;h2 id="kernel-與-userspace-的隔離邊界">Kernel 與 Userspace 的隔離邊界&lt;/h2>
&lt;p>作業系統分成兩個執行空間。Kernel space 負責硬體驅動、記憶體管理、process 排程這些基礎設施。Userspace 跑所有應用程式，包括桌面環境本身。&lt;/p>
&lt;p>兩個空間之間有硬體層級的隔離——CPU 的保護環機制（ring 0 是核心層級、ring 3 是應用程式層級，硬體強制限制 ring 3 的程式碼存取 ring 0 的記憶體）。Userspace 的 process 不管怎麼崩潰，都不會直接影響 kernel。Kernel 會清理掉崩潰的 process、回收它佔用的記憶體，然後繼續運作。&lt;/p>
&lt;p>這個隔離機制解釋了一個關鍵差異：為什麼 Linux 上一個 app crash 通常只是那個視窗消失，而不會拖垮整台機器。&lt;/p>
&lt;h2 id="為什麼-windows-會藍屏">為什麼 Windows 會藍屏&lt;/h2>
&lt;p>Windows 的藍屏（Blue Screen of Death, BSOD）是 kernel panic 的表現——作業系統核心本身遇到無法恢復的錯誤，只能停機。&lt;/p>
&lt;p>Windows 藍屏頻率較高的結構性原因在於&lt;strong>顯示驅動的執行位置&lt;/strong>。Windows 把 GPU 驅動放在 kernel mode（WDDM 架構），NVIDIA 或 AMD 的驅動程式碼直接跑在核心空間。驅動有 bug 時，錯誤發生在 kernel space，清理掉再繼續的選項不存在——繼續執行可能造成資料損壞，只能停機。&lt;/p>
&lt;p>藍屏頻率高是架構選擇的代價。把驅動放在 kernel mode 可以減少 context switch 的效能開銷，GPU 效能更好。代價是驅動 bug 的爆炸半徑從「app crash」升級成「整台停機」。Windows 10/11 已加入 TDR（Timeout Detection and Recovery）機制——GPU driver hang 時系統嘗試 reset driver 而非直接藍屏，大幅降低了 GPU 導致的 BSOD 頻率。但架構上 driver 仍在 kernel mode，藍屏的可能性仍然存在。&lt;/p>
&lt;h2 id="linux-桌面的架構差異">Linux 桌面的架構差異&lt;/h2>
&lt;p>Linux 桌面環境的顯示合成器（Hyprland、Sway、KDE Plasma 的 KWin）跑在 userspace。它們透過 DRM/KMS（Direct Rendering Manager / Kernel Mode Setting，Linux 的顯示子系統介面）跟 kernel 的 GPU 驅動溝通，但合成器本身的程式碼不在 kernel space 裡。&lt;/p>
&lt;p>這個架構選擇的效果：&lt;/p>
&lt;p>&lt;strong>Compositor crash&lt;/strong>。Hyprland 如果遇到 segfault 或其他 fatal error，kernel 終止這個 userspace process。所有由它管理的視窗消失，螢幕回到 TTY 登入畫面或黑屏。但 kernel 還在跑——其他 TTY 可以登入，SSH 可以連線，背景的 service 繼續運作。&lt;/p>
&lt;p>&lt;strong>GPU driver bug&lt;/strong>。Linux 的 GPU 驅動分兩層：kernel module（可動態載入的核心擴充模組，如 &lt;code>nvidia.ko&lt;/code>、&lt;code>amdgpu.ko&lt;/code>）負責硬體操作，userspace 的 Mesa / NVIDIA userspace driver 負責 OpenGL/Vulkan 實作。Kernel module 出問題理論上可以 kernel panic，但實際行為取決於驅動。AMD 的開源 &lt;code>amdgpu&lt;/code> 通常會嘗試 reset GPU 而非直接 panic，常見的表現是畫面凍結幾秒後恢復。NVIDIA 的閉源 &lt;code>nvidia.ko&lt;/code> 是隔離模型的主要例外——kernel 社群無法審查或修復其程式碼，hang 時恢復能力遠弱於 amdgpu，經常拖垮整個 session 且 TTY 切換也受影響。這是後續&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">故障場景&lt;/a>中 NVIDIA 相關 caveat 的根源。&lt;/p></description><content:encoded><![CDATA[<p>Linux 桌面環境的故障隔離建立在一個結構性的設計決策上：<strong>顯示合成器（compositor）是 userspace process，不是 kernel 的一部分</strong>。這意味著 Hyprland 掛了只是桌面消失，作業系統核心還在正常運作。</p>
<p>本文用「桌面環境」泛指使用者看到的圖形介面整體。技術上，Hyprland 是 Wayland compositor——負責視窗合成和輸入管理，不含完整桌面環境（DE）的套件管理、設定面板等元件。GNOME、KDE Plasma 才是完整的 DE。但在故障隔離的討論中，關鍵區分是 kernel space vs userspace，compositor 和 DE 都在 userspace 這一側。</p>
<h2 id="kernel-與-userspace-的隔離邊界">Kernel 與 Userspace 的隔離邊界</h2>
<p>作業系統分成兩個執行空間。Kernel space 負責硬體驅動、記憶體管理、process 排程這些基礎設施。Userspace 跑所有應用程式，包括桌面環境本身。</p>
<p>兩個空間之間有硬體層級的隔離——CPU 的保護環機制（ring 0 是核心層級、ring 3 是應用程式層級，硬體強制限制 ring 3 的程式碼存取 ring 0 的記憶體）。Userspace 的 process 不管怎麼崩潰，都不會直接影響 kernel。Kernel 會清理掉崩潰的 process、回收它佔用的記憶體，然後繼續運作。</p>
<p>這個隔離機制解釋了一個關鍵差異：為什麼 Linux 上一個 app crash 通常只是那個視窗消失，而不會拖垮整台機器。</p>
<h2 id="為什麼-windows-會藍屏">為什麼 Windows 會藍屏</h2>
<p>Windows 的藍屏（Blue Screen of Death, BSOD）是 kernel panic 的表現——作業系統核心本身遇到無法恢復的錯誤，只能停機。</p>
<p>Windows 藍屏頻率較高的結構性原因在於<strong>顯示驅動的執行位置</strong>。Windows 把 GPU 驅動放在 kernel mode（WDDM 架構），NVIDIA 或 AMD 的驅動程式碼直接跑在核心空間。驅動有 bug 時，錯誤發生在 kernel space，清理掉再繼續的選項不存在——繼續執行可能造成資料損壞，只能停機。</p>
<p>藍屏頻率高是架構選擇的代價。把驅動放在 kernel mode 可以減少 context switch 的效能開銷，GPU 效能更好。代價是驅動 bug 的爆炸半徑從「app crash」升級成「整台停機」。Windows 10/11 已加入 TDR（Timeout Detection and Recovery）機制——GPU driver hang 時系統嘗試 reset driver 而非直接藍屏，大幅降低了 GPU 導致的 BSOD 頻率。但架構上 driver 仍在 kernel mode，藍屏的可能性仍然存在。</p>
<h2 id="linux-桌面的架構差異">Linux 桌面的架構差異</h2>
<p>Linux 桌面環境的顯示合成器（Hyprland、Sway、KDE Plasma 的 KWin）跑在 userspace。它們透過 DRM/KMS（Direct Rendering Manager / Kernel Mode Setting，Linux 的顯示子系統介面）跟 kernel 的 GPU 驅動溝通，但合成器本身的程式碼不在 kernel space 裡。</p>
<p>這個架構選擇的效果：</p>
<p><strong>Compositor crash</strong>。Hyprland 如果遇到 segfault 或其他 fatal error，kernel 終止這個 userspace process。所有由它管理的視窗消失，螢幕回到 TTY 登入畫面或黑屏。但 kernel 還在跑——其他 TTY 可以登入，SSH 可以連線，背景的 service 繼續運作。</p>
<p><strong>GPU driver bug</strong>。Linux 的 GPU 驅動分兩層：kernel module（可動態載入的核心擴充模組，如 <code>nvidia.ko</code>、<code>amdgpu.ko</code>）負責硬體操作，userspace 的 Mesa / NVIDIA userspace driver 負責 OpenGL/Vulkan 實作。Kernel module 出問題理論上可以 kernel panic，但實際行為取決於驅動。AMD 的開源 <code>amdgpu</code> 通常會嘗試 reset GPU 而非直接 panic，常見的表現是畫面凍結幾秒後恢復。NVIDIA 的閉源 <code>nvidia.ko</code> 是隔離模型的主要例外——kernel 社群無法審查或修復其程式碼，hang 時恢復能力遠弱於 amdgpu，經常拖垮整個 session 且 TTY 切換也受影響。這是後續<a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">故障場景</a>中 NVIDIA 相關 caveat 的根源。</p>
<p><strong>應用程式 crash</strong>。Firefox、VS Code、任何 GUI 程式崩潰，只有那個視窗消失。Compositor 繼續管理剩下的視窗，桌面環境不受影響。</p>
<h2 id="ttykernel-存活時的首選救生通道">TTY：kernel 存活時的首選救生通道</h2>
<p>TTY（TeleTYpewriter）是 Linux 核心直接提供的純文字終端機介面，獨立於任何桌面環境。systemd 預設配置下有 6 個 virtual console（TTY1-TTY6）。Wayland compositor（如 Hyprland）通常佔用 TTY1，其餘可用。</p>
<p>切換方式：<code>Ctrl+Alt+F2</code>（切到 TTY2）到 <code>Ctrl+Alt+F6</code>（切到 TTY6）。</p>
<p>TTY 的重要性在於它<strong>不依賴 compositor</strong>。Hyprland 掛了、compositor crash 導致桌面消失——只要 kernel 還活著、GPU driver 仍能處理 VT switch，TTY 就能切過去登入操作：</p>
<ul>
<li>用 <code>htop</code> 或 <code>ps</code> 查看哪個 process 出問題</li>
<li><code>kill</code> 有問題的 process</li>
<li>用 <code>vim</code> 或 <code>nano</code> 修改配置檔</li>
<li>重新啟動 Hyprland（<code>Hyprland</code> 指令）</li>
<li>如果需要，正常 <code>reboot</code></li>
</ul>
<p>TTY 切換失效的情境有兩種：kernel panic（極罕見）和 GPU 完全 hang 導致 VT switch 本身卡住（NVIDIA 閉源驅動在 Wayland 上較常見，需確保 <code>nvidia_drm.modeset=1</code>）。後者的替代手段是 SSH 遠端登入或 Magic SysRq 鍵（見<a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景</a>的場景三）。</p>
<h2 id="記憶體耗盡oom的處理機制">記憶體耗盡（OOM）的處理機制</h2>
<p>Linux kernel 有 OOM Killer（Out of Memory Killer）機制——當記憶體和 swap 都用完、kernel 無法再分配新頁面時，自動挑選佔用記憶體最多、重要性最低的 process 強制終止，釋放記憶體讓系統繼續運作。</p>
<p>OOM Killer 的行為有時超出使用者的預期——它可能直接終止 Hyprland（因為 compositor 通常佔用不少記憶體），導致桌面突然消失。但關鍵是：<strong>系統沒有崩潰</strong>。Kernel 還在、TTY 還在、SSH 還在。</p>
<p>預防 OOM 的常見做法：</p>
<ul>
<li>設定 swap（即使用 SSD，2-4GB 的 swap 也能在記憶體壓力大時提供緩衝）</li>
<li>啟用 <code>systemd-oomd</code>（userspace 的 OOM 管理，比 kernel OOM Killer 更早介入、更可控）</li>
<li>監控記憶體用量（<code>btop</code> 或 <code>htop</code> 可以看即時狀態）</li>
</ul>
<h2 id="故障層級速查">故障層級速查</h2>
<table>
  <thead>
      <tr>
          <th>故障層級</th>
          <th>症狀</th>
          <th>系統影響</th>
          <th>恢復手段</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>應用程式 crash</td>
          <td>單一視窗消失</td>
          <td>無</td>
          <td>重開該程式</td>
      </tr>
      <tr>
          <td>工具 crash（waybar 等）</td>
          <td>狀態列 / 通知 / 啟動器消失</td>
          <td>無</td>
          <td>重啟該工具</td>
      </tr>
      <tr>
          <td>Compositor crash</td>
          <td>所有視窗消失、黑屏</td>
          <td>桌面環境不可用</td>
          <td>TTY 登入、重啟 compositor</td>
      </tr>
      <tr>
          <td>GPU driver hang</td>
          <td>畫面凍結</td>
          <td>桌面環境不可用</td>
          <td>TTY 或 SSH、kill compositor</td>
      </tr>
      <tr>
          <td>OOM</td>
          <td>系統極慢或桌面被殺</td>
          <td>部分 process 被終止</td>
          <td>TTY 登入、清理 process</td>
      </tr>
      <tr>
          <td>Kernel panic</td>
          <td>完全停機</td>
          <td>全機不可用</td>
          <td>只能重開機</td>
      </tr>
  </tbody>
</table>
<p>前五個層級都有恢復手段，只有 kernel panic 需要重開機。日常使用中遇到的故障多數落在前三層。</p>
]]></content:encoded></item><item><title>常見故障場景與恢復操作</title><link>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/</guid><description>&lt;p>這篇按故障場景組織，每個場景列出症狀、原因、恢復步驟和預防措施。出問題時按症狀找到對應場景，照步驟操作。&lt;/p>
&lt;h2 id="場景一hyprland-compositor-crash">場景一：Hyprland compositor crash&lt;/h2>
&lt;p>&lt;strong>症狀&lt;/strong>：所有視窗同時消失，螢幕變黑或回到 TTY 登入畫面。滑鼠鍵盤有反應（可以切 TTY），但沒有桌面。&lt;/p>
&lt;p>&lt;strong>原因&lt;/strong>：Compositor process 遇到 fatal error 被 kernel 終止。常見觸發條件包括 plugin 相容性問題、特定 Wayland 協議操作觸發的 bug、GPU driver 回傳異常狀態。&lt;/p>
&lt;p>&lt;strong>恢復步驟&lt;/strong>：&lt;/p>
&lt;p>注意：以下步驟中 &lt;code>killall Hyprland&lt;/code> 或重啟 Hyprland 會終止所有由 compositor 管理的視窗，未存檔的工作會遺失。如果可能，先透過 TTY 或 SSH 嘗試存檔（如 &lt;code>kill -USR1 &amp;lt;pid&amp;gt;&lt;/code> 對支援的應用程式觸發存檔）。&lt;/p>
&lt;ol>
&lt;li>&lt;code>Ctrl+Alt+F2&lt;/code> 切到 TTY2&lt;/li>
&lt;li>用你的帳號登入&lt;/li>
&lt;li>檢查 Hyprland 的最後錯誤訊息：&lt;/li>
&lt;/ol>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 通用方式（不管 Hyprland 怎麼啟動都有效）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">journalctl -b &lt;span class="p">|&lt;/span> grep -i hypr &lt;span class="p">|&lt;/span> tail -30
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 如果 Hyprland 是 systemd user unit，可以更精準地查：&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">journalctl --user -u hyprland -n &lt;span class="m">50&lt;/span> --no-pager&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="4">
&lt;li>重新啟動 Hyprland：&lt;/li>
&lt;/ol>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Hyprland&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;ol start="5">
&lt;li>如果反覆 crash，檢查最近改過的 config：&lt;/li>
&lt;/ol>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">cd&lt;/span> ~/.config/hypr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">git diff &lt;span class="c1"># 如果 dotfile 有版控&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>預防&lt;/strong>：config 改動後用 &lt;code>hyprctl reload&lt;/code> 測試，不要直接重啟。啟用 plugin 前確認版本跟 Hyprland 版本相容。&lt;/p>
&lt;h2 id="場景二單一桌面工具掛了">場景二：單一桌面工具掛了&lt;/h2>
&lt;p>&lt;strong>症狀&lt;/strong>：狀態列（waybar）消失、啟動器（wofi/rofi）叫不出來、通知（mako/dunst）不跳了。桌面其他功能正常，視窗可以操作。&lt;/p>
&lt;p>&lt;strong>原因&lt;/strong>：這些工具各自是獨立的 process。掛了只影響自己的功能，不影響 compositor 或其他工具。常見原因是 config 語法錯誤（改完 config 後觸發）、記憶體洩漏（長時間運作後）、或外部服務連線異常（如 waybar 的某個 module 連不到系統匯流排）。&lt;/p>
&lt;p>&lt;strong>恢復步驟&lt;/strong>：&lt;/p>
&lt;p>判斷啟動方式：如果工具是在 Hyprland config 裡用 &lt;code>exec-once&lt;/code>（Hyprland 的自動啟動指令，compositor 啟動時執行一次）啟動的，用 &lt;code>killall&lt;/code> + 手動重啟；如果是 systemd user unit，用 &lt;code>systemctl --user&lt;/code>。&lt;/p>
&lt;p>&lt;code>exec-once&lt;/code> 啟動方式（多數 Hyprland 安裝的預設做法）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># waybar 掛了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">killall waybar&lt;span class="p">;&lt;/span> waybar &lt;span class="p">&amp;amp;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># wofi 掛了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">killall wofi
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># wofi 只在需要時啟動，不用常駐&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># mako 掛了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">killall mako&lt;span class="p">;&lt;/span> mako &lt;span class="p">&amp;amp;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>systemd user unit 啟動方式：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">systemctl --user restart waybar
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">systemctl --user restart mako&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>確認工具是否在跑&lt;/strong>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pgrep waybar &lt;span class="c1"># 有輸出 = 在跑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">pgrep mako &lt;span class="c1"># 沒輸出 = 沒在跑&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>預防&lt;/strong>：改 config 後重啟對應的工具確認語法正確。Waybar 的 config 是 JSON 格式，語法錯誤會導致它無法啟動——改完後先用 &lt;code>waybar&lt;/code> 前台跑一次看有沒有錯誤訊息。&lt;/p>
&lt;h2 id="場景二點五鎖屏卡死hyprlock-異常結束">場景二點五：鎖屏卡死（hyprlock 異常結束）&lt;/h2>
&lt;p>&lt;strong>症狀&lt;/strong>：鎖屏畫面消失但桌面沒回來，螢幕顯示 Hyprland 的失效保護訊息（「it looks like you locked your screen but the lockscreen app died」），或畫面全黑但系統有回應（SSH 能連、TTY 可能切得到也可能切不到）。&lt;/p>
&lt;p>&lt;strong>原因&lt;/strong>：鎖屏工具（Hyprlock、Swaylock）透過 Wayland 的 ext-session-lock 協議向 compositor 請求鎖定。鎖定狀態由 compositor 持有，唯一正常解鎖動作是鎖屏 client 通過認證後呼叫 unlock_and_destroy。如果鎖屏 client 在持鎖狀態下被殺（&lt;code>pkill&lt;/code>、crash），compositor 沒收到認證信號，會維持鎖定並顯示失效保護畫面。這跟殺 waybar/mako 不同——那些是普通 process，殺了重啟就好；鎖屏 client 持有安全狀態，殺了反而卡住。&lt;/p></description><content:encoded><![CDATA[<p>這篇按故障場景組織，每個場景列出症狀、原因、恢復步驟和預防措施。出問題時按症狀找到對應場景，照步驟操作。</p>
<h2 id="場景一hyprland-compositor-crash">場景一：Hyprland compositor crash</h2>
<p><strong>症狀</strong>：所有視窗同時消失，螢幕變黑或回到 TTY 登入畫面。滑鼠鍵盤有反應（可以切 TTY），但沒有桌面。</p>
<p><strong>原因</strong>：Compositor process 遇到 fatal error 被 kernel 終止。常見觸發條件包括 plugin 相容性問題、特定 Wayland 協議操作觸發的 bug、GPU driver 回傳異常狀態。</p>
<p><strong>恢復步驟</strong>：</p>
<p>注意：以下步驟中 <code>killall Hyprland</code> 或重啟 Hyprland 會終止所有由 compositor 管理的視窗，未存檔的工作會遺失。如果可能，先透過 TTY 或 SSH 嘗試存檔（如 <code>kill -USR1 &lt;pid&gt;</code> 對支援的應用程式觸發存檔）。</p>
<ol>
<li><code>Ctrl+Alt+F2</code> 切到 TTY2</li>
<li>用你的帳號登入</li>
<li>檢查 Hyprland 的最後錯誤訊息：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 通用方式（不管 Hyprland 怎麼啟動都有效）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">journalctl -b <span class="p">|</span> grep -i hypr <span class="p">|</span> tail -30
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 如果 Hyprland 是 systemd user unit，可以更精準地查：</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">journalctl --user -u hyprland -n <span class="m">50</span> --no-pager</span></span></code></pre></div><ol start="4">
<li>重新啟動 Hyprland：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">Hyprland</span></span></code></pre></div><ol start="5">
<li>如果反覆 crash，檢查最近改過的 config：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">cd</span> ~/.config/hypr
</span></span><span class="line"><span class="ln">2</span><span class="cl">git diff  <span class="c1"># 如果 dotfile 有版控</span></span></span></code></pre></div><p><strong>預防</strong>：config 改動後用 <code>hyprctl reload</code> 測試，不要直接重啟。啟用 plugin 前確認版本跟 Hyprland 版本相容。</p>
<h2 id="場景二單一桌面工具掛了">場景二：單一桌面工具掛了</h2>
<p><strong>症狀</strong>：狀態列（waybar）消失、啟動器（wofi/rofi）叫不出來、通知（mako/dunst）不跳了。桌面其他功能正常，視窗可以操作。</p>
<p><strong>原因</strong>：這些工具各自是獨立的 process。掛了只影響自己的功能，不影響 compositor 或其他工具。常見原因是 config 語法錯誤（改完 config 後觸發）、記憶體洩漏（長時間運作後）、或外部服務連線異常（如 waybar 的某個 module 連不到系統匯流排）。</p>
<p><strong>恢復步驟</strong>：</p>
<p>判斷啟動方式：如果工具是在 Hyprland config 裡用 <code>exec-once</code>（Hyprland 的自動啟動指令，compositor 啟動時執行一次）啟動的，用 <code>killall</code> + 手動重啟；如果是 systemd user unit，用 <code>systemctl --user</code>。</p>
<p><code>exec-once</code> 啟動方式（多數 Hyprland 安裝的預設做法）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># waybar 掛了</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">killall waybar<span class="p">;</span> waybar <span class="p">&amp;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># wofi 掛了</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">killall wofi
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># wofi 只在需要時啟動，不用常駐</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># mako 掛了</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">killall mako<span class="p">;</span> mako <span class="p">&amp;</span></span></span></code></pre></div><p>systemd user unit 啟動方式：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">systemctl --user restart waybar
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl --user restart mako</span></span></code></pre></div><p><strong>確認工具是否在跑</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pgrep waybar  <span class="c1"># 有輸出 = 在跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">pgrep mako    <span class="c1"># 沒輸出 = 沒在跑</span></span></span></code></pre></div><p><strong>預防</strong>：改 config 後重啟對應的工具確認語法正確。Waybar 的 config 是 JSON 格式，語法錯誤會導致它無法啟動——改完後先用 <code>waybar</code> 前台跑一次看有沒有錯誤訊息。</p>
<h2 id="場景二點五鎖屏卡死hyprlock-異常結束">場景二點五：鎖屏卡死（hyprlock 異常結束）</h2>
<p><strong>症狀</strong>：鎖屏畫面消失但桌面沒回來，螢幕顯示 Hyprland 的失效保護訊息（「it looks like you locked your screen but the lockscreen app died」），或畫面全黑但系統有回應（SSH 能連、TTY 可能切得到也可能切不到）。</p>
<p><strong>原因</strong>：鎖屏工具（Hyprlock、Swaylock）透過 Wayland 的 ext-session-lock 協議向 compositor 請求鎖定。鎖定狀態由 compositor 持有，唯一正常解鎖動作是鎖屏 client 通過認證後呼叫 unlock_and_destroy。如果鎖屏 client 在持鎖狀態下被殺（<code>pkill</code>、crash），compositor 沒收到認證信號，會維持鎖定並顯示失效保護畫面。這跟殺 waybar/mako 不同——那些是普通 process，殺了重啟就好；鎖屏 client 持有安全狀態，殺了反而卡住。</p>
<p><strong>恢復步驟</strong>：</p>
<ol>
<li>嘗試切到另一個 TTY（<code>Ctrl+Alt+F2</code>）。注意：ext-session-lock 的安全語意允許 compositor 攔截 VT 切換快捷鍵，此時 TTY 切不過去，改用 SSH 從另一台機器連入</li>
<li>允許新的鎖屏 client 接管既有的鎖：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hyprctl --instance <span class="m">0</span> <span class="s1">&#39;keyword misc:allow_session_lock_restore 1&#39;</span></span></span></code></pre></div><ol start="3">
<li>重新拉一個鎖屏 client：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">hyprctl --instance <span class="m">0</span> <span class="s1">&#39;dispatch exec hyprlock&#39;</span></span></span></code></pre></div><ol start="4">
<li>回到鎖屏畫面，用密碼正常解鎖</li>
</ol>
<p><strong>判讀</strong>：<code>loginctl show-session &lt;id&gt; -p LockedHint</code> 可能顯示 <code>LockedHint=no</code>（logind 層認為沒鎖），但畫面仍進不去——因為擋住畫面的是 compositor 的 ext-session-lock，跟 logind 的提示是獨立的兩層。判斷畫面鎖定狀態看 compositor 層，不看 logind。</p>
<p><strong>預防</strong>：測試鎖屏時備好恢復路徑（知道密碼、或預先開 SSH）。不要用殺 process 的方式結束鎖屏——要結束就走認證解鎖。自動化流程若會啟動鎖屏，把「需要人工解鎖」算進代價。鎖屏安全模型的完整說明見 <a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Session Lock</a>。</p>
<h2 id="場景二點六桌面-shell-畫得出來但互動死掉進程活著卻-wedged">場景二點六：桌面 shell 畫得出來但互動死掉（進程活著卻 wedged）</h2>
<p><strong>症狀</strong>：bar / 狀態列還在螢幕上、看起來一切正常，但點它的按鈕（工作區切換、系統匣圖示）沒反應，keybind 叫不出啟動器（wofi / 內建 launcher）。同時焦點視窗（例如終端機）打字完全正常——鍵盤到得了應用程式，只是桌面 shell 的互動死了。</p>
<p><strong>原因</strong>：這是跟場景二（工具掛了）不同的一類故障，關鍵差別在<strong>進程還活著</strong>。場景二是 process 崩潰退出（<code>pgrep</code> 沒輸出），殺了重啟就好；這裡的桌面 shell（如 caelestia / Quickshell）進程還在跑（<code>pgrep</code> 找得到、STAT 是正常的 <code>S</code>、在 <code>poll</code> 等事件、CPU 不高），但它內部的某個子系統初始化失敗了——常見是 QML scene 的某個物件因為上游錯誤沒建起來、變成 null，於是負責「keybind → 開抽屜」「bar 按鈕互動」的模組對 null 讀屬性、整條互動接線死掉。bar 之所以還畫得出來，是它還停在初始化失敗前那一幀的畫面：<strong>畫得出來不等於還活著</strong>，跟鎖屏那課（畫面有密碼框不等於真的在鎖）是同一個陷阱。</p>
<p>上游觸發常是渲染層。實測案例：VM 的 GPU 只提供到 GLSL 1.20，而 shell 的 shader 需要 GLES 100/300/330，pipeline 建不起來（log 狂噴 <code>Failed to build graphics pipeline state</code>），這次渲染失敗把 scene 初始化打斷，drawers 狀態物件變 null。</p>
<p><strong>診斷（別看 pgrep，讀 shell 自己的 log）</strong>：</p>
<p><code>pgrep</code> 在這裡會騙你——它回報「在跑」，但那不等於「在運作」。權威來源是 shell 自己的 log，而且這種 log 常常<strong>不在 journalctl、也不在你猜的路徑</strong>，要用該 shell 專屬的 log 指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># caelestia 的例子：用它自己的 CLI 印 shell log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">caelestia shell -l 2&gt;<span class="p">&amp;</span><span class="m">1</span> <span class="p">|</span> tail -40
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 看的是 QML 的 TypeError：對 null 讀屬性 = 那個子系統死了</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1">#   scene: @modules/Shortcuts.qml: TypeError: Cannot read property &#39;launcher&#39; of null</span></span></span></code></pre></div><p>另一個活性探針是 shell 的 <strong>IPC 回不回真實狀態</strong>：正常時查抽屜列表會回傳名字，子系統死掉時回空——這比「進程在不在」精準得多：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 子系統活著 → 列出 bar/launcher/session…；死掉 → 回空</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">caelestia shell ipc call drawers list</span></span></code></pre></div><p><strong>恢復步驟</strong>：重啟 shell 讓 scene 重建。以 caelestia 為例：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">caelestia shell -k     <span class="c1"># 殺掉卡住的 shell</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">caelestia shell -d     <span class="c1"># 重新啟動（detached）</span></span></span></code></pre></div><p><strong>驗證修好了，看子系統回來、不是看 pgrep</strong>：重啟後 process 一定在（<code>pgrep</code> 本來就會有），要確認的是接線恢復——<code>caelestia shell ipc call drawers list</code> 從「回空」變成列出真實抽屜名、log 不再噴 null 的 TypeError。這對應「重啟成功要驗子系統狀態、不是驗 process 存在」的通用紀律。</p>
<p><strong>判讀與其他場景的界線</strong>：<code>pgrep</code> 有輸出 + bar 畫得出來 → 別急著判「正常」；點不動 / keybind 死掉就是 wedged 的訊號，往 shell 自己的 log 查。這跟場景二（process 真的沒了、<code>pgrep</code> 空）、場景三（compositor 整個凍結、連終端機打字都不行）都不同——這裡 compositor 正常、焦點視窗鍵盤正常，只有 shell 的互動接線死。判「進程活著到底有沒有在運作」的通用招式，見 <a href="/blog/linux/debug/process-service-state-diagnosis/" data-link-title="程序、服務與狀態怎麼判" data-link-desc="要判斷一個程式活著沒、某個系統服務現在由誰提供、桌面 session 有沒有被鎖、或終端機多工器的 session 還在不在時，用對的權威來源而不是靠畫面或猜的名字">程序、服務與狀態怎麼判</a>。</p>
<p><strong>預防</strong>：留意 shell log 裡持續出現的 shader / 渲染 pipeline 錯誤——在 VM 或 GL 支援不足的環境，這類錯誤可能非致命地存在（shell 大致能用），但一次渲染失敗就可能打斷 scene 初始化、把互動接線弄死。VM 環境要確認 GPU 提供的 GL / GLSL 版本足夠（virtio-gpu 走 mesa/zink 提供 GL 3.3+），或在 shell 設定關掉需要高階 shader 的效果。</p>
<h2 id="場景三gpu-driver-hang畫面凍結">場景三：GPU driver hang（畫面凍結）</h2>
<p><strong>症狀</strong>：桌面畫面完全凍結——滑鼠不動、鍵盤不回應、<code>Ctrl+Alt+F2</code> 切 TTY 也沒反應或延遲很久才回應。但如果從另一台機器 SSH 進來，系統是活的，process 都在跑。</p>
<p><strong>原因</strong>：GPU driver 進入異常狀態。NVIDIA 閉源驅動在 Linux 上的穩定性不如 AMD 開源驅動（amdgpu），特別是在 Wayland 環境下。常見觸發條件包括 suspend/resume 之後 GPU 沒正確恢復、某些 OpenGL/Vulkan 操作觸發 driver bug、顯示輸出切換（接上或拔掉外接螢幕）。</p>
<p><strong>恢復步驟</strong>：</p>
<p>方法 A — 如果 TTY 能切過去：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 切到 TTY2</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">Ctrl+Alt+F2
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 殺掉 Hyprland（它會帶走所有視窗）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">killall Hyprland
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 重新啟動</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">Hyprland</span></span></code></pre></div><p>方法 B — 如果 TTY 也凍結、但 SSH 能連：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 從另一台機器 SSH 進來（需事先知道 IP，見下方預防段）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">ssh user@machine-ip
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 殺掉 compositor</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">killall Hyprland
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 如果需要 reset GPU（NVIDIA，且 driver 仍回應）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 前提：所有使用 GPU 的 process 已停止（compositor 已 kill）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">sudo nvidia-smi --gpu-reset
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 切回機器前面重啟 Hyprland</span></span></span></code></pre></div><p>方法 C — 如果完全無回應，先嘗試 Magic SysRq：</p>
<p>Magic SysRq 是 kernel 層級的緊急操作介面，即使 userspace 完全卡死也能回應。按住 <code>Alt+SysRq</code>（筆電通常是 <code>Alt+Fn+SysRq</code>），然後依序按 <code>R E I S U B</code>（每個鍵間隔幾秒）：</p>
<ul>
<li><code>R</code> — 把鍵盤從 raw mode 搶回來</li>
<li><code>E</code> — 對所有 process 送 SIGTERM</li>
<li><code>I</code> — 對所有 process 送 SIGKILL</li>
<li><code>S</code> — sync 所有檔案系統</li>
<li><code>U</code> — remount 所有檔案系統為 read-only</li>
<li><code>B</code> — 立即 reboot</li>
</ul>
<p>這比直接斷電安全——sync + unmount 步驟會盡量保護磁碟上的資料。Arch Linux 預設可能停用 SysRq，需在 <code>/etc/sysctl.d/</code> 設定 <code>kernel.sysrq=1</code> 啟用。</p>
<p>方法 D — 如果 SysRq 也無效，按住電源鍵強制關機：</p>
<p>這是最後手段。Linux 的 ext4/btrfs 檔案系統有 journal 保護，強制關機通常不會損壞<strong>檔案系統結構</strong>。但 journal 保護的是 metadata 一致性，正在寫入的使用者資料（未存檔的文件、正在下載的檔案）仍然可能遺失或損壞。重開機後正常登入 TTY、啟動 Hyprland 即可。如果開機過程有異常，用 <code>journalctl -b -1 -p err</code> 查看上次開機的錯誤訊息，確認是否有檔案系統修復紀錄。</p>
<p><strong>預防</strong>：</p>
<ul>
<li>NVIDIA 用戶：關注 driver 版本的 release notes，已知有 Wayland 問題的版本避開</li>
<li>配置 suspend 後的 GPU 恢復：在 Hyprland config 或 systemd sleep hook 裡加入 GPU reset 操作</li>
<li>事先記錄機器的 IP 位址（<code>ip addr show</code>）或設定固定 hostname（如 mDNS 的 <code>machine.local</code>），桌面凍結時才有辦法從另一台機器 SSH 進來</li>
<li>考慮開啟 SSH server，出問題時可以遠端救援。開啟後應配置 key-based authentication 並停用密碼登入（<code>PasswordAuthentication no</code>），避免在網路上暴露密碼登入通道：</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> sshd
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl start sshd
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 安全配置：停用密碼登入（確保已設好 SSH key 再改）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 編輯 /etc/ssh/sshd_config，設定 PasswordAuthentication no</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 然後 sudo systemctl restart sshd</span></span></span></code></pre></div><h2 id="場景四記憶體耗盡oom">場景四：記憶體耗盡（OOM）</h2>
<p><strong>症狀</strong>：系統變得極慢，操作有明顯延遲（幾秒到幾十秒）。隨後可能某些 process 突然被殺掉——瀏覽器分頁消失、IDE 視窗關閉，嚴重時 Hyprland 本身被 OOM Killer 終止導致桌面消失。</p>
<p><strong>原因</strong>：實體記憶體和 swap 都用完了。常見觸發者是瀏覽器（Chrome/Firefox 的分頁越開越多）、IDE（大型專案的 language server）、Docker container、或應用程式的記憶體洩漏。</p>
<p><strong>恢復步驟</strong>：</p>
<p>如果還能操作：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 找出誰在吃記憶體</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">top -o %MEM
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 或用 htop/btop 的互動介面</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 殺掉佔最多記憶體的 process</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nb">kill</span> &lt;PID&gt;</span></span></code></pre></div><p>如果桌面已經被殺、在 TTY 裡：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 看 OOM Killer 殺了誰</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">journalctl -b <span class="p">|</span> grep -i <span class="s2">&#34;out of memory&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">journalctl -b <span class="p">|</span> grep -i <span class="s2">&#34;oom&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 清理完後重啟桌面</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">Hyprland</span></span></code></pre></div><p><strong>預防</strong>：</p>
<p>設定 swap（即使 RAM 夠大，swap 提供 OOM 前的緩衝時間讓你有機會手動清理 process）。RAM 16GB 以上的機器，2-4GB swap 作緩衝通常足夠：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 查看是否有 swap</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">swapon --show
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 如果沒有，建立一個 4GB 的 swap file（ext4 檔案系統）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">sudo fallocate -l 4G /swapfile
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">sudo chmod <span class="m">600</span> /swapfile
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">sudo mkswap /swapfile
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">sudo swapon /swapfile
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 永久生效：加入 /etc/fstab</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="nb">echo</span> <span class="s1">&#39;/swapfile none swap defaults 0 0&#39;</span> <span class="p">|</span> sudo tee -a /etc/fstab</span></span></code></pre></div><p>Btrfs 檔案系統不支援 <code>fallocate</code> 建立 swap file。Btrfs 用戶需改用 <code>btrfs filesystem mkswapfile</code> 或建立專屬的 swap subvolume，具體做法參考 Arch Wiki 的 Btrfs swap 段落。</p>
<p>啟用 systemd-oomd（比 kernel OOM Killer 更早介入、更可控）。systemd-oomd 在 cgroup 的記憶體壓力達到閾值時就開始清理，預設配置對多數桌面場景足夠。進階調整可透過 <code>/etc/systemd/oomd.conf</code> 設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> systemd-oomd
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl start systemd-oomd</span></span></code></pre></div><h2 id="場景五config-寫錯導致-hyprland-啟動失敗">場景五：Config 寫錯導致 Hyprland 啟動失敗</h2>
<p><strong>症狀</strong>：從 display manager（圖形登入畫面，如 SDDM、GDM）登入後立刻黑屏又回到登入畫面，或直接回到 TTY。如果從 TTY 手動執行 <code>Hyprland</code>，看到錯誤訊息後立即退出。</p>
<p><strong>原因</strong>：Hyprland config 有語法錯誤或引用了不存在的資源。常見錯誤包括 <code>source</code> 指定的檔案不存在、keybind 語法寫錯、monitor 設定格式錯誤。</p>
<p><strong>恢復步驟</strong>：</p>
<ol>
<li>切到 TTY（<code>Ctrl+Alt+F2</code>）</li>
<li>登入後直接跑 Hyprland 看錯誤訊息：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 看 Hyprland 的啟動錯誤（也可用 journalctl -b | grep -i hypr）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">Hyprland
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># Hyprland 如果因 config 錯誤無法啟動，會直接印出錯誤訊息後退出</span></span></span></code></pre></div><ol start="3">
<li>根據錯誤訊息修改 config：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Hyprland 的主 config</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">vim ~/.config/hypr/hyprland.conf
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 如果用了 source 拆分，錯誤訊息會指出是哪個檔案</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">vim ~/.config/hypr/keybinds.conf</span></span></code></pre></div><ol start="4">
<li>修完後重新啟動：</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">Hyprland</span></span></code></pre></div><p><strong>常見 config 錯誤</strong>：</p>
<p><code>source</code> 路徑錯誤——檔案不存在或路徑拼錯：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 確認 source 指定的檔案都存在</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">grep <span class="s2">&#34;^source&#34;</span> ~/.config/hypr/hyprland.conf
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 逐一檢查每個路徑</span></span></span></code></pre></div><p>Monitor 設定錯誤——指定了不存在的螢幕名稱：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 查看系統實際的螢幕名稱</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 在能進桌面時記下來，或用 wlr-randr</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">wlr-randr</span></span></code></pre></div><p>Keybind 語法錯誤——dispatcher 名稱拼錯或參數格式不對。Hyprland 的 keybind 格式是 <code>bind = MOD, key, dispatcher, params</code>，少一個欄位或 dispatcher 拼錯就會報錯。</p>
<p><strong>預防</strong>：config 改動時用 <code>hyprctl reload</code> 即時測試，不要改完 config 就直接重啟 Hyprland。如果 dotfile 用 Git 管理，改壞了可以 <code>git checkout</code> 回退。</p>
<h2 id="場景六suspendresume-後桌面異常">場景六：Suspend/resume 後桌面異常</h2>
<p><strong>症狀</strong>：筆電蓋上或手動 suspend 後喚醒，出現以下任一情況——螢幕黑屏但系統有反應（鍵盤背光亮）、解析度跑掉、多螢幕配置丟失（所有視窗擠到一個螢幕）、compositor 直接 crash 回到 TTY。</p>
<p><strong>原因</strong>：GPU driver 在 suspend/resume 過程中需要保存和恢復 GPU 狀態。NVIDIA 閉源驅動在 Wayland 上的 suspend/resume 支援不如 AMD 開源驅動穩定，特別是多螢幕配置和高刷新率模式下容易出問題。</p>
<p><strong>恢復步驟</strong>：</p>
<p>如果螢幕黑屏但系統有反應：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 切到 TTY</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">Ctrl+Alt+F2
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 檢查 Hyprland 是否還在跑</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">pgrep Hyprland
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 如果在跑但沒畫面，kill 再重啟</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">killall Hyprland
</span></span><span class="line"><span class="ln">9</span><span class="cl">Hyprland</span></span></code></pre></div><p>如果解析度或螢幕配置跑掉：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 在 Hyprland 內重新套用 monitor 設定</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">hyprctl reload</span></span></code></pre></div><p>如果 compositor 已經 crash：按場景一的步驟從 TTY 重啟。</p>
<p><strong>預防</strong>：</p>
<ul>
<li>NVIDIA 用戶：在 <code>/etc/modprobe.d/nvidia.conf</code> 啟用 preserve video memory allocations：</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/modprobe.d/nvidia.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">options nvidia <span class="nv">NVreg_PreserveVideoMemoryAllocations</span><span class="o">=</span><span class="m">1</span></span></span></code></pre></div><p>同時啟用 NVIDIA 的 suspend/resume systemd service：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo systemctl <span class="nb">enable</span> nvidia-suspend
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo systemctl <span class="nb">enable</span> nvidia-resume
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo systemctl <span class="nb">enable</span> nvidia-hibernate</span></span></code></pre></div><ul>
<li>AMD 用戶：amdgpu driver 的 suspend/resume 通常開箱即用，遇到問題先更新 kernel（<code>pacman -Syu linux</code>）。</li>
</ul>
]]></content:encoded></item><item><title>日誌判讀與診斷工具</title><link>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/</guid><description>&lt;p>恢復操作解決的是「怎麼讓桌面回來」，日誌判讀解決的是「為什麼會壞掉」。前者是急救，後者是找病因。如果同一個問題反覆出現，只做急救不找根因會一直繞圈。&lt;/p>
&lt;h2 id="journalctl系統日誌的主要入口">journalctl：系統日誌的主要入口&lt;/h2>
&lt;p>systemd 的日誌系統（journal）集中收錄所有 service、kernel、user session 的 log。&lt;code>journalctl&lt;/code> 是查詢這些日誌的指令。&lt;/p>
&lt;h3 id="基本用法">基本用法&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 本次開機的所有日誌&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">journalctl -b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 本次開機的錯誤以上等級（err + crit + alert + emerg）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">journalctl -b -p err
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 本次開機，只看最後 50 行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">journalctl -b -n &lt;span class="m">50&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 上一次開機的日誌（如果問題發生在上次開機、這次重開後想查）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">journalctl -b -1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># 即時跟蹤新 log（類似 tail -f）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">journalctl -f&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="過濾特定來源">過濾特定來源&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看 Hyprland 相關&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">journalctl -b &lt;span class="p">|&lt;/span> grep -i hypr
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看特定 systemd user unit&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">journalctl --user -u waybar -b
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看 kernel 訊息（等同 dmesg）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">journalctl -b -k
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看某個 process 的 log（用 PID）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">journalctl &lt;span class="nv">_PID&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">12345&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="時間範圍過濾">時間範圍過濾&lt;/h3>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 最近 10 分鐘的 log&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">journalctl --since &lt;span class="s2">&amp;#34;10 min ago&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 指定時間區間&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">journalctl --since &lt;span class="s2">&amp;#34;2026-06-30 14:00&amp;#34;&lt;/span> --until &lt;span class="s2">&amp;#34;2026-06-30 14:30&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="dmesgkernel-層訊息">dmesg：Kernel 層訊息&lt;/h2>
&lt;p>&lt;code>dmesg&lt;/code> 顯示 kernel ring buffer 的內容——硬體偵測、driver 載入、硬體錯誤這些 kernel 層面的事件。排查 GPU driver 問題、USB 裝置問題、磁碟錯誤時需要看這裡。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 所有 kernel 訊息（帶時間戳記）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">dmesg -T
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只看錯誤和警告&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">dmesg -T --level&lt;span class="o">=&lt;/span>err,warn
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># GPU 相關（NVIDIA）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">dmesg -T &lt;span class="p">|&lt;/span> grep -i nvidia
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># GPU 相關（AMD）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">dmesg -T &lt;span class="p">|&lt;/span> grep -i amdgpu
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># USB 相關（鍵盤滑鼠突然不回應時看這裡）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">dmesg -T &lt;span class="p">|&lt;/span> grep -i usb&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>GPU driver 問題在 dmesg 裡的嚴重度差異很大：&lt;/p>
&lt;p>一般 GPU hang（driver 嘗試自動恢復）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">[ 123.456] nvidia-modeset: ERROR: ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">[ 123.789] NVRM: Xid (PCI:0000:01:00): 79, pid=1234, ...
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">[ 124.000] amdgpu: GPU reset begin!
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">[ 124.500] amdgpu: GPU reset succeeded&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>NVIDIA 的 &lt;code>Xid&lt;/code> 錯誤代碼表示不同類型的 GPU 錯誤。常見的 &lt;code>Xid 79&lt;/code> 是 GPU fallback，&lt;code>Xid 31&lt;/code> 是 GPU setup failure。完整代碼表可在 NVIDIA 官方文件搜尋「Xid Errors」。&lt;/p></description><content:encoded><![CDATA[<p>恢復操作解決的是「怎麼讓桌面回來」，日誌判讀解決的是「為什麼會壞掉」。前者是急救，後者是找病因。如果同一個問題反覆出現，只做急救不找根因會一直繞圈。</p>
<h2 id="journalctl系統日誌的主要入口">journalctl：系統日誌的主要入口</h2>
<p>systemd 的日誌系統（journal）集中收錄所有 service、kernel、user session 的 log。<code>journalctl</code> 是查詢這些日誌的指令。</p>
<h3 id="基本用法">基本用法</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 本次開機的所有日誌</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">journalctl -b
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 本次開機的錯誤以上等級（err + crit + alert + emerg）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">journalctl -b -p err
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 本次開機，只看最後 50 行</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">journalctl -b -n <span class="m">50</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 上一次開機的日誌（如果問題發生在上次開機、這次重開後想查）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">journalctl -b -1
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 即時跟蹤新 log（類似 tail -f）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">journalctl -f</span></span></code></pre></div><h3 id="過濾特定來源">過濾特定來源</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 只看 Hyprland 相關</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">journalctl -b <span class="p">|</span> grep -i hypr
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 只看特定 systemd user unit</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">journalctl --user -u waybar -b
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 只看 kernel 訊息（等同 dmesg）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">journalctl -b -k
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 只看某個 process 的 log（用 PID）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">journalctl <span class="nv">_PID</span><span class="o">=</span><span class="m">12345</span></span></span></code></pre></div><h3 id="時間範圍過濾">時間範圍過濾</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 最近 10 分鐘的 log</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">journalctl --since <span class="s2">&#34;10 min ago&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 指定時間區間</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">journalctl --since <span class="s2">&#34;2026-06-30 14:00&#34;</span> --until <span class="s2">&#34;2026-06-30 14:30&#34;</span></span></span></code></pre></div><h2 id="dmesgkernel-層訊息">dmesg：Kernel 層訊息</h2>
<p><code>dmesg</code> 顯示 kernel ring buffer 的內容——硬體偵測、driver 載入、硬體錯誤這些 kernel 層面的事件。排查 GPU driver 問題、USB 裝置問題、磁碟錯誤時需要看這裡。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 所有 kernel 訊息（帶時間戳記）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">dmesg -T
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 只看錯誤和警告</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">dmesg -T --level<span class="o">=</span>err,warn
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># GPU 相關（NVIDIA）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">dmesg -T <span class="p">|</span> grep -i nvidia
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># GPU 相關（AMD）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">dmesg -T <span class="p">|</span> grep -i amdgpu
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># USB 相關（鍵盤滑鼠突然不回應時看這裡）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">dmesg -T <span class="p">|</span> grep -i usb</span></span></code></pre></div><p>GPU driver 問題在 dmesg 裡的嚴重度差異很大：</p>
<p>一般 GPU hang（driver 嘗試自動恢復）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[  123.456] nvidia-modeset: ERROR: ...
</span></span><span class="line"><span class="ln">2</span><span class="cl">[  123.789] NVRM: Xid (PCI:0000:01:00): 79, pid=1234, ...
</span></span><span class="line"><span class="ln">3</span><span class="cl">[  124.000] amdgpu: GPU reset begin!
</span></span><span class="line"><span class="ln">4</span><span class="cl">[  124.500] amdgpu: GPU reset succeeded</span></span></code></pre></div><p>NVIDIA 的 <code>Xid</code> 錯誤代碼表示不同類型的 GPU 錯誤。常見的 <code>Xid 79</code> 是 GPU fallback，<code>Xid 31</code> 是 GPU setup failure。完整代碼表可在 NVIDIA 官方文件搜尋「Xid Errors」。</p>
<p>硬體層級故障（嚴重，可能需要檢查硬體）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">[  123.789] NVRM: Xid (PCI:0000:01:00): 79, pid=1234, GPU has fallen off the bus</span></span></code></pre></div><p><code>GPU has fallen off the bus</code> 表示 GPU 跟主機板的 PCIe 連線完全中斷。偶發一次可能是 driver 問題，反覆出現通常是硬體故障（PCIe 供電不足、顯卡接觸不良、過熱）。</p>
<h2 id="hyprctlhyprland-的-runtime-狀態查詢">hyprctl：Hyprland 的 Runtime 狀態查詢</h2>
<p><code>hyprctl</code> 是 Hyprland 提供的命令列控制工具，可以在 compositor 運行中查詢狀態和執行操作。只有在 Hyprland 正在跑的時候才能使用。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 目前所有視窗的資訊</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">hyprctl clients
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 目前的 monitor 設定</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">hyprctl monitors
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 目前的 workspace 資訊</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">hyprctl workspaces
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># Hyprland 版本和 build 資訊</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">hyprctl version
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 重新載入 config（不重啟 compositor）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">hyprctl reload
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 查看上一次 config reload 是否有錯誤</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">hyprctl systeminfo</span></span></code></pre></div><p><code>hyprctl reload</code> 是測試 config 變更的安全方式。如果 config 有語法錯誤，reload 會報錯但 compositor 繼續用舊 config 跑，不會崩潰。</p>
<h2 id="systemctlservice-狀態管理">systemctl：Service 狀態管理</h2>
<p>桌面環境的工具（waybar、mako 等）如果用 systemd user unit 管理，可以用 <code>systemctl --user</code> 查看狀態和重啟。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 查看某個 user service 的狀態</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">systemctl --user status waybar
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 輸出範例：</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># waybar.service - Highly customizable Wayland bar</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1">#    Loaded: loaded (/usr/lib/systemd/user/waybar.service; enabled)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">#    Active: active (running) since Mon 2026-06-30 10:00:00 CST</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1">#    Main PID: 1234 (waybar)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 重啟</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">systemctl --user restart waybar
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 看最近的 log</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">systemctl --user status waybar -n <span class="m">20</span></span></span></code></pre></div><p>如果這些工具不是 systemd unit（在 Hyprland config 裡用 <code>exec-once</code> 啟動的），就不能用 <code>systemctl</code> 管理。改用 <code>pgrep</code> 和 <code>kill</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pgrep waybar      <span class="c1"># 查看是否在跑</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">killall waybar    <span class="c1"># 停止</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">waybar <span class="p">&amp;</span>          <span class="c1"># 背景啟動</span></span></span></code></pre></div><h2 id="即時資源監控">即時資源監控</h2>
<p>排查效能問題和記憶體耗盡時，需要看即時的系統資源使用情況。</p>
<p><strong>htop</strong>：互動式 process 監控。按 <code>M</code> 可以按記憶體用量排序，按 <code>P</code> 按 CPU 排序。找到佔用異常的 process 後按 <code>F9</code> 可以直接 kill。</p>
<p><strong>btop</strong>：功能更豐富的替代品，顯示 CPU、記憶體、磁碟、網路的即時使用情況，圖形化介面比 htop 直觀。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 安裝</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo pacman -S btop    <span class="c1"># Arch</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">sudo apt install btop  <span class="c1"># Debian/Ubuntu</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 執行</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">btop</span></span></code></pre></div><p><strong>nvidia-smi</strong>：NVIDIA GPU 的專屬監控工具。顯示 GPU 使用率、記憶體、溫度、跑在上面的 process。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 一次性查看</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">nvidia-smi
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 持續監控（每 2 秒更新）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">nvidia-smi -l <span class="m">2</span></span></span></code></pre></div><h2 id="常見-log-pattern-速查">常見 Log Pattern 速查</h2>
<table>
  <thead>
      <tr>
          <th>Pattern</th>
          <th>出處</th>
          <th>代表什麼</th>
          <th>下一步</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>Out of memory: Killed process</code></td>
          <td>journalctl / dmesg</td>
          <td>OOM Killer 殺了某個 process</td>
          <td>檢查被殺的 process 名稱、設定 swap 或 systemd-oomd</td>
      </tr>
      <tr>
          <td><code>GPU has fallen off the bus</code></td>
          <td>dmesg</td>
          <td>NVIDIA GPU 完全失聯</td>
          <td>檢查 PCIe 供電、更新 driver、檢查硬體</td>
      </tr>
      <tr>
          <td><code>Xid ... pid=</code></td>
          <td>dmesg</td>
          <td>NVIDIA GPU 錯誤（Xid 編號對應不同類型的錯誤）</td>
          <td>查 NVIDIA 的 Xid 錯誤代碼表</td>
      </tr>
      <tr>
          <td><code>GPU reset begin</code></td>
          <td>dmesg</td>
          <td>AMD GPU driver 嘗試 reset GPU</td>
          <td>通常會自動恢復，頻繁出現代表 driver 或硬體問題</td>
      </tr>
      <tr>
          <td><code>segfault at</code></td>
          <td>journalctl</td>
          <td>某個 process segfault（記憶體存取違規）</td>
          <td>記下 process 名稱，搜尋該軟體的已知 bug</td>
      </tr>
      <tr>
          <td><code>Failed to start</code></td>
          <td>systemctl status</td>
          <td>systemd unit 啟動失敗</td>
          <td>看完整的 status 輸出和 journalctl log 找原因</td>
      </tr>
      <tr>
          <td><code>config error</code> / <code>parse error</code></td>
          <td>各工具自身的 log</td>
          <td>Config 檔語法錯誤</td>
          <td>檢查最近改過的 config 檔</td>
      </tr>
  </tbody>
</table>
<h2 id="排查流程">排查流程</h2>
<p>這篇是 Hyprland 桌面的具體日誌工具；背後「先讀權威狀態、不靠肉眼猜」的通用診斷心法（每種問題的權威來源、四步流程），見 <a href="/blog/linux/debug/diagnosis-read-authoritative-state/" data-link-title="診斷心法：讀權威狀態，不靠肉眼猜表象" data-link-desc="Linux 上一個現象看起來像 A 卻可能是 B、想建立一套先讀權威狀態再下判斷的除錯紀律、避免看畫面就猜而猜錯時回來讀">Linux 除錯與診斷：診斷心法</a>。遇到桌面環境問題時的判讀順序：</p>
<ol>
<li>
<p><strong>判斷影響範圍</strong>：只有一個視窗壞了、某個工具壞了、整個桌面壞了、還是系統完全不回應？影響範圍決定要看哪一層的 log。</p>
</li>
<li>
<p><strong>看 journalctl</strong>：<code>journalctl -b -p err</code> 先看本次開機有沒有錯誤等級的訊息。大部分 userspace 的問題（compositor crash、工具 crash）會出現在這裡。</p>
</li>
<li>
<p><strong>看 dmesg</strong>：如果 journalctl 沒有明顯線索、或症狀跟硬體有關（畫面凍結、USB 不回應），<code>dmesg -T --level=err,warn</code> 看 kernel 層有沒有硬體或 driver 錯誤。</p>
</li>
<li>
<p><strong>查特定工具的狀態</strong>：<code>systemctl --user status &lt;tool&gt;</code> 或 <code>pgrep &lt;tool&gt;</code> 確認工具是否還活著。如果死了，看它最後的 log 訊息。</p>
</li>
<li>
<p><strong>即時監控</strong>：如果問題是漸進式的（越來越慢、偶爾卡頓），開 <code>btop</code> 或 <code>htop</code> 觀察 CPU 和記憶體的即時趨勢，找出佔用異常的 process。</p>
</li>
</ol>
<h2 id="找到問題後的下一步">找到問題後的下一步</h2>
<p>判讀完 log 確認問題類型後，行動路徑依問題性質分流：</p>
<ul>
<li><strong>Config 錯誤</strong>：直接修 config，用 <code>hyprctl reload</code> 或重啟工具驗證。操作步驟見<a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a>。</li>
<li><strong>軟體 bug</strong>（segfault、特定操作觸發 crash）：到該軟體的 issue tracker（通常在 GitHub）搜尋錯誤訊息。Hyprland 的 issue tracker 在 <code>github.com/hyprwm/Hyprland</code>。回報 bug 時附上 <code>hyprctl systeminfo</code> 的輸出和相關的 journalctl log。</li>
<li><strong>GPU driver 問題</strong>：NVIDIA 用戶檢查是否有更新的 driver 版本（<code>pacman -Syu nvidia</code>）。AMD 用戶的 driver 跟 kernel 綁定，更新 kernel 就更新 driver（<code>pacman -Syu linux</code>）。</li>
<li><strong>硬體故障</strong>（<code>GPU has fallen off the bus</code> 反覆出現）：軟體層面無法解決，需要檢查硬體（PCIe 插槽接觸、供電、溫度）。</li>
</ul>
]]></content:encoded></item><item><title>安裝期套件與網路故障排除：pacman / DNS / mirror / keyring</title><link>https://tarrragon.github.io/blog/linux/install/package-and-network-troubleshooting/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/package-and-network-troubleshooting/</guid><description>&lt;p>裝好 OS、第一次跑套件管理器抓 bootstrap 要的東西時，最常撞的一類故障是「套件裝不下來」。這類故障的第一步判讀，是把它拆成兩層完全不同的問題：&lt;strong>連不到（網路 / DNS / mirror）&lt;/strong>，還是&lt;strong>連得到但被拒（套件管理器自己的狀態）&lt;/strong>。這兩層的檢查工具、根因、修法都不一樣，先分對層再往下查，才不會拿修 DNS 的方法去治簽章過期。這篇以 Arch 的 &lt;code>pacman&lt;/code> 為主要案例（本系列 VM 實測踩過的坑），其他發行版的套件管理器概念對應相同。&lt;/p>
&lt;h2 id="第一步分連不到還是連得到但被拒">第一步：分「連不到」還是「連得到但被拒」&lt;/h2>
&lt;p>錯誤訊息本身就能分層，不用猜：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>訊息提到主機名解不出、連線逾時、retrieving file 失敗&lt;/strong> → 連不到，往網路 / DNS / mirror 查。&lt;/li>
&lt;li>&lt;strong>訊息提到 database lock、signature、trust、conflicting、partial&lt;/strong> → 連得到、封包也拿到了，是套件管理器的狀態問題。&lt;/li>
&lt;/ul>
&lt;p>判準是問一句：「它到底有沒有成功連上 mirror？」有連上才談得到簽章、相依、db 狀態；連都沒連上，那些都還輪不到。剛裝好的最小系統最常見的是前者——網路設定還沒到位。&lt;/p>
&lt;h2 id="連不到那層從實體介面往上查到域名">連不到那層：從實體介面往上查到域名&lt;/h2>
&lt;p>網路不通有好幾層，從最底層往上逐層確認，哪一層斷了一目了然。這條鏈跟&lt;a href="../minimal-install-verify/">最小安裝後的驗證&lt;/a>裡的網路檢查同源，這裡聚焦在「抓套件失敗」這個症狀上：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ip -brief a &lt;span class="c1"># 1. 有沒有拿到 IP？介面 UP 且有位址&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">ping -c1 8.8.8.8 &lt;span class="c1"># 2. IP 層對外通不通？（直接打 IP、跳過 DNS）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">getent hosts archlinux.org &lt;span class="c1"># 3. 域名解得出來嗎？&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">timedatectl &lt;span class="c1"># 4. 時間對嗎？（影響下一層的簽章驗證）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>第 2 步通、第 3 步不通 = DNS 問題&lt;/strong>，這是最小安裝最典型的落點：IP 層明明通（&lt;code>ping 8.8.8.8&lt;/code> 有回應），但域名解不出來，因為 &lt;code>/etc/resolv.conf&lt;/code> 還沒設 nameserver。這時 pacman 會卡在解析 mirror 主機名。修法是給系統一個 resolver——臨時可直接寫 &lt;code>/etc/resolv.conf&lt;/code>（&lt;code>nameserver 1.1.1.1&lt;/code>）。先看它是什麼（&lt;code>ls -l /etc/resolv.conf&lt;/code>）：啟用了 &lt;code>systemd-resolved&lt;/code> 或 NetworkManager 的系統上它是那些服務管理的 symlink，手寫會被覆蓋，治本要透過該網路管理服務設定 DNS；裸 Arch 最小安裝若沒啟用這些服務，它通常就是一個普通檔案，手寫即持久生效。&lt;/p>
&lt;p>&lt;strong>mirror 逾時 / 抓不到&lt;/strong>：DNS 通了、但某個 mirror 慢或掛了。換 &lt;code>/etc/pacman.d/mirrorlist&lt;/code> 到地理近且快的鏡像（實測不同 mirror 速度可差數倍）。這也接回&lt;a href="../install-option-decisions/">安裝選項判讀&lt;/a>裡選 mirror 的決策——裝機當下選錯 mirror，這裡就會慢。&lt;/p>
&lt;h2 id="連得到但被拒那層pacman-自己的狀態">連得到但被拒那層：pacman 自己的狀態&lt;/h2>
&lt;p>連上 mirror、封包也拿到了卻失敗，問題在 pacman 的本地狀態或簽章驗證。這幾種各有明確徵兆與修法：&lt;/p>
&lt;h3 id="database-lock上次沒清乾淨的殘留">database lock：上次沒清乾淨的殘留&lt;/h3>
&lt;p>&lt;code>error: failed to init transaction (unable to lock database)&lt;/code>。pacman 用 &lt;code>/var/lib/pacman/db.lck&lt;/code> 這個鎖檔保證同時只有一個 pacman 在動資料庫；上次 pacman 被中斷（斷電、Ctrl+C、當掉）沒清掉鎖檔就會殘留。&lt;strong>先確認真的沒有 pacman 在跑&lt;/strong>（&lt;code>pgrep -x pacman&lt;/code>），確認沒有再刪鎖檔：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">pgrep -x pacman &lt;span class="o">&amp;amp;&amp;amp;&lt;/span> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;有 pacman 在跑、別刪&amp;#34;&lt;/span> &lt;span class="o">||&lt;/span> sudo rm /var/lib/pacman/db.lck&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>先查再刪這個順序重要——盲刪鎖檔時如果真的有另一個 pacman 在跑，兩個同時寫資料庫會弄壞它。&lt;/p>
&lt;h3 id="簽章--keyring-過期十之八九是時間不對">簽章 / keyring 過期：十之八九是時間不對&lt;/h3>
&lt;p>&lt;code>invalid or corrupted package (PGP signature)&lt;/code> 或 &lt;code>signature is unknown trust&lt;/code>。pacman 驗證每個套件的 GPG 簽章，驗證失敗最常見的根因是&lt;strong>系統時間不對&lt;/strong>——這正是第一步要 &lt;code>timedatectl&lt;/code> 的原因。時間差太多（新裝的 VM、主機板電池沒電的老機器）會讓「簽章的有效期」判斷錯誤，明明有效的簽章被判過期。先校時：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">sudo timedatectl set-ntp &lt;span class="nb">true&lt;/span> &lt;span class="c1"># 開 NTP 自動校時（SSH 進最小系統無 polkit 互動代理、裸跑會被拒，要 sudo）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>時間對了還失敗，才是 keyring 本身的問題（archlinux-keyring 太舊）：&lt;code>sudo pacman -Sy archlinux-keyring&lt;/code> 更新 keyring，必要時 &lt;code>sudo pacman-key --refresh-keys&lt;/code>。順序是先校時再動 keyring，因為時間不對時連 keyring 都更新不了。&lt;/p>
&lt;h3 id="partial-upgrade只同步不升級造成的相依斷裂">partial upgrade：只同步不升級造成的相依斷裂&lt;/h3>
&lt;p>&lt;code>conflicting dependencies&lt;/code> 或裝完某個套件後系統行為異常。根因是在 rolling 發行版上只做了 &lt;code>pacman -Sy&lt;/code>（同步資料庫）就裝新套件，卻沒 &lt;code>-u&lt;/code>（升級既有套件）——新套件依賴新版函式庫，但系統還是舊的，相依對不上。Arch 只支援 full upgrade：&lt;strong>一律 &lt;code>pacman -Syu&lt;/code>，永遠不要單獨 &lt;code>-Sy&lt;/code> 之後裝東西&lt;/strong>。這條規則救掉這一整類故障。&lt;/p></description><content:encoded><![CDATA[<p>裝好 OS、第一次跑套件管理器抓 bootstrap 要的東西時，最常撞的一類故障是「套件裝不下來」。這類故障的第一步判讀，是把它拆成兩層完全不同的問題：<strong>連不到（網路 / DNS / mirror）</strong>，還是<strong>連得到但被拒（套件管理器自己的狀態）</strong>。這兩層的檢查工具、根因、修法都不一樣，先分對層再往下查，才不會拿修 DNS 的方法去治簽章過期。這篇以 Arch 的 <code>pacman</code> 為主要案例（本系列 VM 實測踩過的坑），其他發行版的套件管理器概念對應相同。</p>
<h2 id="第一步分連不到還是連得到但被拒">第一步：分「連不到」還是「連得到但被拒」</h2>
<p>錯誤訊息本身就能分層，不用猜：</p>
<ul>
<li><strong>訊息提到主機名解不出、連線逾時、retrieving file 失敗</strong> → 連不到，往網路 / DNS / mirror 查。</li>
<li><strong>訊息提到 database lock、signature、trust、conflicting、partial</strong> → 連得到、封包也拿到了，是套件管理器的狀態問題。</li>
</ul>
<p>判準是問一句：「它到底有沒有成功連上 mirror？」有連上才談得到簽章、相依、db 狀態；連都沒連上，那些都還輪不到。剛裝好的最小系統最常見的是前者——網路設定還沒到位。</p>
<h2 id="連不到那層從實體介面往上查到域名">連不到那層：從實體介面往上查到域名</h2>
<p>網路不通有好幾層，從最底層往上逐層確認，哪一層斷了一目了然。這條鏈跟<a href="../minimal-install-verify/">最小安裝後的驗證</a>裡的網路檢查同源，這裡聚焦在「抓套件失敗」這個症狀上：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ip -brief a              <span class="c1"># 1. 有沒有拿到 IP？介面 UP 且有位址</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">ping -c1 8.8.8.8         <span class="c1"># 2. IP 層對外通不通？（直接打 IP、跳過 DNS）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">getent hosts archlinux.org   <span class="c1"># 3. 域名解得出來嗎？</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">timedatectl              <span class="c1"># 4. 時間對嗎？（影響下一層的簽章驗證）</span></span></span></code></pre></div><p><strong>第 2 步通、第 3 步不通 = DNS 問題</strong>，這是最小安裝最典型的落點：IP 層明明通（<code>ping 8.8.8.8</code> 有回應），但域名解不出來，因為 <code>/etc/resolv.conf</code> 還沒設 nameserver。這時 pacman 會卡在解析 mirror 主機名。修法是給系統一個 resolver——臨時可直接寫 <code>/etc/resolv.conf</code>（<code>nameserver 1.1.1.1</code>）。先看它是什麼（<code>ls -l /etc/resolv.conf</code>）：啟用了 <code>systemd-resolved</code> 或 NetworkManager 的系統上它是那些服務管理的 symlink，手寫會被覆蓋，治本要透過該網路管理服務設定 DNS；裸 Arch 最小安裝若沒啟用這些服務，它通常就是一個普通檔案，手寫即持久生效。</p>
<p><strong>mirror 逾時 / 抓不到</strong>：DNS 通了、但某個 mirror 慢或掛了。換 <code>/etc/pacman.d/mirrorlist</code> 到地理近且快的鏡像（實測不同 mirror 速度可差數倍）。這也接回<a href="../install-option-decisions/">安裝選項判讀</a>裡選 mirror 的決策——裝機當下選錯 mirror，這裡就會慢。</p>
<h2 id="連得到但被拒那層pacman-自己的狀態">連得到但被拒那層：pacman 自己的狀態</h2>
<p>連上 mirror、封包也拿到了卻失敗，問題在 pacman 的本地狀態或簽章驗證。這幾種各有明確徵兆與修法：</p>
<h3 id="database-lock上次沒清乾淨的殘留">database lock：上次沒清乾淨的殘留</h3>
<p><code>error: failed to init transaction (unable to lock database)</code>。pacman 用 <code>/var/lib/pacman/db.lck</code> 這個鎖檔保證同時只有一個 pacman 在動資料庫；上次 pacman 被中斷（斷電、Ctrl+C、當掉）沒清掉鎖檔就會殘留。<strong>先確認真的沒有 pacman 在跑</strong>（<code>pgrep -x pacman</code>），確認沒有再刪鎖檔：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">pgrep -x pacman <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;有 pacman 在跑、別刪&#34;</span> <span class="o">||</span> sudo rm /var/lib/pacman/db.lck</span></span></code></pre></div><p>先查再刪這個順序重要——盲刪鎖檔時如果真的有另一個 pacman 在跑，兩個同時寫資料庫會弄壞它。</p>
<h3 id="簽章--keyring-過期十之八九是時間不對">簽章 / keyring 過期：十之八九是時間不對</h3>
<p><code>invalid or corrupted package (PGP signature)</code> 或 <code>signature is unknown trust</code>。pacman 驗證每個套件的 GPG 簽章，驗證失敗最常見的根因是<strong>系統時間不對</strong>——這正是第一步要 <code>timedatectl</code> 的原因。時間差太多（新裝的 VM、主機板電池沒電的老機器）會讓「簽章的有效期」判斷錯誤，明明有效的簽章被判過期。先校時：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">sudo timedatectl set-ntp <span class="nb">true</span>     <span class="c1"># 開 NTP 自動校時（SSH 進最小系統無 polkit 互動代理、裸跑會被拒，要 sudo）</span></span></span></code></pre></div><p>時間對了還失敗，才是 keyring 本身的問題（archlinux-keyring 太舊）：<code>sudo pacman -Sy archlinux-keyring</code> 更新 keyring，必要時 <code>sudo pacman-key --refresh-keys</code>。順序是先校時再動 keyring，因為時間不對時連 keyring 都更新不了。</p>
<h3 id="partial-upgrade只同步不升級造成的相依斷裂">partial upgrade：只同步不升級造成的相依斷裂</h3>
<p><code>conflicting dependencies</code> 或裝完某個套件後系統行為異常。根因是在 rolling 發行版上只做了 <code>pacman -Sy</code>（同步資料庫）就裝新套件，卻沒 <code>-u</code>（升級既有套件）——新套件依賴新版函式庫，但系統還是舊的，相依對不上。Arch 只支援 full upgrade：<strong>一律 <code>pacman -Syu</code>，永遠不要單獨 <code>-Sy</code> 之後裝東西</strong>。這條規則救掉這一整類故障。</p>
<h3 id="stale-db-404裝機當下的資料庫已經過期">stale db 404：裝機當下的資料庫已經過期</h3>
<p><code>error: failed retrieving file '...' 404</code>，而且換好幾個 mirror 都一樣。這是 rolling 發行版特有的時序陷阱：Arch 的 mirror 不保留舊版檔案，你裝機時 ISO 內建的套件資料庫指向的檔名，可能幾天內就被輪替掉了——資料庫說有這個檔、mirror 上已經沒有。修法跟上一條同源：<code>pacman -Syu</code> 先把資料庫同步到最新，檔名對上了就抓得到。這也是為什麼「一律 <code>-Syu</code>」是 Arch 的鐵律，而不只是建議。</p>
<h2 id="判讀總表">判讀總表</h2>
<table>
  <thead>
      <tr>
          <th>症狀</th>
          <th>層</th>
          <th>權威檢查</th>
          <th>修法</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主機名解不出</td>
          <td>網路</td>
          <td><code>getent hosts &lt;域名&gt;</code></td>
          <td>設 resolver（注意 symlink）</td>
      </tr>
      <tr>
          <td>ping IP 通、域名不通</td>
          <td>DNS</td>
          <td><code>ping 8.8.8.8</code> vs <code>getent</code></td>
          <td>設 <code>/etc/resolv.conf</code> 或網管服務</td>
      </tr>
      <tr>
          <td>mirror 慢 / 逾時</td>
          <td>網路</td>
          <td>換 mirror 測速</td>
          <td>改 mirrorlist</td>
      </tr>
      <tr>
          <td>unable to lock database</td>
          <td>pacman</td>
          <td><code>pgrep -x pacman</code></td>
          <td>確認無後刪 db.lck</td>
      </tr>
      <tr>
          <td>PGP signature / unknown trust</td>
          <td>pacman</td>
          <td><code>timedatectl</code>（先校時）</td>
          <td>校時 →（仍失敗）更新 keyring</td>
      </tr>
      <tr>
          <td>conflicting / partial</td>
          <td>pacman</td>
          <td>是否只跑了 <code>-Sy</code></td>
          <td><code>pacman -Syu</code>（永遠 full）</td>
      </tr>
      <tr>
          <td>retrieving file 404（多 mirror）</td>
          <td>pacman</td>
          <td>rolling stale db</td>
          <td><code>pacman -Syu</code> 同步再裝</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步">下一步</h2>
<ul>
<li>這幾步用到的網路驗證，完整版在<a href="../minimal-install-verify/">最小安裝後的工具驗證與補足</a>。</li>
<li>裝機時選 mirror / locale / 時區的決策，見<a href="../install-option-decisions/">Linux 安裝選項判讀</a>。</li>
<li>跨發行版時「這個套件名 / 這個旗標在別的發行版叫什麼」的差異判讀，見<a href="../platform-divergence-map/">平台與發行版差異的判讀地圖</a>。</li>
<li>套件抓下來了、但 bootstrap 腳本本身失敗要 debug，見<a href="../observable-bootstrap/">可除錯的 bootstrap</a>。</li>
<li>系統跑起來後才出的套件問題（AUR 建置失敗、<code>-bin</code> 包 soname 斷裂等），屬除錯範疇，見<a href="../../debug/">Linux 除錯與診斷</a>。</li>
</ul>
]]></content:encoded></item><item><title>模組七：桌面環境維護與故障排除</title><link>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/</link><pubDate>Tue, 30 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/</guid><description>&lt;p>模組零到六教的是怎麼建立桌面環境，這個模組教的是壞了怎麼修。&lt;/p>
&lt;p>Linux 桌面環境跟 Windows 在故障模型上有根本的結構差異。Windows 的藍屏（BSOD）是核心層崩潰，整台機器停擺；Linux 桌面環境的大部分故障只影響 userspace，系統核心不受波及。理解這個隔離邊界，是判斷「當下該做什麼」的前提。&lt;/p>
&lt;p>這個模組的閱讀方式跟其他模組不同。其他模組是線性學習——從頭讀到尾建立知識。這個模組是 reference——出問題時根據症狀查對應的恢復操作。第一篇建立概念模型，第二篇按場景查操作，第三篇教怎麼看日誌找根因。&lt;/p>
&lt;h2 id="章節文章">章節文章&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>文章&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/fault-isolation-model/" data-link-title="Linux 桌面的故障隔離模型" data-link-desc="從 Windows 轉過來想知道 Linux 桌面掛了會不會整台崩潰時讀 — kernel vs userspace 隔離、compositor 是 userspace process、TTY 救生通道與其限制">Linux 桌面的故障隔離模型&lt;/a>&lt;/td>
 &lt;td>kernel vs userspace 隔離、compositor 掛了不等於系統崩潰、TTY&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作&lt;/a>&lt;/td>
 &lt;td>compositor crash、工具掛了、GPU hang、OOM、config 錯誤、suspend/resume 異常的處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">日誌判讀與診斷工具&lt;/a>&lt;/td>
 &lt;td>journalctl、dmesg、hyprctl、systemctl 的使用與常見 pattern&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>-&amp;gt; &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置&lt;/a>：Hyprland 的配置結構和 hyprctl 指令&lt;/li>
&lt;li>-&amp;gt; &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計&lt;/a>：waybar / wofi / mako 等工具的配置位置&lt;/li>
&lt;li>-&amp;gt; &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建&lt;/a>：環境損壞到無法修復時的重建策略&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>模組零到六教的是怎麼建立桌面環境，這個模組教的是壞了怎麼修。</p>
<p>Linux 桌面環境跟 Windows 在故障模型上有根本的結構差異。Windows 的藍屏（BSOD）是核心層崩潰，整台機器停擺；Linux 桌面環境的大部分故障只影響 userspace，系統核心不受波及。理解這個隔離邊界，是判斷「當下該做什麼」的前提。</p>
<p>這個模組的閱讀方式跟其他模組不同。其他模組是線性學習——從頭讀到尾建立知識。這個模組是 reference——出問題時根據症狀查對應的恢復操作。第一篇建立概念模型，第二篇按場景查操作，第三篇教怎麼看日誌找根因。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/linux/dotfile/07-desktop-maintenance/fault-isolation-model/" data-link-title="Linux 桌面的故障隔離模型" data-link-desc="從 Windows 轉過來想知道 Linux 桌面掛了會不會整台崩潰時讀 — kernel vs userspace 隔離、compositor 是 userspace process、TTY 救生通道與其限制">Linux 桌面的故障隔離模型</a></td>
          <td>kernel vs userspace 隔離、compositor 掛了不等於系統崩潰、TTY</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a></td>
          <td>compositor crash、工具掛了、GPU hang、OOM、config 錯誤、suspend/resume 異常的處理</td>
      </tr>
      <tr>
          <td><a href="/blog/linux/dotfile/07-desktop-maintenance/log-reading-diagnostic-tools/" data-link-title="日誌判讀與診斷工具" data-link-desc="知道桌面出了問題但不確定原因時回來讀 — journalctl、dmesg、hyprctl、systemctl 的使用方式和常見 log pattern">日誌判讀與診斷工具</a></td>
          <td>journalctl、dmesg、hyprctl、systemctl 的使用與常見 pattern</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>-&gt; <a href="/blog/linux/dotfile/05-hyprland-config/" data-link-title="模組五：Hyprland 配置" data-link-desc="要在 Linux 上設定 Hyprland 平鋪式桌面時回來讀">模組五：Hyprland 配置</a>：Hyprland 的配置結構和 hyprctl 指令</li>
<li>-&gt; <a href="/blog/linux/dotfile/06-rice-design/" data-link-title="模組六：桌面 Rice 設計" data-link-desc="Hyprland 桌面從能用到好看好用 — 狀態列、啟動器、通知、鎖屏、配色系統的設計與配置">模組六：桌面 Rice 設計</a>：waybar / wofi / mako 等工具的配置位置</li>
<li>-&gt; <a href="/blog/linux/dotfile/08-sync-bootstrap/" data-link-title="模組八：同步、Bootstrap 與環境重建" data-link-desc="換機器或重灌時怎麼還原工作環境 — bootstrap script 設計、套件清單管理、跨機器同步策略、secret 排除，以及 VM 快照和 dotfile 重建兩種思路的場景判讀">模組八：同步、Bootstrap 與環境重建</a>：環境損壞到無法修復時的重建策略</li>
</ul>
]]></content:encoded></item><item><title>1.7 排錯方法論：用三層架構做故障定位</title><link>https://tarrragon.github.io/blog/llm/01-local-llm-services/troubleshooting/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/01-local-llm-services/troubleshooting/</guid><description>&lt;p>本地 LLM 工作流出問題時、第一個本能反應常是「重啟試試看」。本章建立另一種反射：用&lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/three-layer-architecture/" data-link-title="0.2 介面 / 伺服器 / 模型三層架構" data-link-desc="把任何本地 LLM 工具放回正確的層級，用三層心智模型看懂工具關係">三層架構&lt;/a>（介面 / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器&lt;/a> / 模型）的視角先確認「哪一層壞」、再針對該層做具體診斷。這個方法不依賴記住每個工具的具體錯誤訊息、跨工具世代都成立。&lt;/p>
&lt;p>具體錯誤訊息對照表（「&lt;code>address already in use&lt;/code> 要這樣修」「&lt;code>model not found&lt;/code> 要那樣修」）不在本章——這些隨工具版本變、查 release notes 跟 GitHub issue 更快。本章寫的是「換工具之後仍成立」的排錯思維。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>看到症狀時、先定位是介面 / 伺服器 / 模型哪一層的問題。&lt;/li>
&lt;li>知道在每一層該看什麼 log。&lt;/li>
&lt;li>用「最小可重現」策略快速縮減問題範圍。&lt;/li>
&lt;li>識別「跨層級的誤判」常見模式、把 server 層問題正確歸位、避開瞎調 model 的繞路。&lt;/li>
&lt;/ol>
&lt;h2 id="故障定位的核心原則先確認哪一層壞">故障定位的核心原則：先確認哪一層壞&lt;/h2>
&lt;p>模組零 &lt;a href="https://tarrragon.github.io/blog/llm/00-foundations/three-layer-architecture/" data-link-title="0.2 介面 / 伺服器 / 模型三層架構" data-link-desc="把任何本地 LLM 工具放回正確的層級，用三層心智模型看懂工具關係">三層架構&lt;/a> 的視角延伸到排錯：故障可能落在介面層（Continue.dev / Cursor 等 IDE 整合）、伺服器層（Ollama / LM Studio / llama.cpp）、或模型層（權重檔本身的能力 / 量化選擇）。在不知道哪一層壞之前、任何修法都是亂槍打鳥——重啟 Continue.dev 解不了模型量化太激進的問題、重 pull 模型解不了 IDE 設定錯的問題。&lt;/p>
&lt;p>先定位再修補的 ROI 高於直接修補、因為沒有定位的修法常常掃過正確答案還不知道是哪個動作生效。定位用的工具不複雜：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>直接 curl 伺服器 API&lt;/strong>：繞過介面層、直接驗證伺服器是否回應正常。&lt;/li>
&lt;li>&lt;strong>&lt;code>ollama ps&lt;/code> / 等價指令&lt;/strong>：看伺服器層 model 狀態、確認 model 真的載入。&lt;/li>
&lt;li>&lt;strong>換 model 試試&lt;/strong>：同樣 prompt、不同 model 表現一致就是介面 / 伺服器層、不一致就是 model 層。&lt;/li>
&lt;li>&lt;strong>換 prompt 試試&lt;/strong>：簡單 prompt OK、複雜 prompt 崩、可能是 context 長度或 model 容量問題。&lt;/li>
&lt;/ul>
&lt;p>這四個動作能 cover 90% 的定位需求。學會這個反射、排錯時間大幅縮短。&lt;/p>
&lt;h2 id="症狀到層級的對應反射">症狀到層級的對應反射&lt;/h2>
&lt;p>不同症狀對應到不同最有可能的故障層、建立對應反射能省下大量試錯時間。下表是寫 code 場景常見症狀的對應：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>症狀&lt;/th>
 &lt;th>最可能層級&lt;/th>
 &lt;th>第一步驗證&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Continue.dev 完全沒回應&lt;/td>
 &lt;td>介面層 / 伺服器層&lt;/td>
 &lt;td>curl 伺服器、看伺服器是否正常&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Continue.dev 報「connection refused」&lt;/td>
 &lt;td>伺服器層&lt;/td>
 &lt;td>伺服器沒在跑 / port 不對&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Continue.dev 顯示請求送出但無回應&lt;/td>
 &lt;td>介面層 / 伺服器層&lt;/td>
 &lt;td>curl 同 prompt、比較行為&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回答內容亂碼 / 一直重複&lt;/td>
 &lt;td>模型層&lt;/td>
 &lt;td>換&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化&lt;/a>等級或換模型試&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>回答邏輯離譜 / 答非所問&lt;/td>
 &lt;td>模型層&lt;/td>
 &lt;td>model 能力不足、考慮換大一點 model&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TTFT 異常變長&lt;/td>
 &lt;td>模型層 / 推論機制&lt;/td>
 &lt;td>prompt 變長了？KV cache 失效？&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>整台 Mac 變慢、Ollama 沒崩&lt;/td>
 &lt;td>伺服器層 / 系統&lt;/td>
 &lt;td>記憶體 swap、看 Activity Monitor&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Ollama 自己 crash&lt;/td>
 &lt;td>伺服器層&lt;/td>
 &lt;td>看 server log、通常 OOM 或 bug&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 session 設定遺失&lt;/td>
 &lt;td>介面層&lt;/td>
 &lt;td>IDE 設定沒存或被 reset&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Tab autocomplete 完全不觸發&lt;/td>
 &lt;td>介面層&lt;/td>
 &lt;td>autocomplete model 沒配對 / 沒 pull&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>對應的具體驗證指令範例：&lt;/p></description><content:encoded><![CDATA[<p>本地 LLM 工作流出問題時、第一個本能反應常是「重啟試試看」。本章建立另一種反射：用<a href="/blog/llm/00-foundations/three-layer-architecture/" data-link-title="0.2 介面 / 伺服器 / 模型三層架構" data-link-desc="把任何本地 LLM 工具放回正確的層級，用三層心智模型看懂工具關係">三層架構</a>（介面 / <a href="/blog/llm/knowledge-cards/inference-server/" data-link-title="Inference Server" data-link-desc="載入模型權重、處理 prompt、產生 token 的常駐 process">推論伺服器</a> / 模型）的視角先確認「哪一層壞」、再針對該層做具體診斷。這個方法不依賴記住每個工具的具體錯誤訊息、跨工具世代都成立。</p>
<p>具體錯誤訊息對照表（「<code>address already in use</code> 要這樣修」「<code>model not found</code> 要那樣修」）不在本章——這些隨工具版本變、查 release notes 跟 GitHub issue 更快。本章寫的是「換工具之後仍成立」的排錯思維。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>看到症狀時、先定位是介面 / 伺服器 / 模型哪一層的問題。</li>
<li>知道在每一層該看什麼 log。</li>
<li>用「最小可重現」策略快速縮減問題範圍。</li>
<li>識別「跨層級的誤判」常見模式、把 server 層問題正確歸位、避開瞎調 model 的繞路。</li>
</ol>
<h2 id="故障定位的核心原則先確認哪一層壞">故障定位的核心原則：先確認哪一層壞</h2>
<p>模組零 <a href="/blog/llm/00-foundations/three-layer-architecture/" data-link-title="0.2 介面 / 伺服器 / 模型三層架構" data-link-desc="把任何本地 LLM 工具放回正確的層級，用三層心智模型看懂工具關係">三層架構</a> 的視角延伸到排錯：故障可能落在介面層（Continue.dev / Cursor 等 IDE 整合）、伺服器層（Ollama / LM Studio / llama.cpp）、或模型層（權重檔本身的能力 / 量化選擇）。在不知道哪一層壞之前、任何修法都是亂槍打鳥——重啟 Continue.dev 解不了模型量化太激進的問題、重 pull 模型解不了 IDE 設定錯的問題。</p>
<p>先定位再修補的 ROI 高於直接修補、因為沒有定位的修法常常掃過正確答案還不知道是哪個動作生效。定位用的工具不複雜：</p>
<ul>
<li><strong>直接 curl 伺服器 API</strong>：繞過介面層、直接驗證伺服器是否回應正常。</li>
<li><strong><code>ollama ps</code> / 等價指令</strong>：看伺服器層 model 狀態、確認 model 真的載入。</li>
<li><strong>換 model 試試</strong>：同樣 prompt、不同 model 表現一致就是介面 / 伺服器層、不一致就是 model 層。</li>
<li><strong>換 prompt 試試</strong>：簡單 prompt OK、複雜 prompt 崩、可能是 context 長度或 model 容量問題。</li>
</ul>
<p>這四個動作能 cover 90% 的定位需求。學會這個反射、排錯時間大幅縮短。</p>
<h2 id="症狀到層級的對應反射">症狀到層級的對應反射</h2>
<p>不同症狀對應到不同最有可能的故障層、建立對應反射能省下大量試錯時間。下表是寫 code 場景常見症狀的對應：</p>
<table>
  <thead>
      <tr>
          <th>症狀</th>
          <th>最可能層級</th>
          <th>第一步驗證</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Continue.dev 完全沒回應</td>
          <td>介面層 / 伺服器層</td>
          <td>curl 伺服器、看伺服器是否正常</td>
      </tr>
      <tr>
          <td>Continue.dev 報「connection refused」</td>
          <td>伺服器層</td>
          <td>伺服器沒在跑 / port 不對</td>
      </tr>
      <tr>
          <td>Continue.dev 顯示請求送出但無回應</td>
          <td>介面層 / 伺服器層</td>
          <td>curl 同 prompt、比較行為</td>
      </tr>
      <tr>
          <td>回答內容亂碼 / 一直重複</td>
          <td>模型層</td>
          <td>換<a href="/blog/llm/knowledge-cards/quantization/" data-link-title="Quantization" data-link-desc="用較少 bits 表示模型權重：壓縮記憶體佔用、加快生字速度，代價是少量品質衰減">量化</a>等級或換模型試</td>
      </tr>
      <tr>
          <td>回答邏輯離譜 / 答非所問</td>
          <td>模型層</td>
          <td>model 能力不足、考慮換大一點 model</td>
      </tr>
      <tr>
          <td>TTFT 異常變長</td>
          <td>模型層 / 推論機制</td>
          <td>prompt 變長了？KV cache 失效？</td>
      </tr>
      <tr>
          <td>整台 Mac 變慢、Ollama 沒崩</td>
          <td>伺服器層 / 系統</td>
          <td>記憶體 swap、看 Activity Monitor</td>
      </tr>
      <tr>
          <td>Ollama 自己 crash</td>
          <td>伺服器層</td>
          <td>看 server log、通常 OOM 或 bug</td>
      </tr>
      <tr>
          <td>跨 session 設定遺失</td>
          <td>介面層</td>
          <td>IDE 設定沒存或被 reset</td>
      </tr>
      <tr>
          <td>Tab autocomplete 完全不觸發</td>
          <td>介面層</td>
          <td>autocomplete model 沒配對 / 沒 pull</td>
      </tr>
  </tbody>
</table>
<p>對應的具體驗證指令範例：</p>
<ul>
<li><strong>回答亂碼 / 重複</strong>：<code>ollama list</code> 確認當前 model tag、改跑 <code>ollama run &lt;較高量化版本&gt;</code>（例如 Q4 → Q5）；同 prompt 換 model 確認是不是 model 本身能力問題、不是伺服器。</li>
<li><strong>TTFT 異常變長</strong>：<code>ollama ps</code> 看 model 是否被 unload 又重載（<a href="/blog/llm/01-local-llm-services/ollama/#%e6%a8%a1%e5%9e%8b%e5%b8%b8%e9%a7%90keep_alive" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">keep_alive</a> 太短）；檢查 prompt 字數是否暴增（10K+ tokens 進入 <a href="/blog/llm/knowledge-cards/prefill/" data-link-title="Prefill" data-link-desc="Prompt 首次處理時的計算階段：把整段輸入跑過模型、產生 KV cache">prefill</a> 痛點區）。</li>
<li><strong>Ollama 自己 crash</strong>：<a href="/blog/llm/knowledge-cards/launchd-service/" data-link-title="launchd Service" data-link-desc="macOS 原生的服務管理機制、把 process 註冊成自動啟動的 daemon 或 agent">launchd service</a> 模式看 <code>/opt/homebrew/var/log/ollama.log</code>、前景模式看啟動 terminal 的 stderr。</li>
</ul>
<p>這張表的核心訊號：</p>
<ul>
<li>「沒回應」「connection 系」→ 通常 server 層。</li>
<li>「內容怪」「答非所問」「重複」→ 通常 model 層。</li>
<li>「設定怪」「快捷鍵不對」→ 通常介面層。</li>
<li>「整機卡」→ 系統資源、不一定哪層的「bug」、可能是規格不夠。</li>
</ul>
<p>把這個 mapping 內化、看症狀立刻有第一手猜測、不用每次從零思考。</p>
<h2 id="log-在三層的角色差異">Log 在三層的角色差異</h2>
<p>每一層的 log 看的東西不同、用法不同：</p>
<h3 id="介面層-log">介面層 log</h3>
<ul>
<li><strong>位置</strong>：IDE plugin 的 console（VS Code Developer Tools、JetBrains 的 plugin log）。</li>
<li><strong>看什麼</strong>：請求是否發出、發到哪個 endpoint、回應 status code、parse error。</li>
<li><strong>常見訊號</strong>：請求根本沒發 → 介面層配置錯；請求發了但伺服器拒 → 伺服器層；請求成功但 parse 失敗 → 介面層或伺服器層回應格式不對。</li>
</ul>
<h3 id="伺服器層-log">伺服器層 log</h3>
<ul>
<li><strong>位置</strong>：Ollama 在 <code>~/.ollama/logs/server.log</code> 或類似位置、LM Studio 在 console 輸出、llama.cpp 在啟動 terminal。</li>
<li><strong>看什麼</strong>：模型載入過程、推論進度、error trace、記憶體狀態。</li>
<li><strong>常見訊號</strong>：載入 model 卡住 / 失敗 → model file 損壞或記憶體不足；推論時 OOM → 量化太激進或 context 太長；連線錯誤 → port 配置或 host binding。</li>
</ul>
<h3 id="模型層的觀察訊號">模型層的觀察訊號</h3>
<p>模型層通常沒有獨立的 log——權重檔本身不會 log、行為要透過伺服器層觀察。判讀模型問題的訊號通常是：</p>
<ul>
<li>「載入成功、推論時崩」→ 量化等級或記憶體配對問題。</li>
<li>「載入成功、推論結果差」→ 模型能力或量化品質問題。</li>
<li>「不同 prompt 表現不一致」→ 可能是 model 對特定 pattern 弱、不是 bug。</li>
</ul>
<p>模型層問題多半不是「壞了」、是「能力上限」——換更大模型或調量化是主要解法、不是「修 bug」。</p>
<h3 id="log-level-預設夠用針對性提升">log level 預設夠用、針對性提升</h3>
<p>實務上 default log level 提供的訊息已涵蓋多數排錯需要；全部開 verbose 反而把 noise 蓋過 signal、要找的關鍵錯誤被淹沒。有問題時針對該層提升 log level（其他層保持 default）、定位完再降回來。</p>
<h2 id="最小可重現的縮減策略">最小可重現的縮減策略</h2>
<p>症狀複雜時、把問題縮到最小、再逐步加回來。這個方法在所有軟體 debug 都通用、套用到 LLM 場景的具體流程：</p>
<ol>
<li>
<p><strong>直接 curl 伺服器、用最簡 prompt 復現</strong>：</p>
<ul>
<li>繞過介面層、確認伺服器本身行為。</li>
<li>prompt 用 <code>&quot;Hello&quot;</code> 這種最短的、排除 prompt 複雜度因素。</li>
<li>如果這步就崩 → 伺服器 / 模型層問題、可以排除介面層。</li>
</ul>
</li>
<li>
<p><strong>換不同 model 試</strong>：</p>
<ul>
<li>同樣 prompt、換 <code>gemma4:e4b</code> 或 <code>llama3.2:1b</code>。</li>
<li>不同 model 都正常 → 原 model 問題。</li>
<li>不同 model 也崩 → 伺服器層問題。</li>
</ul>
</li>
<li>
<p><strong>換不同伺服器試</strong>：</p>
<ul>
<li>Ollama 接不上、用 LM Studio 同模型試。</li>
<li>兩個都崩 → 模型或系統層問題。</li>
<li>一個好一個壞 → 該伺服器特有問題。</li>
</ul>
</li>
<li>
<p><strong>改變一個變數一次</strong>：</p>
<ul>
<li>每次只改一個變數（設定 / model / IDE 重啟三選一）、確保行為變化能對應到具體動作。</li>
<li>每次只改一項、觀察行為變化。</li>
</ul>
</li>
<li>
<p><strong>記錄每一步</strong>：</p>
<ul>
<li>排錯 30 分鐘還沒解時、開始會忘記試過什麼。</li>
<li>簡單 notebook 記錄「改了什麼、行為怎麼變」、避免轉圈。</li>
</ul>
</li>
</ol>
<p>這個方法看起來慢、實際上比「亂試一通」快很多。亂試的代價是「以為改了 A 沒效、其實改 A 跟改 B 互相抵銷、不知道」。最小可重現是 disciplined approach、值得花時間建立習慣。</p>
<h2 id="跨層級的常見誤判">跨層級的常見誤判</h2>
<p>排錯時常踩的陷阱是「把某層的問題誤判成另一層」、修錯方向白費力氣。常見誤判模式：</p>
<h3 id="把伺服器問題誤當模型問題">把伺服器問題誤當模型問題</h3>
<p>例：Ollama 因為 port 被佔啟動失敗、IDE 看到 connection refused、誤以為「model 載不起來、需要換 model」。實際上換 model 也救不了、要看 server log 才知道是 port 問題。</p>
<p>判讀：connection 系問題 → server 層、不是 model 層。</p>
<h3 id="把模型問題誤當伺服器問題">把模型問題誤當伺服器問題</h3>
<p>例：用 Q3 量化跑 7B 模型、輸出全是亂碼、誤以為「Ollama bug」、開 issue 報。實際上是量化太激進、模型本身輸出崩、換 Q4 就好。</p>
<p>判讀：「server 看起來正常、輸出怪」→ 通常 model 層、改量化或換 model。</p>
<h3 id="把介面問題誤當伺服器問題">把介面問題誤當伺服器問題</h3>
<p>例：Continue.dev 的 <code>config.json</code> 寫錯 <code>apiBase</code>、IDE 顯示 connection error、誤以為「Ollama 掛了」。實際上 Ollama 正常、curl 過得去、IDE 配置錯。</p>
<p>判讀：curl 過得去、IDE 過不去 → 介面層配置問題。</p>
<h3 id="把系統資源問題誤當軟體-bug">把系統資源問題誤當軟體 bug</h3>
<p>例：32GB Mac 跑 31B + 同時開大量 app、Mac 整體變慢、誤以為「Ollama 越來越慢」。實際上是記憶體 swap、Ollama 沒問題。</p>
<p>判讀：Activity Monitor 看 Memory Pressure 變紅 / swap 大量、是系統資源、不是軟體 bug。</p>
<h3 id="把-prompt-問題誤當模型問題">把 prompt 問題誤當模型問題</h3>
<p>例：給 model 超長 <a href="/blog/llm/knowledge-cards/context-window/" data-link-title="Context Window" data-link-desc="模型一次能處理的最大 token 數量：prompt 加生成的總和上限">context</a>（30K token）、<a href="/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT</a> 30 秒、誤以為「model 變慢了」。實際上是 <a href="/blog/llm/knowledge-cards/prefill/" data-link-title="Prefill" data-link-desc="Prompt 首次處理時的計算階段：把整段輸入跑過模型、產生 KV cache">prefill</a> 階段需要時間、跟 model 沒變慢無關。</p>
<p>判讀：短 prompt 正常、長 prompt 慢 → prefill 問題、可預期、不是 bug。</p>
<p>每種誤判的根因都是「症狀對應到錯的層級」。內化「症狀 → 層級」對應反射、能避開多數誤判。</p>
<h2 id="排錯工具箱">排錯工具箱</h2>
<p>四個基本工具能 cover 90% 的排錯場景：</p>
<h3 id="curl">curl</h3>
<ul>
<li><strong>角色</strong>：直接打伺服器 API、繞過介面層。</li>
<li><strong>用法</strong>：<code>curl http://localhost:11434/api/version</code> 看伺服器是否回應、<code>curl http://localhost:11434/v1/chat/completions</code> 帶最簡 prompt 試完整流程（11434 是 Ollama 預設 <a href="/blog/llm/knowledge-cards/port-and-localhost/" data-link-title="Port 與 Localhost" data-link-desc="TCP port 與 listen address 如何決定 API server 的對外暴露範圍">port</a>、見 <a href="/blog/llm/01-local-llm-services/ollama/" data-link-title="1.0 Ollama：主流推論伺服器" data-link-desc="一行 brew 裝完、ollama run 一鍵跑 Gemma 4 MTP、OpenAI 相容 API on localhost:11434">1.0 Ollama</a>）。</li>
<li><strong>價值</strong>：排除介面層、確認伺服器層行為。</li>
</ul>
<h3 id="ollama-ps--等價指令"><code>ollama ps</code> / 等價指令</h3>
<ul>
<li><strong>角色</strong>：看伺服器層當前 model 狀態。</li>
<li><strong>用法</strong>：<code>ollama ps</code> 列出載入記憶體的 model、看 size、idle timer。</li>
<li><strong>價值</strong>：確認「我以為載入了」跟「真的載入了」是否一致；看記憶體佔用是否合理。</li>
</ul>
<h3 id="activity-monitor--system-monitor">Activity Monitor / system monitor</h3>
<ul>
<li><strong>角色</strong>：看系統資源狀態。</li>
<li><strong>用法</strong>：Memory Pressure 是否變紅、CPU / GPU 使用率、swap 量、過熱降頻。</li>
<li><strong>價值</strong>：區分「軟體 bug」跟「規格不夠」。多數本地 LLM 慢的問題是規格、不是 bug。</li>
</ul>
<h3 id="ide-開發者工具">IDE 開發者工具</h3>
<ul>
<li><strong>角色</strong>：看介面層請求 / 回應。</li>
<li><strong>用法</strong>：VS Code 的 Help → Toggle Developer Tools、看 Network tab、看 Console。</li>
<li><strong>價值</strong>：確認介面層真的把請求發出去、看 server 回什麼。</li>
</ul>
<p>這四個工具學會用、寫 code 場景 90% 的排錯都能處理。剩 10% 的 deep issue（如 driver 問題、模型權重檔損壞、framework 內部 bug）需要更專業的工具、但這 10% 對寫 code 使用者來說、通常該求助社群或回報 maintainer、不是自己 debug。</p>
<h2 id="排錯流程的決策樹">排錯流程的決策樹</h2>
<p>把上面的內容整合成一個流程：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">症狀出現
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  ↓
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">curl 伺服器（伺服器層活著嗎）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  ├─ curl 失敗 → 看 server log（伺服器層問題）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  │   ├─ port 衝突 → 改 port 或 kill 舊 instance
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  │   ├─ model 載入失敗 → 看 file / 記憶體
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  │   └─ crash → bug report、看版本是否最新
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  └─ curl 成功 → 介面層或 model 層問題
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">      ↓
</span></span><span class="line"><span class="ln">10</span><span class="cl">      換最簡 prompt 試（model 在簡單 prompt 上正常嗎）
</span></span><span class="line"><span class="ln">11</span><span class="cl">      ├─ 簡單 prompt 也崩 → model 層問題
</span></span><span class="line"><span class="ln">12</span><span class="cl">      │   ├─ 換 model 試 → 不同 model 都崩 → 系統或伺服器
</span></span><span class="line"><span class="ln">13</span><span class="cl">      │   └─ 同 model 換量化等級 → 量化太激進
</span></span><span class="line"><span class="ln">14</span><span class="cl">      └─ 簡單 prompt OK、複雜 prompt 崩
</span></span><span class="line"><span class="ln">15</span><span class="cl">          ↓
</span></span><span class="line"><span class="ln">16</span><span class="cl">          看 prompt 長度跟 context 限制
</span></span><span class="line"><span class="ln">17</span><span class="cl">          ├─ context 超出 → 縮短 prompt 或換 long-context model
</span></span><span class="line"><span class="ln">18</span><span class="cl">          └─ context 在範圍內 → model 能力上限、考慮換大 model
</span></span><span class="line"><span class="ln">19</span><span class="cl">              ↓
</span></span><span class="line"><span class="ln">20</span><span class="cl">              （如果伺服器、prompt、model 都檢查過還是壞）
</span></span><span class="line"><span class="ln">21</span><span class="cl">              介面層配置問題
</span></span><span class="line"><span class="ln">22</span><span class="cl">              ├─ 看 IDE plugin developer console
</span></span><span class="line"><span class="ln">23</span><span class="cl">              ├─ 比對 config.json 跟最簡 working example
</span></span><span class="line"><span class="ln">24</span><span class="cl">              └─ reset 設定後重試</span></span></code></pre></div><p>這棵樹不是「按順序跑完」、是「定位後對應到具體分支」。學會用症狀直接 jump 到對應分支、不必每次從根跑起。</p>
<h2 id="何時不適用本章方法論">何時不適用本章方法論</h2>
<p>本章「三層架構定位」假設「單機、單 user、單一伺服器實例、人在駕駛位」的個人開發場景。以下情境的方法論需要擴充：</p>
<table>
  <thead>
      <tr>
          <th>情境</th>
          <th>為什麼三層定位失效 / 需要擴充</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-tenant 共用伺服器</td>
          <td>多個 user 共用 Ollama instance、症狀可能是「不同 user 的請求互相干擾」、單純三層定位看不出、需加 user / session 層</td>
      </tr>
      <tr>
          <td>容器化部署（Docker / k8s）</td>
          <td>介面 / 伺服器之間多一層網路命名空間、connection refused 可能是 container network 配置、不是伺服器層</td>
      </tr>
      <tr>
          <td>跨機器分散式 inference</td>
          <td>伺服器層拆成多 process / 多 node、單一 <code>ollama ps</code> 看不到全貌、需 cluster-level observability</td>
      </tr>
      <tr>
          <td>後端 production 服務</td>
          <td>排錯依賴 SLI / SLO + 監控告警支撐、而非「重啟試試」的探索式做法；本章方法論偏個人開發、production 場景需另尋資料中心 SRE 教材</td>
      </tr>
      <tr>
          <td>Agent loop 內部失敗</td>
          <td>失敗可能在 LLM 規劃 / tool execution / state machine 任一處、超出三層定位、見 <a href="/blog/llm/04-applications/agent-architecture/" data-link-title="4.4 Agent 架構原理" data-link-desc="Agent loop 結構、失敗模式、什麼任務適合 vs 不適合、跟人類審查的協作模型">4.4 Agent 架構</a></td>
      </tr>
  </tbody>
</table>
<p>本章方法論的甜蜜點是「個人 Mac、一個 IDE、一個 Ollama instance」的場景。離開這個甜蜜點、要把「三層」擴充成更多層（user / network / cluster）、或改用 production-grade 觀察工具。</p>
<h2 id="何時過時--何時不過時">何時過時 / 何時不過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>三層架構視角排錯（介面 / 伺服器 / 模型）。</li>
<li>「先定位、再修補」的反射。</li>
<li>最小可重現的縮減策略。</li>
<li>五類跨層級誤判模式的識別。</li>
<li>四個基本工具的概念（curl / process status / system monitor / dev tools）。</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>具體錯誤訊息文字（隨 Ollama / LM Studio / Continue.dev 版本變）。</li>
<li>log 檔位置（隨工具更新可能調整）。</li>
<li>特定指令名稱（如 <code>ollama ps</code> 將來可能改名）。</li>
<li>特定工具的開發者面板路徑。</li>
</ul>
<p>換工具或工具升級之後、本章的方法仍適用、只需要重新對應到「新工具的對應指令在哪」。看到新錯誤訊息時、回到三層架構定位、用最小可重現縮減——這比 google 錯誤訊息字面快得多、也比「重啟一次再試」可靠得多。</p>
<h2 id="下一章">下一章</h2>
<p>下一章：<a href="/blog/llm/02-math-foundations/" data-link-title="模組二：LLM 的數學基礎" data-link-desc="整理 LLM 推論背後需要理解的線性代數、機率與資訊論、最佳化、數值精度等數學概念">模組二 LLM 的數學基礎</a>、或回到 <a href="/blog/llm/01-local-llm-services/" data-link-title="模組一：本地 LLM 服務的安裝與應用" data-link-desc="Ollama、LM Studio、llama.cpp 的安裝與差異、VS Code &#43; Continue.dev 整合、模型選型與期望管理">模組一首頁</a> 看其他章節。</p>
]]></content:encoded></item><item><title>macOS 每個 App 到底吃多少空間：聚合佔用的 app-report 腳本</title><link>https://tarrragon.github.io/blog/other/macos-%E6%AF%8F%E5%80%8B-app-%E5%88%B0%E5%BA%95%E5%90%83%E5%A4%9A%E5%B0%91%E7%A9%BA%E9%96%93%E8%81%9A%E5%90%88%E4%BD%94%E7%94%A8%E7%9A%84-app-report-%E8%85%B3%E6%9C%AC/</link><pubDate>Sat, 27 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/other/macos-%E6%AF%8F%E5%80%8B-app-%E5%88%B0%E5%BA%95%E5%90%83%E5%A4%9A%E5%B0%91%E7%A9%BA%E9%96%93%E8%81%9A%E5%90%88%E4%BD%94%E7%94%A8%E7%9A%84-app-report-%E8%85%B3%E6%9C%AC/</guid><description>&lt;p>&lt;code>du ~/Library/*&lt;/code> 只能列出 Caches、Containers 這些目錄各佔多少，答不出「Steam 這個 App 一共吃了多少」。原因是一個 App 的資料散落在 &lt;code>~/Library&lt;/code> 好幾個不同位置，按目錄統計就拆不回它名下。這篇記錄一個把這些散落佔用聚合回各 App 的 &lt;code>app-report&lt;/code> 腳本——搭配磁碟層的 &lt;a href="../macos_disk_space_diagnosis/">disk-report&lt;/a>，後者找出哪棵子樹最大，這篇把子樹拆到 App。&lt;/p>
&lt;h2 id="一個-app-的真實佔用不等於它的-app-大小">一個 App 的真實佔用不等於它的 .app 大小&lt;/h2>
&lt;p>判斷一個 App 吃多少空間，要算的是它的總足跡（footprint），而不是 &lt;code>/Applications&lt;/code> 裡那顆 &lt;code>.app&lt;/code> 的大小。&lt;code>.app&lt;/code> 只是程式本體，App 跑起來產生的資料（下載內容、快取、登入狀態、設定、日誌）絕大多數寫在 &lt;code>~/Library&lt;/code> 底下的好幾個不同位置，跟 &lt;code>.app&lt;/code> 完全分家。&lt;/p>
&lt;p>這台機器上最極端的例子是 Steam：它的 &lt;code>.app&lt;/code> 只有 10.8M，但遊戲資料佔了 8.1G，兩者差了近 800 倍。只看 &lt;code>/Applications&lt;/code> 的大小排序，Steam 會排在很後面，完全看不出它是全機第一大戶。同樣地，Amazon Kindle 的 &lt;code>.app&lt;/code> 才 138M，書庫卻在沙箱容器裡佔了 3.2G。這就是為什麼「按目錄統計」和「按 App 統計」會給出完全不同的排行；要回答「哪個 App 該清」，必須把佔用聚合回 App。&lt;/p>
&lt;h2 id="佔用散落在-library-的哪些地方">佔用散落在 ~/Library 的哪些地方&lt;/h2>
&lt;p>聚合的第一步是知道一個 App 會把資料寫到哪些固定位置。下表只列與空間相關的主要位置（非 &lt;code>~/Library&lt;/code> 全量），macOS 對它們有約定，每個位置承擔不同責任，也決定了它能不能安全清掉。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>位置&lt;/th>
 &lt;th>放什麼&lt;/th>
 &lt;th>清掉的後果&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>/Applications/*.app&lt;/code>&lt;/td>
 &lt;td>程式本體&lt;/td>
 &lt;td>等於移除 App&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/Library/Caches/&lt;/code>&lt;/td>
 &lt;td>快取&lt;/td>
 &lt;td>下次自動重建，安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/Library/HTTPStorages/&lt;/code>&lt;/td>
 &lt;td>網路快取（cookie / 暫存）&lt;/td>
 &lt;td>多半要重新登入，大致安全&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/Library/Application Support/&lt;/code>&lt;/td>
 &lt;td>設定與使用者資料&lt;/td>
 &lt;td>掉資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/Library/Containers/&lt;/code>&lt;/td>
 &lt;td>沙箱 App 的完整家目錄&lt;/td>
 &lt;td>掉資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/Library/Group Containers/&lt;/code>&lt;/td>
 &lt;td>同廠商 App 共享的資料&lt;/td>
 &lt;td>掉資料、可能影響多個 App&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/Library/Saved Application State/&lt;/code>&lt;/td>
 &lt;td>視窗位置與復原狀態&lt;/td>
 &lt;td>下次開窗位置重置，無傷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>~/Library/Logs/&lt;/code>&lt;/td>
 &lt;td>日誌&lt;/td>
 &lt;td>安全&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的關鍵分界是「快取」與「資料」。&lt;code>Caches&lt;/code> 和 &lt;code>HTTPStorages&lt;/code> 是純衍生物，清掉只是讓 App 下次重新下載或重建，最多重新登入一次，所以是回收空間時的首選。&lt;code>Application Support&lt;/code>、&lt;code>Containers&lt;/code>、&lt;code>Group Containers&lt;/code> 則是使用者資料，Steam 的遊戲、Kindle 的書庫、聊天記錄都在這裡，刪了就真的沒了。&lt;code>Group Containers&lt;/code> 還要多一層留意：它是同一個開發商旗下多個 App 共享的目錄，動它可能同時影響好幾個 App。&lt;/p>
&lt;p>腳本對每個 App 把上面這些位置全部找出來、用 &lt;code>du&lt;/code> 量實際佔用、加總成一個數字，再附上逐項明細，讓人一眼看出「這 4G 裡有多少是可清的快取、多少是動不得的資料」。&lt;/p>
&lt;h2 id="命名不一致是聚合的主要難點">命名不一致是聚合的主要難點&lt;/h2>
&lt;p>把資料夾正確歸給某個 App 的難點在於：macOS 對這些目錄沒有統一的命名規則。有些 App 用它的 bundle id（例如 &lt;code>com.valvesoftware.steam&lt;/code>）當目錄名，有些直接用 App 的顯示名稱（例如 &lt;code>Steam&lt;/code>），同一個 App 的不同位置甚至各用一種。&lt;/p>
&lt;p>腳本的做法是對每個 App 先讀出它的 bundle id，然後 &lt;code>Caches&lt;/code>、&lt;code>Application Support&lt;/code>、&lt;code>Logs&lt;/code> 這幾個位置兩種命名都比對一次，bundle id 專屬的位置（&lt;code>Containers&lt;/code>、&lt;code>HTTPStorages&lt;/code>、&lt;code>Saved Application State&lt;/code>）則用 bundle id 找。&lt;code>Group Containers&lt;/code> 又是另一種格式，名稱前面多一段開發商的 team id（10 碼英數，像 &lt;code>ABCDE12345.group.com.foo&lt;/code>），因此改用 bundle id 做子字串比對。這套規則涵蓋了絕大多數 App，但用罕見自訂命名的資料仍可能漏抓，這是聚合式估算的固有邊界，腳本在輸出裡據實標明「可能漏抓」而不假裝是精確值。&lt;/p>
&lt;h2 id="homebrew-要分開算">Homebrew 要分開算&lt;/h2>
&lt;p>透過 Homebrew 裝的工具不在 &lt;code>/Applications&lt;/code>，需要獨立統計。佔用分兩類（概念詳見 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">Homebrew 知識卡&lt;/a>）：命令列工具與函式庫（&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">formula&lt;/a>）在 &lt;code>Cellar/&lt;/code>，GUI App 的下載 artifact 與 metadata（&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">cask&lt;/a>）在 &lt;code>Caskroom/&lt;/code>。cask 安裝的 &lt;code>.app&lt;/code> 本體實際放在 &lt;code>/Applications&lt;/code>，已被前面的 App 聚合排行計入；&lt;code>Caskroom/&lt;/code> 存的是安裝來源與版本資訊，體積通常遠小於 App 本體，兩邊不重複計。&lt;/p></description><content:encoded><![CDATA[<p><code>du ~/Library/*</code> 只能列出 Caches、Containers 這些目錄各佔多少，答不出「Steam 這個 App 一共吃了多少」。原因是一個 App 的資料散落在 <code>~/Library</code> 好幾個不同位置，按目錄統計就拆不回它名下。這篇記錄一個把這些散落佔用聚合回各 App 的 <code>app-report</code> 腳本——搭配磁碟層的 <a href="../macos_disk_space_diagnosis/">disk-report</a>，後者找出哪棵子樹最大，這篇把子樹拆到 App。</p>
<h2 id="一個-app-的真實佔用不等於它的-app-大小">一個 App 的真實佔用不等於它的 .app 大小</h2>
<p>判斷一個 App 吃多少空間，要算的是它的總足跡（footprint），而不是 <code>/Applications</code> 裡那顆 <code>.app</code> 的大小。<code>.app</code> 只是程式本體，App 跑起來產生的資料（下載內容、快取、登入狀態、設定、日誌）絕大多數寫在 <code>~/Library</code> 底下的好幾個不同位置，跟 <code>.app</code> 完全分家。</p>
<p>這台機器上最極端的例子是 Steam：它的 <code>.app</code> 只有 10.8M，但遊戲資料佔了 8.1G，兩者差了近 800 倍。只看 <code>/Applications</code> 的大小排序，Steam 會排在很後面，完全看不出它是全機第一大戶。同樣地，Amazon Kindle 的 <code>.app</code> 才 138M，書庫卻在沙箱容器裡佔了 3.2G。這就是為什麼「按目錄統計」和「按 App 統計」會給出完全不同的排行；要回答「哪個 App 該清」，必須把佔用聚合回 App。</p>
<h2 id="佔用散落在-library-的哪些地方">佔用散落在 ~/Library 的哪些地方</h2>
<p>聚合的第一步是知道一個 App 會把資料寫到哪些固定位置。下表只列與空間相關的主要位置（非 <code>~/Library</code> 全量），macOS 對它們有約定，每個位置承擔不同責任，也決定了它能不能安全清掉。</p>
<table>
  <thead>
      <tr>
          <th>位置</th>
          <th>放什麼</th>
          <th>清掉的後果</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>/Applications/*.app</code></td>
          <td>程式本體</td>
          <td>等於移除 App</td>
      </tr>
      <tr>
          <td><code>~/Library/Caches/</code></td>
          <td>快取</td>
          <td>下次自動重建，安全</td>
      </tr>
      <tr>
          <td><code>~/Library/HTTPStorages/</code></td>
          <td>網路快取（cookie / 暫存）</td>
          <td>多半要重新登入，大致安全</td>
      </tr>
      <tr>
          <td><code>~/Library/Application Support/</code></td>
          <td>設定與使用者資料</td>
          <td>掉資料</td>
      </tr>
      <tr>
          <td><code>~/Library/Containers/</code></td>
          <td>沙箱 App 的完整家目錄</td>
          <td>掉資料</td>
      </tr>
      <tr>
          <td><code>~/Library/Group Containers/</code></td>
          <td>同廠商 App 共享的資料</td>
          <td>掉資料、可能影響多個 App</td>
      </tr>
      <tr>
          <td><code>~/Library/Saved Application State/</code></td>
          <td>視窗位置與復原狀態</td>
          <td>下次開窗位置重置，無傷</td>
      </tr>
      <tr>
          <td><code>~/Library/Logs/</code></td>
          <td>日誌</td>
          <td>安全</td>
      </tr>
  </tbody>
</table>
<p>這張表的關鍵分界是「快取」與「資料」。<code>Caches</code> 和 <code>HTTPStorages</code> 是純衍生物，清掉只是讓 App 下次重新下載或重建，最多重新登入一次，所以是回收空間時的首選。<code>Application Support</code>、<code>Containers</code>、<code>Group Containers</code> 則是使用者資料，Steam 的遊戲、Kindle 的書庫、聊天記錄都在這裡，刪了就真的沒了。<code>Group Containers</code> 還要多一層留意：它是同一個開發商旗下多個 App 共享的目錄，動它可能同時影響好幾個 App。</p>
<p>腳本對每個 App 把上面這些位置全部找出來、用 <code>du</code> 量實際佔用、加總成一個數字，再附上逐項明細，讓人一眼看出「這 4G 裡有多少是可清的快取、多少是動不得的資料」。</p>
<h2 id="命名不一致是聚合的主要難點">命名不一致是聚合的主要難點</h2>
<p>把資料夾正確歸給某個 App 的難點在於：macOS 對這些目錄沒有統一的命名規則。有些 App 用它的 bundle id（例如 <code>com.valvesoftware.steam</code>）當目錄名，有些直接用 App 的顯示名稱（例如 <code>Steam</code>），同一個 App 的不同位置甚至各用一種。</p>
<p>腳本的做法是對每個 App 先讀出它的 bundle id，然後 <code>Caches</code>、<code>Application Support</code>、<code>Logs</code> 這幾個位置兩種命名都比對一次，bundle id 專屬的位置（<code>Containers</code>、<code>HTTPStorages</code>、<code>Saved Application State</code>）則用 bundle id 找。<code>Group Containers</code> 又是另一種格式，名稱前面多一段開發商的 team id（10 碼英數，像 <code>ABCDE12345.group.com.foo</code>），因此改用 bundle id 做子字串比對。這套規則涵蓋了絕大多數 App，但用罕見自訂命名的資料仍可能漏抓，這是聚合式估算的固有邊界，腳本在輸出裡據實標明「可能漏抓」而不假裝是精確值。</p>
<h2 id="homebrew-要分開算">Homebrew 要分開算</h2>
<p>透過 Homebrew 裝的工具不在 <code>/Applications</code>，需要獨立統計。佔用分兩類（概念詳見 <a href="/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">Homebrew 知識卡</a>）：命令列工具與函式庫（<a href="/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">formula</a>）在 <code>Cellar/</code>，GUI App 的下載 artifact 與 metadata（<a href="/blog/llm/knowledge-cards/homebrew/" data-link-title="Homebrew" data-link-desc="macOS 上社群維護的套件管理器、用一行指令安裝 CLI 工具與背景服務">cask</a>）在 <code>Caskroom/</code>。cask 安裝的 <code>.app</code> 本體實際放在 <code>/Applications</code>，已被前面的 App 聚合排行計入；<code>Caskroom/</code> 存的是安裝來源與版本資訊，體積通常遠小於 App 本體，兩邊不重複計。</p>
<p>這台機器的 formula 前幾名是開發語言執行環境：<code>dotnet@9</code> 634M、兩個版本的 <code>openjdk</code> 合計 600M、<code>mysql</code> 292M、<code>go</code> 258M。formula 會多版本並存（例如 <code>python@3.13</code> 和 <code>python@3.14</code> 各算各的），所以腳本把整個 formula 目錄一起計。除了已安裝的部分，腳本還列出 <code>brew --cache</code> 的下載快取，以及 <code>brew cleanup -n</code> 預估可回收的舊版本（<code>-n</code> 是 dry-run，只報告不刪），跟整支腳本的唯讀原則一致。</p>
<h2 id="聚合一律用-du-取實際佔用">聚合一律用 du 取實際佔用</h2>
<p>App 各位置的聚合一律用 <code>du -skx</code> 取實際佔用，而不是 <code>ls</code> / <code>find -size</code> 的邏輯大小。sparse 檔（稀疏檔）只有寫入過的區塊才真正佔磁碟，宣告的邏輯大小可能是實際佔用的數十倍；容器與資料目錄裡正好常有 VM 映像、容器磁碟這類 sparse 檔，拿邏輯大小加總會把整份聚合排行灌水。完整的 sparse 踩坑案例見 <a href="../macos_disk_space_diagnosis/">disk-report 那篇</a>。</p>
<p><code>-x</code> 讓 <code>du</code> 不跨越檔案系統邊界，避免把掛載進來的卷重複計入；<code>-k</code> 統一用 KB 當單位，方便把各位置的數字加總後再換算成人類可讀的 G / M。</p>
<h2 id="實測結果">實測結果</h2>
<p>下面是這台機器的實測排行（名次因個人使用習慣而異）；要看的是聚合排行和「按目錄統計」給的印象差多少：</p>
<table>
  <thead>
      <tr>
          <th>App</th>
          <th>總佔用</th>
          <th>主要落點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Steam</td>
          <td>8.1G</td>
          <td>data 8.1G（<code>.app</code> 只有 10.8M）</td>
      </tr>
      <tr>
          <td>Xcode</td>
          <td>4.8G</td>
          <td>bundle 4.8G</td>
      </tr>
      <tr>
          <td>Readmoo 看書</td>
          <td>4.6G</td>
          <td>data 3.8G + bundle 816M</td>
      </tr>
      <tr>
          <td>Dia</td>
          <td>4.1G</td>
          <td>cache 1.6G + bundle 1.3G + data 1.1G</td>
      </tr>
      <tr>
          <td>Amazon Kindle</td>
          <td>3.3G</td>
          <td>container 3.2G（<code>.app</code> 才 138M）</td>
      </tr>
  </tbody>
</table>
<p>全機掃到 65 個 App、聚合總計 48.2G。這份排行的價值在於它直接指向「該從哪裡下手」，而判讀邏輯可以套到任何人的排行上：本體小、資料大的 App（這台是 Steam、Kindle）要回收就得處理書庫與遊戲本身；純快取大的（這台是 Dia 的 1.6G）清掉零風險；本體就大的開發工具（Xcode、Android Studio）除非不再開發否則動不得。同一個總數字底下，可清的比例天差地別，這正是逐項明細要回答的問題。</p>
<h2 id="聚合的邊界總計不等於整機">聚合的邊界：總計不等於整機</h2>
<p>這個 48.2G 是「能歸屬到已安裝 App 的部分」之和，不是 <code>~/Library</code> 的全量。<a href="../macos_disk_space_diagnosis/">disk-report 那篇</a>量到的 <code>~/Library</code> 約 70G，差額落在幾類刻意不歸進單一 App 的位置。</p>
<p>最大的一塊是 <code>~/Library/Developer</code>（這台約 5.5G，幾乎全是 Xcode 的 DerivedData、CoreSimulator 與 iOS DeviceSupport）。它們是 Xcode 與模擬器產生的共用產物，硬塞給 Xcode 會誇大這顆 App、塞給別人又不對，app-report 比照 Homebrew 把它單獨列成一段（<code>app-report --dev</code>）。也因為這樣，上面排行裡的 Xcode 只算到 <code>.app</code> 本體，它的建置產物要看 Developer 那段——這也是為什麼 disk-report 會把「Xcode DeviceSupport」列為大戶，而逐 App 排行卻看不到：那筆資料正住在這個不歸單一 App 的位置。</p>
<p>其餘排除的還有 iCloud 與雲端硬碟的本地鏡像（<code>Mobile Documents</code>、<code>CloudStorage</code>）、已移除 App 留下的孤兒資料夾、以及 <code>Preferences</code>。排行掃的是 <code>/Applications</code>、<code>~/Applications</code>、<code>/Applications/Utilities</code> 與 Setapp、Mac App Store 裝的 App；直接從 DMG 跑、沒搬進 Applications 的 App 不會出現在排行，但它的 <code>~/Library</code> 資料若命名對得上仍可能部分計入。</p>
<p>還有一個方向相反的誤差要記得：這是估算不是精算。同一份資料若以 APFS clone 出現在多個被聚合的位置，逐位置分開跑 <code>du</code> 會各自計入（<code>du</code> 只在單次執行內對硬連結以 inode 去重，對 APFS clone 不去重），聚合值可能偏高。要看整個 <code>~/Library</code> 到底多大、由誰佔，回到 disk-report 的逐層 <code>du</code>。</p>
<h2 id="固化成-app-report-腳本">固化成 app-report 腳本</h2>
<p>把這套聚合邏輯寫成腳本，往後想知道「誰在吃空間」就一行重跑，不必每次重想要比對哪些目錄、要怎麼處理命名差異。腳本和 <code>disk-report</code> 收在同一個公開 repo <a href="https://github.com/tarrragon/scripts">tarrragon/scripts</a> 裡，維持「跟專案無關的系統工具放個人 bin」的一致做法。</p>
<p>兩支腳本在同一個 repo；若已為 <code>disk-report</code> clone 過 <code>~/Projects/scripts</code>，跳過 clone、只做 symlink。首次安裝則把 repo clone 下來，再把腳本本體 symlink 到個人的 <code>~/.local/bin</code>，這樣本機呼叫的永遠是 repo 的最新版：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git clone https://github.com/tarrragon/scripts.git ~/Projects/scripts
</span></span><span class="line"><span class="ln">2</span><span class="cl">ln -s ~/Projects/scripts/app-report/app-report ~/.local/bin/app-report</span></span></code></pre></div><p>PATH 設定同 disk-report（見 <a href="../macos_new_machine_setup/">macOS 新機基礎建設</a>）。裝好後直接呼叫：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">app-report           <span class="c1"># 完整報告：App 聚合排行 + Developer + Homebrew</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">app-report --apps    <span class="c1"># 只看 App 聚合排行（預設前 30）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">app-report --apps <span class="m">50</span> <span class="c1"># 排行顯示前 50</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">app-report --dev     <span class="c1"># 只看 ~/Library/Developer 開發工具共用資料</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">app-report --brew    <span class="c1"># 只看 Homebrew</span></span></span></code></pre></div><p>要清哪個 App，看完明細再動手：移掉 <code>.app</code> 並清對應的 <code>~/Library</code> 資料夾（報告每個 App 下方列的路徑就是清除對象；先從 <code>Caches</code> / <code>HTTPStorages</code> 開始，確認再考慮資料目錄），Homebrew 用 <code>brew cleanup -s</code>。</p>
<h2 id="兩支腳本的分工">兩支腳本的分工</h2>
<p><code>disk-report</code> 與 <code>app-report</code> 是磁碟清理的兩個接力棒。前者在卷與目錄層找出最大的子樹，通常落在 <code>~/Library</code>；後者接手把那棵子樹拆到 App，看出具體是誰佔的、各自有多少是可清的快取。先 disk 找方向、再 app 定位到人，兩支都唯讀，回收的最後一步都留在人這一端。</p>
]]></content:encoded></item><item><title>macOS 磁碟空間被吃光的診斷流程</title><link>https://tarrragon.github.io/blog/other/macos-%E7%A3%81%E7%A2%9F%E7%A9%BA%E9%96%93%E8%A2%AB%E5%90%83%E5%85%89%E7%9A%84%E8%A8%BA%E6%96%B7%E6%B5%81%E7%A8%8B/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/other/macos-%E7%A3%81%E7%A2%9F%E7%A9%BA%E9%96%93%E8%A2%AB%E5%90%83%E5%85%89%E7%9A%84%E8%A8%BA%E6%96%B7%E6%B5%81%E7%A8%8B/</guid><description>&lt;p>一台原本還有約 30G 餘裕的 Mac，使用幾小時後空間全部歸零，清過系統各種 cache 也沒有改善。這次排查的重點是順序與判讀依據：用什麼順序找、用哪個數字判斷，最後刪了什麼反而次要。順序對了，就能避開兩個讓人空轉的陷阱。&lt;/p>
&lt;p>最後把整套診斷固化成一個唯讀的 &lt;code>disk-report&lt;/code> 腳本，往後同類情況可以一行指令重跑。&lt;/p>
&lt;h2 id="先確認問題是真的滿還是浮動的假象">先確認問題是「真的滿」還是「浮動的假象」&lt;/h2>
&lt;p>排查磁碟的第一步是分辨空間到底去哪：是被真實檔案佔走，還是被系統的快照與 purgeable（系統可隨時回收的緩衝空間）暫時佔住。這兩者的處理方式完全不同，先分清楚才不會白清。&lt;/p>
&lt;p>在 APFS（Apple File System，macOS 的預設檔案系統）上，根目錄 &lt;code>/&lt;/code> 是唯讀的系統封印卷，真正存放使用者資料的是 &lt;code>/System/Volumes/Data&lt;/code>，而它們和其他卷（Preboot、Recovery、VM、模擬器 runtime）共用同一個 container（容器，APFS 管理空間的最上層單位）的空間池。判斷「還剩多少」要看整個 container 的可用空間，而不是單一卷的數字。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">df -h /System/Volumes/Data
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">diskutil info /System/Volumes/Data &lt;span class="p">|&lt;/span> grep -iE &lt;span class="s2">&amp;#34;Container Free Space|Container Total Space&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這次的結果是資料卷 100% 滿、整個 container 只剩約 591MB。確認確實滿載、不是顯示誤差，後面才值得花力氣找佔用大戶。&lt;/p>
&lt;h2 id="空間掉了又回來的根因本地快照與-purgeable">「空間掉了又回來」的根因：本地快照與 purgeable&lt;/h2>
&lt;p>空間在幾小時內反覆消長、清 cache 卻無效，最常見的原因是 Time Machine 的本地快照（local snapshots）加上 macOS 的 purgeable 空間，而不是某個看得見的檔案。這是排查時要先排除的一條線。&lt;/p>
&lt;p>本地快照的運作方式是：Time Machine 啟用時，系統約每小時自動建立一張快照「凍結」當下狀態，好讓本地也能做時光機回溯。這些被凍結的資料，正是先前以為已刪除、卻怎麼清都不會釋放的空間。快照保留約 24 小時（Apple 的 thinning 策略，觀察值），或在磁碟空間壓力過大時提前清除；後者正是「過一陣子空間又回來」的來源。若從未設定 Time Machine，這條線可跳過——沒啟用就不會有 local snapshot。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">tmutil listlocalsnapshots /System/Volumes/Data&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這次查的時候快照數是 0，但這不代表它不是元兇——恰恰相反，是磁碟已經滿到讓系統把快照全數清光了。判讀訊號是：若這個指令平常列出多筆快照、且磁碟空間在數字上頻繁浮動，浮動量就來自這裡，跟手動清的 cache 無關。根治方向是把總用量降下來、讓磁碟保有餘裕，系統就不會一直貼著上限狂建狂清快照。&lt;/p>
&lt;p>purgeable 是同一條線的另一半，但它沒有好用的精確讀數。&lt;code>diskutil apfs list&lt;/code> 能看 container 層的概況，而 purgeable 主要由快照與系統快取構成、本來就會自己浮動。處理方式跟快照一樣：把總用量降下來、讓系統在空間有壓力時自行釋放，而不是找指令直接清它。「沒有直接讀數」本身就是判讀邊界——看到可用空間和「實際檔案總和」對不上時，差額多半就在這塊浮動緩衝，不必懷疑是哪個檔案在搞鬼。&lt;/p>
&lt;h2 id="用實際佔用值找大戶避開-sparse-假大小">用實際佔用值找大戶，避開 sparse 假大小&lt;/h2>
&lt;p>找佔用大戶要用 &lt;code>du&lt;/code>（實際佔用的磁碟區塊）排序，不能依賴 &lt;code>ls -l&lt;/code> 顯示、或 &lt;code>find -size&lt;/code> 篩選所用的邏輯大小。對一般檔案兩者相同，但對 sparse 檔（稀疏檔）差距可以是好幾十倍，誤判會追錯目標。&lt;/p>
&lt;p>這次就踩到這個陷阱。&lt;code>find&lt;/code> 列出近期修改的大檔時，OrbStack（一套容器與 VM 執行環境）的虛擬磁碟映像顯示為 228G，看起來像頭號兇手；但用 &lt;code>du&lt;/code> 一量，實際佔用只有 1.9G。同樣地，macOS Podcasts 在 tmp 塞的一堆 &lt;code>.tmp.resize.img&lt;/code> 顯示有數十個檔，實際只佔 3.5M。這些都是 sparse 檔：宣告了很大的邏輯大小，但只有寫入過的區塊才真正佔磁碟。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 實際佔用（正確）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">du -sh ~/some/large.img
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 顯示大小（對 sparse 檔會嚴重高估，誤判用）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">ls -lh ~/some/large.img&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>定位順序是由外往內逐層收斂：先看家目錄前 20 大，鎖定最大的子樹（這次是 &lt;code>~/Library&lt;/code> 70G 左右），再往下展開 &lt;code>~/Library/Application Support&lt;/code>、&lt;code>~/Library/Containers&lt;/code>，直到找到具體的檔案或目錄。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">du -shx ~/* ~/.&lt;span class="o">[&lt;/span>!.&lt;span class="o">]&lt;/span>* 2&amp;gt;/dev/null &lt;span class="p">|&lt;/span> sort -rh &lt;span class="p">|&lt;/span> head -20
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">du -shx ~/Library/* 2&amp;gt;/dev/null &lt;span class="p">|&lt;/span> sort -rh &lt;span class="p">|&lt;/span> head -12&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>-x&lt;/code> 讓 &lt;code>du&lt;/code> 不跨越檔案系統邊界，避免把掛載進來的唯讀卷（例如 iOS 模擬器 runtime）重複計入；&lt;code>~/.[!.]*&lt;/code> 這個寫法只展開以單一點開頭的隱藏檔，排除掉 &lt;code>.&lt;/code> 和 &lt;code>..&lt;/code> 兩個會被一般 &lt;code>.*&lt;/code> 誤抓進來、算出整個家目錄大小的假項目。&lt;/p>
&lt;h2 id="這次找到的佔用大戶與處理">這次找到的佔用大戶與處理&lt;/h2>
&lt;p>定位出來的大戶集中在開發工具鏈與閒置的本地資料，多數可逆、刪了之後需要時會自動重建或可重新下載。下面的項目與數字都是這台機器的實測，換一台機器組成會完全不同；值得帶走的是每一項背後的判讀問題，不是這份清單本身。具體刪除指令因工具而異（Android Studio GUI、&lt;code>rm -rf&lt;/code>、&lt;code>ollama rm&lt;/code>），本文只做診斷與定位，刪除操作留給各工具自身的文件。以下逐項說明判讀依據。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>項目&lt;/th>
 &lt;th>實際佔用&lt;/th>
 &lt;th>處理判斷&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>舊版 Android NDK&lt;/td>
 &lt;td>約 3G&lt;/td>
 &lt;td>裝了多版、保留專案實際引用的版本，刪最舊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>用不到的 AVD + system-image&lt;/td>
 &lt;td>約 3G&lt;/td>
 &lt;td>一個 API 版本一組、停用的版本連 AVD 帶映像一起刪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Claude 桌面 Cowork 沙箱 VM&lt;/td>
 &lt;td>約 11G&lt;/td>
 &lt;td>只在使用桌面 App 的本地 agent 功能時才佈建，不用則可刪&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ollama 本地模型&lt;/td>
 &lt;td>約 9G&lt;/td>
 &lt;td>改用雲端後閒置的大模型可刪，小的 embedding 模型常是依賴&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Xcode iOS DeviceSupport&lt;/td>
 &lt;td>約 4.5G&lt;/td>
 &lt;td>實體裝置接線除錯的符號快取，重連會自動重建&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Android NDK 的判讀要回到「誰在用它」：這次專案是 Flutter，NDK 版本由 &lt;code>flutter.ndkVersion&lt;/code> 決定，而不是專案自己 pin。查當前 Flutter 要求的版本後發現，本機裝的兩版都是舊 Flutter 留下的殘留，於是保留較新的一版、刪掉最舊的。判斷可不可刪的關鍵是先確認「現在到底用哪版」，而不是看修改日期就動手。&lt;/p></description><content:encoded><![CDATA[<p>一台原本還有約 30G 餘裕的 Mac，使用幾小時後空間全部歸零，清過系統各種 cache 也沒有改善。這次排查的重點是順序與判讀依據：用什麼順序找、用哪個數字判斷，最後刪了什麼反而次要。順序對了，就能避開兩個讓人空轉的陷阱。</p>
<p>最後把整套診斷固化成一個唯讀的 <code>disk-report</code> 腳本，往後同類情況可以一行指令重跑。</p>
<h2 id="先確認問題是真的滿還是浮動的假象">先確認問題是「真的滿」還是「浮動的假象」</h2>
<p>排查磁碟的第一步是分辨空間到底去哪：是被真實檔案佔走，還是被系統的快照與 purgeable（系統可隨時回收的緩衝空間）暫時佔住。這兩者的處理方式完全不同，先分清楚才不會白清。</p>
<p>在 APFS（Apple File System，macOS 的預設檔案系統）上，根目錄 <code>/</code> 是唯讀的系統封印卷，真正存放使用者資料的是 <code>/System/Volumes/Data</code>，而它們和其他卷（Preboot、Recovery、VM、模擬器 runtime）共用同一個 container（容器，APFS 管理空間的最上層單位）的空間池。判斷「還剩多少」要看整個 container 的可用空間，而不是單一卷的數字。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">df -h /System/Volumes/Data
</span></span><span class="line"><span class="ln">2</span><span class="cl">diskutil info /System/Volumes/Data <span class="p">|</span> grep -iE <span class="s2">&#34;Container Free Space|Container Total Space&#34;</span></span></span></code></pre></div><p>這次的結果是資料卷 100% 滿、整個 container 只剩約 591MB。確認確實滿載、不是顯示誤差，後面才值得花力氣找佔用大戶。</p>
<h2 id="空間掉了又回來的根因本地快照與-purgeable">「空間掉了又回來」的根因：本地快照與 purgeable</h2>
<p>空間在幾小時內反覆消長、清 cache 卻無效，最常見的原因是 Time Machine 的本地快照（local snapshots）加上 macOS 的 purgeable 空間，而不是某個看得見的檔案。這是排查時要先排除的一條線。</p>
<p>本地快照的運作方式是：Time Machine 啟用時，系統約每小時自動建立一張快照「凍結」當下狀態，好讓本地也能做時光機回溯。這些被凍結的資料，正是先前以為已刪除、卻怎麼清都不會釋放的空間。快照保留約 24 小時（Apple 的 thinning 策略，觀察值），或在磁碟空間壓力過大時提前清除；後者正是「過一陣子空間又回來」的來源。若從未設定 Time Machine，這條線可跳過——沒啟用就不會有 local snapshot。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">tmutil listlocalsnapshots /System/Volumes/Data</span></span></code></pre></div><p>這次查的時候快照數是 0，但這不代表它不是元兇——恰恰相反，是磁碟已經滿到讓系統把快照全數清光了。判讀訊號是：若這個指令平常列出多筆快照、且磁碟空間在數字上頻繁浮動，浮動量就來自這裡，跟手動清的 cache 無關。根治方向是把總用量降下來、讓磁碟保有餘裕，系統就不會一直貼著上限狂建狂清快照。</p>
<p>purgeable 是同一條線的另一半，但它沒有好用的精確讀數。<code>diskutil apfs list</code> 能看 container 層的概況，而 purgeable 主要由快照與系統快取構成、本來就會自己浮動。處理方式跟快照一樣：把總用量降下來、讓系統在空間有壓力時自行釋放，而不是找指令直接清它。「沒有直接讀數」本身就是判讀邊界——看到可用空間和「實際檔案總和」對不上時，差額多半就在這塊浮動緩衝，不必懷疑是哪個檔案在搞鬼。</p>
<h2 id="用實際佔用值找大戶避開-sparse-假大小">用實際佔用值找大戶，避開 sparse 假大小</h2>
<p>找佔用大戶要用 <code>du</code>（實際佔用的磁碟區塊）排序，不能依賴 <code>ls -l</code> 顯示、或 <code>find -size</code> 篩選所用的邏輯大小。對一般檔案兩者相同，但對 sparse 檔（稀疏檔）差距可以是好幾十倍，誤判會追錯目標。</p>
<p>這次就踩到這個陷阱。<code>find</code> 列出近期修改的大檔時，OrbStack（一套容器與 VM 執行環境）的虛擬磁碟映像顯示為 228G，看起來像頭號兇手；但用 <code>du</code> 一量，實際佔用只有 1.9G。同樣地，macOS Podcasts 在 tmp 塞的一堆 <code>.tmp.resize.img</code> 顯示有數十個檔，實際只佔 3.5M。這些都是 sparse 檔：宣告了很大的邏輯大小，但只有寫入過的區塊才真正佔磁碟。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 實際佔用（正確）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">du -sh ~/some/large.img
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 顯示大小（對 sparse 檔會嚴重高估，誤判用）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">ls -lh ~/some/large.img</span></span></code></pre></div><p>定位順序是由外往內逐層收斂：先看家目錄前 20 大，鎖定最大的子樹（這次是 <code>~/Library</code> 70G 左右），再往下展開 <code>~/Library/Application Support</code>、<code>~/Library/Containers</code>，直到找到具體的檔案或目錄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">du -shx ~/* ~/.<span class="o">[</span>!.<span class="o">]</span>* 2&gt;/dev/null <span class="p">|</span> sort -rh <span class="p">|</span> head -20
</span></span><span class="line"><span class="ln">2</span><span class="cl">du -shx ~/Library/* 2&gt;/dev/null <span class="p">|</span> sort -rh <span class="p">|</span> head -12</span></span></code></pre></div><p><code>-x</code> 讓 <code>du</code> 不跨越檔案系統邊界，避免把掛載進來的唯讀卷（例如 iOS 模擬器 runtime）重複計入；<code>~/.[!.]*</code> 這個寫法只展開以單一點開頭的隱藏檔，排除掉 <code>.</code> 和 <code>..</code> 兩個會被一般 <code>.*</code> 誤抓進來、算出整個家目錄大小的假項目。</p>
<h2 id="這次找到的佔用大戶與處理">這次找到的佔用大戶與處理</h2>
<p>定位出來的大戶集中在開發工具鏈與閒置的本地資料，多數可逆、刪了之後需要時會自動重建或可重新下載。下面的項目與數字都是這台機器的實測，換一台機器組成會完全不同；值得帶走的是每一項背後的判讀問題，不是這份清單本身。具體刪除指令因工具而異（Android Studio GUI、<code>rm -rf</code>、<code>ollama rm</code>），本文只做診斷與定位，刪除操作留給各工具自身的文件。以下逐項說明判讀依據。</p>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>實際佔用</th>
          <th>處理判斷</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>舊版 Android NDK</td>
          <td>約 3G</td>
          <td>裝了多版、保留專案實際引用的版本，刪最舊</td>
      </tr>
      <tr>
          <td>用不到的 AVD + system-image</td>
          <td>約 3G</td>
          <td>一個 API 版本一組、停用的版本連 AVD 帶映像一起刪</td>
      </tr>
      <tr>
          <td>Claude 桌面 Cowork 沙箱 VM</td>
          <td>約 11G</td>
          <td>只在使用桌面 App 的本地 agent 功能時才佈建，不用則可刪</td>
      </tr>
      <tr>
          <td>ollama 本地模型</td>
          <td>約 9G</td>
          <td>改用雲端後閒置的大模型可刪，小的 embedding 模型常是依賴</td>
      </tr>
      <tr>
          <td>Xcode iOS DeviceSupport</td>
          <td>約 4.5G</td>
          <td>實體裝置接線除錯的符號快取，重連會自動重建</td>
      </tr>
  </tbody>
</table>
<p>Android NDK 的判讀要回到「誰在用它」：這次專案是 Flutter，NDK 版本由 <code>flutter.ndkVersion</code> 決定，而不是專案自己 pin。查當前 Flutter 要求的版本後發現，本機裝的兩版都是舊 Flutter 留下的殘留，於是保留較新的一版、刪掉最舊的。判斷可不可刪的關鍵是先確認「現在到底用哪版」，而不是看修改日期就動手。</p>
<p>Claude 桌面的 <code>vm_bundles</code> 是最大單一項目（11G）。它是桌面 App 的 Cowork 功能在本地沙箱 VM 裡執行程式用的根檔案系統映像。關鍵判讀是：它不是每次開 App 就重建——映像的修改日期停在數月前，是一次性佈建、之後沿用。只有實際使用 Cowork 沙箱時才會佈建和更新。所以對只用終端機 CLI、桌面 App 僅拿來聊天的人，這 11G 是純佔用，可以安全刪除；唯一後果是哪天實際開了 Cowork session，它會重新佈建。</p>
<p>剩下三項的判讀各有自己的關鍵問題。閒置的 AVD 與 system-image 是「一個 API 版本一組」的綁定，停用某個 Android 版本時要連 AVD 帶它依賴的系統映像一起刪，只刪一邊會留下半套。ollama 本地模型的判斷是「改用雲端後還會不會在本地跑」，閒置的大模型可刪，但小的 embedding 模型常被其他工具當依賴、刪了會牽連（ollama 模型的累積速度與專屬清理 idiom，見 <a href="/blog/llm/01-local-llm-services/hands-on/resource-management/" data-link-title="Hands-on：LLM 運行中 &#43; 結束的資源管理" data-link-desc="RAM / 磁碟 / port 三個 dimension 的觀察跟釋放、Ollama keep_alive 跟 ComfyUI 兩種 lifecycle 對比、實測釋放數字">本地 LLM 的資源管理</a>）。Xcode 的 iOS DeviceSupport 則是實體裝置接線除錯時產生的符號快取，可以放心刪——下次接上同一台裝置除錯時 Xcode 會自動重建。</p>
<p>這幾項合計回收約 17G，可用空間從約 591MB 拉回到 18G，磁碟脫離滿載。</p>
<h2 id="把診斷固化成-disk-report-腳本">把診斷固化成 disk-report 腳本</h2>
<p>一次性查完之後，把這套順序寫成腳本的價值是：下次同類情況不必重新回想指令與判讀順序，一行就能重跑，而且固定先看快照、再用實際佔用值，不會又掉進 sparse 假大小的陷阱。</p>
<p>腳本收在公開 repo <a href="https://github.com/tarrragon/scripts">tarrragon/scripts</a>，而不是放進某個專案的 <code>bin/</code>。它跟任何專案無關，連到個人 bin 才能在任何地方直接呼叫，也不會污染專案 repo。安裝方式是 clone 下來、把腳本本體 symlink 到 <code>~/.local/bin</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">git clone https://github.com/tarrragon/scripts.git ~/Projects/scripts
</span></span><span class="line"><span class="ln">2</span><span class="cl">ln -s ~/Projects/scripts/disk-report/disk-report ~/.local/bin/disk-report</span></span></code></pre></div><p>這一步預設 <code>~/.local/bin</code> 已在 PATH 上。若還沒設定，做法見 <a href="../macos_new_machine_setup/">macOS 新機基礎建設</a> 的對應項目。腳本刻意設計成唯讀：只報告、不刪除，刪什麼由人看完報告再決定。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">disk-report              <span class="c1"># 完整診斷：總覽 + 快照狀態 + 各層大戶 + 開發環境可清項</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">disk-report --growing    <span class="c1"># 只看過去 180 分鐘內長大的大檔（抓動態暴增最快）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">disk-report --growing <span class="m">60</span> <span class="c1"># 改成過去 60 分鐘</span></span></span></code></pre></div><p><code>--growing</code> 模式對應的是本文開頭那個「幾小時內暴增」的情境：當空間正在快速消失、想抓現行犯時，直接列出近期被寫入的大檔，比逐層 <code>du</code> 更快定位。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">find <span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">&#34;</span> -type f -size +50M -mmin -180 2&gt;/dev/null <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  -exec du -h <span class="o">{}</span> <span class="se">\;</span> 2&gt;/dev/null <span class="p">|</span> sort -rh <span class="p">|</span> head -25</span></span></code></pre></div><p>50M 的下限是為了過濾日常小檔雜訊、鎖定單一大檔暴增；若懷疑是大量小檔累積吃空間（如快取碎片），這個門檻抓不到，要回逐層 <code>du</code> 看目錄總量。排序依據同樣是 <code>du</code> 的實際佔用值，而不是 <code>find -size</code> 的邏輯大小門檻，理由和前面一致：避免 sparse 檔的邏輯大小把排序帶歪。</p>
<h2 id="排查順序總結">排查順序總結</h2>
<p>這次的方法可以收斂成一條固定順序，往後遇到任何「磁碟莫名變滿」都先照這條走：</p>
<ol>
<li>先看 container 可用空間，確認是真滿還是顯示誤差。</li>
<li>再查本地快照與 purgeable，排除「掉了又回來」的浮動來源。</li>
<li>用 <code>du -shx</code> 由外往內逐層找大戶，全程以實際佔用值判斷，不信 <code>ls</code> / <code>find</code> 的顯示大小。</li>
<li>對每個大戶問「現在誰在用它」再決定刪不刪，可逆的優先清。</li>
<li>把整套順序固化成唯讀腳本，下次一行重跑。</li>
</ol>
<p>第 3 步若收斂到 <code>~/Library</code> 這種多個 App 共用的大目錄，按目錄統計只能看出 Caches、Containers 各多大，看不出是哪幾個 App 佔的。把這棵子樹再按 App 拆開的做法，見 <a href="../macos_app_footprint_report/">macOS App 聚合佔用報告</a>。</p>
]]></content:encoded></item></channel></rss>