网络编程套接字(4): 日志和守护进程
5. 增加日志功能
接着上篇的内容,增加日志功能可以更好地,更高效地帮助我们发现在网络通信中遇到的问题
日志分为5个等级
enum
{
Debug=0,
Info,
Warning,
Error,
Fatal,
Uknown
};
log.hpp
#include<iostream>
#include<string>
#include<sys/types.h>
#include<unistd.h>
#include<stdio.h>
#include<cstdarg>
#include<time.h>
using namespace std;
// 日志系统是有等级的
enum
{
Debug=0,
Info,
Warning,
Error,
Fatal,
Uknown
};
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+1,tmp->tm_mday,
tmp->tm_hour,tmp->tm_min,tmp->tm_sec);
return buffer;
}
// 日志格式: 日志等级 时间 pid 消息体
// logLeft: 日志等级 时间 pid logRight: 消息体
// logMessage(Debug, "hello: %d, %s",12,s.c_str()) Debug, hello: 12, world
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);
}
关于给服务端添加日志打印我这里就不给出了,放到我的码云:lesson35/log_v1/tcpServer.hpp · 遇健/Linux - 码云 - 开源中国 (gitee.com)
添加成功后运行结果:
6. 守护进程
6.1 进程知识补充
(1) 进程组
-
进程组就是一个或多个进程的集合,每个进程除了有一个PID外,还属于一个进程组。
-
每一个进程组,都有一个唯一的标识PGID,属于同一个进程组的进程其PGID相同。
-
进程组中的第一个进程作为组长进程,将其PID作为进程组的PGID
如下,我们同时启动了3个后台进程,它们属于同一进程组,进程组中的第一个进程PID=14378的进程作为组长进程
(2) 任务
启动一个进程就是启动一个任务
-
前台任务:通过终端启动,并且在启动后一直占据终端
-
后台任务:启动时与终端无关,或者通过终端启动后转入后台运行(即释放终端),不影响用户继续在终端中工作
在我们每次登录XShell后,bash会默认占据前台任务,也就是命令行解释器shell(即占用终端的控制权)
当把进程任务自动切换为前台任务时,shell自动切换为后台任务,我们输入的命令就无效了
任务管理命令(Shell中控制进程组的方式):
- jobs:查看所有任务
- fg:把任务提到前台
此时这个任务就会变成前台任务,shell自动切换为后台任务,命令行解释器失效
- ctrl+z:暂停前台任务
- bg:让暂停的任务在后台继续运行
(3) 会话
Linux是多用户多任务的分时系统,所以必须要支持多个用户同时使用一个操作系统。当一个用户登录一次系统就形成一次会话。在一个会话中,用户可以与系统进行交互,执行命令、操作文件、启动程序等。
比如,我们先启动3个后台进程,在启动3个前台进程,后获取这些进程的信息
- 这些进程分别属于2个进程组,它们与同一个终端关联,属于同一个会话
-
启动进程就是启动任务,在每一次登录系统会为我们创建一次会话,会话里至少有bash任务进行命令行解释
-
命令行里可以启动多个任务,每个任务最终以进程组的形式在会话里存在。
-
所以一个会话里可能存在很多进程组
-
大小概念:会话 >= 进程组 >= 进程
6.2 守护进程
(1) 概念
上面的知识概念总结:
-
进程组分为:前台任务和后台任务
-
如果把后台任务提到前台,则老的前台任务无法运行
-
在会话中只能有一个前台任务在运行,所以当我们在命令行启动一个进程的时候,bash就无法运行了
-
如果登录就是创建一个会话,bash任务启动我们的进程就是在当前会话中创建新的前后台任务,那么我们如果退出呢?就会销毁会话,可能会影响会话内部的所有任务!
我们之前的服务器都是这样的:
每一个服务端进程组与bash进程组同属于一个会话,再次登录Xshell启动服务端就会创建新的会话
可是一般的网络服务器,为了不受到用户的登录注销的影响,就必须让服务端自成进程组,自成会话,使其与终端的状态无关,可以一直运行的进程,这样的进程就称作守护进程
(2) 创建守护进程
我们用的setsid
函数自己实现守护进程,不使用linux自带生成守护进程的接口daemon
creates a session and sets the process group ID: 创建会话并设置进程组ID
这个函数的使用关键:调用的进程不能是组长进程
守护进程的创建步骤:
- 让调用进程忽略掉异常信号
- fork()创建子进程,让父进程直接退出,自己不再是组长进程
- 调用
setsid
,新建会话, 子进程自成进程组,成为会话的首进程 - 将标准输入、输出和错误重定向到/dev/null中
- 调用close关闭文件描述符,防止守护进程与终端或其他进程的关联
- 调用
setsid
,新建会话, 自己成为会话的首进程
dev/null是linux下的特殊文件,会对写入的内容进行丢弃,通常被用作丢弃不需要的输出或测试程序在遇到写入错误时的行为。
daemon.hpp
#pragma once
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"
#include "err.hpp"
// 1. setsid();
// 2. setsid(), 调用进程,不能是组长! 我们怎么保证自己不是组长呢?
// 3. 守护进程, 忽略异常信号 b. 0, 1, 2要特殊处理 c. 进程的工作路径可能要更改
// 守护进程的本质: 是孤儿进程的一种
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, code: %d, error string: %s",errno, strerror(errno));
exit(SET_ERR);
}
// 4. 可选: 可以更改守护进程的工作路径
// chdir("/");
// 5. 处理后续对于0,1,2的问题 --- /dev/null 文件就像垃圾桶
int fd=open("/dev/null",O_RDWR);
if(fd<0)
{
logMessage(Fatal,"open error, code: %d, error string: %s",errno,strerror(errno));
exit(SET_ERR);
}
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
}
给服务端加上该代码
运行结果:
-
?表示该进程与终端无关,我们已经让此服务端以守护进程的方式运行
-
该进程的PPID为1,说明OS领养了守护进程,守护进程本质是孤儿进程的一种