守護進程編程
守護進程的含義
定義
守護進程(Daemon Process)是在后臺運行的進程,它獨立于控制終端并且周期性地執行某種任務或等待處理某些發生的事件。守護進程是一種很有用的進程,它在系統后臺運行,為系統或其他進程提供服務,而用戶通常不會直接與它交互。
特點
(1)獨立于終端
守護進程沒有控制終端。它不會因為終端的關閉而停止運行。例如,一個網絡服務器守護進程,它在后臺監聽網絡請求,無論用戶是否登錄終端,它都能正常工作。這使得守護進程能夠持續運行,不受用戶登錄狀態的限制。
(2)生命周期長
守護進程通常在系統啟動時開始運行,并且會一直運行,直到系統關閉。它們的生命周期與系統的運行時間緊密相關。比如,系統日志守護進程(如 syslogd)從系統啟動開始就記錄日志,直到系統關閉,它一直都在后臺工作,記錄各種系統事件的日志信息。
(3)提供服務
守護進程的主要功能是為系統或其他進程提供服務。這些服務可以是網絡服務(如 Web 服務器守護進程 Apache)、打印服務(如 CUPS 打印守護進程)、定時任務(如 cron 守護進程)等。它們在后臺默默運行,確保系統功能的正常實現。
編程實現一個守護進程的主要過程
1. 創建子進程并退出父進程
- 使用 fork() 創建一個子進程。
- 父進程退出,確保守護進程與終端分離。
2. 創建新的會話
- 調用 setsid() 創建一個新的會話,使守護進程成為會話的首進程。
- 這一步可以確保守護進程與終端完全分離。
3. 改變工作目錄
- 將工作目錄改為根目錄(/),避免守護進程依賴于特定的用戶目錄。
- 防止守護進程在用戶注銷時被意外終止。
- 關閉所有文件描述符
- 關閉所有打開的文件描述符,避免資源泄漏。
- 防止守護進程意外地向終端輸出信息。
- 設置文件權限掩碼
- 設置文件權限掩碼(umask),確保守護進程創建的文件具有合適的權限。
- 打開日志文件
- 打開日志文件,用于記錄守護進程的運行狀態。
- 進入主循環
- 守護進程進入主循環,執行其主要功能。
- 例如,定期記錄當前時間到日志文件。
創建一個守護進程一般有 nohup命令、fork()函數和 daemon()函數三種方法,請分別在阿里云服務器、樹莓派上用這三種方式創建一個守護進程。
阿里云
nohup命令
我們以這個簡單的python腳本為例,它每隔10秒鐘記錄一次當前時間到日志文件中
import time
def main():while True:print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} - 守護進程正在運行")time.sleep(10)
if __name__ == "__main__":main()
使用以下命令運行程序
nohup python3 demo.py &python3 demo.py
fork()
使用 fork() 創建守護進程的步驟
第一次 fork():
創建一個子進程,父進程退出。
這樣可以確保子進程與終端分離。
創建新的會話:
調用 setsid() 創建一個新的會話,使子進程成為會話的首進程。
這一步可以確保子進程與終端完全分離。
第二次 fork():
再次創建一個子進程,確保守護進程不能重新打開控制終端。
父進程退出,子進程繼續運行。
改變工作目錄:
將工作目錄改為根目錄(/),避免守護進程依賴于特定的用戶目錄。
關閉所有文件描述符:
關閉所有打開的文件描述符,避免資源泄漏。
設置文件權限掩碼:
設置文件權限掩碼(umask),確保守護進程創建的文件具有合適的權限。
進入主循環:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <string.h>
#include <errno.h>#define LOG_FILE "/tmp/shouhufork.txt"void log_message_to_file(const char *message) {int log_fd = open(LOG_FILE, O_WRONLY | O_APPEND | O_CREAT, 0644);if (log_fd < 0) {perror("Failed to open log file");return;}write(log_fd, message, strlen(message));write(log_fd, "\n", 1);close(log_fd);
}int main() {pid_t pid;// Step 1: First forkpid = fork();if (pid < 0) {perror("First fork failed");exit(EXIT_FAILURE);}if (pid > 0) {// Parent process exitsexit(EXIT_SUCCESS);}// Step 2: Create a new sessionif (setsid() < 0) {perror("Setsid failed");exit(EXIT_FAILURE);}// Step 3: Second forkpid = fork();if (pid < 0) {perror("Second fork failed");exit(EXIT_FAILURE);}if (pid > 0) {// Parent process exitsexit(EXIT_SUCCESS);}// Step 4: Change the working directory to rootif (chdir("/") < 0) {perror("Chdir failed");exit(EXIT_FAILURE);}// Step 5: Close all file descriptorsfor (int i = 0; i < sysconf(_SC_OPEN_MAX); i++) {close(i);}// Step 6: Set the file permission maskumask(0);// Step 7: Open the log filelog_message_to_file("Daemon started successfully");// Step 8: Enter the main loopwhile (1) {time_t now = time(NULL);char *time_str = ctime(&now);char log_message[128];snprintf(log_message, sizeof(log_message), "Current time: %s", time_str);log_message_to_file(log_message);sleep(60); // Sleep for 60 seconds}return 0;
}
將代碼保存為shouhufork.c
使用一下命令編譯代碼:
gcc -o shouhufork shouhufork.c
使用以下命令
./shouhufork
守護進程進入主循環,執行其主要功能。
使用命令
nohup ./shouhufork &
cat nohup.out
daemon函數
使用 daemon() 函數創建守護進程的步驟
調用 daemon() 函數:
daemon() 函數會自動完成以下操作:
- 創建一個子進程,父進程退出。
- 創建一個新的會話,使子進程成為會話的首進程。
- 改變工作目錄到根目錄(/)。
- 關閉所有文件描述符。
- 設置文件權限掩碼(umask)。
進入主循環:
守護進程進入主循環,執行其主要功能。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
#include <string.h>
#include <errno.h>#define LOG_FILE "/tmp/daemon_log.txt"void log_message_to_file(const char *message) {int log_fd = open(LOG_FILE, O_WRONLY | O_APPEND | O_CREAT, 0644);if (log_fd < 0) {perror("Failed to open log file");return;}write(log_fd, message, strlen(message));write(log_fd, "\n", 1);close(log_fd);
}int main() {// Step 1: Call daemon() functionif (daemon(0, 0) == -1) {perror("Failed to start daemon");exit(EXIT_FAILURE);}// Step 2: Open the log filelog_message_to_file("Daemon started successfully");// Step 3: Enter the main loopwhile (1) {time_t now = time(NULL);char *time_str = ctime(&now);char log_message[128];snprintf(log_message, sizeof(log_message), "Current time: %s", time_str);log_message_to_file(log_message);sleep(60); // Sleep for 60 seconds}return 0;
}
保存為daemon.c,使用以下命令編譯代碼
gcc -o daemon daemon.c
使用以下命令運行
./daemon
GDB調試
用法介紹
程序暫停與斷點
斷點(Breakpoint):
- GDB 允許用戶在程序的特定位置設置斷點。當程序運行到斷點時,它會自動暫停。
- 斷點可以設置在函數入口、特定行號或特定地址。
示例
(gdb) break main
(gdb) break file.c:42
暫停程序:
- 當程序運行到斷點時,GDB 會暫停程序的執行,允許用戶檢查程序的狀態。
- 用戶可以查看變量的值、調用棧、寄存器內容等。
示例
(gdb) step # 進入函數內部
(gdb) next # 不進入函數內部
(gdb) continue # 繼續運行到下一個斷點
單步執行
單步執行(Step):
- GDB 提供了單步執行功能,允許用戶逐行或逐指令執行程序。
- 單步執行可以幫助用戶觀察程序的執行流程,檢查變量的變化。
示例
(gdb) step # 進入函數內部
(gdb) next # 不進入函數內部
(gdb) continue # 繼續運行到下一個斷點
查看變量和內存
查看變量:
GDB 允許用戶查看和修改程序中的變量值。
示例
(gdb) print x
(gdb) print *ptr
(gdb) set x = 10
查看內存:
GDB 可以查看和修改內存中的內容。
示例:
(gdb) x/10gx 0x10000000 # 查看從地址 0x10000000 開始的 10 個 8 字節數據
(gdb) set {int}0x10000000 = 42 # 修改內存中的值
調用棧
查看調用棧(Backtrace):
GDB 可以顯示當前程序的調用棧,幫助用戶了解程序的執行路徑。
示例:
(gdb) backtrace
切換棧幀:
用戶可以切換到不同的棧幀,查看不同函數中的變量。
示例
(gdb) frame 2 # 切換到第 2 個棧幀
信號處理
信號(Signal):
- GDB 可以捕獲和處理程序中的信號(如 SIGSEGV、SIGABRT 等)。
- 用戶可以設置信號的處理方式,或者在信號發生時暫停程序。
示例:
(gdb) handle SIGSEGV stop
(gdb) handle SIGSEGV nostop
多線程支持
多線程調試:
GDB 支持多線程程序的調試,可以查看和切換不同的線程。
示例:
(gdb) info threads # 查看所有線程
(gdb) thread 2 # 切換到第 2 個線程
遠程調試
遠程調試:
GDB 支持遠程調試,可以通過網絡連接到運行在其他機器上的程序。
示例:
(gdb) target remote :1234 # 連接到本地端口 1234
工作原理
啟動 GDB:
用戶啟動 GDB 并加載要調試的程序:
gdb ./my_program
設置斷點:
用戶在程序的特定位置設置斷點:
(gdb) break main
運行程序
(gdb) run
暫停程序:
當程序運行到斷點時,GDB 暫停程序的執行。
檢查程序狀態:
用戶可以查看變量、調用棧、內存等信息:
(gdb) print x
(gdb) backtrace
單步執行:
用戶可以逐行或逐指令執行程序:
(gdb) step
(gdb) next
繼續運行
用戶可以繼續運行程序到下一個斷點
(gdb) continue
退出 GDB:
用戶可以退出 GDB:
(gdb) quit
使用gdb調試一個程序
(1)創建 test.c
#include <stdio.h>int multiply(int x, int y) {return x * y;}int divide(int x, int y) {if (y == 0) {fprintf(stderr, "Error: Division by zero\n");return 0;}return x / y;}int main() {int a = 10, b = 0, c = 20, d;d = multiply(a, c);printf("Multiply result: %d\n", d);d = divide(a, b);printf("Divide result: %d\n", d);return 0;
在 main 函數中,變量 a 被初始化為 10,b 被初始化為 0,c 被初始化為 20,d 未初始化
調用 multiply(a, c) 計算 a 和 c 的乘積,即 10 * 20,結果為 200。這個結果被賦值給 d,所以此時 d 的值為 200。接下來打印 Multiply result: 200。然后調用 divide(a, b) 計算 a 和 b 的商,即 10 / 0。由于 b 的值為 0,這將導致除以零的錯誤。divide 函數會打印錯誤信息 “Error: Division by zero” 并返回
0。這個結果被賦值給 d,所以此時d的值變為0.
(2)編譯帶調試信息
gcc -g test.c -o test # -g選項生成調試符號
(3)啟動 gdb 調試
gdb ./test
(4)設置斷點
break multiply
break divide
(5)運行程序
run
(6)單步執行
next
一直next,直到出現divide函數,執行step命令進入到divide含糊內部進行單步調試
step
使用print命令來檢查傳入 divide函數的參數想和y的值,確保他們的預期值
print x
print y
(7)單步執行
step
?
程序已經執行了 divide 函數中的 if (y == 0) 條件檢查。由于 y 的值是 20(不等于0) 程序將繼續執行 if 語句塊之外的代碼,繼續單步執行step
step?
程序已經執行到了 fprintf(stderr, “Error: Division by zero\n”); 這一行,因為在 divide 函數中檢測到了除以零的情況,GDB 顯示了 fprintf 函數的調用信息
(8)檢查d的輸出值(d在main函數里面,要檢查d的值就要退出divide函數并返回到調用點,使用finish命令)
finish
(9)繼續執行程序(程序將繼續執行并打印 Divide result: 后跟 d 的值)
continue
?
完成調試,退出gdb時,使用quit命令