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

可以这样获取当前时间


// 获取当前时间
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;
        //服务器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 头文件中的代码 

// 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 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 就会变成后台进程(暂时无法使用),让出前台进程这个位置(后台进程与前台进程之前是可以进程切换)


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

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 once

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    CONNECT_ERR,
    FORK_ERR,
    SETSID_ERR,
    CHDIR_ERR,
    OPEN_ERR
};

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

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

杀死守护进程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/610945.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

fb设备驱动框架分析

一、字符设备注册过程&#xff1a; 归根到底&#xff0c;fb设备也是一个字符设备&#xff0c;所以逃不开常规的字符设备驱动框架&#xff1a; Linux内核中编写字符设备驱动通常遵循以下步骤&#xff1a; ①、定义主设备号&#xff1a; 在Linux中&#xff0c;每个字符设备都…

MySQL 通过 systemd 启动时 hang 住了……

mysqld&#xff1a;哥&#xff0c;我起不来了…… 作者&#xff1a;贲绍华&#xff0c;爱可生研发中心工程师&#xff0c;负责项目的需求与维护工作。其他身份&#xff1a;柯基铲屎官。 爱可生开源社区出品&#xff0c;原创内容未经授权不得随意使用&#xff0c;转载请联系小编…

如何查看页面对应的Selenium定位参数

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

谷歌外链怎么发?

既要数量也要质量&#xff0c;要保证你的链接广泛分布&#xff0c;在数量上&#xff0c;确实需要你的链接在各种平台上有所展现&#xff0c;这样能提升你网站的知名度和曝光率&#xff0c;但是&#xff0c;光有数量是不够的&#xff0c;如果这些链接的内容不行&#xff0c;那对…

泰迪智能科技企业数据挖掘流程分析及特色服务优势

企业发展会沉淀大量的数据&#xff0c;数据中囊括了企业业务各种维度指标&#xff0c;通过数据挖掘和数据分析 &#xff0c;让企业业务了解过去、现在和未来将要发生什么&#xff0c;从而更好的调整企业发展方向。泰迪智能科技企业数据挖掘平台是面向企业级用户快速处理数据构建…

2024年湖北省专升本C语言程序设计大题真题解析

2024年湖北省的专升本考试已于4月30日举行&#xff0c;考试中&#xff0c;出现了许多不同的考试题目&#xff0c;我在网上找到一所高校专升本的大题&#xff08;好像是湖北师范的&#xff0c;后续会有湖北理工的大题真题解析&#xff0c;敬请期待&#xff09;&#xff0c;那么我…

在新页面中跳转到指定 div容器位置

要在打开新的页面时跳转到指定 div&#xff0c;我们需要结合 HTML、JavaScript 和后端技术来实现。以下是两种常见的方法&#xff1a; 使用 URL 参数传递目标 div 信息 HTML (新页面): 在新页面的链接中&#xff0c;添加参数来指示目标 div 的 id&#xff0c;例如&#xff1a;…

致远M3 Session 敏感信息泄露漏洞复现

0x01 产品简介 M3移动办公是致远互联打造的一站式智能工作平台,提供全方位的企业移动业务管理,致力于构建以人为中心的智能化移动应用场景,促进人员工作积极性和创造力,提升企业效率和效能,是为企业量身定制的移动智慧协同平台。 0x02 漏洞概述 致远M3 server多个日志文…

Vue3自定义指令封装-按钮权限控制v-permission、hasPermissions

背景&#xff1a;平常所接触到的系统权限控制&#xff0c;大部分都是菜单、路由级别的控制&#xff0c;但后台管理系统中&#xff0c;很多操作都是与职责和角色挂钩的&#xff0c;同样一个列表&#xff0c;不同人的操作列并不都一样&#xff0c;有些页面存在一些含有重要数据的…

万物生长大会 | 创邻科技再登杭州准独角兽榜单

近日&#xff0c;由民建中央、中国科协指导&#xff0c;民建浙江省委会、中国投资发展促进会联合办的第八届万物生长大会在杭州举办。 在这场创新创业领域一年一度的盛会上&#xff0c;杭州市创业投资协会联合微链共同发布《2024杭州独角兽&准独角兽企业榜单》。榜单显示&…

MathType2024官方版数学公式编辑器功能全面介绍

在数字化学习和科研的浪潮中&#xff0c;数学公式的编辑与展示成为了不可或缺的一部分。MathType&#xff0c;作为一款专业的数学公式编辑器&#xff0c;凭借其强大的功能和便捷的操作&#xff0c;为科研人员、教师、学生等广大用户提供了极大的便利。下面&#xff0c;我们将对…

基于.NET WinForms 数据CURD功能的实现

使用开发工具 VS 2022 C#&#xff0c;数据库MS SQL SERVER 2019 &#xff0c;基于NET WinForms&#xff0c;实现数据记录的创建(Create)、更新(Update)、读取(Read)和删除(Delete)等功能。主要控件包括&#xff1a;DataGridView&#xff0c;SqlDataApater &#xff0c; DataTab…

字符以及字符串函数

字符以及字符串函数 求字符串长度strlen 长度不受限制的字符串函数strcpystrcatstrcmp 长度受限制的字符串函数strncpystrncatstrncmp 字符串查找strstrstrtok 错误信息报告strerror 字符分类函数字符转换函数tolowertoupper 内存操作函数memcpymemmovememcmpmemset 这篇文章注…

软件开发故事 - 我对 CTO 撒谎并挽救了项目

原文&#xff1a;GrumpyOldDev - 2024.04.18 这是几年前的事情了。还记得在我职业生涯的初期&#xff0c;父亲曾告诉我&#xff0c;做好工作往往意味着要在上司的阻碍下做好需要做的事情。他的意思是&#xff0c;你可以让上司成功并感到快乐&#xff1b;也可以让上司做每一个决…

Linux的编译器

程序编译的过程 程序的编译过程是将源代码转换为可执行文件的一系列步骤。这个过程涉及多个阶段&#xff0c;主要包括预处理、编译、汇编和链接。下面详细介绍每个阶段&#xff1a; 1. 预处理&#xff08;Preprocessing&#xff09; 在实际编译之前&#xff0c;源代码文件首…

让云上用户拥有安全感 可信或成云服务器标配安全能力之一!

什么是虚拟主机 虚拟主机就是利用网络空间技术&#xff0c;把一台服务器分成许多的“虚拟”的主机&#xff0c;每一台网络空间都具有独立的域名和IP地址&#xff0c;具有完整的Internet服务器功能。网络空间之间完全独立&#xff0c;在外界看来&#xff0c;每一台网络空间和一台…

gpustat 不能使用问题

突然间就不能用了&#xff0c;可能是环境出了问题&#xff0c;如果GPU没问题的话&#xff0c;那么换个环境重新安装试一下&#xff08;pip install gpustat&#xff09;&#xff0c;目前是换个环境就可以了&#xff08;做个笔记&#xff09;

【神器来袭】快速解放双手,朋友圈自动转发工具,告别繁琐操作!

朋友圈作为一个重要的营销推广渠道&#xff0c;如果能实现自动转发&#xff0c;那对于很多企业或个人来说&#xff0c;是极好的。下面&#xff0c;就给大家分享一个实用且便捷的朋友圈运营工具——个微管理系统&#xff0c;让大家都能快速推广。 1、多账号登录&#xff0c;定时…

企业如何有效做好源代码防泄密工作之九种干货分享

企业为解决源码泄密风险问题&#xff0c;许多单位采取拆除光驱软驱、封掉USB接口、限制上网等方法来进行限制&#xff1b;或者安装一些监控软件&#xff0c;监控员工的日常工作&#xff0c;使其不敢轻举妄动&#xff1b;或者安装各种网络信息安全防护产品&#xff0c;如防火墙&…

“幽灵“再临!新型攻击瞄准英特尔CPU;微软Outlook漏洞被俄利用,网络间谍攻击捷克德国实体 | 安全周报0510

1. 微软Outlook漏洞被俄罗斯APT28利用&#xff0c;捷克德国实体遭网络间谍攻击&#xff01; 捷克和德国于周五透露&#xff0c;他们成为与俄罗斯有关的APT28组织进行的长期网络间谍活动的目标&#xff0c;此举遭到欧洲联盟&#xff08;E.U.&#xff09;、北大西洋公约组织&…