2026年3月14日 星期六

從 LeetCode 到 Linux Kernel 的工程智慧:解構「快樂字串」

在程式設計的領域中,有些題目表面上是排列組合的益智遊戲,但深挖其背後的邏輯,你會發現它觸及了計算機科學最核心的課題:如何在有限限制下,進行極致的資源調度與導航。

今天我們要聊的是 LeetCode 的「快樂字串」(Happy String)問題。

什麼是快樂字串?

一個長度為 n 的字串被稱為「快樂字串」,需滿足:

  1. 僅由 {'a', 'b', 'c'} 組成。

  2. 無連續重複:相鄰的字元不能相同(例如 "aba" 是快樂的,但 "aa" 不是)。

任務是:在所有按字典序排列的快樂字串中,找出第 k 個。

階段一:直覺的探索 —— 回溯法 (Backtracking)

最直覺的方法是透過深度優先搜尋(DFS)來模擬人類構思字串的過程。

程式碼實作

class Solution {
    bool solve(string& ans, int n, int& k) {
        if (ans.size() == n) {
            return --k == 0; // 找到第 k 個組合時停止
        }
        for (char c : {'a', 'b', 'c'}) {
            if (!ans.empty() && c == ans.back()) continue; // 核心約束
            
            ans.push_back(c);
            if (solve(ans, n, k)) return true; // 提早回傳
            ans.pop_back(); // 回溯
        }
        return false;
    }
public:
    string getHappyString(int n, int k) {
        string ans = "";
        return solve(ans, n, k) ? ans : "";
    }
};

工程思考

這種方法就像是在迷宮中探路。雖然直觀,但當 n 變大時,搜尋空間會呈指數級成長。在系統底層開發中,過深的遞迴會導致 Stack Overflow,這在核心(Kernel)環境中是絕對要避免的。

階段二:極致的跳轉 —— 數學定位法 (Mathematical Positioning)

如果我們能不透過「走迷宮」,而是直接「空降」到第 k 個字串呢?

觀察規律發現:第一個字元決定後,後續每個位置都只有 2 種選擇。這意味著這是一個變相的 二進位導航系統

最佳化程式碼

class Solution {
public:
    string getHappyString(int n, int k) {
        int m = 1 << (n - 1); // 每個分支的組合數 (2^(n-1))
        if (3 * m < k) return ""; // 總數檢查

        string ans = "";
        k--; // 轉為 0-indexed 處理區間
        
        // 1. 確定首位字元
        ans.push_back('a' + k / m);
        k %= m;

        // 2. 數學跳轉確定後續字元
        for (int i = 1; i < n; ++i) {
            m >>= 1; // 剩餘組合數減半
            char next = 'a' + (k / m);
            if (next >= ans.back()) next++; // 巧妙排除重複,維持字典序
            
            ans.push_back(next);
            k %= m;
        }
        return ans;
    }
};

深入核心:Linux Kernel 中的精準對應

我們在上述數學解法中使用了兩個核心技巧:「基於 2 的冪次方的空間劃分」「前置狀態的數學補償」。在 Linux 核心中,這兩個技巧有著極其精準的對應實例。

實例 A:二進位空間劃分 —— 夥伴系統 (Buddy System)

在快樂字串中,我們利用 m = 1 << (n - 1) 來確定區間,並用除法與餘數直接定位分支。 在 Linux 的記憶體管理核心 mm/page_alloc.c 中,著名的**夥伴系統(Buddy Allocator)**使用了完全一樣的數學邏輯來分配實體記憶體。

當系統釋放一個大小為 2^order 的記憶體區塊時,它必須找到相鄰的「夥伴(Buddy)」來合併。核心絕對不會去遍歷記憶體,而是直接透過位元運算算出夥伴的精確位置:

/* Linux Kernel: mm/page_alloc.c 核心邏輯簡化 */
static inline unsigned long
__find_buddy_pfn(unsigned long page_pfn, unsigned int order)
{
    // 利用 XOR 和 1 << order 直接算出相鄰區塊的位置
    // 就像我們用 k / (1 << (n-1)) 定位分支一樣,純數學、零迴圈!
    return page_pfn ^ (1 << order);
}

精準對應:這與我們處理快樂字串時,依賴 $2$ 的冪次方(二進位樹狀結構)來進行 $O(1)$ 的空間跳轉與定位,在數學本質上完全一致。

實例 B:相鄰約束的 $O(1)$ 跳轉 —— 格雷碼 (Gray Code)

快樂字串的最核心限制是:相鄰元素不能重複。我們用了一行神來之筆 if (next >= ans.back()) next++,在 $O(1)$ 時間內算出第 $k$ 個符合約束的狀態。

在 Linux 驅動程式(特別是旋轉編碼器 drivers/input/misc/rotary_encoder.c 等硬體)中,也有一個嚴格的相鄰約束:相鄰的狀態只能有 1 個位元不同(避免硬體雜訊)。這稱為格雷碼(Gray Code)

工程師如何找出第 $k$ 個符合約束的格雷碼?他們不會從 0 開始生成並檢查相鄰狀態,而是直接套用數學偏移公式:

/* Linux Kernel 中常見的格雷碼轉換邏輯 */
unsigned int binary_to_gray(unsigned int k)
{
    // 透過自己與自己右移一位的值做 XOR,直接計算出第 k 個合法狀態
    // 完美滿足「相鄰狀態約束」,且是 O(1) 的純數學計算
    return k ^ (k >> 1);
}

精準對應:快樂字串的約束是「相鄰字元不同」,格雷碼的約束是「相鄰位元僅一處不同」。兩者都放棄了狀態機遍歷(Backtracking),轉而發掘出**「將序號 $k$ 透過偏移/補償,直接對射到合法狀態空間」**的終極數學公式。

跨領域的共鳴:AI、遊戲生成與空間定位

這種「拒絕盲目搜尋,改用數學約束或狀態機直接算出解」的思想,不僅限於底層作業系統。在當今最前沿的領域中,它更是解決效能瓶頸的核心技術。

1. 大型語言模型 (LLM) 的受限解碼 (Constrained Decoding)

在 LLM 的推論過程中,生成文字的本質就是在「巨大的字彙庫中尋找下一個合法的 Token」。這與我們找快樂字串的字元非常相似。 當我們要求 LLM 嚴格輸出特定格式(如 JSON)時,現代推論引擎(如 vLLM, Guidance)會使用有限狀態機 (FSM)。在生成每個 Token 前,系統會先「計算」出目前狀態下哪些字元是合法的(就像我們排除 ans.back()),並將不合法 Token 的機率強制歸零。這保證了模型一步到位生成完美格式,徹底避免了昂貴的回溯與重寫。

2. 電腦圖學與遊戲開發 (Procedural Generation)

在《Minecraft》這類擁有無限大世界的遊戲中,系統不可能把整個地圖存下來,也不可能在玩家移動時慢慢「搜尋」該生成什麼地形。開發者使用了 Perlin Noise 等純數學函數:只要給定一個座標 $(x, y)$(就像是我們題目中的 $k$),系統就能透過位元運算與雜湊,在 $O(1)$ 時間內直接「算出」這格的地形,且完美滿足相鄰區塊平滑過渡的約束條件。

3. 空間資料庫與希爾伯特曲線 (Hilbert Curve)

Google Maps 或外送平台需要在資料庫中快速找到目標。但地圖是 2D 的,而資料庫索引是 1D 的。科學家發明了空間填充曲線:給定一個 1D 序號 $k$,可以透過一系列精妙的位元運算,直接算出它在地圖上的 $(x, y)$ 座標,完全不需遍歷地圖。這與我們算出「第 $k$ 個快樂字串」的數學原理(空間劃分與位移)屬於同一個家族的工程魔法。

結語:從搜尋者進化為計算者

從回溯法進化到數學定位法,代表了程式設計師思維的轉變:從「一個個去試」的狀態搜尋,進化到「洞察規則」後的直接數學對射。

只要問題具備「狀態空間極大」與「明確的相鄰約束」兩大特徵,頂尖的工程師永遠會找出隱藏的數學規律,透過空間映射、位元運算與狀態掩碼,將複雜的搜尋問題降維打擊成常數時間的計算。這種對系統與數學的雙重敏銳度,正是頂尖架構師的標誌。

本文為「程式夥伴」專題系列,探討演算法與底層工程的連結。

2026年3月12日 星期四

從 LeetCode 3600 看工程思維:極致效能 vs 萬用架構

在面對演算法挑戰時,我們常常會陷入「尋求最快解」的迷思。然而,在真實世界的軟體工程中,最快的演算法真的永遠是最好的選擇嗎?

今天我們透過 LeetCode 3600: Maximize Spanning Tree Stability with Upgrades 這道經典的圖論題,來看看兩種截然不同的解題思路,以及它們在現實工業界中各自稱霸的真實場景。

題目背景:尋找最穩定的生成樹

這道題目的核心目標是:在給定的一張圖中,挑選出 $N-1$ 條邊形成「生成樹 (Spanning Tree)」。其中有些邊是「必選的」,有些是「可選的」。我們手上有 $K$ 次升級機會,可以將可選邊的強度乘以 2。 目標:最大化這棵樹的「最弱連結 (最小邊緣強度)」。

面對這個「最大化最小值」的難題,我們有兩種截然不同的解題策略。

策略一:二分搜尋 + 貪心驗證 (Binary Search on Answer)

這是一個非常扎實且通用的工程思維:我們不知道答案是多少,但我們可以「猜」。

運作邏輯:

  1. 確定答案的上下界(例如強度從 $0$$100,000$)。

  2. 猜測一個目標值 mid,並寫一個 check(mid) 函數來驗證:「在 $K$ 次升級內,我們能不能讓所有挑選的邊,強度都 $\ge mid$?」

  3. 根據驗證結果,不斷縮小範圍,直到找到極限值。

💡 真實世界的應用場景:高擴充性的「萬用框架」

這個演算法雖然時間複雜度多了一個 $\log M$,但在真實的工程應用中,它卻具有壓倒性的擴充性 (Extensibility)

  • 電信網路的 SLA 保證與預算規劃 中華電信或 AWS 要牽線連接全球的資料中心,並保證最低可用頻寬。公司今年只給了預算購買 $K$ 台「訊號放大器」。網路架構師會透過這個演算法「二分猜測」保證頻寬,驗證預算是否足夠,藉此找出基礎建設的投資報酬率極限。

  • EDA (電子設計自動化) 與晶片佈線 晶片設計中,工程師可以在線路上插入 Buffer(緩衝器)來增強訊號,但 Buffer 非常耗電且最多只能放 $K$ 個。軟體會猜測最差的訊號延遲時間,去驗證這 $K$ 個 Buffer 該怎麼放。

  • 為什麼它無可取代? 真實世界的規則往往不講武德。如果今天老闆說:「A 線路升級加 500、B 線路升級乘以 1.5、C 線路升級需要消耗 2 次升級機會...」純數學推導會瞬間崩潰,但二分搜尋版完全不受影響!你只需要在 check() 函數裡加上幾行 if-else,這個演算法就能完美存活。

策略二:Kruskal 演算法變形 (Pure Greedy)

這是一個將圖論數學性質發揮到極致的 $O(E \log E)$ 神級解法。它捨棄了「猜答案」,直接在一次遍歷中找出答案。

運作邏輯:

  1. 將所有「可選邊」依照強度由大到小排序。

  2. 依序將邊加入圖中(使用並查集 DSU 避免迴圈)。

  3. 既然有 $K$ 次升級機會,為了讓「最小值」最大化,自然要把機會留給生成樹中最弱的那 $K$ 條邊

  4. 透過追蹤 edgesUsed,精準攔截出「沒有被升級的最弱邊」和「有被升級的最弱邊」,最後比較瓶頸即可。

💡 真實世界的應用場景:極致效能的「特製手術刀」

當規則明確且不變時,這種純貪心演算法能提供無與倫比的效能,特別適合底層系統與即時運算。

  • Linux Kernel 的生成樹協定 (STP) 在網路橋接系統 (net/bridge/br_stp.c) 中,為了解決交換機之間的「廣播風暴」,Kernel 必須在極短時間內找出無迴圈的生成樹。Kruskal 演算法的底層邏輯正是這類網路防護機制的核心。

  • 機器學習:層次分群 (Hierarchical Clustering) 在資料科學中 (例如 Single-linkage Clustering),我們需要將特徵相近的資料點分群。背後運作完全依賴排序與並查集 (DSU),能快速將距離最短的節點合併,達成自動分類。

  • 電腦視覺:影像分割 (Image Segmentation) 讓電腦看懂照片中哪裡是人、哪裡是背景。演算法將像素當作節點,顏色差異當作邊,透過並查集在極短的 $O(1)$ 時間內將相似區塊合併,達成即時去背效果。

總結

LeetCode 3600 教會我們最重要的一課是:演算法沒有絕對的好壞,只有最適合的情境。

  • 如果你面對的是一個規則單純、對效能要求極高的底層系統,請拿出 Kruskal 貪心法 這把特製手術刀。

  • 如果你面對的是一個業務邏輯複雜、需求隨時會變更的商業系統,請架構好 二分搜尋驗證 這個萬用防護罩。

下次在解題時,不妨多想一步:這段程式碼如果放到真實世界,它會扮演什麼樣的角色呢?

2026年1月20日 星期二

修復 v4l2loopback 0.15.0 的 V4L2 緩衝區佇列問題

## 問題現象 使用 GStreamer v4l2sink 將影像送到 v4l2loopback 虛擬攝影機時出現錯誤: ``` WARN v4l2bufferpool: buffer X was not queued, this indicates a driver bug ``` 經過分析發現是 v4l2loopback 0.15.0 違反 V4L2 規格所致。 ## V4L2 緩衝區佇列規格 V4L2 規格定義的緩衝區使用流程: 1. **VIDIOC_REQBUFS** - 配置緩衝區 2. **VIDIOC_QBUF** - 將緩衝區排入驅動程式佇列 3. **VIDIOC_DQBUF** - 從驅動程式佇列取出緩衝區 核心規則:DQBUF 只能回傳先前透過 QBUF 排入的緩衝區。 正確流程:`QBUF → (處理) → DQBUF → QBUF → (處理) → DQBUF ...` v4l2loopback 0.15.0 的錯誤流程:`(預填充) → DQBUF → (自動循環) → DQBUF → (自動循環) → DQBUF ...` 錯誤點:完全不需要 QBUF,DQBUF 永遠有可用緩衝區。 ## v4l2loopback 0.15.0 的問題 ### prepare_buffer_queue() 預先填充佇列 ```c /* 原始碼 */ for (pos = 0; pos < count; ++pos) { bufd = &dev->buffers[pos]; if (list_empty(&bufd->list_head)) list_add_tail(&bufd->list_head, &dev->outbufs_list); } ``` 問題:所有緩衝區在配置後立即被加入輸出佇列,沒有經過 QBUF。 ### vidioc_dqbuf() 循環使用緩衝區 ```c /* 原始碼 */ bufd = list_first_entry_or_null(&dev->outbufs_list, ...); if (bufd) list_move_tail(&bufd->list_head, &dev->outbufs_list); ``` 問題:使用 `list_move_tail()` 將緩衝區移到佇列尾端,形成循環。 ### 違反規格的行為 1. 緩衝區未經 QBUF 就出現在輸出佇列 2. DQBUF 回傳從未排入的緩衝區 3. 緩衝區自動循環使用 實際流程:`DQBUF → DQBUF → DQBUF ...` (完全不需要 QBUF) ### GStreamer 的檢查 GStreamer v4l2bufferpool 會追蹤已排入的緩衝區。收到未排入的緩衝區時報錯: ``` WARN v4l2bufferpool: buffer X was not queued, this indicates a driver bug ``` 這是正確的行為,因為驅動程式確實違反了規格。 ## 修復方法 ### 修改 prepare_buffer_queue() 清空輸出佇列,不要預先填充: ```c /* 清空輸出佇列 */ list_for_each_entry_safe(bufd, n, &dev->outbufs_list, list_head) { list_del_init(&bufd->list_head); } /* 重設緩衝區狀態 */ for (pos = 0; pos < count; ++pos) { bufd = &dev->buffers[pos]; unset_flags(bufd->buffer.flags); dev->bufpos2index[pos % count] = bufd->buffer.index; } ``` ### 修改 vidioc_dqbuf() 移除緩衝區,不要循環使用: ```c bufd = list_first_entry_or_null(&dev->outbufs_list, ...); if (bufd) list_del_init(&bufd->list_head); /* 移除,不循環 */ ``` ### 修復後的行為 - 輸出佇列初始為空 - 緩衝區只在透過 QBUF 排入時才進入輸出佇列 - DQBUF 移除緩衝區,不循環使用 - 符合 V4L2 規格要求 ## 測試驗證 修復前: ```bash gst-launch-1.0 icamerasrc ! v4l2sink device=/dev/video0 # ERROR: buffer X was not queued, this indicates a driver bug ``` 修復後: ```bash gst-launch-1.0 icamerasrc ! v4l2sink device=/dev/video0 # 正常運作,無錯誤訊息 # Firefox/Chrome 可正常使用虛擬攝影機 ``` ## 技術細節 ### 為什麼原設計會預先填充? v4l2loopback 作為虛擬裝置,可能想簡化緩衝區管理,確保永遠有可用緩衝區。但這違反了 V4L2 規格,與嚴格遵循規格的程式(如 GStreamer)不相容。 ### list_del_init vs list_move_tail - `list_del_init()`: 明確表示「緩衝區不在佇列中」 - `list_move_tail()`: 暗示「移到另一個位置」,語義模糊 使用 `list_del_init()` 讓緩衝區狀態更清楚。 ### 多執行緒考量 正確的緩衝區狀態追蹤在多執行緒環境下很重要,可避免競爭條件。 ## 參考資料 - V4L2 Buffer: https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/buffer.html - VIDIOC_QBUF/DQBUF: https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/vidioc-qbuf.html - V4L2 Streaming I/O: https://www.kernel.org/doc/html/latest/userspace-api/media/v4l/io.html - GStreamer v4l2bufferpool: https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-plugins-good/sys/v4l2/gstv4l2bufferpool.c ## 上游貢獻 修補程式已提交至上游:https://github.com/v4l2loopback/v4l2loopback/pull/656 ## 結論 V4L2 緩衝區管理規格的核心原則: 1. 明確的狀態轉換:QBUF → DQBUF 2. 應用程式控制:由應用程式決定何時提供緩衝區 3. 可預測行為:嚴格遵循規格 當應用程式報告 "driver bug" 時,通常確實是驅動程式的問題。

解析位元運算公式 n & ~(((n + 1) & ~n) >> 1)

# 前言 在解決「尋找最小的 $x$ 使得 $x \lor (x + 1) = n$ 」這個問題時,我們得出了一個結論:對於奇數 $n$ ,答案是將 $n$ 的二進位表示中,「末尾連續的 1」序列裡最高位的那一個 `1` 修改為 `0`。 例如:如果 $n$ 的二進位是 `...01111`,我們目標是把它變成 `...00111`。 這個操作可以透過以下這行精簡的位元運算公式完成: ```cpp n & ~(((n + 1) & ~n) >> 1) ``` 這篇筆記旨在拆解這個複合表達式,逐步說明其工作原理。 --- # 公式拆解 這個公式可以分為三個主要步驟來理解,我們由內而外進行分析。 我們的目標 $n$ 範例設為 **23**,二進位表示為 `00010111`。 我們的目標是將末尾三個 `1` 中最左邊的那個(值為 4 的位元)關閉。 ## 步驟一:找出右邊數來第一個「0」的位置 **表達式核心:** `(n + 1) & ~n` 這是非常經典的位元操作技巧,用於定位最低位的 `0`。 1. **`n + 1` 的進位特性**: 當一個整數加 1 時,其二進位末尾所有的連續 `1` 都會因為進位而變成 `0`,直到遇到第一個 `0`,該位置會變成 `1`,進位停止。 * (23) = `00010111` * (24) = `00011000` (注意末尾三個 1 變成了 0,它們左邊的 0 變成了 1) 2. **`~n` 取反**: * `~n` = `11101000` 3. **`&` (AND) 運算**: 將上述兩個結果進行 AND 運算,只會保留同時為 `1` 的位元。 ``` 00011000 (n + 1) & 11101000 (~n) ---------------- 00001000 (結果為 8) ``` **小結**:這一步成功分離出了 從右側開始數第一個非 `1` 的位置。 ## 步驟二:鎖定目標位元(向右移位) **表達式:** `(步驟一的結果) >> 1` 我們在步驟一找到了第一個 `0` 的位置(在範例中是第 3 位,數值為 8)。 但我們的目標是修改這個 `0` 位置**右邊**的那一個 `1`(也就是末尾連續 `1` 序列的最高位)。 因此,我們將步驟一的結果向右移動一位: * `00001000 >> 1` = `00000100` (數值為 4) **小結**:這一步得到了一個「遮罩 (Mask)」,這個遮罩只有我們想要修改的那一個目標位元是 `1`,其他都是 `0`。 ## 步驟三:清除目標位元 **表達式:** `n & ~(步驟二的遮罩)` 現在我們有了目標遮罩 `Mask = 00000100`。我們要利用這個遮罩把 對應位置的 `1` 變成 `0`,並保持其他位置不變。 這是標準的「清除位元」操作: 1. **`~Mask` (遮罩取反)**: 製造一個工具,目標位置為 `0`,其他位置全為 `1`。 * `~Mask` = `11111011` 2. **`n & (~Mask)`**: 將原始的 與這個反向遮罩做 AND。 * 目標位置:`1 & 0` 結果為 `0`(成功清除)。 * 其他位置:`x & 1` 結果仍為 `x`(保持不變)。 ``` 00010111 (n, 即 23) & 11111011 (~Mask) ---------------- 00010011 (結果為 19) ``` # 總結 回顧整個流程: 1. `00010111` (原始 n) 2. `00001000` (找到第一個 0) 3. `00000100` (右移,鎖定目標 1) 4. `11111011` (取反,準備清除工具) 5. `00010011` (與原數 AND,完成清除) 這個公式 `n & ~(((n + 1) & ~n) >> 1)` 利用了加法進位的特性和基本的邏輯閘操作,在不使用任何迴圈的情況下,精確地完成了「關閉末尾連續 1 中最高位」的任務。這是一種高效且常見於底層優化的寫法。

2026年1月17日 星期六

在 Copilot CLI 使用 Spec Kit 流程圖

我把使用的大概細節跟過程記錄在 https://hackmd.io/@fourdollars/SpecKit 上面,用 feat: Add shared storage support for multi-unit Concourse CI deployments #5 這個 Pull Request 來練習完整走過整個流程,總共產生了 122 commits 跟 43 個檔案變更,過程有苦有樂也有辛酸,感覺在帶一個做事很快不會抱怨也很主動積極的新同事,但是就是要處處盯著他做事,幫他解決一些卡住的地方。

Concourse CI Machine Charm 是我正在開發的一個使用 Juju 快速架構管理維護 Concourse CI 服務的一套解決方案,已經可以從 edge channel 拿來使用了,不過還在開發中還有許多地方沒有完善,所以還沒有穩定釋出版本。

2026年1月6日 星期二

深入解析 grep 的「Broken pipe」錯誤:隨機出現的原因與優雅解法

最近我在 [ChromeOS Flex](https://chromeos.google/products/chromeos-flex/) 的 Linux 環境中使用 [Homebrew](https://brew.sh/) 時,總是會隨機看到 `grep: write error: Broken pipe`。後來去檢查相關程式碼,發現是這行 `grep -E "^(flags|Features)" /proc/cpuinfo | grep -q "ssse3"` 指令造成的。 這個錯誤雖然不一定影響最終結果,但在特定環境下出現總是讓人感到困惑。為什麼這行看似普通的管線指令會隨機報錯?今天就來深入探討其成因,並提供修正方案。 ## 「破裂的管線」錯誤成因 這個錯誤的核心,在於 Linux 作業系統處理**行程(Process)**與**管線(Pipe)**的機制。 當我們執行上述指令時,發生了以下連鎖反應: 1. **前端寫入(Producer)**: 第一個指令 `grep -E ...` 負責讀取 CPU 資訊,並持續將符合條件的資料寫入管線。 2. **後端讀取與提早退出(Consumer)**: 管線另一端的 `grep -q "ssse3"` 負責接收資料。關鍵在於 `-q` (quiet) 參數,它告訴 `grep` 一旦找到 "ssse3",就**立即停止讀取並退出程式**。 3. **管線斷裂(Broken Pipe)**: 當後端 `grep` 退出後,管線的讀取端隨之關閉。然而,前端的 `grep` 此時可能還在嘗試寫入剩餘的資料。當它試圖寫入這條「已關閉」的管線時,系統便會發送 **SIGPIPE** 訊號,導致行程終止並印出 `write error: Broken pipe`。 ## 為何錯誤是隨機出現的? 既然機制如此,為何不是「每次」都報錯?這歸因於**競爭條件(Race Condition)**。 電腦的排程器決定了兩個程式執行的快慢: * **沒事發生的情況**:前端 `grep` 產出資料的速度夠快,或者資料量剛好夠小,在後端退出前就已經寫完並結束了。 * **報錯的情況**:後端 `grep -q` 很快就找到了 "ssse3" 並關閉管線,而前端還來不及寫完剩下的資料,導致寫入失敗。 這種執行時間上的微小差異,決定了你是否會看到這行錯誤訊息。 ## 更優雅的解決方案 既然問題出在「多個指令透過管線傳輸」的時間差,最穩健的解法就是**避免使用管線**,將邏輯整合到單一 `grep` 指令中。 我們可以將原本的指令: ```bash grep -E "^(flags|Features)" /proc/cpuinfo | grep -q "ssse3" ``` 改寫為: ```bash grep -qE '^(flags|Features).*\bssse3\b' /proc/cpuinfo ``` ### 改寫後的優點: * **消除競爭條件**:單一行程運作,完全根除「管線破裂」的可能性。 * **效能提升**:省去了啟動兩個行程與建立管線的開銷。 * **精準度提高**: * `^(flags|Features)`:鎖定行首。 * `.*\bssse3\b`:利用 `\b`(單詞邊界)精確匹配 "ssse3",避免誤判類似字串。 下次若在 Script 中發現類似的隨機管線錯誤,不妨檢查是否使用了會提早退出的指令(如 `grep -q`、`head` 等),並嘗試將其整合以提升穩定性! 順便生了一個 [pull request](https://github.com/Homebrew/brew/pull/21366) 給 Homebrew

2026年1月5日 星期一

自幹了一個 Agent Skills - Launchpad Skill

放在 https://github.com/fourdollars/lp-api/ 專案底下的 launchpad 目錄底下,當然就是用 lp-api 這個小工具來串接。

我自己有在 OpenCode, GitHub Copilot CLI, Gemini CLI 上面成功掛載起來使用。

基本上就是把 AI Agent 接上 Launchpad,用自然語言去叫 AI 做自己想要做的事情。

以下是幾張 OpenCode 上使用的示範。

有興趣的人可以去看看 Agent Skills 了解一下。