【C++】日志/守护进程组件

一、日志小组件

日志:程序运行过程中记录的程序运行状态信息

作用:记录了程序运行状态信息,以便程序猿能够随时根据状态信息,对系统程序的运行状态进行分析。能让用户非常简便的进行详细的日志输出以及控制

日志等级

在小组件中我们将日志分为6个等级,以便我们对信息进行分类

// 日志等级
#define DEBUG   0
#define INFO    1
#define WARNING 2
#define ERROR   3
#define FATAL   4 
#define UNKNOW  5

日志格式

日志输出格式: 日志等级 时间 文件名称 行号 PID 消息体

格式化日志等级

方法一:使用函数

static std::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 "UNKNOW"; 
    }
}

我们可以通过构建函数进行数组和字符串的转换来获取日志等级

格式化输出时间

static std::string getTime() {
    // int tm_sec;			/* Seconds.	[0-60] (1 leap second) */
    // int tm_min;			/* Minutes.	[0-59] */
    // int tm_hour;			/* Hours.	[0-23] */
    // int tm_mday;			/* Day.		[1-31] */
    // int tm_mon;			/* Month.	[0-11] */
    // int tm_year;			/* Year	- 1900.  */
    // int tm_wday;			/* Day of week.	[0-6] */
    // int tm_yday;			/* Days in year.[0-365]	*/
    // int tm_isdst;			/* DST.		[-1/0/1]*/
    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 + 1, tmp->tm_yday, tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
    return buffer;
}

这里我们使用了time(), localtime()两个函数

time函数用于获取当前时间的时间戳

time_t time(time_t *t);

然而时间戳是一个非常大的整数,不利于人类识别时间,我们要使用localtime()函数将时间戳转换成可读状态

struct tm *localtime(const time_t *timep);

struct tm类型的定义如下,我们会使用到tm_year, tm_mon, tm_yday, tm_hour, tm_min, tm_sec需要注意的是该函数的年是从1900年开始算的,月份是从0 - 11月,我们使用时需要将年份加上1900,将月份加上1

       struct tm {
           int tm_sec;         /* seconds */
           int tm_min;         /* minutes */
           int tm_hour;        /* hours */
           int tm_mday;        /* day of the month */
           int tm_mon;         /* month */
           int tm_year;        /* year */
           int tm_wday;        /* day of the week */
           int tm_yday;        /* day in the year */
           int tm_isdst;       /* daylight saving time */
       };

这里我们还使用了snprintf()函数,其使用方法和printf非常像,printf是向标准输出文件中写,而snprintf是向指定缓冲区中写

int snprintf(char *str, size_t size, const char *format, ...);

获取文件名称,行号

我们可以使用C语言为我们提供的__FILE__和__LINE__两个宏来获取文件名称和行号

printf("%s:%d\n", __FILE__, __LINE__);

[clx@VM-20-6-centos thread_pool_tcp_2.0]$ ./clx_test 
test.cpp:4

用户消息处理

用户传入的消息是一个可变参数列表 ,下面我们先解释一下可变参数列表

可变参数列表
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);

我们可以用一段代码来理解这一系列借口的使用

#include <cstdio>
#include <cstdarg>

#define LOG(fmt, ...) printf("[%s : %d] " fmt, __FILE__, __LINE__, ##__VA_ARGS__);

void printNum(int count, ...) {									// count 不定参数的个数
    va_list ap;                                 // va_list实际就是一个char*类型的指针
    va_start(ap, count);                        // 将char*类型指针指向不定参数的起始位置
    for (int i = 0; i < count; i++) {            
        int num = va_arg(ap, int);              // 从ap位置取一个整形大小空间数据拷贝给num,并将ap向后移动一个整形大小空间
        printf("param[%d], %d\n", i, num);       
    }
    va_end(ap);                                 // 将ap指针置空
}

int main() {
    printNum(2, 666, 222);
    return 0;
}

  • va_list ap: 就是定义一个char* 类型的指针
  • va_start : 让指针指向不定参数的起始位置,第二个参数传的是不定参数的前一个参数,因为函数调用过程中是会将实参一个个压入函数栈帧中的,所以参数之间都是紧挨着的。我们找到了前一个参数count的地址,也就等于找到了不定参数的起始地址
  • va_arg : 用于从不定参数中解析参数,第一个参数数据的起始位置,第二个参数指定参数类型,根据类型我们可以推导出参数的大小,从而将参数数据解析出来
  • va_end : 将ap指针置空

这里我们解释传入类型只能是int类型,我们如何使用上述接口将不定参数分离的原理,那么printf这类函数是如何将不定参数分离的呢??**这是因为我们在使用printf函数开始传递了format参数,其中包含了%s, %d这类的信息,printf内部通过对format 参数进行解析就知道了后面的参数依次都是什么类型的,然后将类型依次放入va_arg函数,就可以将参数全部提取出来了

void myprintf(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    char* res;
    int ret = vasprintf(&res, fmt, ap);
    if (ret != -1) {
        printf(res);
        free(res);
    }
    va_end(ap);
}
  • vasprintf 函数会帮助提取不定参数并且将其拼接到格式化字符串中,并开辟空间将处理好的字符串数据放入空间,并将我们传入的指针指向这块空间
  • 成功返回打印的字节数,失败返回-1
vsnprintf()函数

这里我们选择使用vsnprintf()函数来处理我们的不定参数,我们只需要获取不定参数的起始位置,该函数就会自动帮我们根据 format取出不定参数进行拼接,然后输出到我们指定的位置

int vsnprintf(char *str, size_t size, const char *format, va_list ap);
char logRight[1024];	 		 // 用于获取格式化字符串
va_list p; 								
va_start(p, format);				
vsnprintf(logRight, sizeof(logRight), format, p);
va_end(p);

这样我们就成功获取到用户描写的输出信息了

拼接信息写入文件

这里我们使用C语言的方式进行文件写入,以append的方式写入数据

   FILE *fp = fopen(filename.c_str(), "a+");  
    if (fp == nullptr) {
        return;
    }
    fprintf(fp, "%s%s\n", logLeft, logRight);
    fflush(fp);   // 如果把文件放在这可以不写,因为马上就要关闭文件了。如果将文件放在tcp_server 的成员变量中,因为不会重复打开和关闭,所以就需要刷新
    fclose(fp);

日志组件代码

#pragma once

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdarg>
#include <ctime>
#include <string>
#include <unistd.h>
#include <sys/types.h>

static const std::string filename = "log/tcp_server.log";

// 日志等级
#define DEBUG   0
#define INFO    1
#define WARNING 2
#define ERROR   3
#define FATAL   4 
#define UNKNOW  5

// 日志格式: 日志等级 时间 文件 行 PID 消息体 
// logMessage(DEBUG, "hello : %d, %s", 12, s.c_str()); /

static std::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 "UNKNOW"; 
    }
}

static std::string getTime() {
    // int tm_sec;			/* Seconds.	[0-60] (1 leap second) */
    // int tm_min;			/* Minutes.	[0-59] */
    // int tm_hour;			/* Hours.	[0-23] */
    // int tm_mday;			/* Day.		[1-31] */
    // int tm_mon;			/* Month.	[0-11] */
    // int tm_year;			/* Year	- 1900.  */
    // int tm_wday;			/* Day of week.	[0-6] */
    // int tm_yday;			/* Days in year.[0-365]	*/
    // int tm_isdst;			/* DST.		[-1/0/1]*/
    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 + 1, tmp->tm_yday, tmp->tm_hour, tmp->tm_min, tmp->tm_sec);
    return buffer;
}

static void logMessage(int level, const char* format, ...) {
    char logLeft[1024];

    std::string level_string = toLevelString(level);
    std::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);   // 如果把文件放在这可以不写,因为马上就要关闭文件了。如果将文件放在tcp_server 的成员变量中,因为不会重复打开和关闭,所以就需要刷新
    fclose(fp);

二、守护进程

守护进程即常说的 Daemon 进程,是 Linux 中的后台服务进程,周期性地执行某种任务或者等待某些发生的事件。

我们在终端中可以使用./的方式让程序在前台运行,也可以在后面加上一个&让程序在后台运行,并可以使用jobs来查看后台运行的进程。也可以使用Ctrl + z快捷键让程序停止并放置后台

jobs 指令

  • jobs -l : 查看后台进程

  • bg + %n: 让后台停止进程继续运行

  • fg + %n(编号) : 将该进程提到前台运行

Linux会话概念

会话(session)是若干进程的集合,系统中的每一个进程也必须从属于某一个会话。 一个会话最多只有一个控制终端(也可以没有),该终端为会话中所有进程组中的进程所共有。 一个会话中只会有一个前台进程组,只有前台进程组中的进程才可以和控制终端进行交互 在拥有控制终端的会话中,session leader 也被称为控制进程(controlling process),一般来说控制进程也就是登入系统的 shell 进程(login shell)

当我们启动一个程序,可以使用ps指令查看这个进程的一些属性信息

[clx@VM-20-6-centos ~]$ sleep 10000 | sleep 20000 | sleep 3000 &
[1] 28705
[clx@VM-20-6-centos ~]$ ps -axj | head -1 && ps -axj | grep sleep | grep -v grep
 PPID   PID  PGID   SID TTY      TPGID STAT   UID   TIME COMMAND
 1212   953  1130  1130 ?           -1 S        0   0:00 sleep 60
 7094  7331  7094  1177 ?           -1 Sl       0  11:57 /bin/sh -c sleep 100
31712 31868 31868 31712 pts/0     1172 S     1001   0:00 sleep 10000
31712 31869 31868 31712 pts/0     1172 S     1001   0:00 sleep 20000
31712 31870 31868 31712 pts/0     1172 S     1001   0:00 sleep 30000

首先我们先解释一下各个属性的名称

  • PPID : 父进程的PID
  • PID : 进程的PID
  • PGID :process group id 进程组ID
  • SID : 会话ID
  • TTY : 终端关联文件 打问号的说明该进程和终端没有关系,pts/0是终端文件,可以使用ls /dev/pts/16 -al查看终端文件

我们使用| 分隔的的程序之间实际是通过匿名管道通信的,所以他们三条程序启动后就是三个进程,前为后的父进程。可以看到这三个进程的PPID相同(因为他们的父亲都是我们的bash), 我们可以使用指令验证一下

[clx@VM-20-6-centos ~]$ ps -axj | grep 31712
31712  1492  1492 31712 pts/0     1492 R+    1001   0:00 ps -axj
31712  1493  1492 31712 pts/0     1492 S+    1001   0:00 grep --color=auto 31712
31711 31712 31712 31712 pts/0     1492 Ss    1001   0:00 -bash   
31712 31868 31868 31712 pts/0     1492 S     1001   0:00 sleep 10000
31712 31869 31868 31712 pts/0     1492 S     1001   0:00 sleep 20000
31712 31870 31868 31712 pts/0     1492 S     1001   0:00 sleep 30000

然后我们发现这三个线程属于同一个线程组,并且三个线程也拥有相同的会话ID,我们还可以发现会话ID的值和bash进程的PID相同,进程组的ID和进程组内第一个进程的ID相同

实际上我们的终端链接我们的Linux服务器系统时,Linux内核会给我们创建一个会话,我们的所有的进程呀,线程呀都在这个会话中运行。

而我们使用Xshell链接云服务器的本质就是,内核给我们创建一个会话,而其中开始运行的第一个进程也就是我们的bash,所以会话ID和bash的ID是相同的。而bash 又可以创建很多的子程序,这些程序都运行在这个会话中,而我们的终端本质就是一个文件,其就在/dev/pts

[clx@VM-20-6-centos ~]$ ls /dev/pts
10  11  12  13  14  15  16  0  5  6  7  8  9  ptmx
[clx@VM-20-6-centos ~]$ ls /dev/pts/16 -al
crw--w---- 1 clx tty 136, 16 717 23:34 /dev/pts/16

如果后台任务提到前台,老的前台任务无法运行在会话中只能有一个前台任务正在运行! – 我们命令行启动一个进程后,bash就无法运行了。如果登陆就是创建一个会话,bash任务,启动我们的进程,就是在当前会话中创建新的前后台任务,那么如果我们退出呢。销毁会话可能会影响会话内所有任务,不同操作系统的操作不同,所以一般网络服务器需要以守护进程的方式运行

企业的服务器肯定不能受终端的退出而影响,我们要保证其100%概率正确运行。所以这些服务器一般都以守护进程的方式运行在操作系统中

创建独立会话

要想我们的程序不被当前会话影响,方法就是让这个程序脱离当前会话,自己成为一个独立的会话。比如我们想要让我们的TCP服务器成为一个守护进程该怎么办呢?

setsid() :谁调用这个函数,谁就成为会话,SID就等于这个进程的PID。但是规定调用的进程不能是组长。

pid_t setsid(void);

我们的TCP服务器就只有一个进程,它肯定就是组长了,如何让其成为组员呢??

只需要创建一个进程,然后让它的子进程运行我们的TCP服务器即可,父进程我们就直接让其退出。这样我们进程就会被爷爷进程也就是1号进程领养。这时候我们就不是组长了,就可以调用setsid()函数了

所以守护进程的本质就是孤儿进程的一种。所以守护进程是直属操作系统的会话,那么我们在守护进程中调用cin,cout等函数我们的终端就收不到了,我们可以选择强制关闭0, 1, 2文件,但是这样过于暴力,如果我们的代码中出现了cin或者是cout的代码会直接报错

我们最常见的选择是将使用系统给我们提供的/dev/null设备,其是一个字符设备,类似于垃圾桶,会自动丢弃掉向其写入的任何信息,我们可以从其内部读取到随机数。我们将0, 1, 2都重定向到这个字符设备就可以了

守护进程组件代码

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


//  守护进程的本质 : 是孤儿进程的一种
void Daemon() {
    // 1、 ignore signal
    signal(SIGPIPE, SIG_IGN); signal(SIGCHLD, SIG_IGN);

    // 2、 let father process quit
    if (fork() > 0) exit(0);   

    // 3、 new session , child process become the first process 
    pid_t ret = setsid();
    if (static_cast<int>(ret) == -1) {
        logMessage(FATAL, "deamon error, code: %d, string: %s", errno, strerror(errno));
        exit(SETSID_ERR);
    }
    // 4、 更改守护进程的工作路径

    // 5、 处理后续的对于0,1,2 的问题
    // 直接强制关闭0, 1, 2 过于暴力,如果代码中出现cin,cout则会直接终止进程
    // 将012重定向到垃圾桶, 将输出的东西丢掉垃圾桶(/dev/null)
    int fd = open("/dev/null", O_RDONLY | O_RDWR);
    if (fd < 0) {
        logMessage(FATAL, "deamon error, code : %d, string : %s", errno, strerror(errno));
        exit(OPEN_ERR);
    }
    dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); close(fd);
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白在进击

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值