目录
一、预备知识
日志是有等级的,表明该条日志的重要程度,一般分为以下几个级别:
#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);
}