許多系統服務(如資料庫、Web Server、監控代理)要求**同一時間僅能有一個實例運行**。常見解決方案包括:PID 文件鎖、Unix Domain Socket、Systemd 單實例模式、共享記憶體旗標等。本文實作基於**文件獨佔建立(O_EXCL)**與**標準雙重 Fork 守護進程化**的組合方案。
O_EXCL 原子性檢查與建立#define LOCK_FILE "/var/cxl_single_file"
int fd = open(LOCK_FILE, O_RDWR | O_CREAT | O_EXCL, 0664);
if (fd < 0) {
if (errno == EEXIST) {
printf("%s is running...\n", argv[0]);
_exit(-1);
}
}
// 成功建立 → 取得鎖,繼續執行
O_EXCL 配合 O_CREAT 保證「檢查存在性 + 建立檔案」的原子性,避免競態條件。程式啟動時嘗試建立鎖檔,成功則持有鎖;失敗且 errno == EEXIST 表示已有實例運行。
void create_daemon(void) {
pid_t pid = fork();
if (pid < 0) { perror("fork"); _exit(-1); }
if (pid > 0) _exit(0); // 父進程退出,子進程成為孤兒進程,由 init 收養
// 子進程:建立新會話,脫離控制終端
if (setsid() < 0) { perror("setsid"); _exit(-1); }
// 忽略 SIGHUP,防止終端關閉導致進程退出
signal(SIGHUP, SIG_IGN);
// 再次 fork,確保進程不是會話組長,無法重新獲得控制終端
pid = fork();
if (pid < 0) { perror("fork2"); _exit(-1); }
if (pid > 0) _exit(0);
// 孫進程:標準守護進程環境設定
chdir("/"); // 切換工作目錄至根目錄,避免掛載點無法卸載
umask(0); // 重設檔案權限遮罩
// 關閉所有繼承的檔案描述符
int maxfd = sysconf(_SC_OPEN_MAX);
for (int i = 0; i < maxfd; i++) close(i);
// 重新導向標準輸入/輸出/錯誤至 /dev/null
open("/dev/null", O_RDWR); // fd 0
open("/dev/null", O_RDWR); // fd 1
open("/dev/null", O_RDWR); // fd 2
}
#include#include #include #include #include #include #include #include #define LOCK_FILE "/var/cxl_single_file" void create_daemon(void); void delete_file(void); int main(int argc, char **argv) { int fd = open(LOCK_FILE, O_RDWR | O_CREAT | O_EXCL, 0664); if (fd < 0) { if (errno == EEXIST) { printf("%s is running...\n", argv[0]); _exit(-1); } } openlog(argv[0], LOG_PID | LOG_CONS, LOG_USER); syslog(LOG_INFO, "Create lock file success!"); atexit(delete_file); // 正常退出時刪除鎖檔(SIGKILL 不會執行) close(fd); closelog(); create_daemon(); while (1) sleep(1); // 實際業務邏輯 return 0; } void delete_file(void) { remove(LOCK_FILE); }
| 議題 | 說明 |
|---|---|
| 鎖檔路徑 | 放置於 /var 或 /run(tmpfs),重啟自動清理;避免放在使用者家目錄。 |
| 權限問題 | 非 root 使用者無法寫入 /var,需預先建立目錄並授權,或改用 /tmp/<user>_<app>.lock。 |
atexit 限制 | kill -9 不會觸發 atexit 註冊函式,鎖檔會殘留。下次啟動會誤判為「已在運行」。 |
| 改進方案 | 啟動時檢查鎖檔對應 PID 是否存活(kill(pid, 0)),若不存活則強制清理鎖檔並重試。 |
| Systemd 整合 | 現代 Linux 建議直接使用 Systemd Type=simple + ExecStart,由 init 系統管理單實例與守護化,無需自行實作。 |
「O_EXCL 檔案鎖 + 雙重 Fork 守護進程」是傳統 Unix 系統實現單實例後台服務的經典組合。雖然現代部署多改用 Systemd/Docker/K8s 等編排工具管理進程生命週期,但理解底層機制對於除錯遺留系統、開發輕量級嵌入式服務、面試考核仍具核心價值。