用 Bash 實作自旋鎖防止重複執行
在 Linux 系統管理與自動化運維中,我們經常會撰寫 Cron 定時任務或背景腳本來處理數據備份、日誌輪轉或系統監控。然而,一個常見且致命的問題是:如果前一次任務執行時間過長,尚未結束時,下一次定時任務又觸發了,就會導致多個腳本實例並行執行。這不僅會浪費系統資源,更可能引發資料競爭(Race Condition),導致檔案損毀或狀態不一致。
為了避免這種情況,我們需要一種機制來確保同一個腳本在任一時刻只有一個實例在運行。雖然 flock 命令是解決此問題的金標準,但在某些受限環境或需要極低延遲鎖定的場景下,利用 Bash 內建功能實作一個簡單的「自旋鎖」(Spinlock)或更準確地說,是一個基於檔案屬性的互斥鎖(Mutex),也是一個非常實用且輕量的解決方案。
本文将介紹如何透過 Bash 的檔案描述符與 flock 的非阻塞特性,實作一個能立即判斷並中止重複執行的腳本機制。
核心概念:為什麼選擇檔案鎖定?
在 Linux 中,檔案系統本身提供了強大的鎖定機制。我們可以利用 flock 命令對一個特定的鎖檔案(Lock File)進行鎖定。關鍵在於使用 -n(或 --nonblock)參數,這告訴 flock 如果無法立即獲得鎖,不要等待,而是直接返回錯誤代碼。
這種方法相較於輪詢檢查檔案是否存在(例如檢查 PID 檔案)更為可靠,因為它避免了「檢查後執行」(Time-of-check to time-of-use, TOCTOU)的競爭條件,確保了原子性的獲取與釋放。
實作步驟與完整腳本
我們將建立一個名為 spin_lock_demo.sh 的腳本。這個腳本模擬一個需要長時間運行的任務,並在開頭嘗試獲取鎖。如果獲取失敗,則立即退出並提示錯誤。
首先,請確保你的系統已安裝 util-linux 套件(通常預設已安裝),其中包含 flock 命令。
#!/bin/bash
# 定義鎖檔案路徑,建議使用 /tmp 或 /var/lock
LOCK_FILE="/tmp/my_script.lock"
# 定義鎖檔案描述符
# 我們使用文件描述符 200 來綁定鎖,這樣當腳本結束時,
# Bash 會自動關閉該描述符並釋放鎖,無需手動執行 unlock
exec 200>"$LOCK_FILE"
# 嘗試以非阻塞模式獲取排他鎖
# -n: 非阻塞,如果鎖已被佔用,立即返回失敗
# 200: 對應上面的文件描述符
if ! flock -n 200; then
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] 腳本已在執行中,拒絕重複啟動。" >&2
exit 1
fi
# 如果成功獲取鎖,執行後續邏輯
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] 成功獲取鎖,開始執行任務..."
# 模擬長時間運行的任務 (例如:5 秒)
# 在真實場景中,這裡可以是 rsync, mysqldump 等命令
sleep 5
echo "$(date '+%Y-%m-%d %H:%M:%S') [INFO] 任務執行完畢,自動釋放鎖並退出。"
# 註:這裡不需要手動執行 flock -u 200
# 因為腳本結束時,文件描述符 200 會被關閉,鎖也會隨之釋放
exit 0
如何測試這個機制?
為了驗證鎖是否有效,我們可以在終端機中開啟兩個視窗,並同時執行該腳本。
在終端機 1 中執行:
chmod +x spin_lock_demo.sh
./spin_lock_demo.sh
在終端機 1 還在 sleep 的 5 秒內,迅速在終端機 2 中執行:
./spin_lock_demo.sh
你應該會看到終端機 2 立即輸出:
2023-10-27 10:00:05 [ERROR] 腳本已在執行中,拒絕重複啟動。
這證明了當第一個實例持有鎖時,第二個實例無法獲取鎖並被正確拒絕。
常見問題與注意事項
1. 鎖檔案殘留問題
如果腳本在持有鎖期間被強制終止(如 kill -9),文件描述符會關閉,鎖通常會自動釋放。但在極少數情況下,如果腳本崩潰導致系統重啟或檔案系統未正確同步,可能會出現鎖檔案殘留。不過,由於我們使用的是 flock 基於文件描述符的鎖定,只要描述符關閉,鎖就會釋放,因此殘留風險遠低於傳統的 PID 檔案方法。若擔心極端情況,可在腳本開頭檢查鎖檔案是否過期(例如超過 1 小時),但對於大多數應用場景,上述實作已足夠安全。
2. 權限問題
確保腳本有權限在指定的目錄(如 /tmp 或 /var/lock)創建和讀寫鎖檔案。如果腳本以 root 身份運行,而鎖目錄僅允許 root 寫入,則無問題。若以一般使用者運行,請確認該使用者對鎖目錄有寫入權限。
小結
透過結合 Bash 的文件描述符與 flock 的非阻塞模式,我們能夠以極低的程式碼成本實作一個健壯的互斥鎖機制。這種方法不僅避免了複雜的 PID 管理與檔案競爭條件,還確保了腳本執行的原子性與安全性。對於需要防止重複執行的 Shell 腳本來說,這是一個值得推薦的最佳實踐。下次在撰寫定時任務時,不妨加上這層保護,讓你的自動化腳本更加穩健可靠。