一 日志的重要性
在之前的编程经历中,如果我们的程序运行出现了问题,都是通过 标准输出 或 标准错误 将 错误信息 直接输出到屏幕上,以此来排除程序中的错误。
这在我们以往所写的程序中使用没啥问题,但如果出错的是一个不断在运行中的服务,那问题就大了,因为服务器是不间断运行中,直接将 错误信息 输出到屏幕上,会导致错误排查变得极为困难。
其实,我们可以将各种 错误信息 组织管理,使 每种错误有属于自己的格式(包括时间、文件名及行号、错误等级等),利于排查问题,同时,把这些错误写入一个单独的地方,便于我们查找和阅读(因为错误信息繁多,我们一般写入文件中)。
这种错误信息的集合,我们便称为日志。
所以接下来我们将会实现一个简易版日志器,用于定向输出我们的日志信息。
二 可变参数
日志需要我们指定格式并输出,依赖于可变参数。
因此我们需要认识一下可变参数的使用,主要是几个宏。
cpp
#include <stdarg.h>
va_list // 指向可变参数列表的指针
va_start() // 将指针指向起始地址
va_arg() // 根据类型,提取可变参数列表中的参数
va_end() // 将指针置为空
示例,我们通过可变参数实现参数遍历:
cpp
#include <stdio.h>
#include <stdarg.h>
void foreach(int format, ...){
va_list p;
va_start(p, format);
// 接下来就是获取其中的每一个参数
for(int i = 0; i < format; i++){
printf("%d ", va_arg(p, int));
}
printf("\n");
// 置空
va_end(p);
}
int main(){
foreach(5, 1,2,3,4,5);
return 0;
}
这种依靠自己动手的方法比较麻烦,我们也可以借助标准库提供的 vsnprintf()
函数进行参数解析
cpp
//头文件:
#include <stdio.h>
//函数声明:
int vsnprintf(char* str, size_t size, const char* format, va_list ap);
- char *str , 把生成的格式化的字符串存放在这里.
- size_t size , str可接受的最大字符数 ,防止产生数组越界.
- const char *format , 指定输出格式的字符串,它决定了你需要提供的可变参数的类型、个数和顺序。
- va_list ap , va_list变量.
函数功能:将可变参数格式化输出到一个字符数组。
返回值:执行成功,返回最终生成字符串的长度,若生成字符串的长度大于size,则将字符串的前size个字符复制到str,同时将原串的长度返回(不包含终止符);执行失败,返回负值,并置errno.
cpp
#include<iostream>
#include<stdio.h>
#include <stdarg.h>
using namespace std;
void logtest(int format,...){
va_list a;
va_start(a,format);
char msg[1024];
int n = vsnprintf(msg,sizeof(msg),"%d-%d-%d-%d-%d",a);
if(n < 0 ){
cout<<"可变参数写入失败"<<endl;
}
cout<<msg<<endl;
va_end(a);
}
int main(){
logtest(5,1,2,3,4,5);
return 0;
}
三 日志器的实现
3.1 日志器的等级
日志是有等级的,一般分为五级:
Debug
用于调试Info
提示信息Warning
警告Errorr
错误Fatal
致命错误
错误等级越高,代表影响越大
当然难免有不明确的错误,可以再添加一级:UnKnow
未知错误。
cpp
#include<vector>
#include<string>
// 日志等级
enum
{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
string getLevel(int level){
//可直接用一个容器存储这些日志等级
vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};
//避免非法情况
if(level < 0 || level >= vs.size() - 1)
return vs[vs.size() - 1];
return vs[level];
}
3.2 获取时间
接下来是获取时间信息,可以通过 time() 函数获取当前时间戳,然后再利用 localtime() 函数构建 struct tm 结构体对象,这个对象会将时间戳解析成 年月日 时分秒 等详细信息,直接获取即可
strcut tm 结构体的信息如下,细节:年份已经 -1900 了,使用时需要加上 1900;月份从 0 开始,使用时需要 +1。
cpp
/* Used by other time functions. */
struct tm
{
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]*/
# ifdef __USE_BSD
long int tm_gmtoff; /* Seconds east of UTC. */
const char *tm_zone; /* Timezone abbreviation. */
# else
long int __tm_gmtoff; /* Seconds east of UTC. */
const char *__tm_zone; /* Timezone abbreviation. */
# endif
};
可以这样获取当前时间
cpp
// 获取当前时间
string getTime(){
time_t t = time(nullptr); //获取时间戳
struct tm *st = localtime(&t); //获取时间相关的结构体
char buff[128];
//将时间按照特定格式写入字符串中
snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);
return buff;
}
3.3 日志格式
日志的格式我们一般可以自己规定,这里我们规定我们日志的格式为:
<日志等级> [时间] [PID] {消息体}
接下来就是获取进程 PID
,这个简单,直接使用 getpid()
函数获取即可,最后是解析参数,需要用到 vsnprintf()
函数,只要传入缓冲区和 va_list
指针,该函数就可以自动解析出参数,并存入缓冲区中 。
cpp
void logMessage(int level, const char* format, ...){
//截获主体消息
char msgbuff[1024];
va_list p;
va_start(p, format); //将 p 定位至 format 的起始位置
vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
va_end(p);
}
形成测试版日志信息函数
cpp
//处理信息
void logMessage(int level, const char* format, ...){
//日志格式:<日志等级> [时间] [PID] {消息体}
string logmsg = getLevel(level); //获取日志等级
logmsg += " " + getTime(); //获取时间
logmsg += " [" + to_string(getpid()) + "]"; //获取进程PID
//截获主体消息
char msgbuff[1024];
va_list p;
va_start(p, format); //将 p 定位至 format 的起始位置
vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
va_end(p);
logmsg += " {" + string(msgbuff) + "}"; //获取主体消息
printf("%s\n", logmsg); //这里先打印 方便进行测试
}
为什么日志消息最后还是向屏幕输出?这样组织日志消息的好处是什么?
因为现在还在测试阶段,等测试完成后,可以将日志消息存入文件中,做到持久化存储;至于统一组织的好处不言而喻,能够确保每条日志消息都包含必要信息,便于排查错误
3.4 Log.hpp 头文件代码
cpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
using namespace std;
enum{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
string getLevel(int level){
vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};
//避免非法情况
if(level < 0 || level >= vs.size() - 1) {
return vs[vs.size() - 1];
}
return vs[level];
}
string getTime(){
time_t t = time(nullptr); //获取时间戳
struct tm *st = localtime(&t); //获取时间相关的结构体
char buff[128];
snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);
return buff;
}
//处理信息
void logMessage(int level, const char* format, ...){
//日志格式:<日志等级> [时间] [PID] {消息体}
string logmsg = getLevel(level); //获取日志等级
logmsg += " " + getTime(); //获取时间
logmsg += " [" + to_string(getpid()) + "]"; //获取进程PID
//截获主体消息
char msgbuff[1024];
va_list p;
va_start(p, format); //将 p 定位至 format 的起始位置
vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
va_end(p);
logmsg += " {" + string(msgbuff) + "}"; //获取主体消息
cout<<logmsg<<endl;
}
3.5 写入程序中
这里我们借用我们上一篇文章写的TCP程序
我们先将client.hpp 文件中的错误信息日志化:
cpp
//client.hpp
#pragma once
#include<iostream>
#include<string>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cerrno>
#include<cstring>
#include "err.hpp"
#include <unistd.h>
#include"Log.hpp"
namespace My_client{
class client
{
private:
/* data */
//套接字
int _sock;
//服务器ip
std::string server_ip;
//服务器端口号
uint16_t server_port;
public:
client(const std::string &ip,const uint16_t &port)
:server_ip(ip)
,server_port(port)
{}
~client(){
}
//初始化客户端
void InitClient(){
//1 创建套接字
_sock = socket(AF_INET,SOCK_STREAM,0);
if(_sock == -1){
logMessage(Fatal, "Create Socket Fail! %s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(Debug, "Create Sock Succeess! %d", _sock);
}
// 启动客户端
void StartClient(){
//填充服务器的sockaddr_int 结构体信息
struct sockaddr_in server;
socklen_t len=sizeof(server);
bzero(&server,len);
server.sin_family = AF_INET;
server.sin_addr.s_addr = inet_addr(server_ip.c_str());
// inet_aton(server_ip.c_str(), &server.sin_addr); // 将点分十进制转化为二进制IP地址的另一种方法
server.sin_port = htons(server_port);
//尝试重连五次
int n=5;
while(n){
//开始连接
int ret = connect(_sock,(const struct sockaddr*)&server,len);
if(ret==0){
// 连接成功,可以跳出循环
break;
}
// 尝试进行重连
logMessage(Warning, "网络异常,正在进行重连... 剩余连接次数: %d", --n);
sleep(1);
}
// 如果剩余重连次数为 0,证明连接失败
if(n == 0) {
logMessage(Fatal, "连接失败! %s", strerror(errno));
close(_sock);
exit(CONNECT_ERR);//新加错误标识符
}
// 连接成功
logMessage(Info, "连接成功!");
// 进行业务处理
Service();
}
// 业务处理
void Service(){
char buff[1024];
std::string who = server_ip + "-" + std::to_string(server_port);
while(true){
// 由用户输入信息
std::string msg;
std::cout << "Please Enter >> ";
std::getline(std::cin, msg);
// 发送信息给服务器
write(_sock, msg.c_str(), msg.size());
// 接收来自服务器的信息
ssize_t n = read(_sock, buff, sizeof(buff) - 1);
if(n > 0) {
// 正常通信
buff[n] = '\0';
std::cout << "Client get: " << buff << " from " << who << std::endl;
}
else if(n == 0){
// 读取到文件末尾(服务器关闭了)
logMessage(Error, "Server %s quit! %s", who.c_str(), strerror(errno));
close(_sock); // 关闭文件描述符
break;
}
else{
// 读取异常
logMessage(Error, "Read Fail! %s", strerror(errno));
close(_sock); // 关闭文件描述符
break;
}
}
}
};
}
连接成功的例子,显然其它日志信息也一样显示在屏幕中:
改动server.hpp 头文件中的代码
cpp
// server.hpp
#pragma once
#include<iostream>
#include<string>
#include<functional>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include"err.hpp"
#include<cstring>
#include<unistd.h>
#include<cerrno>
#include"ThreadPool.hpp"
#include"Task.hpp"
#include"Log.hpp"
namespace My_server{
// 默认端口号
const uint16_t default_port = 1111;
//全连接队列的最大长度
const int backlog = 32;
using func_t = std::function<std::string(std::string)>;
//前置声明
class server;
//包含我们所需参数的类型
class ThreadData{
public:
ThreadData(int sock,const std::string&ip,const uint16_t&port,server*ptr)
:_sock(sock)
,_clientip(ip)
,_clientport(port)
,_current(ptr)
{}
public:
int _sock;
std::string _clientip;
uint16_t _clientport;
server* _current;
};
class server
{
private:
/* data */
//套接字
int _listensock;
//端口号
uint16_t _port;
// 判断服务器是否结束运行
bool _quit;
// 外部传入的回调函数
func_t _func;
public:
server(const func_t &func,const uint16_t &port = default_port)
:_func(func)
,_port(port)
,_quit(false)
{}
~server(){}
//初始化服务器
void InitServer(){
//1 创建套接字
_listensock = socket(AF_INET,SOCK_STREAM,0);
if(_listensock == -1){
//绑定失败
logMessage(Fatal, "Create Socket Fail! %s", strerror(errno));
exit(SOCKET_ERR);
}
logMessage(Debug, "Create Sock Succeess! %d", _listensock);
//2 绑定端口号和IP地址
struct sockaddr_in local;
bzero(&local,sizeof(local));
local.sin_family = AF_INET;
local.sin_port = htons(_port);
local.sin_addr.s_addr = INADDR_ANY;
if(bind(_listensock,(const sockaddr*)&local,sizeof(local))){
logMessage(Fatal, "Bind IP&&Port Fali %s", strerror(errno));
exit(BIND_ERR);
}
//3 开始监听
if(listen(_listensock,backlog)== -1){
logMessage(Fatal, "Listen Fail: %s", strerror(errno));
//新增一个报错
exit(LISTEN_ERR);
}
logMessage(Debug, "Listen Success!");
}
//启动服务器
void StartServer(){
while(!_quit){
//1 处理连接请求
struct sockaddr_in client;
socklen_t len = sizeof(client);
int sock = accept(_listensock,(struct sockaddr*)&client,&len);
//2 如果连接失败 继续尝试连接
if(sock == -1){
logMessage(Warning,"Accept Fail!: %s",strerror(errno));
continue;
}
// 连接成功,获取客户端信息
std::string clientip = inet_ntoa(client.sin_addr);
uint16_t clientport = ntohs(client.sin_port);
//std::cout<<"Server accept"<< clientip + "-"<< clientport <<sock<<" from "<<_listensock << "success!"<<std::endl;
logMessage(Debug,"Server accept %s - %d %d from %d success",clientip.c_str(),clientport,sock,_listensock);
// 3.构建任务对象 注意:使用 bind 绑定 this 指针
My_task::Task t(sock, clientip, clientport, std::bind(&server::Service, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
// 4.通过线程池操作句柄,将任务对象 push 进线程池中处理
//s
//std::cout<<std::endl<<"push Task"<<std::endl;
My_pool::ThreadPool<My_task::Task>::getInstance()->pushTask(t);
}
}
void Service(int sock,const std::string &clientip,const uint16_t &clientport){
char buff[1024];
std::string who = clientip + "-" + std::to_string(clientport);
while(true){
// 以字符串格式读取,预留\0的位置
ssize_t n = read(sock,buff,sizeof(buff)-1);
if(n>0){
//读取成功
buff[n]='\0';
logMessage(Debug,"Server get: %s from %s",buff,who.c_str());
//std::cout<<"Server get: "<< buff <<" from "<<who<<std::endl;
//实际处理可以交给上层逻辑指定
std::string respond = _func(buff);
write(sock,buff,strlen(buff));
}
else if(n==0){
//表示当前读到了文件末尾,结束读取
//std::cout<<"Client "<<who<<" "<<sock<<" quit!"<<std::endl;
logMessage(Error,"Client %s %d quit!",who.c_str(),sock);
close(sock);
}
else{
// 读取出问题(暂时)
logMessage(Error, "Read Fail! %s", strerror(errno));
close(sock); // 关闭文件描述符
break;
}
}
}
};
}
示例:
3.6 持久化存储
所谓持久化存储就是将日志消息输出至文件中,修改 log.hpp
中的代码即可
- 指定日志文件存放路径
- 打开文件,将日志消息追加至文件中
log.hpp
日志头文件
cpp
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
#include <time.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdarg.h>
using namespace std;
enum{
Debug = 0,
Info,
Warning,
Error,
Fatal
};
static const string file_name = "TCP.Log"; //在当前目录下创建一个TCP.Log文件
string getLevel(int level){
vector<string> vs = {"<Debug>", "<Info>", "<Warning>", "<Error>", "<Fatal>", "<Unknown>"};
//避免非法情况
if(level < 0 || level >= vs.size() - 1) {
return vs[vs.size() - 1];
}
return vs[level];
}
string getTime(){
time_t t = time(nullptr); //获取时间戳
struct tm *st = localtime(&t); //获取时间相关的结构体
char buff[128];
snprintf(buff, sizeof(buff), "%d-%d-%d %d:%d:%d", st->tm_year + 1900, st->tm_mon + 1, st->tm_mday, st->tm_hour, st->tm_min, st->tm_sec);
return buff;
}
//处理信息
void logMessage(int level, const char* format, ...){
//日志格式:<日志等级> [时间] [PID] {消息体}
string logmsg = getLevel(level); //获取日志等级
logmsg += " " + getTime(); //获取时间
logmsg += " [" + to_string(getpid()) + "]"; //获取进程PID
//截获主体消息
char msgbuff[1024];
va_list p;
va_start(p, format); //将 p 定位至 format 的起始位置
vsnprintf(msgbuff, sizeof(msgbuff), format, p); //自动根据格式进行读取
va_end(p);
logmsg += " {" + string(msgbuff) + "}"; //获取主体消息
//持久化。写入文件中
FILE* fp = fopen(file_name.c_str(), "a"); //以追加的方式写入
if(fp == nullptr)
return; //不太可能出错
fprintf(fp, "%s\n", logmsg.c_str());
fflush(fp); //手动刷新一下
fclose(fp);
}
示例:
四 守护进程
守护进程 的意思就是让进程不间断的在后台运行,即便是 bash
关闭了,也能照旧运行。守护进程 就是现实生活中的服务器,因为服务器是需要 24H
不间断运行的
4.1.会话、进程组、进程
当前我们的程序在启动后属于 前台进程 ,前台进程 是由 bash
进程替换而来的,因此会导致 bash
暂时无法使用.
但是我们的server程序此时又没什么用,还影响着原本bash进程的使用,我们该怎么做呢?
如果在启动程序时,带上
&
符号,程序就会变成 后台进程 ,后台进程 并不会与bash
进程冲突,bash
仍然可以使用
后台进程 也可以实现服务器不间断运行,但问题在于 如果当前 bash
关闭了,那么运行中的后台进程也会被关闭 ,最好的解决方案是使用 守护进程
在正式学习 守护进程 之前,需要先了解一组概念:会话、进程组、进程
分别运行一批 前台、后台进程,并通过指令查看进程运行情况 。
cpp
sleep 1000 | sleep 2000 | sleep 3000 &
sleep 100 | sleep 200 | sleep 300
ps -ajx | head -1 && ps -ajx | grep sleep | grep -v grep
其中 会话 == SID
进程组 == PGID
进程 == PID
显然,sleep 1000、2000、3000 处于同一个管道中(有血缘关系),属于同一个 进程组,所以他们的 PGID 都是一样的,都是 4261;
至于 sleep 100、200、300 属于另一个 进程组,PGID 为 4308;再仔细观察可以发现 每一组的进程组 PGID 都与当前组中第一个被创建的进程 PID 一致,这个进程被称为 组长进程。
无论是 后台进程 还是 前台进程 ,都是从同一个 bash
中启动的,所以它们处于同一个 会话 中,SID
都是 1939
,并且关联的 终端文件 TTY
都是 pts/1。
会话 >= 进程组 >= 进程
Linux
中一切皆文件,终端文件也是如此,这里的终端其实就是当前bash
输出结果时使用的文件(也就是屏幕,屏幕也是一个文件),终端文件位于dev/pts
目录下,如果向指定终端文件中写入数据,那么对方也可以直接收到(关联终端文件说白了就是打开了文件,一方写,一方读,不就是管道吗)
在同一个
bash
中启动前台、后台进程,它们的SID
都是一样的,属于同一个 会话 ,关联了同一个 终端 (SID
其实就是bash
的PID
)
我们使用 XShell
等工具登录 Linux
服务器时,会在服务器中创建一个 会话 (bash
),可以在该会话内创建 进程 ,当 进程 间有关系时,构成一个 进程组 ,组长 进程的 PID
就是该 进程组 的 PGID。
在同一个会话中,只允许一个前台进程在运行,默认是 bash,如果其他进程运行了,bash 就会变成后台进程(暂时无法使用),让出前台进程这个位置(后台进程与前台进程之前是可以进程切换)
如何将一个 后台进程 变成 前台进程?
首先通过指令查看当前 会话 中正在运行的 后台进程,获取 任务号
cpp
jobs
查看当前会话中所有的后台进程
接下来通过 任务号 将 后台进程 变成 前台进程 ,此时 bash
就无法使用了。
cpp
fg 后台进程号
那如何将 前台进程 变成 后台进程 ?
首先是通过 ctrl + z
发送 19
号 SIGSTOP
信号,暂停正在运行中的 前台进程.
cpp
键盘输入 ctrl + z
然后通过 任务号 ,可以把暂停中的进程变成 后台进程.
4.2 守护进程化
一般网络服务器为了不受到用户登录重启的影响,会以 守护进程 的形式运行,有了上面那一批前置知识后,就可以很好的理解 守护进程 的本质了
守护进程:进程单独成一个会话,并且以后台进程的形式运行
说白了就是让服务器不间断运行,可以直接使用 daemon()
函数完成 守护进程化。
cpp
#include <unistd.h>
int daemon(int nochdir, int noclose);
参数解读:
- nochdir 改变进程的工作路径
- noclose 重定向标准输入、标准输出、标准错误
返回值:成功返回 0,失败返回 -1
一般情况下,daemon() 函数的两个参数都只需要传递 0,默认工作在 / 路径下,默认重定向至 /dev/null
/dev/null 就像是一个 黑洞,可以把所有数据都丢入其中,相当于丢弃数据
使用 damon() 函数使之前的server.cc 守护进程化
server.cc 服务器源文件
cpp
//智能指针头文件
#include<memory>
#include"server.hpp"
#include<string>
using namespace My_server;
// 业务处理回调函数(字符串回响)其实这里啥也不干
std::string echo(std::string request){
return request;
}
int main(){
// 直接守护进程化
daemon(0, 0);
std::unique_ptr<server> usvr(new server(echo));
usvr->InitServer();
usvr->StartServer();
return 0;
}
现在服务器启动后,会自动变成 后台进程 ,并且自成一个 新会话 ,归操作系统管(守护进程 本质上是一种比较坚强的 孤儿进程)
注意: 现在标准输出、标准错误都被重定向至 /dev/null
中了,之前向屏幕输出的数据,现在都会直接被丢弃,如果想保存数据,可以选择使用日志。
可见内容被吞噬了(舍弃)
如果想终止 守护进程 ,需要通过 kill pid
杀死目标进程 。
使用系统提供的接口一键 守护进程化 固然方便,不过大多数程序员都会选择手动 守护进程化(可以根据自己的需求定制操作)
原理是 使用 setsid()
函数新设一个会话,谁调用,会话 SID
就是谁的,成为一个新的会话后,不会被之前的会话影响。
cpp
#include <unistd.h>
pid_t setsid(void);
返回值:成功返回该进程的 pid,失败返回 -1
注意: 调用该函数的进程,不能是组长进程,需要创建子进程后调用
手动实现守护进程时需要注意以下几点:
- 忽略异常信号
- 0、1、2 要做特殊处理(文件描述符)
- 进程的工作路径可能要改变(从用户目录中脱离至根目录)
具体实现步骤如下:
1、忽略常见的异常信号:SIGPIPE、SIGCHLD
2、如何保证自己不是组长? 创建子进程 ,成功后父进程退出,子进程变成守护进程
3、新建会话,自己成为会话的 话首进程
4、(可选)更改守护进程的工作路径:chdir
5、处理后续对于 0、1、2 的问题
对于 标准输入、标准输出、标准错误 的处理方式有两种
暴力处理:直接关闭 fd
优雅处理:将 fd 重定向至 /dev/null,也就是 daemon() 函数的做法
这里我们选择后者,守护进程 的函数实现如下:
Daemon.hpp
守护进程头文件
cpp
#pragma once
#include <iostream>
#include <cstring>
#include <cerrno>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "err.hpp"
#include "Log.hpp"
static const char *path = "/home/Manta/cpp/Internet/Log/Log1";
void Daemon()
{
// 1、忽略常见信号
signal(SIGPIPE, SIG_IGN);
signal(SIGCHLD, SIG_IGN);
// 2、创建子进程,自己退休
pid_t id = fork();
if (id > 0)
exit(0);
else if (id < 0)
{
// 子进程创建失败
logMessage(Error, "Fork Fail: %s", strerror(errno));
exit(FORK_ERR);
}
// 3、新建会话,使自己成为一个单独的组
pid_t ret = setsid();
if (ret == -1)
{
// 守护化失败
logMessage(Error, "Setsid Fail: %s", strerror(errno));
exit(SETSID_ERR);
}
// 4、更改工作路径
int n = chdir(path);
if (n == -1)
{
// 更改路径失败
logMessage(Error, "Chdir Fail: %s", strerror(errno));
exit(CHDIR_ERR);
}
// 5、重定向标准输入输出错误
int fd = open("/dev/null", O_RDWR);
if (fd == -1)
{
// 文件打开失败
logMessage(Error, "Open Fail: %s", strerror(errno));
exit(OPEN_ERR);
}
// 重定向标准输入、标准输出、标准错误
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
当然相应的错误码也需要更新
err.hpp
错误码头文件
cpp
#pragma once
enum
{
USAGE_ERR = 1,
SOCKET_ERR,
BIND_ERR,
LISTEN_ERR,
CONNECT_ERR,
FORK_ERR,
SETSID_ERR,
CHDIR_ERR,
OPEN_ERR
};
StartServer()
服务器启动函数 --- 位于 server.hpp
服务器头文件中
现在服务器在启动后,会自动新建会话,以 守护进程 的形式运行
杀死守护进程