当前位置: 首页 > news >正文

Linux -- 日志

一 日志的重要性

  在之前的编程经历中,如果我们的程序运行出现了问题,都是通过 标准输出 或 标准错误 将 错误信息 直接输出到屏幕上,以此来排除程序中的错误。

  这在我们以往所写的程序中使用没啥问题,但如果出错的是一个不断在运行中的服务,那问题就大了,因为服务器是不间断运行中,直接将 错误信息 输出到屏幕上,会导致错误排查变得极为困难。

  其实,我们可以将各种 错误信息 组织管理,使 每种错误有属于自己的格式(包括时间、文件名及行号、错误等级等),利于排查问题,同时,把这些错误写入一个单独的地方,便于我们查找和阅读(因为错误信息繁多,我们一般写入文件中)。

  这种错误信息的集合,我们便称为日志。

所以接下来我们将会实现一个简易版日志器,用于定向输出我们的日志信息。

二 可变参数

日志需要我们指定格式并输出,依赖于可变参数。

因此我们需要认识一下可变参数的使用,主要是几个宏。

#include <stdarg.h>va_list 	// 指向可变参数列表的指针va_start()	// 将指针指向起始地址va_arg()	// 根据类型,提取可变参数列表中的参数va_end()	// 将指针置为空 

示例,我们通过可变参数实现参数遍历:
 

#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() 函数进行参数解析

//头文件:
#include <stdio.h>
//函数声明:
int vsnprintf(char* str, size_t size, const char* format, va_list ap);
  1. char *str ,  把生成的格式化的字符串存放在这里.
  2. size_t size , str可接受的最大字符数 ,防止产生数组越界.
  3. const char *format , 指定输出格式的字符串,它决定了你需要提供的可变参数的类型、个数和顺序。
  4. va_list ap , va_list变量. 

函数功能:将可变参数格式化输出到一个字符数组。

返回值:执行成功,返回最终生成字符串的长度,若生成字符串的长度大于size,则将字符串的前size个字符复制到str,同时将原串的长度返回(不包含终止符);执行失败,返回负值,并置errno. 

#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 日志器的等级

日志是有等级的,一般分为五级:

  1. Debug 用于调试
  2. Info 提示信息
  3. Warning 警告
  4. Errorr 错误
  5. Fatal 致命错误

错误等级越高,代表影响越大

当然难免有不明确的错误,可以再添加一级:UnKnow 未知错误。

#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。

/* 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_BSDlong int tm_gmtoff;		/* Seconds east of UTC.  */const char *tm_zone;		/* Timezone abbreviation.  */
# elselong int __tm_gmtoff;		/* Seconds east of UTC.  */const char *__tm_zone;	/* Timezone abbreviation.  */
# endif
};

可以这样获取当前时间


// 获取当前时间
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 指针,该函数就可以自动解析出参数,并存入缓冲区中  。

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);}

形成测试版日志信息函数

//处理信息
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 头文件代码

#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 文件中的错误信息日志化:

//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;//服务器ipstd::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 头文件中的代码 

// 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 日志头文件

#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 关闭了,那么运行中的后台进程也会被关闭,最好的解决方案是使用 守护进程

  在正式学习 守护进程 之前,需要先了解一组概念:会话、进程组、进程

  分别运行一批 前台、后台进程,并通过指令查看进程运行情况  。

sleep 1000 | sleep 2000 | sleep 3000 &sleep 100 | sleep 200 | sleep 300ps -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 就会变成后台进程(暂时无法使用),让出前台进程这个位置(后台进程与前台进程之前是可以进程切换)


如何将一个 后台进程 变成 前台进程?
首先通过指令查看当前 会话 中正在运行的 后台进程,获取 任务号

jobs

查看当前会话中所有的后台进程

接下来通过 任务号 将 后台进程 变成 前台进程,此时 bash 就无法使用了。  

fg 后台进程号

那如何将 前台进程 变成 后台进程 ?

首先是通过 ctrl + z 发送 19 号 SIGSTOP 信号,暂停正在运行中的 前台进程.

键盘输入 ctrl + z

然后通过 任务号,可以把暂停中的进程变成 后台进程.

4.2 守护进程化

一般网络服务器为了不受到用户登录重启的影响,会以 守护进程 的形式运行,有了上面那一批前置知识后,就可以很好的理解 守护进程 的本质了

守护进程:进程单独成一个会话,并且以后台进程的形式运行

说白了就是让服务器不间断运行,可以直接使用 daemon() 函数完成 守护进程化。

#include <unistd.h>int daemon(int nochdir, int noclose);

参数解读:

  1. nochdir 改变进程的工作路径
  2. noclose 重定向标准输入、标准输出、标准错误

返回值:成功返回 0,失败返回 -1

一般情况下,daemon() 函数的两个参数都只需要传递 0,默认工作在 / 路径下,默认重定向至 /dev/null

/dev/null 就像是一个 黑洞,可以把所有数据都丢入其中,相当于丢弃数据

使用 damon() 函数使之前的server.cc 守护进程化

server.cc 服务器源文件

//智能指针头文件
#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 就是谁的,成为一个新的会话后,不会被之前的会话影响。

#include <unistd.h>pid_t setsid(void);

返回值:成功返回该进程的 pid,失败返回 -1

注意: 调用该函数的进程,不能是组长进程,需要创建子进程后调用

手动实现守护进程时需要注意以下几点:

  1. 忽略异常信号
  2. 0、1、2 要做特殊处理(文件描述符)
  3. 进程的工作路径可能要改变(从用户目录中脱离至根目录)

具体实现步骤如下:

1、忽略常见的异常信号:SIGPIPE、SIGCHLD

2、如何保证自己不是组长? 创建子进程 ,成功后父进程退出,子进程变成守护进程

3、新建会话,自己成为会话的 话首进程

4、(可选)更改守护进程的工作路径:chdir

5、处理后续对于 0、1、2 的问题

对于 标准输入、标准输出、标准错误 的处理方式有两种

暴力处理:直接关闭 fd

优雅处理:将 fd 重定向至 /dev/null,也就是 daemon() 函数的做法

这里我们选择后者,守护进程 的函数实现如下:

Daemon.hpp 守护进程头文件

#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 错误码头文件

#pragma onceenum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERR,SETSID_ERR,CHDIR_ERR,OPEN_ERR
};

StartServer() 服务器启动函数 — 位于 server.hpp 服务器头文件中

现在服务器在启动后,会自动新建会话,以 守护进程 的形式运行

杀死守护进程

相关文章:

Linux -- 日志

一 日志的重要性 在之前的编程经历中&#xff0c;如果我们的程序运行出现了问题&#xff0c;都是通过 标准输出 或 标准错误 将 错误信息 直接输出到屏幕上&#xff0c;以此来排除程序中的错误。 这在我们以往所写的程序中使用没啥问题&#xff0c;但如果出错的是一个不断在运行…...

WebRtc 视频通话,语音通话实现方案

先了解一下流程 和 流程图(chatGpt的回答) 实现 (底层代码实现, 可作为demo熟悉) 小demo <template><div><video ref"localVideo" autoplay muted></video> <!-- 本地视频元素&#xff0c;用于显示本地视频 --><video ref"r…...

IndyTcpServer使用详解

1、IndyTCPserver的创建 IdTCPServer1.DefaultPort:= 8000; IdTCPServer1.ListenQueue:= 1024; //同时处理请求队列数限制 IdTCPServer1.MaxConnections:= 1024; //同时连接数量限制,为0不限制连接数 IdTCPServer1.ContextClass:= TNewIdServerContext; //设置为自定义TIdSe…...

pytest + yaml 框架 - 参数化读取文件路径优化

针对小伙伴提出参数化时读取外部文件&#xff0c;在项目根路径运行没问题&#xff0c;但是进入到项目下子文件夹运行用例&#xff0c;就会找不到文件问题做了优化。 关于参数化读取外部文件相关内容参考前面这篇pytest yaml 框架 -25.参数化数据支持读取外部文件txt/csv/json/…...

C++:多态-重写和重载

重写&#xff08;Override&#xff09;和重载&#xff08;Overload&#xff09;是面向对象编程中常用的两个概念&#xff0c;它们虽然都涉及到方法的定义&#xff0c;但是在实现和使用上有着不同的特点。 重写&#xff08;Override&#xff09;&#xff1a; 重写是指在子类中重…...

element ui的table多选

使用el-table的selection-change事件来获取选中的值&#xff1b; 例&#xff1a; html代码&#xff1a; <el-button type"primary" click"openTableSet">列表设置</el-button><!-- 列表设置弹框 --> <el-dialog :close-on-click-mo…...

python基础---基础运算

基础运算 可以使用type获取一个变量的类型 常见的数据类型 整形, 可以存储任意大小的整数, 支持二进制&#xff08;如0b100&#xff0c;换算成十进制是4&#xff09;、八进制&#xff08;如0o100&#xff0c;换算成十进制是64&#xff09;、十进制&#xff08;100&#xff09;…...

【数学】泰勒公式

目录 引言 一、泰勒公式 1.泰勒公式及推导 &#xff08;1&#xff09;推导 &#xff08;2&#xff09;公式 2.泰勒中值定理 &#xff08;1&#xff09;定理1&#xff08;佩亚诺余项&#xff09; &#xff08;2&#xff09;定理2&#xff08;拉格朗日余项&#xff09; …...

C++基础-编程练习题及答案

文章目录 前言一、查找“支撑数”二、数组元素的查找三、爬楼梯四、数字交换五、找高于平均分的人 前言 C基础-编程练习题和答案 一、查找“支撑数” 【试题描述】 在已知一组整数中&#xff0c; 有这样一种数非常怪&#xff0c; 它们不在第一个&#xff0c; 也不在最后一个&…...

eNSP-抓包解析HTTP、FTP、DNS协议

一、环境搭建 1.http服务器搭建 2.FTP服务器搭建 3.DNS服务器搭建 二、抓包 三、http协议 1.HTTP协议&#xff0c;建立在TCP协议之上 2.http请求 3.http响应 请求响应报文参考&#xff1a;https://it-chengzi.blog.csdn.net/article/details/113809803 4.浏览器开发者工具抓包…...

【栈】Leetcode 验证栈序列

题目讲解 946. 验证栈序列 算法讲解 在这里就只需要模拟一下这个栈的出栈顺序即可&#xff1a;使用一个stack&#xff0c;每次让pushed里面的元素入栈&#xff0c;如果当前栈顶的元素等于poped容器中的当前元素&#xff0c;因此就需要让栈顶元素出栈&#xff0c;poped的遍历…...

【数据库原理及应用】期末复习汇总高校期末真题试卷08

试卷 一、选择题(每题 2 分&#xff0c;共 30 分)    1. ___ ____是长期存储在计算机内的有组织,可共享的数据集合. A.数据库管理系统 B.数据库系统 C.数据库 D.文件组织 2. 数据库类型是按照 来划分…...

每天五分钟深度学习:数学中的极值

本文重点 在数学领域中,极值是一个极其重要的概念,它不仅在纯数学理论研究中占据核心地位,而且在工程、物理、经济等实际应用领域也发挥着不可替代的作用。极值问题涉及函数的最大值和最小值,是微积分学中的一个基本问题。本文旨在详细介绍数学中的极值概念、性质、求解方…...

【Linux】Linux——Centos7安装Tomcat

1.下载Tomcat 安装包 官网地址&#xff1a;Apache Tomcat - Apache Tomcat 9 Software Downloadshttps://tomcat.apache.org/download-90.cgi 2.将下载的安装包上传到 Xftp 上&#xff0c;我是直接放到 usr 下了 3.将安装包解压到 /usr/local/ tar -zxvf apache-tomcat-9.0.8…...

SpringBoot+vue实现右侧登录昵称展示

目录 1. 定义User数据 1.1.在created方法获取数据 1.2.头部导航栏绑定User数据 1.3.在data中定义User数据 2. 获取数据 2.1.接收父组件传递的值 2.2.展示数据 3. 页面效果 在SpringBoot和 Vue.js 结合的项目中实现右侧登录昵称展示&#xff0c;通常涉及到前端的用户界面…...

【网络原理】UDP协议 | UDP报文格式 | 校验和 | UDP的特点 | 应用层的自定义格式

文章目录 一、UDP协议1.UDP的传输流程发送方接收方 2.UDP协议报文格式&#xff1a;长度受限校验和如何校验&#xff1a;CRC算法&#xff1a;循环冗余算法md5算法&#xff1a; 2.UDP的特点 二、开发中常见的自定义格式1.xml&#xff08;古老&#xff09;2.json&#xff08;最流行…...

NodeJs入门知识

**************************************************************************************************************************************************************************** 1、配置Node.js与npm下载&#xff08;精力所致&#xff0c;必有精品&#xff09; …...

代码随想录学习Day 34

62.不同路径 题目链接 讲解链接 动归五部曲&#xff1a; 1.确定dp数组及其下标的含义&#xff1a;dp[i][j]的含义是从(0, 0)走到(i, j)所需的步数&#xff1b; 2.确定递推公式&#xff1a;因为只能往右或者往下&#xff0c;所以dp[i][j] dp[i - 1][j] dp[i][j - 1]。 3.…...

由于找不到MSVCP120D.dll,无法继续执行代码。重新安装程序可能会解决此问题

由于找不到MSVCP120D.dll,无法继续执行代码。重新安装程序可能会解决此问题 一、问题详细描述二、问题产生背景三、问题原因四、解决办法1、安装缺少的库2、直接更换更高版本的opencv 五、vs版本对应vc1、版本对应2、vs对应vc查看方法 一、问题详细描述 同样可能会报 &#xff…...

【前端】输入时字符跳动动画实现

输入时字符跳动动画实现 在前端开发中&#xff0c;为了提升用户体验&#xff0c;我们经常需要为用户的交互行为提供即时的反馈。这不仅让用户知道他们的操作有了响应&#xff0c;还可以让整个界面看起来更加生动、有趣。本文将通过一个简单的例子讲解如何实现在用户输入字符时…...

C语言面试重点问题

1. 冒泡排序法 2. strlen、strcpy、strcat、strcmp的用法和原理 3. 大小端的区分 3.1 主函数区分大小端 #include <stdio.h>int main(void) {int num 0x11223344;char *p (char *)&num;if (0x11 *p){printf("大端!\n");}else if (0x44 *p){printf(…...

antlr4略解

文章目录 1. antlr4是用来干什么的&#xff1f;2. 什么是lexer和parser&#xff1f;3. 使用antlr4生成某语言的lexer和parser的具体过程4. 其他 1. antlr4是用来干什么的&#xff1f; 是用来生成某语言lexer和parser的。 通俗点说&#xff0c;就是输入一个语言的规则描述文件&…...

超级好用的C++实用库之文件目录操作

&#x1f4a1; 需要该C实用库源码的大佬们&#xff0c;可扫码关注文章末尾的微信公众号二维码&#xff0c;或搜索微信公众号“希望睿智”。添加关注后&#xff0c;输入消息“超级好用的C实用库”&#xff0c;即可获得源码的下载链接。 概述 文件和目录操作是操作系统层面上的基…...

结合kimi chat的爬虫实战思路

背景 想钻研一下项目组件&#xff0c;找找之后的学习方向。不能自以为是&#xff0c;所以借着网开源项目网站上公布的项目内容看一下&#xff0c;那些是我可以努力去学习的&#xff08;入门的&#xff09;。首先需要获取相关内容&#xff0c;于是爬取整理。 任务1&#xff1a…...

UnsupportedClassVersionError异常如何解决?

下面是异常报错的详细描述 java -version java version "17.0.11" 2024-04-16 LTS Java(TM) SE Runtime Environment (build 17.0.117-LTS-207) Java HotSpot(TM) 64-Bit Server VM (build 17.0.117-LTS-207, mixed mode, sharing) 环境变量已经是jdk17&#xff0c;但…...

LeetCode热题100|动态规划Part.1|70.爬楼梯、118.杨辉三角、198.打家劫舍

70.爬楼梯 代码随想录原题&#xff0c;看这篇文章&#xff1a;C动态规划Part.1|动态规划理论基础、509.斐波那契数、70.爬楼梯、746.使用最小花费爬楼梯 118.杨辉三角 题目链接&#xff1a;118.杨辉三角 一刷代码 时间复杂度和空间复杂度都造到 O ( n u m R o w s 2 ) O(num…...

python 根据网址和关键词批量下载影像

最近用到了GLASS的LAI产品&#xff0c;但这个产品的文件夹分得很细&#xff0c;我需要的影像又有8个瓦片&#xff0c;一个一个点击很麻烦&#xff0c;于是探索了批量下载的方法 一、下载1幅 import requests import re import os import requests import re# 网页URLurl &…...

爬虫-无限debug场景 解决方式

解决无限debug 场景1 1. 鼠标右键 选择 continue to here&#xff08;此处不停留&#xff09;2. 鼠标右键 选择 edite breakpoint 设置 10 保证条件不成立 这行永远不执行3.方法置空 1. 方法调用加断点2. 控制台 setInterval function name() {}4. 替换文件 5. hoo…...

[链表专题]力扣206, 203, 19

1. 力扣206 : 反转链表 (1). 题 : 图略 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。示例 1&#xff1a;输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3,2,1] 示例 2&#xff1a;输入&#xff1a;head [1,2] 输出&#x…...

秋招后端开发面试题 - MySQL基础

目录 MySQL基础前言面试题MySQL 基础篇Mysql 的基础架构&#xff1f;MySQL 的长连接和短连接长连接引起的异常重启问题&#xff1f;说一下 MySQL 执行一条查询语句的内部执行过程&#xff1f;MySQL 查询缓存的功能有何优缺点&#xff1f;MySQL 的常用引擎都有哪些&#xff1f;I…...