1. 创建第一个守护进程

创建第一个守护进程 学习卡片

任务说明

本案例要求实现一个标准的Linux守护进程创建流程,核心目标是通过fork()、setsid()、chdir()、umask()四个关键步骤,使进程脱离控制终端独立运行。最终验证方式为:启动该守护进程后,关闭终端或退出登录,进程仍持续运行,且其父进程PID变为1(init/systemd)。

守护进程实现

// daemonize.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
#include <string.h>

#define DAEMON_NAME "mydaemon"
#define WORK_DIR "/tmp"
#define LOG_FILE "/tmp/mydaemon.log"

void write_log(const char *msg) {
    int fd = open(LOG_FILE, O_WRONLY | O_CREAT | O_APPEND, 0644);
    if (fd < 0) return;
    char buf[256];
    int len = snprintf(buf, sizeof(buf), "[%ld] %s\n", (long)getpid(), msg);
    write(fd, buf, len);
    close(fd);
}

int main(void) {
    // 第一步:fork,父进程退出
    pid_t pid = fork();
    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    }
    if (pid > 0) {
        // 父进程退出,使子进程成为孤儿进程,由init收养
        exit(EXIT_SUCCESS);
    }

    // 第二步:创建新会话,脱离控制终端
    if (setsid() < 0) {
        perror("setsid");
        exit(EXIT_FAILURE);
    }

    // 第三步:修改工作目录,避免占用挂载点
    if (chdir(WORK_DIR) < 0) {
        perror("chdir");
        exit(EXIT_FAILURE);
    }

    // 第四步:重置文件权限掩码
    umask(0);

    // 关闭标准输入/输出/错误,避免占用终端
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 将标准文件描述符重定向到/dev/null,防止意外错误
    int devnull = open("/dev/null", O_RDWR);
    if (devnull >= 0) {
        dup2(devnull, STDIN_FILENO);
        dup2(devnull, STDOUT_FILENO);
        dup2(devnull, STDERR_FILENO);
        if (devnull > STDERR_FILENO) close(devnull);
    }

    // 忽略SIGCHLD信号,避免产生僵尸进程
    signal(SIGCHLD, SIG_IGN);

    // 守护进程主循环:每5秒写一次日志
    write_log("daemon started");
    while (1) {
        sleep(5);
        write_log("daemon is running");
    }

    return 0;
}

编译与运行

# 编译
gcc -o mydaemon daemonize.c

# 启动守护进程
./mydaemon

# 查看进程状态(确认PPID为1,且无控制终端tty)
ps -o pid,ppid,pgid,sid,tty,cmd -p $(pgrep -x mydaemon)

验证方法与判定标准

执行以下步骤验证守护进程是否正确脱离终端:

  1. 启动./mydaemon后,终端应立即返回提示符(父进程已退出),不会阻塞。
  2. 执行ps -o pid,ppid,pgid,sid,tty,cmd -p $(pgrep -x mydaemon),观察输出:PPID应为1(被init/systemd收养),TTY应为?(表示无控制终端),PGID与SID应等于进程自身的PID(因setsid()创建了新会话)。
  3. 关闭当前终端窗口或执行exit退出登录,重新打开终端后执行pgrep -x mydaemon,进程仍然存在。
  4. 查看日志文件/tmp/mydaemon.log,应持续追加包含PID和运行状态的行,证明主循环正常工作。

以上条件全部满足,即判定守护进程创建成功。

关键步骤说明

守护进程创建流程
┌─────────────────────────────────────────────────────────────┐
│                    终端启动 ./mydaemon                        │
└──────────────────────────────┬──────────────────────────────┘
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  fork() ──→ 父进程退出,子进程被init收养                        │
└──────────────────────────────┬──────────────────────────────┘
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  setsid() ──→ 创建新会话,子进程成为会话首进程,脱离控制终端      │
└──────────────────────────────┬──────────────────────────────┘
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  chdir("/tmp") ──→ 切换工作目录,避免占用文件系统挂载点          │
└──────────────────────────────┬──────────────────────────────┘
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  umask(0) ──→ 清除文件权限掩码,确保创建文件权限可控             │
└──────────────────────────────┬──────────────────────────────┘
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  关闭stdin/stdout/stderr → 重定向到/dev/null                  │
└──────────────────────────────┬──────────────────────────────┘
                               ▼
┌─────────────────────────────────────────────────────────────┐
│  主循环:每5秒写日志,持续运行                                 │
└─────────────────────────────────────────────────────────────┘

预期输出示例

执行ps命令后,预期输出格式如下(PID数值因系统而异):

  PID  PPID  PGID   SID TTY      CMD
 1234     1  1234  1234 ?        ./mydaemon

其中PPID=1、TTY=?是守护进程脱离终端的关键标志。日志文件/tmp/mydaemon.log内容示例:

[1234] daemon started
[1234] daemon is running
[1234] daemon is running