<?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>Linux 除錯與診斷 on Tarragon</title><link>https://tarrragon.github.io/blog/linux/debug/</link><description>Recent content in Linux 除錯與診斷 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/linux/debug/index.xml" rel="self" type="application/rss+xml"/><item><title>診斷心法：讀權威狀態，不靠肉眼猜表象</title><link>https://tarrragon.github.io/blog/linux/debug/diagnosis-read-authoritative-state/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/diagnosis-read-authoritative-state/</guid><description>&lt;p>診斷一個 Linux 問題時，第一個動作不是猜「這看起來像什麼」，而是問「這件事的權威狀態在哪裡、我怎麼去讀它」。畫面上的現象、終端機捲過的輸出、一個視窗長什麼樣，都是表象；表象會騙人。真正能定案的是系統裡記錄這件事的那個權威來源——程式自己的 log、服務註冊表、核心與 systemd 的狀態、資源用量。把判斷建立在權威狀態上，而不是肉眼看到的樣子，是快速且不猜錯的除錯的核心。&lt;/p>
&lt;p>這篇講的是一套判讀紀律，不是某個特定工具。後面幾篇（&lt;a href="../ssh-and-terminal-troubleshooting/">遠端連線與終端機問題&lt;/a>、&lt;a href="../machine-unreachable/">機器連不到或起不來&lt;/a>、&lt;a href="../process-service-state-diagnosis/">程序、服務與狀態怎麼判&lt;/a>）是這套紀律在各種具體情境的應用。&lt;/p>
&lt;h2 id="表象會騙人一個判斷被畫面帶偏兩次的實例">表象會騙人：一個判斷被畫面帶偏兩次的實例&lt;/h2>
&lt;p>一個具體案例最能說明為什麼不能靠肉眼。在一次桌面 shell（畫桌面 UI 的圖形程式，不是 bash/zsh 那種命令列 shell）的除錯裡，畫面中央出現一個「輸入密碼」的覆蓋層，配著時鐘、天氣、通知的整片儀表板。第一眼的判斷很自然：螢幕被鎖住了。&lt;/p>
&lt;p>接著幾個看似合理的檢查反而把判斷帶得更偏：&lt;code>loginctl&lt;/code> 查不到這個 session 的 &lt;code>LockedHint&lt;/code>、&lt;code>pgrep&lt;/code> 找不到任何獨立的鎖屏程式、那個 shell 的 CLI 也沒有 lock 指令。三個訊號湊起來，得出一個「更正」的結論：這不是真的鎖，只是一個長得像鎖屏的儀表板面板。&lt;/p>
&lt;p>這個「更正」是錯的。真正定案是靠讀那個 shell &lt;strong>自己寫的 log&lt;/strong>：log 裡明明白白有鎖屏模組被載入、有 idle 計時器在數秒數、時間到就觸發鎖定。它是一個真的螢幕鎖，走的是 Wayland 的 session-lock 協議。&lt;/p>
&lt;p>為什麼前面三個檢查會誤導？因為它們讀的是&lt;strong>錯的權威來源&lt;/strong>。&lt;code>loginctl&lt;/code> 的 &lt;code>LockedHint&lt;/code> 是 logind（systemd 的登入管理）那一層的鎖定狀態，而這個鎖走的是 Wayland 合成器（compositor，負責把視窗合成到螢幕、管輸入輸出的核心程式）那一層的協議，兩者是獨立機制——查 logind 對合成器層的鎖天生查不到，不是「沒鎖」，是查錯地方。&lt;code>pgrep&lt;/code> 找不到獨立程式，是因為鎖屏畫面由 shell 主程式在自己的行程內畫，本來就沒有另一個可執行檔可抓。真正記錄「有沒有鎖、為什麼鎖」的權威來源，是那個 shell 的 log；讀到它，一次就定案。&lt;/p>
&lt;p>肉眼加上讀錯層的檢查，猜錯了兩次；讀對權威來源，一次就對。教訓不是「那些工具沒用」，是&lt;strong>要先確認你讀的是不是這件事的權威狀態&lt;/strong>。&lt;/p>
&lt;h2 id="每種問題都有它的權威狀態來源">每種問題都有它的權威狀態來源&lt;/h2>
&lt;p>除錯的第一步，是為眼前的現象找到記錄它的權威來源。不同類別的問題，權威來源不同：&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;/td>
 &lt;td>那個程式自己的 log 檔&lt;/td>
 &lt;td>程式的 log 路徑、&lt;code>journalctl -u &amp;lt;服務&amp;gt;&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務由誰提供&lt;/td>
 &lt;td>D-Bus / socket 的服務註冊&lt;/td>
 &lt;td>&lt;code>busctl&lt;/code>、&lt;code>ss&lt;/code>、&lt;code>lsof&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>登入 / 鎖定狀態&lt;/td>
 &lt;td>logind&lt;/td>
 &lt;td>&lt;code>loginctl show-session&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務有沒有在跑&lt;/td>
 &lt;td>systemd unit 狀態&lt;/td>
 &lt;td>&lt;code>systemctl status&lt;/code>、&lt;code>systemctl is-active&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式有沒有活著&lt;/td>
 &lt;td>行程表（比對正確的 comm 名）&lt;/td>
 &lt;td>&lt;code>pgrep -x&lt;/code>、&lt;code>ps&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>網路通不通&lt;/td>
 &lt;td>介面 / 路由 / 鄰居表&lt;/td>
 &lt;td>&lt;code>ip -brief a&lt;/code>、&lt;code>ip neigh&lt;/code>、&lt;code>ss&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>磁碟 / 記憶體&lt;/td>
 &lt;td>檔案系統與記憶體用量&lt;/td>
 &lt;td>&lt;code>df -h&lt;/code>、&lt;code>du -sh&lt;/code>、&lt;code>free&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>核心 / 硬體 / 被殺行程&lt;/td>
 &lt;td>kernel ring buffer&lt;/td>
 &lt;td>&lt;code>dmesg&lt;/code>、&lt;code>journalctl -k&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式 log 沉默時的 syscall&lt;/td>
 &lt;td>系統呼叫層&lt;/td>
 &lt;td>&lt;code>strace -f -e trace=file&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>這張表的用法不是背它，是養成一個反射：看到現象先問「這件事的權威狀態記在哪張表裡」，再去讀那張表，而不是從畫面推測。下面幾個常見的判錯，都是讀了表象而不是權威來源。&lt;/p>
&lt;h3 id="讀對權威來源但查詢條件要對">讀對權威來源、但查詢條件要對&lt;/h3>
&lt;p>有時權威來源對了，還是會被誤導——因為查詢的條件寫錯。判程式活著沒，行程表是對的權威、&lt;code>pgrep&lt;/code> 是對的工具，但你得比對它&lt;strong>實際的行程名&lt;/strong>：一個程式可能以 symlink 的短名在跑，用你以為的名字 &lt;code>pgrep&lt;/code> 就掃不到、誤判成掛了。判服務由誰提供，權威是服務註冊表而非畫面（送一則通知看畫面有沒有跳不可靠——沒跳可能是勿擾吃掉或根本沒送出）。這兩類的具體查法（&lt;code>pgrep -x&lt;/code>、&lt;code>busctl&lt;/code> 查 D-Bus 擁有者）見 &lt;a href="../process-service-state-diagnosis/">程序、服務與狀態怎麼判&lt;/a>。重點是：權威來源對，還要問對地方、用對條件。&lt;/p>
&lt;h3 id="卡住是資源問題還是相容問題先看資源別先怪相容性">卡住是資源問題還是相容問題：先看資源，別先怪相容性&lt;/h3>
&lt;p>一個耗時的操作中途停住時，很容易直接跳到「是不是這個平台不相容 / 這個東西在這台機器上跑不起來」。但這個結論成本很高（可能讓你放棄一條其實可行的路），而它的權威狀態很好查。一次原始碼編譯跑到一半停住，第一個該看的是資源：&lt;code>df -h&lt;/code> 看磁碟是不是滿了、記憶體是不是被吃光——一次實際的案例就是主機磁碟寫滿把編譯中途打斷，清出空間後同一份原始碼接著編就過，跟平台相容性完全無關。先讀資源狀態排除掉最廉價的解釋，再去懷疑相容性這種昂貴的結論。&lt;/p>
&lt;h2 id="讀程式自己的-log從症狀往上游找">讀程式自己的 log：從症狀往上游找&lt;/h2>
&lt;p>當現象是「某個程式行為不對」，它自己的 log 幾乎總是比終端機捲過的畫面更接近真相。很多程式在終端機只印一段摘要，卻同時把詳細執行紀錄寫進一個 log 檔或系統日誌；當畫面上的訊息不足以定位時，那份 log 裡往往就有明確答案。&lt;/p>
&lt;p>找 log 的常見去處：程式自己的 log 檔（常在 &lt;code>~/.local/state/&amp;lt;程式&amp;gt;/&lt;/code> 或 &lt;code>~/.cache/&amp;lt;程式&amp;gt;/&lt;/code> 底下）、systemd 服務的 &lt;code>journalctl -u &amp;lt;服務名&amp;gt;&lt;/code>、或程式啟動時印出的 log 路徑。找到之後，關鍵是&lt;strong>用症狀當關鍵字往上游搜&lt;/strong>——&lt;code>grep -iE 'error|fail|not found|does not exist' &amp;lt;log&amp;gt;&lt;/code> 挑出異常行，或在 &lt;code>less&lt;/code> 裡用 &lt;code>?pattern&lt;/code> 往回找「第一個」異常（不是停在最後一個下游錯）。一個指令因為前面某個檔案不存在而失敗，終端機可能只報一個看似無關的下游錯誤，但 log 裡會有那句 &lt;code>File does not exist&lt;/code> 直指源頭。一個實際案例：某 shell 換了配色卻沒生效，畫面上什麼錯都沒有，是它的 log 裡一句「讀取 scheme 檔失敗：檔案不存在」點出根因——原來那個檔在 shell 啟動當下還沒被建出來。畫面沉默，log 說話。&lt;/p>
&lt;p>這一層跟 &lt;a href="../../install/observable-bootstrap/">可除錯的 bootstrap&lt;/a> 是一體兩面：那篇談怎麼讓你自己寫的腳本&lt;strong>產生&lt;/strong>一份可診斷的 log，這裡談除錯時怎麼&lt;strong>去讀&lt;/strong>程式自己的 log。兩邊的共同紀律是：不要只盯著終端機捲動，去找那份持久的、詳細的權威紀錄。&lt;/p></description><content:encoded><![CDATA[<p>診斷一個 Linux 問題時，第一個動作不是猜「這看起來像什麼」，而是問「這件事的權威狀態在哪裡、我怎麼去讀它」。畫面上的現象、終端機捲過的輸出、一個視窗長什麼樣，都是表象；表象會騙人。真正能定案的是系統裡記錄這件事的那個權威來源——程式自己的 log、服務註冊表、核心與 systemd 的狀態、資源用量。把判斷建立在權威狀態上，而不是肉眼看到的樣子，是快速且不猜錯的除錯的核心。</p>
<p>這篇講的是一套判讀紀律，不是某個特定工具。後面幾篇（<a href="../ssh-and-terminal-troubleshooting/">遠端連線與終端機問題</a>、<a href="../machine-unreachable/">機器連不到或起不來</a>、<a href="../process-service-state-diagnosis/">程序、服務與狀態怎麼判</a>）是這套紀律在各種具體情境的應用。</p>
<h2 id="表象會騙人一個判斷被畫面帶偏兩次的實例">表象會騙人：一個判斷被畫面帶偏兩次的實例</h2>
<p>一個具體案例最能說明為什麼不能靠肉眼。在一次桌面 shell（畫桌面 UI 的圖形程式，不是 bash/zsh 那種命令列 shell）的除錯裡，畫面中央出現一個「輸入密碼」的覆蓋層，配著時鐘、天氣、通知的整片儀表板。第一眼的判斷很自然：螢幕被鎖住了。</p>
<p>接著幾個看似合理的檢查反而把判斷帶得更偏：<code>loginctl</code> 查不到這個 session 的 <code>LockedHint</code>、<code>pgrep</code> 找不到任何獨立的鎖屏程式、那個 shell 的 CLI 也沒有 lock 指令。三個訊號湊起來，得出一個「更正」的結論：這不是真的鎖，只是一個長得像鎖屏的儀表板面板。</p>
<p>這個「更正」是錯的。真正定案是靠讀那個 shell <strong>自己寫的 log</strong>：log 裡明明白白有鎖屏模組被載入、有 idle 計時器在數秒數、時間到就觸發鎖定。它是一個真的螢幕鎖，走的是 Wayland 的 session-lock 協議。</p>
<p>為什麼前面三個檢查會誤導？因為它們讀的是<strong>錯的權威來源</strong>。<code>loginctl</code> 的 <code>LockedHint</code> 是 logind（systemd 的登入管理）那一層的鎖定狀態，而這個鎖走的是 Wayland 合成器（compositor，負責把視窗合成到螢幕、管輸入輸出的核心程式）那一層的協議，兩者是獨立機制——查 logind 對合成器層的鎖天生查不到，不是「沒鎖」，是查錯地方。<code>pgrep</code> 找不到獨立程式，是因為鎖屏畫面由 shell 主程式在自己的行程內畫，本來就沒有另一個可執行檔可抓。真正記錄「有沒有鎖、為什麼鎖」的權威來源，是那個 shell 的 log；讀到它，一次就定案。</p>
<p>肉眼加上讀錯層的檢查，猜錯了兩次；讀對權威來源，一次就對。教訓不是「那些工具沒用」，是<strong>要先確認你讀的是不是這件事的權威狀態</strong>。</p>
<h2 id="每種問題都有它的權威狀態來源">每種問題都有它的權威狀態來源</h2>
<p>除錯的第一步，是為眼前的現象找到記錄它的權威來源。不同類別的問題，權威來源不同：</p>
<table>
  <thead>
      <tr>
          <th>問題類別</th>
          <th>權威狀態來源</th>
          <th>讀它的工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>某程式的行為</td>
          <td>那個程式自己的 log 檔</td>
          <td>程式的 log 路徑、<code>journalctl -u &lt;服務&gt;</code></td>
      </tr>
      <tr>
          <td>服務由誰提供</td>
          <td>D-Bus / socket 的服務註冊</td>
          <td><code>busctl</code>、<code>ss</code>、<code>lsof</code></td>
      </tr>
      <tr>
          <td>登入 / 鎖定狀態</td>
          <td>logind</td>
          <td><code>loginctl show-session</code></td>
      </tr>
      <tr>
          <td>服務有沒有在跑</td>
          <td>systemd unit 狀態</td>
          <td><code>systemctl status</code>、<code>systemctl is-active</code></td>
      </tr>
      <tr>
          <td>程式有沒有活著</td>
          <td>行程表（比對正確的 comm 名）</td>
          <td><code>pgrep -x</code>、<code>ps</code></td>
      </tr>
      <tr>
          <td>網路通不通</td>
          <td>介面 / 路由 / 鄰居表</td>
          <td><code>ip -brief a</code>、<code>ip neigh</code>、<code>ss</code></td>
      </tr>
      <tr>
          <td>磁碟 / 記憶體</td>
          <td>檔案系統與記憶體用量</td>
          <td><code>df -h</code>、<code>du -sh</code>、<code>free</code></td>
      </tr>
      <tr>
          <td>核心 / 硬體 / 被殺行程</td>
          <td>kernel ring buffer</td>
          <td><code>dmesg</code>、<code>journalctl -k</code></td>
      </tr>
      <tr>
          <td>程式 log 沉默時的 syscall</td>
          <td>系統呼叫層</td>
          <td><code>strace -f -e trace=file</code></td>
      </tr>
  </tbody>
</table>
<p>這張表的用法不是背它，是養成一個反射：看到現象先問「這件事的權威狀態記在哪張表裡」，再去讀那張表，而不是從畫面推測。下面幾個常見的判錯，都是讀了表象而不是權威來源。</p>
<h3 id="讀對權威來源但查詢條件要對">讀對權威來源、但查詢條件要對</h3>
<p>有時權威來源對了，還是會被誤導——因為查詢的條件寫錯。判程式活著沒，行程表是對的權威、<code>pgrep</code> 是對的工具，但你得比對它<strong>實際的行程名</strong>：一個程式可能以 symlink 的短名在跑，用你以為的名字 <code>pgrep</code> 就掃不到、誤判成掛了。判服務由誰提供，權威是服務註冊表而非畫面（送一則通知看畫面有沒有跳不可靠——沒跳可能是勿擾吃掉或根本沒送出）。這兩類的具體查法（<code>pgrep -x</code>、<code>busctl</code> 查 D-Bus 擁有者）見 <a href="../process-service-state-diagnosis/">程序、服務與狀態怎麼判</a>。重點是：權威來源對，還要問對地方、用對條件。</p>
<h3 id="卡住是資源問題還是相容問題先看資源別先怪相容性">卡住是資源問題還是相容問題：先看資源，別先怪相容性</h3>
<p>一個耗時的操作中途停住時，很容易直接跳到「是不是這個平台不相容 / 這個東西在這台機器上跑不起來」。但這個結論成本很高（可能讓你放棄一條其實可行的路），而它的權威狀態很好查。一次原始碼編譯跑到一半停住，第一個該看的是資源：<code>df -h</code> 看磁碟是不是滿了、記憶體是不是被吃光——一次實際的案例就是主機磁碟寫滿把編譯中途打斷，清出空間後同一份原始碼接著編就過，跟平台相容性完全無關。先讀資源狀態排除掉最廉價的解釋，再去懷疑相容性這種昂貴的結論。</p>
<h2 id="讀程式自己的-log從症狀往上游找">讀程式自己的 log：從症狀往上游找</h2>
<p>當現象是「某個程式行為不對」，它自己的 log 幾乎總是比終端機捲過的畫面更接近真相。很多程式在終端機只印一段摘要，卻同時把詳細執行紀錄寫進一個 log 檔或系統日誌；當畫面上的訊息不足以定位時，那份 log 裡往往就有明確答案。</p>
<p>找 log 的常見去處：程式自己的 log 檔（常在 <code>~/.local/state/&lt;程式&gt;/</code> 或 <code>~/.cache/&lt;程式&gt;/</code> 底下）、systemd 服務的 <code>journalctl -u &lt;服務名&gt;</code>、或程式啟動時印出的 log 路徑。找到之後，關鍵是<strong>用症狀當關鍵字往上游搜</strong>——<code>grep -iE 'error|fail|not found|does not exist' &lt;log&gt;</code> 挑出異常行，或在 <code>less</code> 裡用 <code>?pattern</code> 往回找「第一個」異常（不是停在最後一個下游錯）。一個指令因為前面某個檔案不存在而失敗，終端機可能只報一個看似無關的下游錯誤，但 log 裡會有那句 <code>File does not exist</code> 直指源頭。一個實際案例：某 shell 換了配色卻沒生效，畫面上什麼錯都沒有，是它的 log 裡一句「讀取 scheme 檔失敗：檔案不存在」點出根因——原來那個檔在 shell 啟動當下還沒被建出來。畫面沉默，log 說話。</p>
<p>這一層跟 <a href="../../install/observable-bootstrap/">可除錯的 bootstrap</a> 是一體兩面：那篇談怎麼讓你自己寫的腳本<strong>產生</strong>一份可診斷的 log，這裡談除錯時怎麼<strong>去讀</strong>程式自己的 log。兩邊的共同紀律是：不要只盯著終端機捲動，去找那份持久的、詳細的權威紀錄。</p>
<h2 id="遠端除錯反而逼出好紀律">遠端除錯反而逼出好紀律</h2>
<p>透過 SSH 遠端除錯時，你看不到那台機器的畫面——這個限制反而是好事。看不到畫面，你就沒得靠肉眼猜，只能去讀權威狀態：查 log、查服務註冊、查行程表、查資源。很多在本地會犯的「看畫面就下結論」的錯，在遠端因為根本沒畫面可看而自動被避開。</p>
<p>反過來說，在本地（或看得到畫面的 VM）除錯時，畫面的存在是個誘惑：它讓你以為看到了就懂了。前面那個鎖屏誤判，正是發生在「看得到畫面」的情境——畫面上的密碼框太有說服力，反而蓋過了去讀 log 的動作。把遠端那套「沒有畫面、只信權威狀態」的紀律，也用在本地，就不會被畫面帶偏。</p>
<h2 id="判讀紀律四步">判讀紀律：四步</h2>
<p>把上面的東西收成一套每次都能跑的流程：</p>
<ol>
<li><strong>描述症狀</strong>：現象是什麼，先講清楚，不要在這步就急著下結論（「畫面出現密碼框」，不是「螢幕鎖了」）。</li>
<li><strong>定位權威來源</strong>：這件事的權威狀態記在哪——log、服務註冊、logind / systemd、行程表、資源用量（用上面那張表對照）。</li>
<li><strong>用對的工具讀它</strong>：讀那個權威來源，不是讀畫面、不是讀終端機捲過的殘影。</li>
<li><strong>權威跟表象矛盾時，信權威</strong>：如果讀到的權威狀態跟你肉眼的第一印象打架，信權威狀態、回頭修正第一印象——那個矛盾點通常就是你原本會猜錯的地方。</li>
</ol>
<p>這套流程的價值不在任何單一工具，在於它讓你的判斷有一個可回溯的依據，而不是一串越猜越偏的直覺。</p>
<h2 id="下一步">下一步</h2>
<ul>
<li>這套心法在遠端連線與終端機情境的應用，見 <a href="../ssh-and-terminal-troubleshooting/">遠端連線與終端機問題</a>。</li>
<li>機器連不到、或根本起不來時怎麼從權威狀態往下查，見 <a href="../machine-unreachable/">機器連不到或起不來</a>。</li>
<li>程序在不在、服務歸誰、狀態怎麼判的具體招式，見 <a href="../process-service-state-diagnosis/">程序、服務與狀態怎麼判</a>。</li>
<li>怎麼讓你自己的 bootstrap 腳本產生可讀的 log，見 <a href="../../install/observable-bootstrap/">可除錯的 bootstrap</a>。</li>
</ul>
]]></content:encoded></item><item><title>遠端連線與終端機問題</title><link>https://tarrragon.github.io/blog/linux/debug/ssh-and-terminal-troubleshooting/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/ssh-and-terminal-troubleshooting/</guid><description>&lt;p>遠端操作 Linux 時，很多問題出在「你的終端機」與「遠端 session」之間那條連線的狀態，而不在遠端那台機器本身。終端機被上一個程式留在奇怪的模式、字元編碼與終端機能力沒對上、或你想從一條純文字的 SSH 連線去驅動一個需要實體螢幕的圖形桌面——這些問題的共同點是：現象發生在連線的某一層，判斷對是哪一層，修復就很直接。&lt;/p>
&lt;p>SSH「連不上」本身（&lt;code>Permission denied&lt;/code>、&lt;code>Host key verification failed&lt;/code>、&lt;code>Connection refused&lt;/code>）的判讀與修復，見 &lt;a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑&lt;/a> 的重連段落。這篇處理的是「連上了、但終端機或 session 的狀態不對」的那些情況。&lt;/p>
&lt;h2 id="ssh-斷線後本機終端機噴亂碼狂跳字元">SSH 斷線後本機終端機噴亂碼、狂跳字元&lt;/h2>
&lt;p>一個嚇人但無害的情況：SSH 連線被中斷後，你本機的終端機開始瘋狂輸出像 &lt;code>&amp;lt;35;80;24M&lt;/code> 這樣的序列，尤其在你移動滑鼠時狂跳。這不是遠端機器在打字，是&lt;strong>你本機的終端機被卡在滑鼠回報模式&lt;/strong>。&lt;/p>
&lt;p>判讀關鍵在「什麼時候噴」：如果那串亂碼只在你移動滑鼠時出現、而且形如 &lt;code>數字;數字M&lt;/code>，那就是滑鼠座標回報。成因是遠端跑的某個全螢幕程式（TUI、編輯器、終端機多工器）啟動時對終端機開了滑鼠追蹤模式，SSH 被硬斷時它來不及送出「關閉滑鼠模式」的序列就死了，於是你本機終端機還停在回報模式，滑鼠一動就把游標座標當輸入送進來。&lt;/p>
&lt;p>修復是重置終端機的模式，跟遠端機器無關：&lt;/p>
&lt;ul>
&lt;li>最快：開一個新的終端機分頁 / 視窗。模式是「那個終端機 session」的狀態，新視窗是乾淨的。&lt;/li>
&lt;li>救現有視窗：先把滑鼠移開別動（洪流會停），盲打 &lt;code>reset&lt;/code> 再 Enter，送出終端機重置。&lt;/li>
&lt;li>若 &lt;code>reset&lt;/code> 沒清掉，補送關閉滑鼠回報的序列：&lt;code>printf '\033[?1000l\033[?1002l\033[?1003l\033[?1006l'&lt;/code>。&lt;/li>
&lt;/ul>
&lt;p>同一類的還有「alternate screen 沒還原」——遠端的全螢幕程式異常結束時，本機終端機可能卡在替代畫面緩衝區，看起來像畫面清空或凍結。&lt;code>reset&lt;/code> 同樣能救。歸納起來：&lt;strong>SSH 被硬斷後本機終端機行為異常，先懷疑「對端程式來不及還原終端機模式」，用 &lt;code>reset&lt;/code> 或開新視窗處理本機終端機狀態，不必急著重連遠端。&lt;/strong>&lt;/p>
&lt;h2 id="遠端打字變亂碼重複位置錯亂">遠端打字變亂碼、重複、位置錯亂&lt;/h2>
&lt;p>連上遠端後，如果互動式輸入變得不對——打一個字出現好幾個、游標位置錯亂、畫面重繪殘影——通常是兩層問題之一，判讀方式是分開排除。&lt;/p>
&lt;p>第一層是&lt;strong>字元編碼（locale）&lt;/strong>。從某些本機（例如 macOS）SSH 進 Linux 時，本機會把 &lt;code>LC_CTYPE&lt;/code> 之類的變數帶過去；如果遠端沒有對應的 locale、就會退回 POSIX/C locale，讓終端機的行編輯（ZLE、readline）對多位元組字元的寬度判斷出錯，表現為輸入重複或錯位。判斷方式是在遠端 &lt;code>locale&lt;/code> 看目前值、&lt;code>locale -a&lt;/code> 看有沒有裝對應的 UTF-8 locale。修法是在遠端明確設好 &lt;code>LANG&lt;/code> / &lt;code>LC_CTYPE&lt;/code> 到一個實際存在的 UTF-8 locale，而不是讓它繼承一個遠端不認得的值。&lt;/p>
&lt;p>第二層是&lt;strong>終端機能力資料庫（terminfo）&lt;/strong>。你本機終端機的 &lt;code>TERM&lt;/code> 值（例如某些新終端機用 &lt;code>xterm-ghostty&lt;/code> 之類的自訂值）如果在遠端沒有對應的 terminfo 條目，遠端程式就不知道怎麼正確地清行、移動游標、重繪，畫面就會亂。判斷方式是在遠端 &lt;code>echo $TERM&lt;/code> 看值、&lt;code>infocmp $TERM&lt;/code> 看遠端認不認得。修法是把本機的 terminfo 條目送過去讓遠端安裝：&lt;code>infocmp -x $TERM | ssh &amp;lt;遠端&amp;gt; 'tic -x -'&lt;/code>。&lt;/p>
&lt;p>先分清是 locale 還是 terminfo，兩者症狀相似但修法不同：locale 是編碼寬度、terminfo 是繪製指令。查 &lt;code>locale&lt;/code> 跟查 &lt;code>$TERM&lt;/code> + &lt;code>infocmp&lt;/code> 就能分開。&lt;/p>
&lt;h2 id="從-ssh-操控遠端的圖形桌面">從 SSH 操控遠端的圖形桌面&lt;/h2>
&lt;p>想從一條純文字的 SSH 連線去操作遠端的 Wayland 圖形桌面（例如啟動應用、截圖、送 IPC 指令）時，會撞到兩類界線，判斷對是哪一類就知道怎麼繞。&lt;/p>
&lt;p>第一類是&lt;strong>圖形程式需要知道連到哪個顯示&lt;/strong>。SSH 進來的 shell 預設沒有圖形環境的環境變數，直接跑圖形程式會找不到 display。要對著遠端那個已經在跑的 Wayland session 操作，得補上它的環境變數：&lt;code>XDG_RUNTIME_DIR&lt;/code>（通常 &lt;code>/run/user/&amp;lt;uid&amp;gt;&lt;/code>）、&lt;code>WAYLAND_DISPLAY&lt;/code>（socket 名，如 &lt;code>wayland-1&lt;/code>）、必要時還有該 compositor 的 instance 變數與 &lt;code>DBUS_SESSION_BUS_ADDRESS&lt;/code>。這些值怎麼撈：socket 名用 &lt;code>ls /run/user/$(id -u)/wayland-*&lt;/code> 看；其餘變數直接從那個圖形 session 既有行程的環境複製最準——&lt;code>cat /proc/&amp;lt;compositor-pid&amp;gt;/environ | tr '\0' '\n' | grep -E 'WAYLAND_DISPLAY|XDG_RUNTIME_DIR|DBUS_SESSION|_INSTANCE_'&lt;/code>（&lt;code>&amp;lt;compositor-pid&amp;gt;&lt;/code> 用 &lt;code>pgrep -x Hyprland&lt;/code> 之類找）。撈到後 &lt;code>export&lt;/code> 進當前 SSH shell，這條連線就能對遠端的圖形 session 下指令、&lt;code>grim&lt;/code> 截圖。&lt;/p>
&lt;p>第二類是&lt;strong>有些東西必須從實體圖形終端機（VT，即 &lt;code>Ctrl+Alt+F1&lt;/code>~&lt;code>F6&lt;/code> 切換的那些文字主控台）啟動，SSH 的 pty 起不來&lt;/strong>。Wayland 的合成器（compositor，畫桌面、把視窗合成到螢幕、管輸入輸出的核心程式，如 Hyprland）需要一個真正的圖形 VT 上的登入 session，拿到 DRM master（對顯示卡的獨佔繪圖控制權）與 logind seat（一組綁在一起的實體螢幕／鍵鼠裝置）才能啟動；從 SSH 的 pty 起它的&lt;strong>預設 backend&lt;/strong> 會直接失敗（例如報 backend 建立失敗），因為預設 backend 要的 DRM master 與 seat 在 SSH 這條連線上不存在。判讀訊號：合成器一啟動就報 seat / DRM / backend 相關的錯，而你是從 SSH 起的——那就是這個界線。（例外：合成器多半有 headless backend，例如設 &lt;code>WLR_BACKENDS=headless&lt;/code> 就不要 DRM master、不需 VT，專給 CI、雲端、自動化測試用；nested（跑在另一個 Wayland session 裡）也不需要。所以精確說是「預設 backend 需要圖形 VT」，不是「合成器一定起不來」。）&lt;/p></description><content:encoded><![CDATA[<p>遠端操作 Linux 時，很多問題出在「你的終端機」與「遠端 session」之間那條連線的狀態，而不在遠端那台機器本身。終端機被上一個程式留在奇怪的模式、字元編碼與終端機能力沒對上、或你想從一條純文字的 SSH 連線去驅動一個需要實體螢幕的圖形桌面——這些問題的共同點是：現象發生在連線的某一層，判斷對是哪一層，修復就很直接。</p>
<p>SSH「連不上」本身（<code>Permission denied</code>、<code>Host key verification failed</code>、<code>Connection refused</code>）的判讀與修復，見 <a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑</a> 的重連段落。這篇處理的是「連上了、但終端機或 session 的狀態不對」的那些情況。</p>
<h2 id="ssh-斷線後本機終端機噴亂碼狂跳字元">SSH 斷線後本機終端機噴亂碼、狂跳字元</h2>
<p>一個嚇人但無害的情況：SSH 連線被中斷後，你本機的終端機開始瘋狂輸出像 <code>&lt;35;80;24M</code> 這樣的序列，尤其在你移動滑鼠時狂跳。這不是遠端機器在打字，是<strong>你本機的終端機被卡在滑鼠回報模式</strong>。</p>
<p>判讀關鍵在「什麼時候噴」：如果那串亂碼只在你移動滑鼠時出現、而且形如 <code>數字;數字M</code>，那就是滑鼠座標回報。成因是遠端跑的某個全螢幕程式（TUI、編輯器、終端機多工器）啟動時對終端機開了滑鼠追蹤模式，SSH 被硬斷時它來不及送出「關閉滑鼠模式」的序列就死了，於是你本機終端機還停在回報模式，滑鼠一動就把游標座標當輸入送進來。</p>
<p>修復是重置終端機的模式，跟遠端機器無關：</p>
<ul>
<li>最快：開一個新的終端機分頁 / 視窗。模式是「那個終端機 session」的狀態，新視窗是乾淨的。</li>
<li>救現有視窗：先把滑鼠移開別動（洪流會停），盲打 <code>reset</code> 再 Enter，送出終端機重置。</li>
<li>若 <code>reset</code> 沒清掉，補送關閉滑鼠回報的序列：<code>printf '\033[?1000l\033[?1002l\033[?1003l\033[?1006l'</code>。</li>
</ul>
<p>同一類的還有「alternate screen 沒還原」——遠端的全螢幕程式異常結束時，本機終端機可能卡在替代畫面緩衝區，看起來像畫面清空或凍結。<code>reset</code> 同樣能救。歸納起來：<strong>SSH 被硬斷後本機終端機行為異常，先懷疑「對端程式來不及還原終端機模式」，用 <code>reset</code> 或開新視窗處理本機終端機狀態，不必急著重連遠端。</strong></p>
<h2 id="遠端打字變亂碼重複位置錯亂">遠端打字變亂碼、重複、位置錯亂</h2>
<p>連上遠端後，如果互動式輸入變得不對——打一個字出現好幾個、游標位置錯亂、畫面重繪殘影——通常是兩層問題之一，判讀方式是分開排除。</p>
<p>第一層是<strong>字元編碼（locale）</strong>。從某些本機（例如 macOS）SSH 進 Linux 時，本機會把 <code>LC_CTYPE</code> 之類的變數帶過去；如果遠端沒有對應的 locale、就會退回 POSIX/C locale，讓終端機的行編輯（ZLE、readline）對多位元組字元的寬度判斷出錯，表現為輸入重複或錯位。判斷方式是在遠端 <code>locale</code> 看目前值、<code>locale -a</code> 看有沒有裝對應的 UTF-8 locale。修法是在遠端明確設好 <code>LANG</code> / <code>LC_CTYPE</code> 到一個實際存在的 UTF-8 locale，而不是讓它繼承一個遠端不認得的值。</p>
<p>第二層是<strong>終端機能力資料庫（terminfo）</strong>。你本機終端機的 <code>TERM</code> 值（例如某些新終端機用 <code>xterm-ghostty</code> 之類的自訂值）如果在遠端沒有對應的 terminfo 條目，遠端程式就不知道怎麼正確地清行、移動游標、重繪，畫面就會亂。判斷方式是在遠端 <code>echo $TERM</code> 看值、<code>infocmp $TERM</code> 看遠端認不認得。修法是把本機的 terminfo 條目送過去讓遠端安裝：<code>infocmp -x $TERM | ssh &lt;遠端&gt; 'tic -x -'</code>。</p>
<p>先分清是 locale 還是 terminfo，兩者症狀相似但修法不同：locale 是編碼寬度、terminfo 是繪製指令。查 <code>locale</code> 跟查 <code>$TERM</code> + <code>infocmp</code> 就能分開。</p>
<h2 id="從-ssh-操控遠端的圖形桌面">從 SSH 操控遠端的圖形桌面</h2>
<p>想從一條純文字的 SSH 連線去操作遠端的 Wayland 圖形桌面（例如啟動應用、截圖、送 IPC 指令）時，會撞到兩類界線，判斷對是哪一類就知道怎麼繞。</p>
<p>第一類是<strong>圖形程式需要知道連到哪個顯示</strong>。SSH 進來的 shell 預設沒有圖形環境的環境變數，直接跑圖形程式會找不到 display。要對著遠端那個已經在跑的 Wayland session 操作，得補上它的環境變數：<code>XDG_RUNTIME_DIR</code>（通常 <code>/run/user/&lt;uid&gt;</code>）、<code>WAYLAND_DISPLAY</code>（socket 名，如 <code>wayland-1</code>）、必要時還有該 compositor 的 instance 變數與 <code>DBUS_SESSION_BUS_ADDRESS</code>。這些值怎麼撈：socket 名用 <code>ls /run/user/$(id -u)/wayland-*</code> 看；其餘變數直接從那個圖形 session 既有行程的環境複製最準——<code>cat /proc/&lt;compositor-pid&gt;/environ | tr '\0' '\n' | grep -E 'WAYLAND_DISPLAY|XDG_RUNTIME_DIR|DBUS_SESSION|_INSTANCE_'</code>（<code>&lt;compositor-pid&gt;</code> 用 <code>pgrep -x Hyprland</code> 之類找）。撈到後 <code>export</code> 進當前 SSH shell，這條連線就能對遠端的圖形 session 下指令、<code>grim</code> 截圖。</p>
<p>第二類是<strong>有些東西必須從實體圖形終端機（VT，即 <code>Ctrl+Alt+F1</code>~<code>F6</code> 切換的那些文字主控台）啟動，SSH 的 pty 起不來</strong>。Wayland 的合成器（compositor，畫桌面、把視窗合成到螢幕、管輸入輸出的核心程式，如 Hyprland）需要一個真正的圖形 VT 上的登入 session，拿到 DRM master（對顯示卡的獨佔繪圖控制權）與 logind seat（一組綁在一起的實體螢幕／鍵鼠裝置）才能啟動；從 SSH 的 pty 起它的<strong>預設 backend</strong> 會直接失敗（例如報 backend 建立失敗），因為預設 backend 要的 DRM master 與 seat 在 SSH 這條連線上不存在。判讀訊號：合成器一啟動就報 seat / DRM / backend 相關的錯，而你是從 SSH 起的——那就是這個界線。（例外：合成器多半有 headless backend，例如設 <code>WLR_BACKENDS=headless</code> 就不要 DRM master、不需 VT，專給 CI、雲端、自動化測試用；nested（跑在另一個 Wayland session 裡）也不需要。所以精確說是「預設 backend 需要圖形 VT」，不是「合成器一定起不來」。）</p>
<p>繞法是回到那台機器的實體圖形終端機去啟動 compositor，但「回到 VT」這件事也可以從 SSH 遠端做：</p>
<ul>
<li><code>sudo chvt &lt;N&gt;</code> 從 SSH 切換那台機器目前顯示的虛擬終端機到第 N 個，比在虛擬機視窗裡跟宿主的 <code>Ctrl+Alt+Fn</code> 快捷鍵搏鬥穩定。</li>
<li>切過去卻是空白、沒有登入提示，通常是那個 VT 上沒有 getty 在跑：<code>sudo systemctl start getty@tty&lt;N&gt;</code>（開機時 <code>enabled</code> 但 <code>inactive</code> 是常見狀態，logind 的 autovt 沒觸發）。</li>
<li><code>sudo fgconsole</code> 確認目前是哪個 VT 在前景。</li>
</ul>
<p>還有一個容易混淆的點：一台虛擬機可能同時有「序列主控台」跟「圖形顯示」兩個獨立輸出。在 guest 內 <code>chvt</code> 只切圖形那側，序列主控台看到的畫面不會變。如果你在虛擬機軟體裡看的是序列主控台，圖形桌面得切到顯示輸出那個 view 才看得到。判讀：切了 VT 但畫面沒反應，先確認你正在看的是哪個輸出。</p>
<h2 id="判讀路由">判讀路由</h2>
<p>遠端 / 終端機問題的分流：</p>
<ul>
<li>本機終端機噴亂碼、只在動滑鼠時噴 → 滑鼠回報模式沒關（本機終端機狀態），<code>reset</code> 或開新視窗。</li>
<li>遠端打字重複 / 錯位 → 分 locale（查 <code>locale</code>）與 terminfo（查 <code>$TERM</code> + <code>infocmp</code>）。</li>
<li>圖形程式在 SSH 下找不到 display → 補 <code>WAYLAND_DISPLAY</code> / <code>XDG_RUNTIME_DIR</code> 等環境變數。</li>
<li>compositor 從 SSH 起不來、報 seat/DRM 錯 → 它需要實體 VT，用 <code>chvt</code> + <code>getty@tty&lt;N&gt;</code> 回到圖形 VT 啟動。</li>
<li>SSH 連不上（拒絕 / host key / refused）→ 見 <a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑</a>。</li>
</ul>
<p>這幾種分流的共同底線是先讀權威狀態（<code>locale</code>、<code>$TERM</code>、runtime 目錄、<code>loginctl</code>、<code>fgconsole</code>）再下判斷；背後的方法論見 <a href="../diagnosis-read-authoritative-state/">診斷心法</a>。</p>
]]></content:encoded></item><item><title>機器連不到或起不來</title><link>https://tarrragon.github.io/blog/linux/debug/machine-unreachable/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/machine-unreachable/</guid><description>&lt;p>一台原本能連的機器突然連不上，或一台虛擬機根本開不起來，判讀的方向是「從你這端往那台機器，一層一層確認哪裡斷了」，而不是反覆重試同一個連線動作。連線失敗是最終症狀，真正斷掉的可能是網路、可能是那台機器的某個服務沒起來、可能是虛擬機的宿主側出問題、也可能是一個把上面全部拖下水的共同根因：磁碟滿。這篇從網路層與宿主側的權威狀態切入，把「連不上」拆成可定位的環節。&lt;/p>
&lt;h2 id="遠端機器突然連不上先分清是哪一段斷">遠端機器突然連不上：先分清是哪一段斷&lt;/h2>
&lt;p>一台昨天還能 SSH 的機器今天連不上，第一步是確認「網路層通不通」，跟「SSH 服務在不在」分開。連線在 TCP 就 timeout（連 port 22 卡住沒回應），多半是網路層或機器沒在跑；連線有回應但被拒（&lt;code>Connection refused&lt;/code>），是網路通、但那台機器上沒有服務在聽 port 22。&lt;/p>
&lt;p>對虛擬機或同網段的機器，一個很有用的權威來源是&lt;strong>鄰居表&lt;/strong>（IP 對 MAC 的對應）。要填起來需要對方在鏈路層有回應，所以它直接反映「對方在不在」。用 &lt;code>ip neigh&lt;/code> 看目標 IP 的條目——優先用 &lt;code>ip neigh&lt;/code> 而不是 &lt;code>arp -a&lt;/code>，因為 &lt;code>ip&lt;/code>（iproute2）在現代最小系統一定有，&lt;code>arp&lt;/code>（net-tools）常常沒裝、跑了會 command not found 反而誤導。如果狀態是 &lt;code>INCOMPLETE&lt;/code>（&lt;code>arp -a&lt;/code> 顯示的是 &lt;code>incomplete&lt;/code>），代表這個 IP 在鏈路層上根本沒有機器回應——不是 SSH 的問題，是那台機器的網路沒起來、或根本沒在跑。一個實際案例：一台虛擬機 SSH timeout，鄰居表顯示整個網段的 guest 位址全是 incomplete、只有閘道（宿主那側的橋接介面）是好的——這就定位到「宿主的橋沒問題，但橋的另一頭沒有 VM 在講話」，方向立刻從「調 SSH」轉到「去看 VM 的網路或開機狀態」。&lt;/p>
&lt;p>定位到「機器在跑但網路沒起來」後，去那台機器的主控台（不是 SSH，SSH 正是連不上的東西）確認：&lt;code>ip -brief a&lt;/code> 看有沒有拿到 IP、&lt;code>systemctl status &amp;lt;網路服務&amp;gt;&lt;/code>（&lt;code>dhcpcd&lt;/code> / &lt;code>systemd-networkd&lt;/code>）看網路服務起了沒，需要時 &lt;code>sudo systemctl restart &amp;lt;網路服務&amp;gt;&lt;/code> 重拉。IP 回來、鄰居表的條目從 incomplete 變成有 MAC，就通了。&lt;/p>
&lt;p>還有一個常見誤區是 IP 變了。SSH 的別名、金鑰、&lt;code>known_hosts&lt;/code> 都綁在特定機器身分上；換機器 / 重裝 / DHCP 重配後 IP 或 host key 變了，用舊別名會連錯或被 host key 檢查擋。這條的判讀與修法（&lt;code>ssh user@新IP&lt;/code> 直連、&lt;code>ssh-keygen -R&lt;/code>）見 &lt;a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑&lt;/a>。&lt;/p>
&lt;h2 id="網路通但域名解析不了">網路通、但域名解析不了&lt;/h2>
&lt;p>有一種故障看起來像「網路壞了」，其實是 DNS 解析斷了：能連 IP、卻連不上任何用域名的東西——&lt;code>ping 8.8.8.8&lt;/code> 通、但 &lt;code>ping google.com&lt;/code>、&lt;code>pacman -Sy&lt;/code>、&lt;code>curl https://...&lt;/code> 全失敗。判讀要跟前面「網路沒起來」分開，因為網路層是通的，斷的是「域名 → IP」這一步。權威檢查：&lt;code>ping &amp;lt;IP&amp;gt;&lt;/code> 通而 &lt;code>ping &amp;lt;域名&amp;gt;&lt;/code> 不通、或 &lt;code>getent hosts &amp;lt;域名&amp;gt;&lt;/code>（&lt;code>resolvectl query &amp;lt;域名&amp;gt;&lt;/code> 若有 systemd-resolved）解不出位址，就定位到 DNS。常見成因是 &lt;code>/etc/resolv.conf&lt;/code> 沒有可用的 nameserver（新裝或網路重設後沒填），或負責 DNS 的服務沒起來。修：確認 &lt;code>/etc/resolv.conf&lt;/code> 有一行 &lt;code>nameserver&lt;/code>（如 &lt;code>nameserver 1.1.1.1&lt;/code>）、&lt;code>systemctl status systemd-resolved&lt;/code>（若用它）。這一層在剛裝好的最小系統特別常撞到——&lt;code>ip -brief a&lt;/code> 明明有 IP，&lt;code>pacman&lt;/code> 或 bootstrap 卻抓不到套件，看起來像「網路好好的卻裝不了東西」，根因是 DNS 沒設。&lt;/p>
&lt;h2 id="虛擬機開不起來分清-guest-內部還是宿主側">虛擬機開不起來：分清 guest 內部還是宿主側&lt;/h2>
&lt;p>虛擬機開機失敗時，關鍵判斷是「錯誤來自 guest 內部（作業系統層）還是宿主側（虛擬化軟體 / QEMU 層）」。宿主側的錯誤訊息通常來自虛擬機軟體本身、在 guest 還沒開始開機前就跳出來，跟 guest 裡裝了什麼無關。&lt;/p>
&lt;p>一個實例是 QEMU 報「找不到某個 ROM 檔」（例如 &lt;code>efi-virtio.rom&lt;/code>）而拒絕啟動。第一反應可能是「檔案不見了要重裝」，但正確的第一步是&lt;strong>去確認那個檔在不在&lt;/strong>——實際去虛擬機軟體的安裝目錄裡找（&lt;code>find &amp;lt;安裝目錄&amp;gt; -name '&amp;lt;rom名&amp;gt;'&lt;/code>），會發現 ROM 檔明明就在。既然檔案在，「找不到」就不是缺檔，是 QEMU 執行時&lt;strong>在它預期的路徑下找不到&lt;/strong>——成因隨宿主 OS 不同。&lt;strong>在 macOS + UTM 宿主上&lt;/strong>，最常見的是 Gatekeeper app translocation：帶隔離屬性的 app 被搬到一個隨機唯讀路徑跑，讓 QEMU 解析資源的相對路徑失效，明明存在的檔案在那個執行路徑下就找不到。&lt;strong>在 Linux 宿主上&lt;/strong>（沒有 translocation 這回事），同樣的「找不到 ROM」通常是缺對應套件（&lt;code>ovmf&lt;/code> / &lt;code>ipxe-roms&lt;/code> / &lt;code>edk2-ovmf&lt;/code>）、libvirt XML 指的 ROM 路徑錯、或檔案權限不對——一樣先確認檔在哪、QEMU 是用哪個路徑去找。&lt;/p>
&lt;p>另外兩個常見的「VM 起不來」故障也順手一起排除，它們不會特定產生「找不到 ROM」但常伴隨出現：上一次崩潰殘留的 helper 行程卡著（&lt;code>pgrep -af 'qemu|&amp;lt;虛擬機軟體名&amp;gt;'&lt;/code> 找，沒清乾淨會佔住資源），以及宿主磁碟滿（&lt;code>df -h&lt;/code>，啟動要寫暫存 / 狀態檔）。多數情況下，完全退出虛擬機軟體（連殘留 helper 一起清）+ 清出宿主磁碟空間 + 重新啟動，就恢復了。&lt;/p></description><content:encoded><![CDATA[<p>一台原本能連的機器突然連不上，或一台虛擬機根本開不起來，判讀的方向是「從你這端往那台機器，一層一層確認哪裡斷了」，而不是反覆重試同一個連線動作。連線失敗是最終症狀，真正斷掉的可能是網路、可能是那台機器的某個服務沒起來、可能是虛擬機的宿主側出問題、也可能是一個把上面全部拖下水的共同根因：磁碟滿。這篇從網路層與宿主側的權威狀態切入，把「連不上」拆成可定位的環節。</p>
<h2 id="遠端機器突然連不上先分清是哪一段斷">遠端機器突然連不上：先分清是哪一段斷</h2>
<p>一台昨天還能 SSH 的機器今天連不上，第一步是確認「網路層通不通」，跟「SSH 服務在不在」分開。連線在 TCP 就 timeout（連 port 22 卡住沒回應），多半是網路層或機器沒在跑；連線有回應但被拒（<code>Connection refused</code>），是網路通、但那台機器上沒有服務在聽 port 22。</p>
<p>對虛擬機或同網段的機器，一個很有用的權威來源是<strong>鄰居表</strong>（IP 對 MAC 的對應）。要填起來需要對方在鏈路層有回應，所以它直接反映「對方在不在」。用 <code>ip neigh</code> 看目標 IP 的條目——優先用 <code>ip neigh</code> 而不是 <code>arp -a</code>，因為 <code>ip</code>（iproute2）在現代最小系統一定有，<code>arp</code>（net-tools）常常沒裝、跑了會 command not found 反而誤導。如果狀態是 <code>INCOMPLETE</code>（<code>arp -a</code> 顯示的是 <code>incomplete</code>），代表這個 IP 在鏈路層上根本沒有機器回應——不是 SSH 的問題，是那台機器的網路沒起來、或根本沒在跑。一個實際案例：一台虛擬機 SSH timeout，鄰居表顯示整個網段的 guest 位址全是 incomplete、只有閘道（宿主那側的橋接介面）是好的——這就定位到「宿主的橋沒問題，但橋的另一頭沒有 VM 在講話」，方向立刻從「調 SSH」轉到「去看 VM 的網路或開機狀態」。</p>
<p>定位到「機器在跑但網路沒起來」後，去那台機器的主控台（不是 SSH，SSH 正是連不上的東西）確認：<code>ip -brief a</code> 看有沒有拿到 IP、<code>systemctl status &lt;網路服務&gt;</code>（<code>dhcpcd</code> / <code>systemd-networkd</code>）看網路服務起了沒，需要時 <code>sudo systemctl restart &lt;網路服務&gt;</code> 重拉。IP 回來、鄰居表的條目從 incomplete 變成有 MAC，就通了。</p>
<p>還有一個常見誤區是 IP 變了。SSH 的別名、金鑰、<code>known_hosts</code> 都綁在特定機器身分上；換機器 / 重裝 / DHCP 重配後 IP 或 host key 變了，用舊別名會連錯或被 host key 檢查擋。這條的判讀與修法（<code>ssh user@新IP</code> 直連、<code>ssh-keygen -R</code>）見 <a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑</a>。</p>
<h2 id="網路通但域名解析不了">網路通、但域名解析不了</h2>
<p>有一種故障看起來像「網路壞了」，其實是 DNS 解析斷了：能連 IP、卻連不上任何用域名的東西——<code>ping 8.8.8.8</code> 通、但 <code>ping google.com</code>、<code>pacman -Sy</code>、<code>curl https://...</code> 全失敗。判讀要跟前面「網路沒起來」分開，因為網路層是通的，斷的是「域名 → IP」這一步。權威檢查：<code>ping &lt;IP&gt;</code> 通而 <code>ping &lt;域名&gt;</code> 不通、或 <code>getent hosts &lt;域名&gt;</code>（<code>resolvectl query &lt;域名&gt;</code> 若有 systemd-resolved）解不出位址，就定位到 DNS。常見成因是 <code>/etc/resolv.conf</code> 沒有可用的 nameserver（新裝或網路重設後沒填），或負責 DNS 的服務沒起來。修：確認 <code>/etc/resolv.conf</code> 有一行 <code>nameserver</code>（如 <code>nameserver 1.1.1.1</code>）、<code>systemctl status systemd-resolved</code>（若用它）。這一層在剛裝好的最小系統特別常撞到——<code>ip -brief a</code> 明明有 IP，<code>pacman</code> 或 bootstrap 卻抓不到套件，看起來像「網路好好的卻裝不了東西」，根因是 DNS 沒設。</p>
<h2 id="虛擬機開不起來分清-guest-內部還是宿主側">虛擬機開不起來：分清 guest 內部還是宿主側</h2>
<p>虛擬機開機失敗時，關鍵判斷是「錯誤來自 guest 內部（作業系統層）還是宿主側（虛擬化軟體 / QEMU 層）」。宿主側的錯誤訊息通常來自虛擬機軟體本身、在 guest 還沒開始開機前就跳出來，跟 guest 裡裝了什麼無關。</p>
<p>一個實例是 QEMU 報「找不到某個 ROM 檔」（例如 <code>efi-virtio.rom</code>）而拒絕啟動。第一反應可能是「檔案不見了要重裝」，但正確的第一步是<strong>去確認那個檔在不在</strong>——實際去虛擬機軟體的安裝目錄裡找（<code>find &lt;安裝目錄&gt; -name '&lt;rom名&gt;'</code>），會發現 ROM 檔明明就在。既然檔案在，「找不到」就不是缺檔，是 QEMU 執行時<strong>在它預期的路徑下找不到</strong>——成因隨宿主 OS 不同。<strong>在 macOS + UTM 宿主上</strong>，最常見的是 Gatekeeper app translocation：帶隔離屬性的 app 被搬到一個隨機唯讀路徑跑，讓 QEMU 解析資源的相對路徑失效，明明存在的檔案在那個執行路徑下就找不到。<strong>在 Linux 宿主上</strong>（沒有 translocation 這回事），同樣的「找不到 ROM」通常是缺對應套件（<code>ovmf</code> / <code>ipxe-roms</code> / <code>edk2-ovmf</code>）、libvirt XML 指的 ROM 路徑錯、或檔案權限不對——一樣先確認檔在哪、QEMU 是用哪個路徑去找。</p>
<p>另外兩個常見的「VM 起不來」故障也順手一起排除，它們不會特定產生「找不到 ROM」但常伴隨出現：上一次崩潰殘留的 helper 行程卡著（<code>pgrep -af 'qemu|&lt;虛擬機軟體名&gt;'</code> 找，沒清乾淨會佔住資源），以及宿主磁碟滿（<code>df -h</code>，啟動要寫暫存 / 狀態檔）。多數情況下，完全退出虛擬機軟體（連殘留 helper 一起清）+ 清出宿主磁碟空間 + 重新啟動，就恢復了。</p>
<p>判讀通則：<strong>虛擬機開不起來，先讀錯誤訊息判斷是 guest 還是宿主側；宿主側報「找不到某資源」而資源其實存在時，往「QEMU 是用哪個路徑找、那條路徑對不對」查（macOS 是 translocation、Linux 是缺套件 / 路徑 / 權限），再順手排除殘留行程與磁碟滿，而不是急著重裝。</strong></p>
<h2 id="磁碟滿是連鎖故障的共同根因">磁碟滿是連鎖故障的共同根因</h2>
<p>很多看起來各自獨立的故障，共同根因是磁碟滿。磁碟一滿，寫入就會失敗，而系統裡太多東西依賴寫入：SSH session 可能因為寫不了而被斷、正在跑的編譯 / 安裝會中途失敗、log 寫不進去、虛擬機狀態檔存不下導致連不上或開不起來。所以當你在短時間內撞到「連線斷了 + 某個任務失敗 + 服務怪怪的」這種一串症狀時，<code>df -h</code> 應該是很早就要做的檢查——一個廉價的檢查就可能一次解釋掉全部。</p>
<p>這裡有一個容易搞錯的點：<strong>清錯了地方</strong>。宿主跟 guest 是兩個獨立的檔案系統；虛擬機的宿主磁碟滿，跟 guest 內部磁碟滿，是兩件事。如果你 SSH 進 guest 裡 <code>df</code> 看到還有空間就以為沒事，但真正滿的是宿主的磁碟，那問題不會解決。判讀時要分清這串故障是「哪一台機器的哪個檔案系統」滿了——在宿主上 <code>df -h</code> 看宿主、在 guest 裡 <code>df -h</code> 看 guest，兩邊都要確認。清空間也要清在對的那一側。</p>
<h2 id="判讀路由">判讀路由</h2>
<ul>
<li>SSH timeout（TCP 卡住）→ 網路層或機器沒跑，查 <code>ip neigh</code>（<code>INCOMPLETE</code> = 對方沒回應）→ 去主控台看 <code>ip -brief a</code> / 網路服務。</li>
<li><code>Connection refused</code> → 網路通、但沒有服務在聽 → 去機器上確認 sshd 起了沒。</li>
<li>能 ping IP、不能用域名（<code>pacman</code> / <code>curl</code> 失敗）→ DNS 解析問題，查 <code>/etc/resolv.conf</code> 有沒有 nameserver、<code>systemd-resolved</code> 起了沒，不是網路層斷。</li>
<li>連錯 / host key 被擋 → IP 或身分變了，見 <a href="../../install/ssh-keyless-bootstrap/">外部連入與無 key 的 bootstrap 路徑</a>。</li>
<li>虛擬機開不起來、宿主側報「找不到資源」但資源在 → 主因查路徑隔離，再排除殘留行程（<code>pgrep -af 'qemu\|...'</code>）/ 磁碟。</li>
<li>一串症狀同時發生 → 早點 <code>df -h</code>，宿主與 guest 兩側都查，磁碟滿常是共同根因。</li>
</ul>
<p>連不上只是最終症狀，真正的定位靠網路表、服務狀態、資源用量這些權威來源一層層往回推——完整的判讀紀律見 <a href="../diagnosis-read-authoritative-state/">診斷心法</a>。</p>
]]></content:encoded></item><item><title>程序、服務與狀態怎麼判</title><link>https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/process-service-state-diagnosis/</guid><description>&lt;p>判斷「某個東西現在是什麼狀態」——程式活著沒、服務由誰提供、螢幕鎖了沒、session 還在不在——是除錯裡最常做、也最常判錯的一步。判錯多半不是工具不對，是問錯了來源：用一個猜的名字去掃行程、用畫面有沒有反應去推服務狀態、用畫面上有沒有某個元素去斷定 session 狀態。這篇把幾個常見的狀態判斷，對到它們各自的權威來源與正確工具。&lt;/p>
&lt;p>底層的心法（讀權威狀態、不靠肉眼）見 &lt;a href="../diagnosis-read-authoritative-state/">診斷心法&lt;/a>，這篇是它在「程序 / 服務 / 狀態」這一類的具體招式。&lt;/p>
&lt;h2 id="程式活著沒比對正確的行程名">程式活著沒：比對正確的行程名&lt;/h2>
&lt;p>判斷一個程式在不在，行程表是權威來源，&lt;code>pgrep&lt;/code> / &lt;code>ps&lt;/code> 是對的工具，但成敗在於&lt;strong>比對正確的行程名&lt;/strong>（comm，行程表裡記的執行檔短名，可從 &lt;code>/proc/&amp;lt;pid&amp;gt;/comm&lt;/code> 看）。一個實際的坑：某個桌面 shell（畫桌面 UI 的圖形程式，不是 bash/zsh 那種命令列 shell）的可執行檔叫 &lt;code>quickshell&lt;/code>，但透過名為 &lt;code>qs&lt;/code> 的 symlink 啟動時，它在行程表裡的 comm 是 &lt;code>qs&lt;/code>。這時 &lt;code>pgrep quickshell&lt;/code> 找不到，很容易誤判成程式掛了、甚至誤觸「重啟」而引發更大的問題，實際上它以 &lt;code>qs&lt;/code> 這個名字好好跑著。&lt;/p>
&lt;p>可靠的做法：&lt;/p>
&lt;ul>
&lt;li>先確認實際的 comm 名：&lt;code>ps -eo pid,comm | grep -i &amp;lt;關鍵字&amp;gt;&lt;/code>，或看你啟動它的實際指令。&lt;/li>
&lt;li>用精確比對：&lt;code>pgrep -x &amp;lt;comm&amp;gt;&lt;/code>（&lt;code>-x&lt;/code> 要求完全相符），或 &lt;code>pgrep -af &amp;lt;pattern&amp;gt;&lt;/code> 連完整命令列一起比對，避免被 symlink 名 / 縮寫名騙。&lt;/li>
&lt;li>另一個 comm 的坑：kernel 把 comm 截在 15 字元（&lt;code>TASK_COMM_LEN&lt;/code>），名字超過 15 字的程式用 &lt;code>pgrep -x &amp;lt;完整長名&amp;gt;&lt;/code> 反而 miss——這時改用 &lt;code>pgrep -af &amp;lt;pattern&amp;gt;&lt;/code> 比對完整命令列。&lt;/li>
&lt;li>別用一個「你以為的名字」掃過去就下生死結論——行程表沒騙你，是查詢條件寫錯。&lt;/li>
&lt;/ul>
&lt;h3 id="進程活著--內部子系統活著">進程活著 ≠ 內部子系統活著&lt;/h3>
&lt;p>比對到了正確的 comm、&lt;code>pgrep&lt;/code> 也有輸出，只證明「這個進程存在」，不證明「它內部在正常運作」。有一類故障是進程好端端活著（&lt;code>pgrep&lt;/code> 找得到、STAT 是正常的 &lt;code>S&lt;/code>、在 &lt;code>poll&lt;/code> 等事件、CPU 不高），但它內部某個子系統已經 wedged——例如一個圖形 shell 的 QML scene 因為上游錯誤（渲染 pipeline 建失敗之類）某個物件沒建起來變 null，於是負責互動的模組全部失效。表現是 bar 還畫得出來、卻點不動，keybind 叫不出東西，但焦點視窗打字正常。這時 &lt;code>pgrep&lt;/code> 會騙你說「在跑」。&lt;/p>
&lt;p>這種情況權威來源不是行程表，是&lt;strong>程式自己的 log&lt;/strong>，而且這種 log 常常不在 &lt;code>journalctl&lt;/code>、也不在你猜的路徑，要用該程式專屬的 log 指令（例如某桌面 shell 的 &lt;code>&amp;lt;shell&amp;gt; -l&lt;/code>）。log 裡的 &lt;code>TypeError: Cannot read property 'X' of null&lt;/code> 這類訊息，才是「進程活著但子系統死了」的定案證據。另一個更精準的活性探針是程式的 &lt;strong>IPC 回不回真實狀態&lt;/strong>：正常時查詢會回傳資料、子系統死掉時回空——這比「進程在不在」可靠得多。判「進程活著到底有沒有在運作」時，讀它自己的 log 與 IPC，不是看 &lt;code>pgrep&lt;/code> 有沒有輸出。桌面 shell 的具體案例與恢復（讀 &lt;code>caelestia shell -l&lt;/code> 抓到 null 根因、重啟重建 scene）見 &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;/p>
&lt;h2 id="服務由誰提供問註冊表">服務由誰提供：問註冊表&lt;/h2>
&lt;p>「某個系統服務現在由哪個程式在提供」，權威來源是服務註冊，不是畫面。桌面服務多半註冊在 &lt;strong>D-Bus&lt;/strong>（Linux 桌面的行程間訊息匯流排）上：一個服務用一個名字掛在上面，而&lt;strong>同一個名字同一時間只能被一個行程擁有&lt;/strong>。以桌面通知為例，&lt;code>org.freedesktop.Notifications&lt;/code> 這個 D-Bus 名同一時間只有一個擁有者——兩個通知 daemon（例如 mako 跟某個桌面 shell 內建的通知服務）不能共存，誰先註冊誰佔著，後者只能等前者退出。&lt;/p>
&lt;p>想知道現在是誰接管，查註冊表而不是送一則通知看畫面：&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"># 查 org.freedesktop.Notifications 目前被哪個連線擁有&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nv">owner&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> org.freedesktop.DBus GetNameOwner s org.freedesktop.Notifications &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $2}&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> tr -d &lt;span class="s1">&amp;#39;&amp;#34;&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 把那個連線換算成 PID，再看行程名&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="nv">pid&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> org.freedesktop.DBus GetConnectionUnixProcessID s &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$owner&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{print $2}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">ps -o &lt;span class="nv">comm&lt;/span>&lt;span class="o">=&lt;/span> -p &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$pid&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>停掉舊 daemon 前擁有者是舊的、停掉後換成新的，就確認接管成功。這比「送通知看畫面有沒有跳」可靠——畫面沒跳可能是勿擾模式吃掉、可能根本沒送出，畫面反應不等於服務歸屬。切換兩個搶同一服務名的 daemon 時，這也解釋了為什麼「新的裝了卻沒作用」：舊的還佔著名字，新的靜默註冊失敗（通常只在它的 log 留一行 warning），得先停掉舊的。&lt;/p></description><content:encoded><![CDATA[<p>判斷「某個東西現在是什麼狀態」——程式活著沒、服務由誰提供、螢幕鎖了沒、session 還在不在——是除錯裡最常做、也最常判錯的一步。判錯多半不是工具不對，是問錯了來源：用一個猜的名字去掃行程、用畫面有沒有反應去推服務狀態、用畫面上有沒有某個元素去斷定 session 狀態。這篇把幾個常見的狀態判斷，對到它們各自的權威來源與正確工具。</p>
<p>底層的心法（讀權威狀態、不靠肉眼）見 <a href="../diagnosis-read-authoritative-state/">診斷心法</a>，這篇是它在「程序 / 服務 / 狀態」這一類的具體招式。</p>
<h2 id="程式活著沒比對正確的行程名">程式活著沒：比對正確的行程名</h2>
<p>判斷一個程式在不在，行程表是權威來源，<code>pgrep</code> / <code>ps</code> 是對的工具，但成敗在於<strong>比對正確的行程名</strong>（comm，行程表裡記的執行檔短名，可從 <code>/proc/&lt;pid&gt;/comm</code> 看）。一個實際的坑：某個桌面 shell（畫桌面 UI 的圖形程式，不是 bash/zsh 那種命令列 shell）的可執行檔叫 <code>quickshell</code>，但透過名為 <code>qs</code> 的 symlink 啟動時，它在行程表裡的 comm 是 <code>qs</code>。這時 <code>pgrep quickshell</code> 找不到，很容易誤判成程式掛了、甚至誤觸「重啟」而引發更大的問題，實際上它以 <code>qs</code> 這個名字好好跑著。</p>
<p>可靠的做法：</p>
<ul>
<li>先確認實際的 comm 名：<code>ps -eo pid,comm | grep -i &lt;關鍵字&gt;</code>，或看你啟動它的實際指令。</li>
<li>用精確比對：<code>pgrep -x &lt;comm&gt;</code>（<code>-x</code> 要求完全相符），或 <code>pgrep -af &lt;pattern&gt;</code> 連完整命令列一起比對，避免被 symlink 名 / 縮寫名騙。</li>
<li>另一個 comm 的坑：kernel 把 comm 截在 15 字元（<code>TASK_COMM_LEN</code>），名字超過 15 字的程式用 <code>pgrep -x &lt;完整長名&gt;</code> 反而 miss——這時改用 <code>pgrep -af &lt;pattern&gt;</code> 比對完整命令列。</li>
<li>別用一個「你以為的名字」掃過去就下生死結論——行程表沒騙你，是查詢條件寫錯。</li>
</ul>
<h3 id="進程活著--內部子系統活著">進程活著 ≠ 內部子系統活著</h3>
<p>比對到了正確的 comm、<code>pgrep</code> 也有輸出，只證明「這個進程存在」，不證明「它內部在正常運作」。有一類故障是進程好端端活著（<code>pgrep</code> 找得到、STAT 是正常的 <code>S</code>、在 <code>poll</code> 等事件、CPU 不高），但它內部某個子系統已經 wedged——例如一個圖形 shell 的 QML scene 因為上游錯誤（渲染 pipeline 建失敗之類）某個物件沒建起來變 null，於是負責互動的模組全部失效。表現是 bar 還畫得出來、卻點不動，keybind 叫不出東西，但焦點視窗打字正常。這時 <code>pgrep</code> 會騙你說「在跑」。</p>
<p>這種情況權威來源不是行程表，是<strong>程式自己的 log</strong>，而且這種 log 常常不在 <code>journalctl</code>、也不在你猜的路徑，要用該程式專屬的 log 指令（例如某桌面 shell 的 <code>&lt;shell&gt; -l</code>）。log 裡的 <code>TypeError: Cannot read property 'X' of null</code> 這類訊息，才是「進程活著但子系統死了」的定案證據。另一個更精準的活性探針是程式的 <strong>IPC 回不回真實狀態</strong>：正常時查詢會回傳資料、子系統死掉時回空——這比「進程在不在」可靠得多。判「進程活著到底有沒有在運作」時，讀它自己的 log 與 IPC，不是看 <code>pgrep</code> 有沒有輸出。桌面 shell 的具體案例與恢復（讀 <code>caelestia shell -l</code> 抓到 null 根因、重啟重建 scene）見 <a href="/blog/linux/dotfile/07-desktop-maintenance/common-failures-recovery/" data-link-title="常見故障場景與恢復操作" data-link-desc="Hyprland 黑屏、waybar 消失、畫面凍結、記憶體爆掉或 config 寫錯導致進不了桌面時，按症狀查恢復操作">常見故障場景與恢復操作</a> 的「畫得出來但互動死掉」場景。</p>
<h2 id="服務由誰提供問註冊表">服務由誰提供：問註冊表</h2>
<p>「某個系統服務現在由哪個程式在提供」，權威來源是服務註冊，不是畫面。桌面服務多半註冊在 <strong>D-Bus</strong>（Linux 桌面的行程間訊息匯流排）上：一個服務用一個名字掛在上面，而<strong>同一個名字同一時間只能被一個行程擁有</strong>。以桌面通知為例，<code>org.freedesktop.Notifications</code> 這個 D-Bus 名同一時間只有一個擁有者——兩個通知 daemon（例如 mako 跟某個桌面 shell 內建的通知服務）不能共存，誰先註冊誰佔著，後者只能等前者退出。</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"># 查 org.freedesktop.Notifications 目前被哪個連線擁有</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nv">owner</span><span class="o">=</span><span class="k">$(</span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  org.freedesktop.DBus GetNameOwner s org.freedesktop.Notifications <span class="p">|</span> awk <span class="s1">&#39;{print $2}&#39;</span> <span class="p">|</span> tr -d <span class="s1">&#39;&#34;&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 把那個連線換算成 PID，再看行程名</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="nv">pid</span><span class="o">=</span><span class="k">$(</span>busctl --user call org.freedesktop.DBus /org/freedesktop/DBus <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  org.freedesktop.DBus GetConnectionUnixProcessID s <span class="s2">&#34;</span><span class="nv">$owner</span><span class="s2">&#34;</span> <span class="p">|</span> awk <span class="s1">&#39;{print $2}&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">ps -o <span class="nv">comm</span><span class="o">=</span> -p <span class="s2">&#34;</span><span class="nv">$pid</span><span class="s2">&#34;</span></span></span></code></pre></div><p>停掉舊 daemon 前擁有者是舊的、停掉後換成新的，就確認接管成功。這比「送通知看畫面有沒有跳」可靠——畫面沒跳可能是勿擾模式吃掉、可能根本沒送出，畫面反應不等於服務歸屬。切換兩個搶同一服務名的 daemon 時，這也解釋了為什麼「新的裝了卻沒作用」：舊的還佔著名字，新的靜默註冊失敗（通常只在它的 log 留一行 warning），得先停掉舊的。</p>
<h2 id="桌面-session-有沒有被鎖認清是哪一層的鎖">桌面 session 有沒有被鎖：認清是哪一層的鎖</h2>
<p>判斷一個圖形 session 有沒有被鎖，最容易被畫面帶偏，因為「畫面上有密碼框」很有說服力、卻不等於 session 真的被鎖（現代桌面 shell 的儀表板常內嵌鎖屏樣式的 widget）。而且鎖有不同層，查錯層會得到誤導的答案。</p>
<p>關鍵是分清兩種鎖：</p>
<ul>
<li><strong>logind 層的鎖</strong>：systemd 登入管理的 session 鎖，權威狀態是 <code>loginctl show-session &lt;id&gt; -p LockedHint</code>。</li>
<li><strong>Wayland 合成器層的鎖</strong>：走 <code>ext-session-lock</code> 協議、由<strong>合成器</strong>（compositor，Wayland 下負責把各視窗合成到螢幕、管輸入輸出的核心程式，約當 X11 時代的視窗管理器加顯示伺服器；Hyprland、Sway 等都是）管的鎖，跟 logind 是獨立機制。這種鎖 <code>loginctl</code> 的 <code>LockedHint</code> <strong>查不到</strong>——不是沒鎖，是查錯層。（用 GNOME / KDE 的鎖屏走的機制不同，以下的 <code>ext-session-lock</code> 判法與復原針對 wlroots 系的 Wayland 合成器。）</li>
</ul>
<p>所以「<code>loginctl</code> 沒有 <code>LockedHint</code>、<code>pgrep</code> 找不到獨立鎖屏程式」不足以斷定「沒鎖」：合成器層的鎖不歸 logind、而鎖屏畫面可能由 shell 主程式在自己行程內畫（沒有獨立可執行檔可抓）。這種情況真正的權威來源是那個 shell 自己的 log（有沒有載入鎖屏模組、idle 計時器有沒有觸發鎖定），或直接看 compositor 的 session-lock 狀態。判鎖看合成器 / shell 的 log，不是 <code>loginctl</code>、更不是畫面有沒有密碼框。</p>
<h3 id="鎖屏程式死掉造成的死局與復原">鎖屏程式死掉造成的死局與復原</h3>
<p><code>ext-session-lock</code> 有一個安全設計：持鎖的鎖屏程式若在鎖定狀態下崩潰 / 被中止，compositor <strong>會保持鎖定</strong>、不會因為鎖屏程式沒了就解鎖（否則殺掉鎖屏程式就成了繞過鎖的漏洞）。表現是畫面卡在「鎖屏程式已死」的安全提示。復原要從另一個 VT 或 SSH 用 <code>hyprctl keyword misc:allow_session_lock_restore 1</code> 允許新鎖屏 client 接管、再 <code>hyprctl dispatch exec hyprlock</code> 起一個接管後輸密碼解鎖。完整機制、兩層鎖的關係、各 compositor 的差異，見 <a href="/blog/linux/dotfile/knowledge-cards/session-lock/" data-link-title="Wayland Session Lock（鎖屏安全狀態）" data-link-desc="hyprlock / swaylock 畫面卡住、pkill 後進不了桌面、或要在 VM / 自動化環境測試鎖屏時回來讀">Wayland Session Lock 卡</a>。</p>
<p>診斷紀律：<strong>測鎖屏、或 <code>pkill</code> 一個持鎖的鎖屏程式時，要預期它把 session 卡在鎖定——這是協議的安全設計，不是 bug。</strong> 自動化 / 無人值守流程尤其要避免在持鎖狀態下殺鎖屏程式。</p>
<h2 id="終端機多工器的-session-還在不在">終端機多工器的 session 還在不在</h2>
<p>用 zellij / tmux 這類多工器跑遠端長任務時，判斷「重連後那個 session 還在不在」的權威來源是多工器自己的 session 列表，不是「我 SSH 斷了所以應該還在吧」的假設。<code>zellij ls</code>（或 <code>tmux ls</code>）會列出 session 與狀態：多工器是常駐在遠端的程序，SSH 斷不影響它，所以只要那台機器沒重開，<code>attach</code> 就能接回去；但如果機器重開過、或那個 session 因為資源不足（例如磁碟滿觸發的連鎖）被殺，列表會顯示它已 <code>EXITED</code> / 不存在，這種接不回去。</p>
<p>這裡有個順序上的紀律：<strong>當一個 session 可能已經死掉、而它裡面跑的任務有你在意的產出時，先確認產出有沒有被安全保存，再處理 session。</strong> 例如任務是在改 git repo，先 <code>git -C &lt;repo&gt; status</code> 跟 <code>git log @{u}..</code>（本地有、遠端沒有的 commit）確認有沒有沒推送的東西、把該推的推掉，再去 <code>zellij delete</code> 清死 session。搞反順序、先清了 session，可能連帶失去唯一還記得那些改動的地方。權威狀態（git 的推送狀態、多工器的 session 列表）先讀清楚，再動手。</p>
<h2 id="判讀路由">判讀路由</h2>
<ul>
<li>判程式活著 → <code>pgrep -x &lt;正確 comm&gt;</code> / <code>pgrep -af &lt;pattern&gt;</code>，先確認實際 comm 名，別用猜的名字。</li>
<li>判進程活著但「有沒有在運作」→ 讀程式自己的 log（可能要用它專屬的 log 指令、不在 journalctl）+ 它的 IPC 回不回真實狀態，不是看 <code>pgrep</code> 有輸出就當正常。</li>
<li>判服務歸誰 → <code>busctl</code> 查 D-Bus name 擁有者 → 換算 PID → comm，不看畫面反應。</li>
<li>判 session 鎖沒鎖 → 分清 logind 層（<code>loginctl LockedHint</code>）vs 合成器層（<code>ext-session-lock</code>，看 compositor / shell log），不看畫面有沒有密碼框。</li>
<li>鎖屏程式死掉卡住 → <code>allow_session_lock_restore</code> + 重起鎖屏程式接管解鎖。</li>
<li>判多工器 session 存活 → <code>zellij ls</code> / <code>tmux ls</code>；可能已死且有在意的產出時，先確認產出已保存 / 已推送再清 session。</li>
</ul>
<p>判不準時，<a href="../diagnosis-read-authoritative-state/">診斷心法</a> 的四步（描述症狀、定位權威來源、用對工具讀、矛盾時信權威）是通用的回退。</p>
]]></content:encoded></item><item><title>服務掛了怎麼自動知道：從肉眼盯到主動告警</title><link>https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/</link><pubDate>Thu, 02 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/debug/service-failure-monitoring/</guid><description>&lt;p>服務掛了不需要用肉眼盯——systemd 本來就在追蹤每個 unit 的狀態，你要做的是把「讀權威狀態」這件事自動化，並在狀態變成失敗時主動推播給自己。這篇跟本系列其他篇的差別在時機：診斷是出事後回頭找根因，監控是讓系統在出事的當下就告訴你。兩者共用同一個地基——權威狀態。診斷是手動讀一次權威狀態，監控是訂閱權威狀態的變化、變壞就推播。&lt;/p>
&lt;p>理解這個框架後，監控就不是「裝一套很重的東西」，而是分層選擇：從 systemd 內建的失敗鉤子（不裝任何額外服務），到推播管道，到「整台機器死掉」的體外心跳，到完整的指標儀表板。多數人只需要前一兩層。&lt;/p>
&lt;h2 id="你現在手動在做的事要被取代的基線">你現在手動在做的事（要被取代的基線）&lt;/h2>
&lt;p>在自動化之前，先認清手動版本——這也是所有告警底層讀的同一個權威來源：&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 --failed &lt;span class="c1"># 現在有哪些 unit 處於 failed（開機後系統怪怪的先掃這個）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">systemctl is-failed &amp;lt;unit&amp;gt; &lt;span class="c1"># 單一 unit 明確判失敗（比 is-active 直接）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">journalctl -u &amp;lt;unit&amp;gt; -f &lt;span class="c1"># 即時跟一個 unit 的 log&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>systemctl --failed&lt;/code> 就是「服務死活」的權威清單。手動版的問題不是不準，是你得記得去看。下面每一層都是把「記得去看」換成「壞了它來找你」。&lt;/p>
&lt;h2 id="第一層systemd-原生-onfailure-鉤子不裝額外服務">第一層：systemd 原生 &lt;code>OnFailure&lt;/code> 鉤子（不裝額外服務）&lt;/h2>
&lt;p>systemd 每個 unit 進入 failed 狀態時，可以自動觸發另一個 unit。這是最正統、零額外依賴的做法——告警邏輯就寫成一個普通的 systemd service。它由三塊組成：一個負責送通知的處理器 unit、一個實際送出的腳本、以及在你要監控的 unit 上掛一行 &lt;code>OnFailure=&lt;/code>。&lt;/p>
&lt;p>&lt;strong>通知處理器&lt;/strong>是一個 template unit（&lt;code>@&lt;/code> 表示可帶參數），參數 &lt;code>%i&lt;/code> 會是失敗的那個 unit 名：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># /etc/systemd/system/alert@.service&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">Description&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">Alert on failure of %i&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="na">Type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">oneshot&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="na">ExecStart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">/usr/local/bin/notify-failure %i&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>送出腳本&lt;/strong>負責把「哪個 unit、在哪台機、什麼時候」推出去。這裡有個實測踩到的坑：在 systemd service 的執行環境下，&lt;code>hostname&lt;/code> 指令可能回傳空字串，要改用 &lt;code>uname -n&lt;/code> 或讀 &lt;code>/etc/hostname&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">&lt;span class="cp">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="cp">&lt;/span>&lt;span class="c1"># /usr/local/bin/notify-failure （記得 chmod +x）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nv">unit&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$1&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 只在「真正放棄」時告警：OnFailure 每次失敗都觸發（含 auto-restart 中途，見下節實測），&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># auto-restart 中途 ActiveState 是 activating、撞重試上限才進 failed。gate 掉中途避免洗告警。&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nv">state&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>systemctl show &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> -p ActiveState --value&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="o">[&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$state&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="o">=&lt;/span> failed &lt;span class="o">]&lt;/span> &lt;span class="o">||&lt;/span> &lt;span class="nb">exit&lt;/span> &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="nv">host&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>uname -n&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="c1"># 不要用 hostname，systemd 環境下可能回空&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="nv">ts&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>date -Is&lt;span class="k">)&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="nv">topic&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s2">&amp;#34;你的私密topic&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">curl -fsS &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Title: &lt;/span>&lt;span class="nv">$host&lt;/span>&lt;span class="s2">: &lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2"> failed&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="nv">$unit&lt;/span>&lt;span class="s2"> 於 &lt;/span>&lt;span class="nv">$ts&lt;/span>&lt;span class="s2"> 進入 failed&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="s2">&amp;#34;https://ntfy.sh/&lt;/span>&lt;span class="nv">$topic&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>在要監控的 unit 掛上鉤子&lt;/strong>。針對單一 unit，加一行：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="na">OnFailure&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">alert@%n.service # %n 是本 unit 的全名，會展開成 alert@&amp;lt;本unit&amp;gt;.service&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>要&lt;strong>一次套用到所有 service&lt;/strong>，用 top-level drop-in（放在 &lt;code>service.d/&lt;/code> 這個型別目錄下的設定會套用到每個 &lt;code>.service&lt;/code>）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># /etc/systemd/system/service.d/onfailure.conf&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">OnFailure&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">alert@%n.service&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>改完 &lt;code>sudo systemctl daemon-reload&lt;/code>。&lt;strong>一個必須注意的遞迴陷阱&lt;/strong>：全域 drop-in 也會套到 &lt;code>alert@&lt;/code> 自己，它若失敗會觸發自己。給 &lt;code>alert@.service&lt;/code> 一個清空 &lt;code>OnFailure=&lt;/code> 的 override（&lt;code>[Unit]&lt;/code> 段寫 &lt;code>OnFailure=&lt;/code>）擋掉。&lt;/p>
&lt;p>這條鏈是實測驗證過的：故意讓一個 &lt;code>ExecStart=/bin/false&lt;/code> 的測試 service 失敗，systemd log 出現 &lt;code>Triggering OnFailure= dependencies&lt;/code>、&lt;code>alert@&lt;/code> 處理器被觸發跑完、&lt;code>curl&lt;/code> 推到 ntfy 回 HTTP 200——通知確實送出，全程沒有肉眼介入。&lt;/p>
&lt;h3 id="先自動重啟放棄了才吵你">先自動重啟、放棄了才吵你&lt;/h3>
&lt;p>多數暫時性失敗（一次連線抖動、一個 race）自己重試就好，不值得半夜叫醒你。把「自動復原」跟「告警」分兩段：讓 systemd 先重啟幾次，撐過重試上限才真的算放棄。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-ini" data-lang="ini">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">[Service]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="na">Restart&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">on-failure&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="na">RestartSec&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">5&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="k">[Unit]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="na">StartLimitBurst&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">3 # 重試 3 次&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="na">StartLimitIntervalSec&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="s">60 # 60 秒內都失敗才進 failed（start-limit-hit）&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>這裡有個實測踩到、跟直覺相反的坑&lt;/strong>：&lt;code>OnFailure&lt;/code> 不是「放棄才觸發」，而是&lt;strong>每一次失敗都觸發&lt;/strong>——包含 &lt;code>Restart=on-failure&lt;/code> 的每次 auto-restart 中途。實測一個反覆 crash 的服務（重試 3 次後放棄）觸發了 &lt;strong>4 次&lt;/strong> &lt;code>OnFailure&lt;/code>（3 次 auto-restart + 1 次最終 &lt;code>start-limit-hit&lt;/code>）。所以只靠 &lt;code>Restart=&lt;/code> + &lt;code>StartLimit=&lt;/code> 這段 config，你會被每次瞬斷洗告警。&lt;/p></description><content:encoded><![CDATA[<p>服務掛了不需要用肉眼盯——systemd 本來就在追蹤每個 unit 的狀態，你要做的是把「讀權威狀態」這件事自動化，並在狀態變成失敗時主動推播給自己。這篇跟本系列其他篇的差別在時機：診斷是出事後回頭找根因，監控是讓系統在出事的當下就告訴你。兩者共用同一個地基——權威狀態。診斷是手動讀一次權威狀態，監控是訂閱權威狀態的變化、變壞就推播。</p>
<p>理解這個框架後，監控就不是「裝一套很重的東西」，而是分層選擇：從 systemd 內建的失敗鉤子（不裝任何額外服務），到推播管道，到「整台機器死掉」的體外心跳，到完整的指標儀表板。多數人只需要前一兩層。</p>
<h2 id="你現在手動在做的事要被取代的基線">你現在手動在做的事（要被取代的基線）</h2>
<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">systemctl --failed          <span class="c1"># 現在有哪些 unit 處於 failed（開機後系統怪怪的先掃這個）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl is-failed &lt;unit&gt;  <span class="c1"># 單一 unit 明確判失敗（比 is-active 直接）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">journalctl -u &lt;unit&gt; -f     <span class="c1"># 即時跟一個 unit 的 log</span></span></span></code></pre></div><p><code>systemctl --failed</code> 就是「服務死活」的權威清單。手動版的問題不是不準，是你得記得去看。下面每一層都是把「記得去看」換成「壞了它來找你」。</p>
<h2 id="第一層systemd-原生-onfailure-鉤子不裝額外服務">第一層：systemd 原生 <code>OnFailure</code> 鉤子（不裝額外服務）</h2>
<p>systemd 每個 unit 進入 failed 狀態時，可以自動觸發另一個 unit。這是最正統、零額外依賴的做法——告警邏輯就寫成一個普通的 systemd service。它由三塊組成：一個負責送通知的處理器 unit、一個實際送出的腳本、以及在你要監控的 unit 上掛一行 <code>OnFailure=</code>。</p>
<p><strong>通知處理器</strong>是一個 template unit（<code>@</code> 表示可帶參數），參數 <code>%i</code> 會是失敗的那個 unit 名：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/alert@.service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Description</span><span class="o">=</span><span class="s">Alert on failure of %i</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/local/bin/notify-failure %i</span></span></span></code></pre></div><p><strong>送出腳本</strong>負責把「哪個 unit、在哪台機、什麼時候」推出去。這裡有個實測踩到的坑：在 systemd service 的執行環境下，<code>hostname</code> 指令可能回傳空字串，要改用 <code>uname -n</code> 或讀 <code>/etc/hostname</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="cp">#!/bin/bash
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="cp"></span><span class="c1"># /usr/local/bin/notify-failure   （記得 chmod +x）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nv">unit</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 只在「真正放棄」時告警：OnFailure 每次失敗都觸發（含 auto-restart 中途，見下節實測），</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># auto-restart 中途 ActiveState 是 activating、撞重試上限才進 failed。gate 掉中途避免洗告警。</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nv">state</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>systemctl show <span class="s2">&#34;</span><span class="nv">$unit</span><span class="s2">&#34;</span> -p ActiveState --value<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="o">[</span> <span class="s2">&#34;</span><span class="nv">$state</span><span class="s2">&#34;</span> <span class="o">=</span> failed <span class="o">]</span> <span class="o">||</span> <span class="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="nv">host</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>uname -n<span class="k">)</span><span class="s2">&#34;</span>                     <span class="c1"># 不要用 hostname，systemd 環境下可能回空</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nv">ts</span><span class="o">=</span><span class="s2">&#34;</span><span class="k">$(</span>date -Is<span class="k">)</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nv">topic</span><span class="o">=</span><span class="s2">&#34;你的私密topic&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">curl -fsS <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  -H <span class="s2">&#34;Title: </span><span class="nv">$host</span><span class="s2">: </span><span class="nv">$unit</span><span class="s2"> failed&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  -d <span class="s2">&#34;</span><span class="nv">$unit</span><span class="s2"> 於 </span><span class="nv">$ts</span><span class="s2"> 進入 failed&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  <span class="s2">&#34;https://ntfy.sh/</span><span class="nv">$topic</span><span class="s2">&#34;</span></span></span></code></pre></div><p><strong>在要監控的 unit 掛上鉤子</strong>。針對單一 unit，加一行：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">OnFailure</span><span class="o">=</span><span class="s">alert@%n.service    # %n 是本 unit 的全名，會展開成 alert@&lt;本unit&gt;.service</span></span></span></code></pre></div><p>要<strong>一次套用到所有 service</strong>，用 top-level drop-in（放在 <code>service.d/</code> 這個型別目錄下的設定會套用到每個 <code>.service</code>）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/service.d/onfailure.conf</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">OnFailure</span><span class="o">=</span><span class="s">alert@%n.service</span></span></span></code></pre></div><p>改完 <code>sudo systemctl daemon-reload</code>。<strong>一個必須注意的遞迴陷阱</strong>：全域 drop-in 也會套到 <code>alert@</code> 自己，它若失敗會觸發自己。給 <code>alert@.service</code> 一個清空 <code>OnFailure=</code> 的 override（<code>[Unit]</code> 段寫 <code>OnFailure=</code>）擋掉。</p>
<p>這條鏈是實測驗證過的：故意讓一個 <code>ExecStart=/bin/false</code> 的測試 service 失敗，systemd log 出現 <code>Triggering OnFailure= dependencies</code>、<code>alert@</code> 處理器被觸發跑完、<code>curl</code> 推到 ntfy 回 HTTP 200——通知確實送出，全程沒有肉眼介入。</p>
<h3 id="先自動重啟放棄了才吵你">先自動重啟、放棄了才吵你</h3>
<p>多數暫時性失敗（一次連線抖動、一個 race）自己重試就好，不值得半夜叫醒你。把「自動復原」跟「告警」分兩段：讓 systemd 先重啟幾次，撐過重試上限才真的算放棄。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="na">Restart</span><span class="o">=</span><span class="s">on-failure</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">RestartSec</span><span class="o">=</span><span class="s">5</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">[Unit]</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="na">StartLimitBurst</span><span class="o">=</span><span class="s">3          # 重試 3 次</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="na">StartLimitIntervalSec</span><span class="o">=</span><span class="s">60   # 60 秒內都失敗才進 failed（start-limit-hit）</span></span></span></code></pre></div><p><strong>這裡有個實測踩到、跟直覺相反的坑</strong>：<code>OnFailure</code> 不是「放棄才觸發」，而是<strong>每一次失敗都觸發</strong>——包含 <code>Restart=on-failure</code> 的每次 auto-restart 中途。實測一個反覆 crash 的服務（重試 3 次後放棄）觸發了 <strong>4 次</strong> <code>OnFailure</code>（3 次 auto-restart + 1 次最終 <code>start-limit-hit</code>）。所以只靠 <code>Restart=</code> + <code>StartLimit=</code> 這段 config，你會被每次瞬斷洗告警。</p>
<p>真正做到「只在放棄才吵」，靠的是上面送出腳本開頭那道 gate：<code>systemctl show &lt;unit&gt; -p ActiveState</code> 在 auto-restart 中途是 <code>activating</code>、撞上限進 failed 才是 <code>failed</code>，腳本只在 <code>failed</code> 才送。加上 gate 後同一個 crash 測試從 4 次告警降到 1 次（只剩最終放棄那次）。config 負責「重試幾次」，handler 的 gate 負責「只在終局告警」——兩段合起來才是完整的「先重啟、放棄才吵」。</p>
<h3 id="抓進程活著但沒在做事外部健康探針">抓「進程活著但沒在做事」：外部健康探針</h3>
<p><code>OnFailure</code> 抓的是「進程狀態變了」——crash、exit、被 kill。但服務可能<strong>進程還在、卻沒在做事</strong>：hung、deadlock、內部子系統壞掉。這種 systemd 看它還 <code>active</code>、不會觸發任何告警——正是<a href="../process-service-state-diagnosis/">「進程活著 ≠ 在運作」</a>那條，搬到監控場景。</p>
<p>要抓這種，得從外面<strong>主動戳它、看它回不回應</strong>：一個 timer 定時對服務發一個健康請求（HTTP 服務就 curl 它的 <code>/health</code>）並設逾時；戳不動、逾時失敗，就讓「那個檢查」自己 failed，一樣走 <code>OnFailure</code> 告警。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># health-check.service（oneshot）+ 一個每 2 分鐘跑的 .timer</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">/usr/bin/curl -fsS --max-time 5 http://127.0.0.1:8899/health</span></span></span></code></pre></div><p>實測對照最清楚：讓一個健康服務卡在 <code>sleep</code>（進程還在、單執行緒不再回應），<code>systemctl is-active</code> 仍顯示 <code>active</code>——systemd 沒察覺；但這個外部探針 curl <code>/health</code> 5 秒逾時、check 失敗、告警發出。<strong>systemd 抓進程死、外部探針抓進程活著但 hung，兩層互補、缺一漏一種。</strong></p>
<h3 id="canary先證明告警管線本身是好的">canary：先證明告警管線本身是好的</h3>
<p>監控最怕的失效模式是「出事時才發現它早就不會叫了」。防這個的辦法是養一隻 <strong>canary</strong>——一個你可控的假服務，專門用來確認整條管線是活的。它一物兩用：</p>
<ul>
<li><strong>驗證管線</strong>：故意弄掛它，看「失敗 → OnFailure → 推送」真的一路通到你手機，不必拿 sshd 這種真服務去冒險。</li>
<li><strong>當活性訊號</strong>：它自己若無故失敗告警，等於告訴你告警系統本身還在運作。</li>
</ul>
<p>做法是一個極簡 HTTP 服務（stdlib 就夠、不必框架），留幾個測試入口：<code>/health</code> 正常回、<code>/crash</code> 故意退出（測 <code>OnFailure</code>）、<code>/hang</code> 進程活著但不回應（測外部探針）。這樣任何時候都能一鍵重驗監控沒有默默失效。</p>
<h2 id="第二層推去哪裡關鍵是能離開這台機器">第二層：推去哪裡（關鍵是能離開這台機器）</h2>
<p>處理器腳本裡那一段 <code>curl</code> 可以換成任何管道：</p>
<ul>
<li><strong>ntfy</strong>（<code>ntfy.sh</code> 或自架）：一行 <code>curl</code> 推到手機，最省事，上面的例子就是。它怎麼運作、公共站 vs 自架、以及「topic 名稱就是唯一的密碼」這個安全模型，見 <a href="../ntfy-push-notification-service/">ntfy：推送通知服務</a>。</li>
<li><strong>email</strong>：要先設好一個 MTA（如 <code>msmtp</code>），腳本改成 <code>mail</code> / <code>sendmail</code>。</li>
<li><strong>Telegram bot、Apprise</strong>（一個工具打多個目標）等。</li>
</ul>
<p>判準只有一條：<strong>告警要送到機器外</strong>。送桌面 <code>notify-send</code> 只有你正盯著螢幕時才有用；送手機或 email，離開座位、人在外面也收得到。一台跑正事的機器，告警管道應該落在它之外。</p>
<h2 id="第三層整台機器死掉怎麼辦監控自己的盲點">第三層：整台機器死掉怎麼辦（監控自己的盲點）</h2>
<p><code>OnFailure</code> 有個根本限制：<strong>它靠 systemd 觸發，機器整台掛了（當機、斷電、kernel panic），systemd 自己都沒了，發不出任何告警。</strong> 這是所有「機器自己監控自己」方案的共同盲點——它報得了服務的死，報不了自己這台的死。</p>
<p>覆蓋這一層要反過來做：讓機器定時對一個<strong>體外</strong>的服務「報平安」，平安訊號一停，由那個體外服務替你告警。這叫 dead-man&rsquo;s switch（心跳監控）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># /etc/systemd/system/heartbeat.service</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">[Service]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="na">Type</span><span class="o">=</span><span class="s">oneshot</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="na">ExecStart</span><span class="o">=</span><span class="s">curl -fsS https://hc-ping.com/&lt;你的-uuid&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 搭配一個 heartbeat.timer，OnUnitActiveSec=5min 定時打</span></span></span></code></pre></div><p>心跳超過設定時間沒到，healthchecks.io（或自架的 Uptime Kuma）就通知你。<strong>體內的監控管不了自己這台的死亡，一定要有體外的一隻眼睛</strong>——這跟本系列 <a href="../machine-unreachable/">機器連不到或起不來</a> 是同一個問題的兩面：那篇是機器已經不回應時從外面怎麼查，心跳是讓「不回應」這件事本身自動觸發告警。</p>
<h2 id="第四層要指標趨勢門檻不只是-updown">第四層：要指標、趨勢、門檻（不只是 up/down）</h2>
<p>當你要的不只是「掛了沒」，而是 CPU、記憶體、磁碟、延遲的趨勢與門檻告警（例如磁碟用量超過 80% 就先警告，接上本系列反覆出現的「磁碟滿連鎖」），就進到完整監控堆疊：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>定位</th>
          <th>什麼時候選它</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Netdata</td>
          <td>開箱即用、自帶大量預設告警</td>
          <td>單機、想要圖表 + 門檻告警、最不想設定</td>
      </tr>
      <tr>
          <td>Monit</td>
          <td>輕量、每服務健康檢查 + 自動動作</td>
          <td>要「掛了自動跑一段修復腳本」、超出 systemd <code>Restart=</code> 能表達的邏輯</td>
      </tr>
      <tr>
          <td>Prometheus + Alertmanager</td>
          <td>指標抓取 + 告警規則引擎</td>
          <td>多台機器、要歷史數據與可擴展的告警規則</td>
      </tr>
      <tr>
          <td>Uptime Kuma</td>
          <td>自架的 up/down + 心跳面板</td>
          <td>想要一個面板統一看多台/多服務、也能當第三層的心跳接收端</td>
      </tr>
  </tbody>
</table>
<p>這一層不是每個人都需要。單機、只想知道某個服務死活，第一層就夠；要看趨勢、跨機、設門檻，才值得付這層的設定與維運成本。</p>
<h2 id="先確認有沒有沒有就從最簡單開始">先確認有沒有，沒有就從最簡單開始</h2>
<p>監控最好在出事之前就建好，不是等第一次沒人發現的當機才想到。有兩個時機該主動確認這台機器有沒有在監控自己：<strong>裝好一台新機器時</strong>，跟<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">systemctl --failed                      <span class="c1"># 現在有沒有 failed 的</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">systemctl show sshd -p OnFailure        <span class="c1"># 關鍵服務有沒有掛告警鉤子</span></span></span></code></pre></div><p>沒有任何監控的話，<strong>從最簡單那層開始建，別一開始就上重的</strong>：第一層的 <code>OnFailure</code> + ntfy 就能讓「服務掛了」主動找上你，零額外 daemon、幾個檔案就設好。遠端機器至少把 sshd 掛上——它掛了你就失聯，是最該先監控的一個。等你真的需要趨勢圖、跨機、或告警內容不能經過第三方時，再往自架 ntfy（帳號 + ACL）跟完整監控堆疊爬。多數單機、個人用的情境，停在第一層就夠。</p>
<h2 id="依情境選">依情境選</h2>
<p>把上面四層對回你實際要監控的東西：</p>
<ul>
<li><strong>某個 service 掛了想被通知</strong> → 第一層 <code>OnFailure</code> drop-in + ntfy。不裝額外 daemon，最貼近 systemd。</li>
<li><strong>希望先自動重啟、救不回來才告警</strong> → 第一層再加 <code>Restart=on-failure</code> + <code>StartLimit*</code>。</li>
<li><strong>怕整台機器當掉沒人知道</strong> → 第三層心跳 / dead-man switch。這層體內方案覆蓋不到，必須體外。</li>
<li><strong>要看資源趨勢、跨多台、設門檻告警</strong> → 第四層，單機用 Netdata、多機用 Prometheus 堆疊。</li>
</ul>
<p>判準是先分清你要監控的層級：<strong>單一 service 的死活、整台機器的死活、還是資源的趨勢</strong>——三種對應不同層，別拿其中一種去蓋另一種。最常見的誤區是以為體內的 <code>OnFailure</code> 能報自己這台的當機，那正是它的盲點。</p>
<h2 id="下一步">下一步</h2>
<ul>
<li>告警把你叫來之後，怎麼判那個服務到底是什麼狀態（failed、restart loop、還是活著但子系統 wedged）→ <a href="../process-service-state-diagnosis/">程序、服務與狀態怎麼判</a>。</li>
<li>機器完全不回應、心跳斷掉之後從外面怎麼查 → <a href="../machine-unreachable/">機器連不到或起不來</a>。</li>
<li>底層那套「讀權威狀態、不靠肉眼猜」的判讀紀律 → <a href="../diagnosis-read-authoritative-state/">診斷心法</a>。</li>
</ul>
]]></content:encoded></item></channel></rss>