【Linux】日志与守护进程

目录

一、预备知识

二、打印日志

三、守护进程

1、前置知识

2、守护进程


一、预备知识

日志是有等级的,表明该条日志的重要程度,一般分为以下几个级别:

复制代码
#define DEBUG 0 //调试信息
#define INFO  1 //正常运行
#define WARNING 2 //报警
#define ERROR 3 //正常错误
#define FATAL 4 //严重错误

在打印日志时,通常需要用到可变参数列表。对于可变参数列表的读取,可以使用以下几个宏:

  • va_list

  • va_arg

  • va_start

  • va_end;

    void logMessage(int level, char* format, ...)
    {
    va_list p; //char* 类型指针
    va_start(p, format); //把指针p指向可变参数部分的起始地址
    int a = va_arg(p. int); //根据指定的类型提取参数
    va_end(p); //p = NULL
    }

二、打印日志

在打印日志时,一般都会有固定的格式,把日志格式放到一个缓冲区里,日志内容放到另一个缓冲区里。

例如:现在我们想打印日志的格式是 日志等级 时间 进程pid 消息体。那么就可以把 日志等级 时间 进程pid 放到 logleft 缓冲区中,消息体放到 logright 缓冲区中。

把可变参数列表元素打印到文件的函数:

复制代码
int vsnprintf(char *str, size_t size, const char *format, va_list ap);

获取当前时间函数:

复制代码
time_t time(time_t *tloc);

将时间转换成对应结构体的函数:

复制代码
struct tm *localtime(const time_t *timep);

完整代码:

复制代码
#pragma once

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <sys/types.h>
#include <unistd.h>
#include <ctime>
using namespace std;

// 日志是有日志等级的
enum
{
    DEBUG = 0,
    INFO,
    WARNING,
    ERROR,
    FATAL,
    UKNOWN
};

string filename = "logfile";

static string toLevelString(int level)
{
    switch (level)
    {
        case DEBUG: return "DEBUG";
        case INFO: return "INFO";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default: return "UKNOWN";
    }
}

string getTime()
{
    time_t curr = time(nullptr);
    struct tm* tmp = localtime(&curr);

    char buffer[128];
    snprintf(buffer, sizeof(buffer), "[%d-%d-%d %d:%d:%d]", tmp->tm_year + 1900, tmp->tm_mon, tmp->tm_mday,
                                                          tmp->tm_hour, tmp->tm_min, tmp->tm_sec);

    return buffer;
}

//日志格式:左半部分:日志等级 时间 pid 
//         右半部分:消息体
void logMessage(int level, const char* format, ...)
{
    char logLeft[1024];
    string level_string = toLevelString(level);
    string curr_time = getTime();
    snprintf(logLeft, sizeof(logLeft), "[%s %s %d]", level_string.c_str(), curr_time.c_str(), getpid());

    char logRight[1024];
    va_list p;
    va_start(p, format);
    vsnprintf(logRight, sizeof(logRight), format, p);
    va_end(p);

    //打印日志
    //printf("%s %s\n", logLeft, logRight);

    //保存到文件中
    FILE* fp = fopen(filename.c_str(), "a");
    if(fp == nullptr) return;

    fprintf(fp, "%s%s\n", logLeft, logRight);
    fflush(fp);

    fclose(fp);
}

三、守护进程

1、前置知识

首先后台创建几个进程:

查看刚刚创建的进程:

进程的属性中除了我们熟知的PPID与PID外, PGID 是进程组。 SID 是会话编号。 TTY 是终端文件。

进程组的组长,都是多个进程中的第一个,进程组的编号就是第一个进程的PID。

当我们从本地或远端登录Linux时,Linux会给用户分配一个命令行解释器,即bash进程。basn进程自己成立进程组,自己成立一个会话,会话编号就是bash进程的PID。未来所起的所有进程与进程组都属于这个会话。

一个进程组被创建出来,是为了完成一个任务,查看后台任务的指令:

复制代码
jobs

每一组进程都有一个编号,称为任务编号。

把后台任务提到前台的指令:

复制代码
fg [任务编号]

把前台任务放到后台的指令:

复制代码
ctrl + z
bg [任务编号]

创建进程组是为了完成任务的。在用户的视角,可以把一个进程组叫做一个任务。进程组可能包含一个或多个进程。

任务分为前台任务与后台任务。如果把后台任务提到前台,老的前台任务就无法运行了。在一个会话中,任何时刻,都只能有一个前台任务在运行。这就是为什么我们在命令行启动一个进程时,bash就无法运行了的原因。

用户登录就是创建一个会话并启动bash任务,在命令行中启动进程,就是创建新的前后台任务。用户退出就是销毁会话,可能会影响会话内部的所有任务。

网络服务器为了不受到用户的登录与注销的影响,一般就会通过守护进程的方式运行。

2、守护进程

守护进程是把一个任务独立出来,自己成为一个会话,以免受到其他会话的影响。

创建守护进程的函数:

复制代码
pid_t setsid(void);

谁调用这个函数,谁就把自己设置为守护进程。函数调用成功,返回调用这个函数的进程的pid。失败则返回-1,错误码被设置。

需要注意的是,一个进程组的组长,不能调用 setsid 函数

再创建守护进程时,有时会需要更改守护进程的工作路径,更改函数:

复制代码
int chdir(const char *path);

当一个进程编程守护进程时,他就不应该与标准输出、标准输入、标准错误文件有交互了。我们可以接用文件黑洞 /dev/null 来处理。 /dev/null 在任何一个Linux系统里都一定存在,向这个文件中写入的所有数据都会消失,读取这个文件直接返回。

守护进程完整代码:

复制代码
#pragma once

#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"
#include "error.hpp"

//守护进程是孤儿进程的一种
void Daemon()
{
    //1.忽略一些异常信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);

    //2.让自己不要成为组长
    if(fork() > 0) exit(0); //让父进程直接退出,这样保证子进程一定不是组长,一定可以调用setsid
                            //因为进程组的编号是父进程的pid

    //3.新建会话,自己成为会话的话首进程
    pid_t ret = setsid();
    if((int)ret == -1)
    {
        logMessage(FATAL, "deamon error, code: %d, string: %s", errno, strerror(errno));
        exit(SETSID_ERR);
    }

    //4.可以更换守护进程的工作目录
    //chdir

    //5.处理后续的对于文件描述符0, 1, 2的问题
    int fd = open("/dev/null", O_RDWR);
    if(fd < 0)
    {
        logMessage(FATAL, "open error, code: %d, string: "%s", error, strerror(errno));
        exit(OPEN_ERR);
    }

    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd);
}
相关推荐
伤不起bb2 小时前
MySQL 高可用
linux·运维·数据库·mysql·安全·高可用
shykevin4 小时前
python开发Streamable HTTP MCP应用
开发语言·网络·python·网络协议·http
tmacfrank5 小时前
网络编程中的直接内存与零拷贝
java·linux·网络
数据与人工智能律师7 小时前
虚拟主播肖像权保护,数字时代的法律博弈
大数据·网络·人工智能·算法·区块链
QQ2740287567 小时前
Soundness Gitpod 部署教程
linux·运维·服务器·前端·chrome·web3
qwfys2007 小时前
How to configure Linux mint desktop
linux·desktop·configure·mint
南方以南_7 小时前
Ubuntu操作合集
linux·运维·ubuntu
purrrew7 小时前
【Java ee初阶】HTTP(2)
网络·网络协议·http
冼紫菜8 小时前
[特殊字符]CentOS 7.6 安装 JDK 11(适配国内服务器环境)
java·linux·服务器·后端·centos
Chuncheng's blog9 小时前
RedHat7 如何更换yum镜像源
linux