【C语言】守护进程(daemon)的输出到一个文本文件

时间:2024-03-20 19:36:32

一、常用的守护进程函数

void daemonize ()
{
    //deamonize
    pid_t pid = fork();
    if( pid > 0 )
    {       
        //parent exit
        exit(0);
    }

    //child continue
    setsid();
    chdir("/");
    close(0);
    open("/dev/null", O_RDWR);
    //no env debug
    if(!getenv("debug"))
    {
        close(1);
        close(2);
        dup(0);
        dup(0);
    }
}

这段代码的目的是让一个程序在后台以守护进程(daemon)的形式运行。让我们逐步了解每一行代码的作用:

pid_t pid = fork();

这行代码创建了一个新的进程,这是通过`fork()`系统调用实现的。`fork()`会创建一个和当前进程几乎完全相同的子进程。`fork()`调用会在父进程中返回新创建的子进程的进程ID,在子进程中则返回0。如果返回值大于0,那么代码运行于父进程;如果是0,则表示在子进程中。

if( pid > 0 )
{       
    //parent exit
    exit(0);
}

如果`fork()`的返回值大于0,表示当前代码段在父进程中运行。因为我们的目的是让程序在后台运行,我们不希望保留父进程。所以父进程会调用`exit(0)`正常退出。

//child continue
setsid();

接下来的部分是在子进程中继续执行的。`setsid()`会创建一个新的会话,并设置子进程为这个新会话的领头进程。一个会话可以包含一个或多个进程组;由`setsid`创建的新会话有一个新的进程组,且子进程是这个进程组的领头进程,并且没有控制终端。

chdir("/");

通过`chdir("/")`将当前工作目录更改为根目录("/")。这是因为守护进程通常应该不与任何特定目录关联,尤其是不应该继续驻留在启动它们的目录中,可能会妨碍卸载文件系统等操作。

close(0);
open("/dev/null", O_RDWR);

这两行代码关闭了标准输入(文件描述符0),然后打开`/dev/null`设备用于读写。所有写入到`/dev/null`的数据都会被丢弃,读取`/dev/null`会立即返回文件结束。

if(!getenv("debug"))
{
    close(1);
    close(2);
    dup(0);
    dup(0);
}

最后这部分首先检查是否存在名为"debug"的环境变量。如果没有(`getenv("debug")`返回NULL),则进行以下操作:
- 关闭标准输出(文件描述符1)。
- 关闭标准错误(文件描述符2)。
接下来,调用`dup(0)`复制文件描述符0(也就是之前打开的`/dev/null`),因为在文件描述符1和2被关闭之后,`dup`调用会使用最低的、未被使用的文件描述符号,也就是先是1然后是2,因此这一步相当于重新定向了进程的标准输出到`/dev/null`,然后又将标准错误也重定向到了`/dev/null`。
所以,这段代码的整体作用是生成一个子进程,让它脱离终端和工作目录,且默认情况下把所有的输入、输出重定向到`/dev/null`,使之成为一个后台运行的守护进程。如果设置了“debug”环境变量,则标准输出和错误不会被重定向。

二、守护进程(daemon)的输出到一个文本文件

将守护进程的输出重定向到一个文本文件,需要在代码中用`open`系统调用打开希望输出到的文件,并且用`dup2`或`dup`系统调用将标准输出(文件描述符1)和/或标准错误(文件描述符2)重定向到这个文件描述符上。
以下是将输出重定向到指定的日志文件函数:

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>

void daemonize ()
{
    // Daemonize
    pid_t pid = fork();
    if (pid > 0) 
    {       
        // Parent exits
        exit(0);
    }
    
    // Child (daemon) continues
    setsid();
    chdir("/");
    
    // Redirect standard file descriptors to /dev/null or a log file
    close(STDIN_FILENO);
    open("/dev/null", O_RDWR); // STDIN

    // Redirect STDOUT and STDERR to a log file
    const char* logFilePath = "/var/log/daemon.log";
    int logFile = open(logFilePath, O_RDWR | O_CREAT | O_APPEND, 0600);

    if (logFile == -1) {
        // Handle error, e.g., exit or print an error message
    } else {
        if (!getenv("debug")) {
            close(STDOUT_FILENO); // Close the standard output
            close(STDERR_FILENO); // Close the standard error
            dup2(logFile, STDOUT_FILENO); // Redirect standard output to the log file
            dup2(logFile, STDERR_FILENO); // Redirect standard error to the log file
        }
        // At this point, whether debugging or not, STDOUT and STDERR go to the log file
    }
    
    if (logFile != STDOUT_FILENO && logFile != STDERR_FILENO) {
        close(logFile); // We don't need this anymore
    }
}

这里我们使用函数`open`创建或打开日志文件。文件被设置为可读写(`O_RDWR`),如果未存在则创建它(`O_CREAT`),并以追加模式打开(`O_APPEND`)。文件权限被设置为0600,即只有拥有者可以读写。
此外,我们会先关闭已打开的标准输出和错误文件描述符,然后使用`dup2`,把 logFile 的文件描述符复制到标准输出和错误的文件描述符上。如果文件的打开操作失败,可能需要适当地处理这个错误,比如打印一个错误信息或者退出程序。
现在,守护进程的标准输出和错误都会被写入到`/var/log/daemon.log`日志文件中。确保程序具有创建和写入该日志文件的权限,尤其是当程序以特权用户(如`root`)运行时。

使用`dup`或`dup2`系统调用只能复制一个现有的文件描述符到另一个文件描述符,不能直接打开新的文件。 如果想重定向守护进程的输出到 /var/log/daemon.log ,需要使用`open`系统调用首先打开这个文件,然后才能用`dup`或`dup2`复制相应的文件描述符。
以下是如何修改守护进程的代码以重定向输出到 /var/log/daemon.log 的示例:

if(!getenv("debug"))
{
    close(1);
    close(2);

    // 打开或创建一个文件用来写入日志
    int log_file = open("/var/log/daemon.log", O_RDWR | O_CREAT | O_APPEND, 0600);

    if (log_file < 0) {
        // 无法打开日志文件,可能是因为权限问题或其他问题
        exit(1);
    }

    // 重定向标准输出到日志文件
    if (dup2(log_file, 1) < 0) {
        // 无法重定向标准输出
        exit(1);
    }

    // 重定向标准错误到日志文件
    if (dup2(log_file, 2) < 0) {
        // 无法重定向标准错误
        exit(1);
    }

    // 现在我们已经复制了日志文件到标准输出和标准错误,关闭原始的日志文件描述符
    if (log_file > 2) {
        close(log_file);
    }
}

在上面的代码中,`open`系统调用用于打开(或创建,如果还不存在的话) /var/log/daemon.log 文件。`O_RDWR` 表示文件是以读写模式打开的,`O_CREAT` 表示如果文件不存在,就创建它。`O_APPEND` 保证所有写操作都是追加到文件末尾,而`0600`表示创建的文件权限(只有拥有者有读写权限)。
dup2系统调用用于把日志文件的文件描述符复制到标准输出(1)和标准错误(2)。如果`dup2`调用成功,它会关闭旧的文件描述符,并将它替换为新的文件描述符。注意,如果发生错误(例如,文件打开失败或`dup2`调用失败),则程序会退出。
最后,检查一下原始的日志文件描述符是否大于2,因为0、1和2是标凑输入、输出和错误。如果原始的描述符大于这些,我们就关闭它,因为标准输出和错误已经被重定向到我们的日志文件。
使用这种方式,守护进程的标准输出和错误都会被记录到 /var/log/daemon.log 文件中。 注意,在生产环境中,直接把日志输出到文件可能是不够的,可能还需使用日志旋转等措施管理日志文件的增长。 

三、dup和dup2

dup 和 dup2 是 Unix 和类 Unix 系统 (如 Linux) 的系统调用,用于复制(duplicate)文件描述符。

dup 系统调用会复制一个旧的文件描述符,返回一个新的文件描述符。新文件描述符指向旧文件描述符所指向的文件。新描述符会是当前未使用的最小值的文件描述符。比如:

int new_fd = dup(old_fd);

在这段代码中,`old_fd` 是旧的文件描述符,`new_fd` 是新创建的文件描述符。新的 new_fd 指向和 old_fd 相同的文件,拥有相同的文件偏移量和访问模式(比如读、写)。

dup2 函数和 dup 类似,也是用来复制文件描述符。不过,`dup2` 允许指定新文件描述符的值。如果指定的值已经是一个打开的文件描述符,`dup2` 会先关闭它,然后再复制。这个操作是原子性的,即系统保证这两步(关闭和复制)是连续执行的,没有其他的调用会插入其中。例如:

dup2(old_fd, TARGET_FD);

这将复制 old_fd,并确保新的文件描述符的值是 TARGET_FD。

在一个守护进程中重定向输出到 /dev/null 使用 dup 是因为它会自动复制到当前未使用的最低的文件描述符上。因为在之前的代码中,标准输入、输出和错误(文件描述符0、1、2)已经关闭,所以调用 dup(0) 将会分别把 /dev/null 分配给标准输出和标准错误 (文件描述符1和2)。
重定向输出到一个具体的文件,如 /var/log/daemon.log,使用 dup2 是因为需要更精确地控制使用哪个文件描述符。比如,想确保标准输出和错误分别被送到文件描述符1和2:

int fd = open("/var/log/daemon.log", O_RDWR | O_CREAT | O_APPEND, 0600);
if (fd < 0) {
    // Error handling
}
dup2(fd, STDOUT_FILENO); // STDOUT_FILENO typically is 1
dup2(fd, STDERR_FILENO); // STDERR_FILENO typically is 2

在这段代码中,`open` 调用创建或打开日志文件,然后 dup2 函数重定向标准输出和错误到这个文件(文件描述符1和2),保证所有守护进程的输出都被写入到指定日志文件中。使用 dup2 而不是 dup 使得能精确指定要被复制的新文件描述符的数值。

对于将守护进程的输出重定向到`/var/log/daemon.log`而言,简单地使用`dup`可能是不够的。
想要将守护进程的标准输出和标准错误重定向到`/var/log/daemon.log`,可以采用如下方法:
首先打开日志文件:

int log_fd = open("/var/log/daemon.log", O_WRONLY|O_CREAT|O_APPEND, 0600);
if (log_fd < 0) {
    // Handle error if file opening fails
}

在这里,`O_WRONLY`表示以只写方式打开文件,`O_CREAT`表示如果文件不存在就创建它,`O_APPEND`表示写入时总是追加到文件的末尾。`0600`是文件的权限位,表示文件所有者可以读写,而其他用户没有任何权限。
之后将标准输出和标准错误重定向到这个文件:

dup2(log_fd, STDOUT_FILENO); // STDOUT_FILENO is 1
dup2(log_fd, STDERR_FILENO); // STDERR_FILENO is 2

在这里,`dup2`首先会关闭第二个参数指定的文件描述符(如果它已经打开),然后将第二个参数的文件描述符复制为第一个参数的文件描述符。在上述代码中,我们先将`log_fd`复制到标准输出(`STDOUT_FILENO`),然后再复制到标准错误(`STDERR_FILENO`)。
最后,既然标准输出和标准错误已被重定向,`log_fd`就不再需要了,因此应该关闭它:

if (log_fd > STDERR_FILENO) { // Check if it's not one of the standard descriptors
    close(log_fd);
}

对于标准输入,通常会继续将其重定向到`/dev/null`。
通过上述步骤,守护进程的标准输出和错误就会被写入到`/var/log/daemon.log`文件中去。记得要考虑`open`和`dup2`可能失败的情况,并进行适当的错误处理。