目录
守护进程化
引入
介绍
如何实现
思路
接口 -- 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 1024 class 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,并设置错误码