欢迎来到Cefler的博客😁
🕌博客主页:折纸花满衣
🏠个人专栏:题目解析
🌎推荐文章:进程状态、类型、优先级、命令行参数概念、环境变量(重要)、程序地址空间
目录
- 👉🏻守护进程
- 👉🏻写一个守护进程
- daemon函数
- setsid函数
- Daemon.hpp
- main.cc
- 👉🏻将tcp通信进程变为守护进程
- Main.cc
👉🏻守护进程
🌈 概念
Linux 的守护进程(Daemon Process)是一种在后台运行的特殊类型的进程,它们与用户没有交互界面,通常用于执行系统任务、服务或守护程序。
守护进程在 Linux 系统中被设计为长期运行的进程,独立于任何控制终端,并在系统启动时自动启动。它们通常在系统启动过程中由初始化进程(init process)启动,并在系统关闭时由系统管理器来终止。
而我们的网络服务,不能在bash中以前台进程的方式运行,真正的服务器,必须在Linux后台,以守护进程的方式运行
我们平常登录Linux,操作系统都会给我提供一个bash(命令行解释器),在此页面中我们也称为会话,在同一个会话中创建的进程,无非分为前台和后台进程,但是,后台进程可以有很多个,而前台进程只能有一个
同时启动的多个进程,可以是属于同一个进程组的,组ID一般是多个进程中的第一个进程
如图,我们启动一个睡眠进程,此时睡眠进程在前台,bash就退到后台去了,所以我们此时发的任何命令都无法被解析。
此时我们可以ctrl+Z终止当前进程,睡眠进程就会停止了。
fg 进程号数
放前台,bg 进程号数
放后台
👉🏻写一个守护进程
daemon函数
daemon()
函数是一个Linux/Unix系统
中用来创建守护进程(daemon)的函数,它通过一系列步骤将当前进程转变为一个守护进程。在标准的C库中没有daemon()
函数,但是在一些系统中(如GNU libc)提供了这个函数。
以下是daemon()
函数的一般形式:
int daemon(int nochdir, int noclose);
nochdir
:如果该参数非零,表示在调用daemon()
函数之后,守护进程的工作目录将不会被修改,即不会改变当前工作目录为根目录。noclose
:如果该参数非零,表示在调用daemon()
函数之后,守护进程的标准输入、输出和错误输出将不会被关闭,即不会关闭文件描述符0、1和2。
daemon()
函数的主要作用是将当前进程转变为一个守护进程。守护进程是在后台运行的系统服务,通常独立于终端会话,以提供某种服务或执行某些系统任务。通常,守护进程需要满足以下几个特性:
-
与终端分离:守护进程通常不应该与任何终端相关联,因此在创建守护进程时,需要将其与任何终端分离。
-
独立于父进程:守护进程应该是一个独立的进程,不受父进程的影响,即使父进程退出,守护进程也能够继续运行。
-
关闭文件描述符:通常,守护进程需要关闭与标准输入、输出和错误输出相关联的文件描述符,以确保不会受到终端会话的影响。
daemon()
函数在实现这些特性时会完成一系列的操作,包括创建子进程、关闭父进程、改变工作目录、设置文件掩码等。它简化了创建守护进程的过程,使得开发者能够更容易地编写具有守护进程特性的程序。
setsid函数
setsid
函数是一个Unix系统调用,用于创建一个新的会话并设置当前进程的会话ID(Session ID)为新会话的ID。它通常在守护进程(daemon)中使用,以确保进程独立于其父进程运行,并且不受终端的影响。
在调用setsid
函数之后,当前进程将成为一个新的会话的领导者(session leader),同时成为一个新的进程组的领导者,并且没有控制终端。这样做有几个作用:
- 摆脱控制终端:会话领导者不再有控制终端,这意味着即使用户退出登录或关闭终端,该进程仍然可以继续运行。
- 独立于父进程:新的会话和进程组意味着进程不再受父进程的影响,即使父进程退出,进程也可以继续运行。
- 改变工作目录:新的会话通常会将进程的工作目录更改为根目录,以确保不受当前工作目录的影响。
setsid
函数的原型如下:
pid_t setsid(void);
其中,setsid
不接受任何参数,它返回一个pid_t
类型的值,表示新的会话ID。如果调用成功,返回值为新的会话ID,如果失败则返回-1,并设置errno
来指示错误的原因。
要使用setsid
函数,通常需要在程序中先调用fork
创建一个子进程,然后在子进程中调用setsid
函数。这样可以确保新的会话和进程组不会受到父进程的影响。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>int main() {pid_t pid = fork();if (pid < 0) {perror("fork");exit(EXIT_FAILURE);} else if (pid > 0) {// 父进程退出exit(EXIT_SUCCESS);}// 在子进程中调用setsid函数pid_t sid = setsid();if (sid < 0) {perror("setsid");exit(EXIT_FAILURE);}// 这里是新的会话领导者// 关闭标准输入、输出和错误输出close(STDIN_FILENO);close(STDOUT_FILENO);close(STDERR_FILENO);// 守护进程的主体逻辑return 0;
}
以上是一个简单的守护进程的示例,其中在子进程中调用了setsid
函数,创建了一个新的会话,并关闭了标准输入、输出和错误输出,最后进入守护进程的主体逻辑。
为什么要先调用fork创建一个子进程? 🤔
父进程在创建子进程后立即退出,这样子进程就不会成为孤儿进程,而是被init进程
接管。这一步确保了守护进程不会有父进程,从而独立于任何终端会话。
父进程在创建子进程后立即退出,这样子进程就不会成为孤儿进程,而是被init进程接管。这一步确保了守护进程不会有父进程,从而独立于任何终端会话。
总的来说,就是如果不创建子进程,父进程成为当前会话的唯一进程组,也就是进程组的组长,父进程如果一旦退出,整个会话也就退出玩完了。
Daemon.hpp
#pragma once
#include<iostream>
#include<cstdlib>
#include<signal.h>
#include<unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>const char *root = "/";
const char *dev_null = "/dev/null";// /dev/null: 凡是向这个目录写入的内容,自动被丢弃void daemon()
{// 1. 忽略可能引起程序异常退出的信号signal(SIGCHLD, SIG_IGN);signal(SIGPIPE, SIG_IGN);// 2. 让自己不要成为组长if (fork() > 0)exit(0);// 3. 设置让自己成为一个新的会话, 后面的代码其实是子进程在走setsid();// 4. 每一个进程都有自己的CWD(当前目录),是否将当前进程的CWD更改成为 / 根目录if (ischdir)chdir(root);// 5. 已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了if (isclose){close(0);close(1);close(2);}else{// 这里一般建议就用这种int fd = open(dev_null, O_RDWR);if (fd > 0){dup2(fd, 0);//重定向,原本向标准输入中读的,现在从fd中读dup2(fd, 1);dup2(fd, 2);close(fd);}}
}
main.cc
#include "Daemon.hpp"
#include <unistd.h>int main()
{// 变成守护进程Daemon(true, false);// 是我要执行的核心代码while(true){sleep(1);}return 0;
}
👉🏻将tcp通信进程变为守护进程
代码参考:【Linux】socket编程3
代码目录:
Main.cc
#include <memory>#include "TcpServer.hpp"
#include "CommErr.hpp"
#include "Translate.hpp"
#include "Daemon.hpp"void Usage(std::string proc)
{std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}
void Interact(int sockfd)
{while(true){char buffer[1024];ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);//读取客户端信息if (n > 0){buffer[n] = 0;cout<<"clinet say:"<<buffer<<endl;string message = buffer;if(write(sockfd, message.c_str(), message.size())<0)//发送回客户端cout<<"send fail"<<endl;}else if (n == 0) // read如果返回值是0,表示读到了文件结尾(对端关闭了连接!){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}
}
void Ping(int sockfd, InetAddrtoLocal addr)
{lg.LogMessage(Debug, "%s select %s success, fd : %d\n", addr.Info().c_str(), "ping", sockfd);// 一直进行IOstd::string message = "Ping Service Start...";write(sockfd, message.c_str(), message.size());//先回应来自客户端的消息Interact(sockfd);}void Translate(int sockfd,InetAddrtoLocal addr)
{lg.LogMessage(Debug, "%s select %s success, fd : %d\n", addr.Info().c_str(), "Translate", sockfd);std::string message = "Translate Service Start...";write(sockfd, message.c_str(), message.size());//先回应来自客户端的消息Translater trans;while(true){char buffer[1024];ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);//读取客户端信息if (n > 0){buffer[n] = 0;cout<<"clinet say:"<<buffer<<endl;string message = trans.Excute(buffer);if(write(sockfd, message.c_str(), message.size())<0)//发送回客户端cout<<"send fail"<<endl;}else if (n == 0) // read如果返回值是0,表示读到了文件结尾(对端关闭了连接!){lg.LogMessage(Info, "client quit...\n");break;}else{lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));break;}}
}// 改成大写,字符串改成大写
void Transform(int sockfd, InetAddrtoLocal addr)
{lg.LogMessage(Debug, "%s select %s success, fd : %d\n", addr.Info().c_str(), "Transform", sockfd);}
// ./tcp_server 8888
int main(int argc, char *argv[])
{if(argc != 2){Usage(argv[0]);return Usage_Err;}uint16_t port = stoi(argv[1]);//进程变为守护进程Daemon(false, false);lg.Enable(ClassFile);//使日志可以向显示屏打印std::unique_ptr<TcpServer> tsvr = make_unique<TcpServer>(port);//在内核中注册服务类型tsvr->RegisterFunc("ping", Ping);tsvr->RegisterFunc("translate", Translate);tsvr->RegisterFunc("transform", Transform);//初始化和启动服务端tsvr->Init();tsvr->Start();return 0;
}
- /dev/null: 凡是向这个目录写入的内容,自动被丢弃
- kill -9 PID 删除进程
- daemon函数:将进程->守护进程化
如上便是本期的所有内容了,如果喜欢并觉得有帮助的话,希望可以博个点赞+收藏+关注🌹🌹🌹❤️ 🧡 💛,学海无涯苦作舟,愿与君一起共勉成长