文章目录
- 前言
- 一. 日志介绍
- 二. 简单日志
- 1. 左字符串
- 2. 右字符串
- 三. 守护进程
- 1. ps -axj命令
- 2. 会话
- 扩展命令
- 3. 创建守护进程
- 结束语
前言
本系列文章是计算机网络学习
的笔记,欢迎大佬们阅读,纠错,分享相关知识。希望可以与你共同进步。
本篇博客介绍简单,较为基础的日志。
日志和守护进程都是辅助服务器的,一个是服务器的运行信息,一个是服务器的运行方式
一. 日志介绍
日志是记录事件,运行结果的工具
日志文件是重要的系统文件,其中记录了很多重要的系统运行的事件。包括用户的登录信息,系统的启动信息,系统的安全信息,各种服务相关信息
日志对于安全来说也很重要,它记录了每天系统发生的各种事情,通过日志来检查错误发送的原因,或受到攻击时攻击者留下的痕迹
日志管理服务
日志级别分为:
debug | 有调试信息的,日志通信最多 |
---|---|
info | 一般信息日志,最常用 |
notic | 最具有重要性的普通条件的信息 |
warning | 警告级别 |
err | 错误级别,组织某个功能或者模块不能正常工作的信息 |
crit | 严重级别,阻止整个系统或者整个软件不能正常工作的信息 |
alert | 需要立刻修改的信息 |
emerg | 内核崩溃等重要信息 |
fatal | 致命错误 |
none | 什么都不记录 |
注意: 从上到下,级别从低到高,记录信息越来越少
二. 简单日志
本篇博客的日志是以函数的形式完成的,调用方式如下:
logMessage(Warning,"read error,%d,errno:%d",strerror(errno),errno);
参数有三个:日志级别,格式控制,可变参数
对应如下:
void logMessage(int level,const char*format,...){}
注意:format类型需要时const char*,因为大部分是以常量字符串的形式传参
我们期望最后的日志信息是这样的:[日志级别] [时间] [进程号] 消息内容(format)
可以将日志信息分成两部分,左字符串和右字符串,前三个为一组,消息内容使用vsnprintf
1. 左字符串
#pragma once
#include<iostream>
#include<string>
#include<ctime>
#include<unistd.h>
#include<sys/types.h>
#include<stdarg.h>
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal,
Uknown
};
//获取日志等级字符串
static std::string getLevelString(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";
}
}
//获取时间字符串
static std::string gettime()
{
time_t cur=time(nullptr);
struct tm* tmp=localtime(&cur);
char buffer[128];
snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",tmp->tm_year+1,tmp->tm_mon+1,tmp->tm_mday+1,tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
return buffer;
}
//日志信息
void logMessage(int level,char*format,...)
{
char logLeft[1024];
std::string level_string=getLevelString(level);//日志级别
std::string time_string=gettime();//时间
std::string pid_string=std::to_string(getpid());//进程号
//[日志级别] [时间] [进程号]
snprintf(logLeft,sizeof(logLeft),"[%s] [%s] [%s] ",level_string.c_str(),time_string.c_str(),pid_string.c_str());
}
2. 右字符串
接下来介绍va系列——解析可变参数
在C语言学习中,函数的调用会创建栈帧,而函数传参会进行压栈,可变参数也是如此,所以可变参数的前一个参数的后一位就是可变参数的起始地址
va_list
:类似指针,可以指向可变参数的起始和结束,可以遍历可变参数va_start
:将va_list定位到last后面va_arg
:将从va_list开始的数据,按照type类型进行提取返回va_end
:清空va_listva_copy
:将src拷贝给dest
而vsnprintf可以帮我们遍历可变参数,不需要我们自己控制
vsnprintf()
vsnprintf是一个标准的 C 函数,用于格式化字符串并将生成的字符存储在缓冲区中。它与函数类似,但有一个关键区别:函数不是直接采用可变长度的参数列表,而是采用参数,该参数是已使用宏初始化的参数列表。
以下是该函数的工作原理:
str
:写入的缓冲区size
:缓冲区大小format
:格式控制ap
:va_list 指向可变参数的指针
使用如下:
const std::string filename="./log/tcpserver.log";
void logMessage(int level,char*format,...)
{
char logLeft[1024];
std::string level_string=getLevelString(level);
std::string time_string=gettime();
std::string pid_string=std::to_string(getpid());
snprintf(logLeft,sizeof(logLeft),"[%s] [%s] [%s] ",level_string.c_str(),time_string.c_str(),pid_string.c_str());
char logRight[1024];
va_list p;//类似指针
va_start(p,format);//将p定位到可变参数首地址
vsnprintf(logRight,sizeof(logRight),format,p);//按format格式将可变参数写入logRight
va_end(p);//将va_list清理
汇总两个字符串,打印
//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);
}
完整代码如下:
log.hpp
#pragma once
#include<iostream>
#include<string>
#include<ctime>
#include<unistd.h>
#include<sys/types.h>
#include<stdarg.h>
const std::string filename = "./log/tcpserver.log";
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal,
Uknown
};
static std::string getLevelString(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";
}
}
static std::string gettime()
{
time_t cur=time(nullptr);
struct tm* tmp=localtime(&cur);
char buffer[128];
snprintf(buffer,sizeof(buffer),"%d-%d-%d %d:%d:%d",tmp->tm_year+1,tmp->tm_mon+1,tmp->tm_mday+1,
tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
return buffer;
}
//日志组成:日志等级 时间 PID 消息内容
//日志等级 格式控制 可变参数
void logMessage(int level,char*format,...)
{
char logLeft[1024];
std::string level_string=getLevelString(level);
std::string time_string=gettime();
std::string pid_string=std::to_string(getpid());
snprintf(logLeft,sizeof(logLeft),"[%s] [%s] [%s] ",level_string.c_str(),time_string.c_str(),pid_string.c_str());
char logRight[1024];
va_list p;//类似指针
va_start(p,format);//将p定位到可变参数首地址
vsnprintf(logRight,sizeof(logRight),format,p);//按format格式将可变参数写入logRight
va_end(p);//将va_list清理
打印日志信息
//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);
}
三. 守护进程
服务器一方面需要24小时不间断运行,另一方面还需要不被其他程序所影响,更准确点,是避免被其他程序的任何终端所产生信息所打断
。这就要求服务器要守护进程化
守护进程
守护进程,也就是通常说的 Daemon进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存周期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件
守护进程本质是孤儿进程,脱离终端。避免被任何终端所产生的信息所打断,其在执行过程的信息也不在任何终端上显示,一般是是使用日志文件持久化信息。
由于在Linux中,每一个系统与用户进行交流的界面被称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程就会自动关闭
Linux的终端文件路径一般在 /dev中
1. ps -axj命令
使用ps -axj
命令查看进程
- PPID:父进程进程ID
- PID:进程ID
- PGID:进程组ID
- SID:会话ID
- TTY:控制终端;
?表示没有控制终端
- TPGID:终端进程组ID
- STAT:状态
R
:进程正在运行或在运行队列中等待
S
:进程处于休眠状态,等待某个条件的形成或接收到信号。S+代表前台运行
。s表示会话领导
,Ss表示既在休眠,又是会话领导
D
:进程不可中断状态,收到信号不唤醒和不可运行,进程必须等待直到有中断发生,通常是IO
Z
:进程已终止,但进程描述符存在,直到父进程调用pidwait()回收后释放。僵尸进程
T
:进程已停止,进程收到SIGSTOP,SIGSTP,SIGTIN,SIGTOU信号后停止运行
X
:已经死掉的进程 - UID: 执行者身份
- COMMAND:程序名/运行该程序的指令
2. 会话
在ps -axj中可以看到有进程组ID和会话ID。二者分别是什么呢
可以看到,一条命令的三个sleep形成了三个进程,进程号依次递增
PGID(进程组号)相同,和第一个进程ID相同;SID(会话ID)相同,终端文件相同,TPGID(终端进程组)=PGID
再起三个sleep任务
可以看到PGID不同,SID相同
- 会话 >= 进程组 >= 进程
- 会话关联一个终端文件
- 进程组的组长,都是多个进程中的第一个
扩展命令
接下来介绍一些命令:
ctrl+z
:将当前前台进程调到后台,并停止
jobs
:查看当前会话的后台任务,会话的概念稍后讲解
jobs只能查看本会话的后台任务,无法查看其他会话的后台任务
fg+任务号
:将后台程序调到前台运行
bg+任务号
:让后台停止的任务开始运行
接下来回归会话的讲解
当我们通过Xshell连接云服务器时,我们登录成功,会为本次登录创建一个会话。每一次登录成功,都会创建会话。每个会话会关联一个终端文件。当我们退出时,其实只是将该会话资源回收
其中创建的进程组和内部进程,都是在当前会话中。因为一个会话只有一个控制终端,所以如果后台任务提到前台,老的前台任务就无法运行
一个会话只能有一个前台任务在运行
而退出会销毁会话,所以如果运行服务器的会话关闭,那么服务器也会停止运行。
所以,一般的网络服务器,为了不受到用户的登录注销等其他影响,网络服务器都需要以守护进程的方式进行
3. 创建守护进程
守护进程本质是自成会话的孤儿进程
而一个进程要想成为守护进程,需要满足以下几个要求:
- 不能是进程组组长。因为要调用函数独立出去,如果进程组组长,则会影响进程组的其他进程
- 需要忽略异常信号
- 读写错误输入输出需要特殊处理
- 进程的工作路径可能要改
新建会话的函数:setsid()
返回值
:成功返回新的进程ID,失败返回-1并设置错误码
哪个进程调用这个函数,哪个进程的的资源就会被转移到这个自成会话的进程
代码如下:
daemon.hpp
#pragma once
#include<cerrno>
#include<cstdlib>
#include<cstring>
#include<signal.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include"log.hpp"
void Daemon()
{
//1.忽略异常信号
signal(SIGPIPE,SIG_IGN);
signal(SIGCHLD,SIG_IGN);
//2.守护进程不能是进程组组长
if(fork()>0)
exit(0);//父进程退出
//子进程不会是进程组组长
//3.新建会话,自己成为会话的话首进程
pid_t ret=setsid();
if((int)ret==-1)
{
logMessage(Fatal,"deamon error,%s,errno:%d",strerror(errno),errno);
exit(1);
}
//4.可选,更改守护进程的工作路径
//chdir("/");
//5.处理后续的读写错误——文件描述符0,1,2
int fd=open("/dev/null",O_RDWR);
if(fd<0)
{
logMessage(Fatal,"open null error,%s,errno:%d",strerror(errno),errno);
exit(2);
}
//将0,1,2的内容输入到null文件
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
}
/dev/null是一个文件
,像是一个黑洞一样,扔进去的数据不会显示,也从里面读不到数据
正好可以将读写错误——0,1,2文件的输入输出数据丢进这个黑洞文件中。
但如此,日志系统就不能以打印显示日志信息,而需要创建一个日志文件,将日志信息写入文件中进行持久化。如此也不会影响其他进程
结束语
本篇博客到此结束,感谢看到此处。
欢迎大家纠错和补充
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。