目录
守护进程化
引入
介绍
如何实现
思路
接口 -- setsid
注意点
实现代码
daemon.hpp
log.hpp
运行情况
前情提要 -- 前后台任务介绍(区别+命令),session+sid介绍,session退出后的情况(nuhup,终端进程控制组),任务+进程组概念,任务与进程组的关系,-bash介绍-CSDN博客
守护进程化
引入
如果让他们可以不受这些的影响,这就是守护进程化
介绍
是一种使进程脱离终端会话控制,并在后台持续运行的技术
- 守护进程通常是作为服务在系统中运行的后台进程,它们不会受到终端会话退出的影响,并且可以在系统启动时自动启动
如何实现
思路
我们已经知道,启动的任务会自动属于当前的会话 -> 那么就会随着会话的退出而受到影响,且这个影响未知(有可能被终止,有可能还存在)
- 如果我们想要让启动的任务不受到会话的影响,就可以让他自成一个会话
- 也就是我们自己创建一个新会话,把它迁移过去
- 这样,这个任务就不会受到原来那个会话的影响了,因为它已经归属其他会话了
- (当然,这个新的会话不需要与键盘文件交互)
接口 -- setsid
创建一个会话,让当前进程脱离当前会话,成为新会话的进程组,并且让调用这个函数的进程的pgid作为sid
但他有一个条件,进程组的组长不能调用这个函数
- 就相当于 -- 你在公司如果是个组长的职位,你告诉老板说自己想出去单干,老板肯定不愿意,他会说,你走不行,你下面的组员走可以
- 所以,结合任务中第一个进程作为组长来看,我们可以考虑借助父子进程的特性来实现
- 父进程会是组长,让他退出
- 子进程作为组员,去执行守护进程化的工作
- (毕竟父进程干等着也没啥用,工作是子进程的,唯一需要的就是释放子进程的资源,但这一工作可以让init进程干,也就是托孤给它)
注意点
如果我们将服务端变成守护进程,那我们是不需要它在显示器上打印的
- 所以我们可以考虑将标准输出/错误关闭
- 标准输入也不需要,因为后台任务本身就无法和键盘交互
但我们的服务端之前写过两种需要的打印:日志和debug语句
- 如果直接关闭文件描述符,会导致写入错误
- 所以,可以将他们重定向
如果需要日志的话,可以将它重定向到log.txt文件里
但dug语句确实不需要了,其中有两种做法:
- 删除相关代码(但在代码量过大时,就不太方便了)
- 将标准流重定向到/dev/null (它会将收到的数据直接丢弃,相当于垃圾桶)
当然,不要忘记处理SIGCHLD和SIGHUP这两个信号的处理方式
- 因为我们的代码中可能会涉及到这两个信号:父子进程,终端退出等
- 其他信号根据自己来设定,如果不希望这个服务端被暂停,也可以忽略SIGSTOP信号]
除此之外,守护进程也许需要修改自己的工作目录
- 而且大部分的服务端都会修改自己的工作目录在根目录下,很可能还会将自己部署到系统中:
- 总之,是有这个需求的
- 所以,我们这里也提供支持
- 也就是使用我们的chdir函数(之前在简单模拟shell中使用过 -- 模拟实现简易版shell(需要单独处理 ls+cd+export)_模拟实现一个简单的shell设计-CSDN博客):
实现代码
daemon.hpp
#include <iostream> #include <cstdlib> #include <unistd.h> #include <signal.h> #include <string> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h>const std::string null_path = "/dev/null";void daemon(const std::string &path = "") {// 忽略信号signal(SIGCHLD, SIG_IGN);signal(SIGHUP, SIG_IGN);// 创建子进程if (fork() > 0){exit(0);}// 子进程成为守护进程setsid();// 修改工作目录if (!path.empty()){chdir(path.c_str());}// 重定向int fd = open(null_path.c_str(), O_RDWR);if (fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);} }
log.hpp
#pragma once#include <iostream> #include <time.h> #include <stdarg.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h>#define INFO 0 #define DEBUG 1 #define WARNING 2 #define ERROR 3 #define FATAL 4 // 致命的错误#define SCREEN 1 #define ONEFILE 2#define DEF_NAME "log.txt" #define DEF_PATH "./log/"#define SIZE 1024class Log { public:Log(): method_(SCREEN), path_(DEF_PATH){}void enable(){method_ = ONEFILE;}void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);printLog(logtxt);}~Log(){}private:std::string levelToString(int level){switch (level){case INFO:return "INFO";case DEBUG:return "DEBUG";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "NONE";}}void printLog(const std::string &logtxt){switch (method_){case SCREEN:std::cout << logtxt << std::endl;break;case ONEFILE:printOneFile(logtxt);break;default:break;}}void printOneFile(const std::string &info){std::string path = path_ + DEF_NAME;int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);//int fd = open("DEF_NAME", O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd > 0){write(fd, info.c_str(), info.size());close(fd);}else{return;}}private:int method_;std::string path_; };Log lg;
运行情况
服务端成功变成守护进程(孤儿进程+与终端无关+自成会话)
并且,随着客户端的登录/退出,log.txt也成功增加内容:
守护进程化的接口 -- daemon
参数
他有两个参数:
- 如果想要把工作目录更换至根目录,nochdir=0;否则为当前路径
- 如果想要将三个标准流重定向至/dev/null,noclose=0;否则不会对他们做处理
返回值
- 成功返回0
- 失败返回-1,并设置错误码