守護進程是運行在後台、無控制終端、獨立於用戶登錄會話的長壽命進程。典型例子:systemd、sshd、crond、nginx 主進程。
為確保進程完全脫離終端、不成為會話組長 (防止重新獲取控制終端),採用經典「雙 Fork」模型:
#include
#include
#include
#include
void init_daemon(void) {
pid_t pid;
int i;
// 第一次 fork:父進程退出,子進程成為孤兒進程,由 init 收養
if ((pid = fork()) > 0) exit(0); // 父進程退出
else if (pid < 0) exit(1); // fork 失敗
// 子進程:創建新會話,成為會話組長,失去控制終端
if (setsid() < 0) exit(1);
// 第二次 fork:確保進程不再是會話組長,無法重新打開控制終端
if ((pid = fork()) > 0) exit(0); // 第一子進程退出
else if (pid < 0) exit(1);
// 第二子進程:現在是真正的守護進程
// 關閉所有繼承的文件描述符
for (i = 0; i < getdtablesize(); i++) close(i);
// 改變工作目錄到根目錄,防止掛載點無法卸載
chdir("/");
// 重置文件創建掩碼,獲得完整權限控制
umask(0);
// 可選:重定向標準輸入/輸出/錯誤到 /dev/null
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > 2) close(fd);
}
| 步驟 | 系統調用 | 作用 |
|---|---|---|
| 1. 脫離父進程控制 | fork() + 父進程 exit() | Shell 認以命令已結束,繼續提示符 |
| 2. 創建新會話 | setsid() | 成為會話組長、進程組長;無控制終端 |
| 3. 防止重獲終端 | 第二次 fork() + 第一子進程 exit() | 第二子進程非會話組長,無法 open("/dev/tty") |
| 4. 關閉文件描述符 | close() 循環 | 釋放繼承的打開文件,避免資源洩漏/干擾 |
| 5. 改變工作目錄 | chdir("/") | 防止當前目錄在掛載點導致磁盤無法卸載 |
| 6. 重置 umask | umask(0) | 確保後續創建文件權限由程式完全控制 |
| 7. 重定向標準流 | open("/dev/null") + dup2() | 防止庫函數向終端輸出導致阻塞/錯誤 |
現代 Linux (systemd) 推薦編寫 .service 文件,由系統管理守護化:
# /etc/systemd/system/mydaemon.service
[Unit]
Description=My Custom Daemon
After=network.target
[Service]
Type=simple
ExecStart=/usr/local/bin/mydaemon
Restart=on-failure
User=nobody
StandardOutput=syslog
StandardError=syslog
[Install]
WantedBy=multi-user.target
優點:自動處理守護化、日誌收集 (journald)、依賴管理、資源限制 (cgroups)、安全沙箱。
SIGTERM 優雅退出,SIGHUP 重載配置,忽略 SIGPIPE。flock() 鎖定 PID 文件,防止多實例運行。syslog() 或寫入固定路徑日誌文件 (配合 logrotate)。setuid() 切換到非特權用戶運行。prlimit --core=unlimited 或在代碼中 setrlimit(RLIMIT_CORE, ...) 便於事後調試。雙 Fork 模型是 Unix 系統編程的經典範式,理解其每一步的「為什麼」比死記代碼更重要。現代項目優先考慮 systemd 托管,僅在極簡環境、容器內或兼容舊系統時手寫守護化邏輯。