Linux -- 日志

一 日志的重要性

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

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

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

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

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

二 可变参数

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

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

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);
  1. char *str , 把生成的格式化的字符串存放在这里.
  2. size_t size , str可接受的最大字符数 ,防止产生数组越界.
  3. const char *format , 指定输出格式的字符串,它决定了你需要提供的可变参数的类型、个数和顺序。
  4. 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 日志器的等级

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

  1. Debug 用于调试
  2. Info 提示信息
  3. Warning 警告
  4. Errorr 错误
  5. 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 其实就是 bashPID

我们使用 XShell 等工具登录 Linux 服务器时,会在服务器中创建一个 会话bash),可以在该会话内创建 进程 ,当 进程 间有关系时,构成一个 进程组组长 进程的 PID 就是该 进程组PGID。

在同一个会话中,只允许一个前台进程在运行,默认是 bash,如果其他进程运行了,bash 就会变成后台进程(暂时无法使用),让出前台进程这个位置(后台进程与前台进程之前是可以进程切换)

如何将一个 后台进程 变成 前台进程?

首先通过指令查看当前 会话 中正在运行的 后台进程,获取 任务号

cpp 复制代码
jobs

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

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

cpp 复制代码
fg 后台进程号

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

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

cpp 复制代码
键盘输入 ctrl + z

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

4.2 守护进程化

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

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

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

cpp 复制代码
#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 服务器源文件

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

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

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

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

具体实现步骤如下:

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 服务器头文件中

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

杀死守护进程

相关推荐
程序猿小D2 小时前
第二百三十五节 JPA教程 - JPA Lob列示例
java·数据库·windows·oracle·jdk·jpa
iummature4 小时前
ZLMediaKit Windows编译以及使用
windows
周伯通*7 小时前
Windows上,使用远程桌面连接Ubuntu
linux·windows·ubuntu
GDAL10 小时前
GNU力量注入Windows:打造高效跨平台开发新纪元
服务器·windows·gnu
小徐敲java13 小时前
Windows本地制作java证书(与jeecgboot配置本地证书ssl问题)
java·windows·ssl
春蕾夏荷_72829772513 小时前
electron nsis打包windows应用程序
javascript·windows·electron·nsis
偷偷小野猪13 小时前
Windows 常用的键盘快捷键总结
windows
Splashtop高性能远程控制软件15 小时前
centos远程桌面连接windows
linux·windows·centos·远程控制·远程桌面
楚钧艾克18 小时前
Windows系统通过部署wsl + Goland进行跨平台开发
linux·windows·后端·ubuntu·golang
wow2ok18 小时前
天融信把桌面explorer.exe删了,导致开机之后无windows桌面,只能看到鼠标解决方法
windows·计算机外设