文章目录
- 一、TCP服务器日志
- 二、守护进程预备知识
- 2.1 守护进程概念
- 2.2 前台任务和后台任务
- 2.3 进程组与组长ID
- 2.4 前台进程后台进程的切换
- 2.5 自成会话
- 三、实现守护进程
- 3.1 自建会话setsid
- 3.2 守护进程的条件
- 3.3 代码实现
一、TCP服务器日志
上一章我们写了一个TCP网络服务器【网络编程】demo版TCP网络服务器实现。
为了方便观察到每一步我们可以封装一个日志:
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <time.h>
#include <ctime>
#include <unistd.h>
#include <cstdio>
#define DEBUG 0// 调试
#define NORMAL 1// 正常
#define WARNING 2// 警告
#define ERROR 3// 错误
#define FATAL 4// 致命错误
const char* to_string_level(int level)
{
switch(level)
{
case DEBUG: return "DEBUG";
case NORMAL: return "NORMAL";
case WARNING: return "WARNING";
case ERROR: return "ERROR";
case FATAL: return "FATAL";
default : return nullptr;
}
}
void logMessage(int level, const char* format, ...)
{
// [日志等级][时间][pid][信息]
char logprefix[1024];
time_t now;
time(&now);
struct tm *ptm = localtime(&now);
char timebuf[1024];
snprintf(timebuf, sizeof timebuf, "%d年%d月%d日 %d:%d:%d", ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday, ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
snprintf(logprefix, sizeof logprefix, "[%s][%s][pid: %d]", to_string_level(level), timebuf, getpid());
char logline[1024];
va_list arg;
va_start(arg, format);
vsnprintf(logline, sizeof logline, format, arg);
std::cout << logprefix << logline << std::endl;
}
运行结果:
二、守护进程预备知识
2.1 守护进程概念
但是我们发现这个服务器存在问题,但是我们如果把云服务器关掉的话服务器就自动退出了。
服务器肯定不是这样运行的,服务器应该是启动后不再受用户的登录和注销的影响。除非我们不想用这个服务器了,把它kill掉。
我们把这种进程就叫做守护进程。
2.2 前台任务和后台任务
当Xhell连接云服务器时,Linux服务器会提供一个bash
(命令行解释),我们可以启动一系列前台和后台任务。我们把所有的前台后台任务叫做会话。
这里的bash就是前台任务,一个会话允许存在一个前台任务和多个(或0个)后台任务。
在命令的后边加上&
符号就可以把这个进程运行在后台。
jobs命令用于显示Linux中的任务列表及任务状态,包括后台运行的任务。
我们把红色框框的部分称作作业,最左边的序号就是作业号。
2.3 进程组与组长ID
创建一批后台进程:
可以看到PGID相同的就属于同一个进程组。而PID和PGID相同的就是组长进程。
同一个组的成员共同完成一个作业。
这里的SID就是会话ID。所有的进程属于同一个会话。其实就是bash的ID。
2.4 前台进程后台进程的切换
使用fg + 作业号
就可以把一个作业放在前台。如果再[Ctrl] + z
暂停就会又回到后台
bg
命令用于将作业放到后台运行,使前台可以执行其他任务。该命令的运行效果与在运行命令后面添加符号 & 的效果是相同的,都是将其放到系统后台执行。
使用这些命令就可以让进程进行前后台切换。
综上可得知前台只能允许一个进程运行,当我们把一个作业切换到前台,此时的前台进程就会被自动切换到后台。
而如果我们关闭Xshell,全部的进程都会被清理掉(会受到用户登录和注销的影响)。
如果不想受到影响就不能放到前台或者后台,而是要自成会话。
2.5 自成会话
当一个进程自成会话了,就不会受到终端设备(Xshell)的影响了。
三、实现守护进程
3.1 自建会话setsid
#include <unistd.h>
pid_t setsid(void);
RETURN VALUE
Upon successful completion, setsid() shall return the value of the new process group ID of the calling process.
Otherwise, it shall return (pid_t)-1 and set errno to indicate the error.
这个函数的作用就是新建一个会话,如果只有一个进程,那么组长就是自己。
这里有一个要求,调用该函数的进程不能是组长。
3.2 守护进程的条件
1️⃣ 要让进程忽略掉异常信号,因为客户端关闭的时候我们正在写就会发送异常导致服务端退出。
2️⃣ 必须让进程不是组长。
3️⃣ 守护进程必须脱离终端,所以要关闭或者重定向以前进程默认打开的文件(0, 1, 2)。
这里不建议直接关闭文件描述符,因为有可能会导致写入不存在的文件(显示器)导致报错。我们可以重定向到
/dev/null
如果希望执行某个命令,但又不希望在屏幕上显示出输出的结果,那么可以将输出重定向到/dev/null
。
/dev/null
是一个特殊的文件,写入到它的内容都会被丢弃;如果尝试从该文件读取内容,那么什么也读取不到。
但是/dev/null
文件非常的有用,将命令的输出重定向到它,会起到“静止输出”的效果。
3.3 代码实现
#pragma once
#include <unistd.h>
#include <signal.h>
#include <cassert>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "daemon.hpp"
void Daemon()
{
signal(SIGPIPE, SIG_IGN);
// 保证不是组长
if(fork() > 0) exit(1);
// 子进程
pid_t n = setsid();
assert(n != -1);
int fd = open("/dev/null", O_RDWR);
if(fd < 0)
{
// 无奈之举
close(0);
close(1);
close(2);
}
else
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
}
}
// TCPServeer.cc
#include "TCPServer.hpp"
#include <memory>
#include "daemon.hpp"
int main(int argc, char *argv[])
{
if(argc != 2)
{
std::cout << "incorrect number of parameters" << std::endl;
exit(1);
}
uint16_t port = atoi(argv[1]);
std::unique_ptr<TCPServer> ptr(new TCPServer(port));
ptr->InitServer();
Daemon();
ptr->start();
return 0;
}
而为了获取服务器的日志信息,我们可以重定向到文件中。
// log.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstdarg>
#include <time.h>
#include <ctime>
#include <unistd.h>
#include <cstdio>
#define DEBUG 0// 调试
#define NORMAL 1// 正常
#define WARNING 2// 警告
#define ERROR 3// 错误
#define FATAL 4// 致命错误
#define LOG_NOR "log.txt"
#define LOG_ERR "log.error"
const char* to_string_level(int level)
{
switch(level)
{
case DEBUG: return "DEBUG";
case NORMAL: return "NORMAL";
case WARNING: return "WARNING";
case ERROR: return "ERROR";
case FATAL: return "FATAL";
default : return nullptr;
}
}
void logMessage(int level, const char* format, ...)
{
// [日志等级][时间][pid][信息]
char logprefix[1024];
time_t now;
time(&now);
struct tm *ptm = localtime(&now);
char timebuf[1024];
snprintf(timebuf, sizeof timebuf, "%d年%d月%d日 %d:%d:%d", ptm->tm_year + 1900, ptm->tm_mon + 1, ptm->tm_mday, ptm->tm_hour, ptm->tm_min, ptm->tm_sec);
snprintf(logprefix, sizeof logprefix, "[%s][%s][pid: %d]", to_string_level(level), timebuf, getpid());
char logline[1024];
va_list arg;
va_start(arg, format);
vsnprintf(logline, sizeof logline, format, arg);
// std::cout << logprefix << logline << std::endl;
FILE *nor = fopen(LOG_NOR, "a");
FILE *err = fopen(LOG_ERR, "a");
if(nor && err)
{
if(level == DEBUG || level == NORMAL || level == WARNING)
{
fprintf(nor, "%s%s\n", logprefix, logline);
}
else
{
fprintf(err, "%s%s\n", logprefix, logline);
}
fclose(nor);
fclose(err);
}
}
客户端:
服务端: