2. 守护进程的日志记录

任务说明

本案例为第2个案例,目标是为守护进程添加日志记录功能。我们将基于第1个案例中创建的守护进程框架,实现一个独立的日志模块,将运行信息(如启动、退出、心跳)写入指定日志文件,每条日志包含时间戳和日志级别(INFO、WARN、ERROR)。本案例代码可独立编译运行,不依赖前文代码。

实现方案

创建一个日志模块 logger.c 和头文件 logger.h,提供以下接口:

  • log_init(const char *path):打开日志文件,失败返回 -1。
  • log_write(int level, const char *fmt, ...):写入一条日志,自动添加时间戳和级别前缀。
  • log_close(void):关闭日志文件。

同时提供一个 main.c,演示守护进程化后周期性写入心跳日志,并处理 SIGTERM 信号以记录退出日志。日志文件路径通过命令行参数传入,默认 /tmp/daemon.log。

代码实现

// logger.h
#ifndef LOGGER_H
#define LOGGER_H

#define LOG_INFO  0
#define LOG_WARN  1
#define LOG_ERROR 2

int log_init(const char *path);
void log_write(int level, const char *fmt, ...);
void log_close(void);

#endif
// logger.c
#include <stdio.h>
#include <stdarg.h>
#include <time.h>
#include <string.h>
#include <sys/time.h>
#include "logger.h"

static FILE *log_fp = NULL;

static const char *level_str(int level) {
    switch (level) {
        case LOG_INFO:  return "INFO";
        case LOG_WARN:  return "WARN";
        case LOG_ERROR: return "ERROR";
        default:        return "UNKNOWN";
    }
}

int log_init(const char *path) {
    log_fp = fopen(path, "a");
    if (!log_fp) return -1;
    setvbuf(log_fp, NULL, _IOLBF, 0); // 行缓冲,便于实时查看
    return 0;
}

void log_write(int level, const char *fmt, ...) {
    if (!log_fp) return;

    struct timeval tv;
    struct tm tm;
    char timebuf[32];

    gettimeofday(&tv, NULL);
    localtime_r(&tv.tv_sec, &tm);
    strftime(timebuf, sizeof(timebuf), "%Y-%m-%d %H:%M:%S", &tm);

    fprintf(log_fp, "[%s.%03ld] [%s] ", timebuf, tv.tv_usec / 1000, level_str(level));

    va_list args;
    va_start(args, fmt);
    vfprintf(log_fp, fmt, args);
    va_end(args);

    fprintf(log_fp, "\n");
}

void log_close(void) {
    if (log_fp) {
        fclose(log_fp);
        log_fp = NULL;
    }
}
// main.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include "logger.h"

static volatile sig_atomic_t running = 1;

static void handle_signal(int sig) {
    (void)sig;
    running = 0;
}

static void daemonize(void) {
    pid_t pid = fork();
    if (pid < 0) exit(EXIT_FAILURE);
    if (pid > 0) exit(EXIT_SUCCESS); // 父进程退出

    setsid();
    pid = fork();
    if (pid < 0) exit(EXIT_FAILURE);
    if (pid > 0) exit(EXIT_SUCCESS);

    umask(0);
    chdir("/");
    int fd = open("/dev/null", O_RDWR);
    if (fd >= 0) {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        if (fd > 2) close(fd);
    }
}

int main(int argc, char *argv[]) {
    const char *log_path = "/tmp/daemon.log";
    if (argc > 1) log_path = argv[1];

    daemonize();

    if (log_init(log_path) != 0) {
        exit(EXIT_FAILURE);
    }

    signal(SIGTERM, handle_signal);
    signal(SIGINT, handle_signal);

    log_write(LOG_INFO, "daemon started, pid=%d", getpid());

    int counter = 0;
    while (running) {
        sleep(5);
        counter++;
        log_write(LOG_INFO, "heartbeat #%d", counter);
    }

    log_write(LOG_INFO, "daemon stopped, total heartbeats=%d", counter);
    log_close();
    return 0;
}

编译与运行

gcc -Wall -o daemon_logger main.c logger.c
./daemon_logger /tmp/daemon.log

程序启动后立即守护化并返回 shell。日志写入 /tmp/daemon.log(或指定路径)。

验证方法

  1. 运行上述编译命令,确认无编译错误。
  2. 执行 ./daemon_logger,shell 应立即返回,无前台输出。
  3. 等待约 12 秒后执行 cat /tmp/daemon.log,应看到类似以下内容(时间戳和心跳编号会随实际运行变化):
[2025-01-15 10:30:01.123] [INFO] daemon started, pid=12345
[2025-01-15 10:30:06.456] [INFO] heartbeat #1
[2025-01-15 10:30:11.789] [INFO] heartbeat #2
  1. 执行 kill -TERM 12345(将 12345 替换为实际 pid),再查看日志末尾,应包含 daemon stopped 记录。

判定标准:日志文件存在且每条记录包含 [日期 时间.毫秒] [级别] 前缀;启动后每 5 秒新增一条心跳记录;发送 SIGTERM 后进程退出并写入停止记录。

日志格式与观测点

日志文件单行结构 [2025-01-15 10:30:01.123] [INFO] daemon started, pid=12345 时间戳(含毫秒) 级别 消息内容 验证观测点 启动后立即出现 started 记录 每 5 秒新增 heartbeat 记录 SIGTERM 后出现 stopped 记录 每条记录含毫秒级时间戳与级别