1 相关概念
1.1 守护进程的概念
守护进程也叫做精灵进,是运行在后台的一种特殊进程。它独立于控制终端并且可以周期性的执行某种任务或者处理某些发生的事件。
- 守护进程是非常有用的进程,在Linux当中大多数服务器用的就是守护进程。比如,web服务器http等,同时守护进程完成很多系统的任务。当Linux系统启动的时候,会启动很多系统服务,这些进程服务是没有终端的,也就是你把终端关闭了,这些系统服务是不会停止的,它们一直运行着。它们有一个名字,就叫做守护进程。
一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程的方式在服务器中工作的,一旦启动之后,除非用户主动关闭,否则,一直会在运行。)
1.2 进程组和会话
进程组的相关概念:
- 进程除了有进程的PID之外,还有一个进程组,进程组是一个进程或者多个进程组成。通常他们与同一作业相关联,可以收到同一终端的信号;
- 每个进程组有唯一的进程组ID,每一个进程组有一个进程组组长。如何判断一个进程是不是这个进程组的组长?通常进程ID等于进程该进程组ID,那么该进程就是该进程组的组长。
会话组的相关概念:
- 会话是有一个或者多个进程组组成的集合
- 一个会话可以有一个终端,建立与控制终端连接的会话首进程被成为控制进程,一个会话的几个进程组可以分为前台进程和后台进程,而这些进程组的控制终端相同,也就是sesion id是一样的。当用户使用Ctrl + c 产生SIGINT信号时,内核会发送信号给相应的前台进程组的所有进程。
- 如果运行一个程序,我想把它放到后台运行,可以在可执行程序后面加一个&;
- 如果想把后台进程提到前台,可以使用fg
- jobs指令可以查看当前会话的后台进程
- 将前台进程放到后台,Ctrl + z | bg + 任务编号
观察现象:
下面来介绍上述选项的意义:
![]()
![]()
![]()
![]()
综上:
- 我们在命令行中启动一个进程,现在就可以叫做在会话中启动一个进程组,来完成某种任务;
- 所有会话内的进程fork创建子进程,一般而言依旧属于当前会话。
像平时,当我们觉得Windows卡顿的时候,我们可能会重新注销一下。注销就是让用户退出登录后再重新登陆,那么此时就相当于给你新建一个会话。卡顿是因为你本次登陆过程中启动了很多任务,且都属于同一个会话,注销本质就是把你内部会话的所有进程组删掉。
注意:
- 在登录的状态时,新起了一个网络服务器,创建好之后,在派生的子进程也属于当前会话,所以我们就不能让这个网络服务器属于这个会话内容,要不然它会受到用户的登录和注销的影响。
- 所以,当我们有个网络服务的时候,应该脱离这个会话,让它独立的在计算机里自成进程组,自成新会话。这样在两个用户同时登录的时候,形成的两个会话是独立的,在操作各自的bash不会相互影响。
- 像这种自成进程组,自成新会话,而且周而复始的进程称为守护进程(精灵进程)。
2 守护进程的方式
我们这里有三种方式让自己的进程守护进程化:
- 自己写daemon函数,推荐使用这种方式
- 用系统的daemon函数
- nohup命令
2.1 TCP网络程序(守护进程化)
之前的TCP网络程序是在前台运行的,但是实际上服务器并不是在前台运行的,而是在后台运行的。所以现在对TCP网络程序的代买进行修改,加上一个小组件,使其守护进程化,让服务器在后台运行。编写daemon.hpp文件完成守护进程的主要逻辑,具体如下:
- 忽略一些不需要的异常信号,防止进程被信号杀死,如:调用signal函数忽略SIGPIPE信号;
- 更改进程的工作目录(选做);
- fork创建子进程,exit让父进程退出。让执行服务的进程不是进程组组长,从而保证后续不会再和其他终端相关联;
- 调用setsid函数设置自己是一个独立的会话(setsid不能设置进程组组长的进程);
- 将标准输入、标准输出、标准错误重定向到/dev/null(一种文件,不能写也不能读);
进程守护化需要调用setsid()函数,注意点如下:
- 调用setsid创建新会话的目的,是让当前进程自成会话,与当前bash脱离关系(创建守护进程的核心)。
- 调用setsid创建新会话时,要求调用进程不能是进程组长,但是当我们在命令行上启动多个进程协同完成某种任务时,其中第一个被创建出来的进程就是组长进程,因此我们需要fork创建子进程,让子进程调用setsid创建新会话并执行后续代吗,而父进程直接exit退出即可。此时子进程就不是组长进程了,而是独立会话的守护进程。
- 当服务端给客户端写入时,但是客户端突然关掉了,那就是向一个不存在的文件描述符写入,此时服务端会收到SIGPIPE信号而自动终止,所以我们要忽略该信号。
- 当前进程有自己的工作目录,有时候守护进程想要更改自己的工作目录,一般会将守护进程的工作目录设置为根目录,便于让守护进程以绝对路径的形式访问某种资源。我们可以使用chdir函数更改进程的工作目录,不过此操作不做强求。
- 守护进程不能直接和用户交互,也就是说守护进程与终端去关联了,因此一般我们会将守护进程的标准输入。标准输出、标准错误都重定向到/dev/null,/dev/null是一个字符文件(设备),类似于Linux的一个“文件黑洞”or“垃圾桶”,通常用于屏蔽/丢弃输入输出信息。(建议这么做)
【daemon.hpp实现如下】
#pragma once
#include <cstdlib>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include "log.hpp"
#include "err.hpp"
//守护进程的本质:孤儿进程
void Daemon()
{
// 1. 忽略异常信号
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
// 2. 创建子进程,让子进程成为新的会话
if (fork() > 0)
exit(0); // 父进程退出
// 子进程
// 3. 设置子进程为新会话
pid_t id = setsid();
if (id < 0)
{
logMessage(Fatal, "setsid error:%s\n", strerror(errno));
exit(SETSID_ERR);
}
logMessage(Info, "setsid successful.\n");
//4. 可选,更改工作目录
//chdir("/");
//5. 处理0,1,2的问题
int fd=open("/dev/null",O_RDWR);
if(fd<0)
{
logMessage(Fatal,"open /dev/null error:%s\n",strerror(errno));
exit(OPEN_ERR);
}
logMessage(Info,"open /dev/null successful!\n");
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd);
}
出现问题:进程变成守护进程后,打印的日志信息不见了,要怎么办?
解决问题:日志持久化,将日志输出追加打印到log.txt文件中!
最后一个步骤:我们只需要在服务端的main函数命令行参数信息处理后调用此daemon函数即可:
测试结果:
- 现在我们运行服务端,通过下面的监控脚本辅助观察信息:
[xzy@ecs-333953 tcp]$ ps axj | head -1 && ps axj | grep serverTcp
[xzy@ecs-333953 tcp]$ ps axj | head -1 && ps axj | grep sshd