守护进程编程
守护进程的含义
定义
守护进程(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命令