守护进程也叫做精灵进,是运行在后台的一种特殊进程。它独立于控制终端并且可以周期性的执行某种任务或者处理某些发生的事件。
- 守护进程是非常有用的进程,在Linux当中大多数服务器用的就是守护进程。比如,web服务器http等,同时守护进程完成很多系统的任务。当Linux系统启动的时候,会启动很多系统服务,这些进程服务是没有终端的,也就是你把终端关闭了,这些系统服务是不会停止的,它们一直运行着。它们有一个名字,就叫做守护进程。
一般以服务器的方式工作,对外提供服务的服务器,都是以守护进程(精灵进程的方式在服务器中工作的,一旦启动之后,除非用户主动关闭,否则,一直会在运行。)
进程组概念:
- 进程除了有进程的PID之外,还有一个进程组,进程组是一个进程或者多个进程组成。通常他们与同一作业相关联,可以收到同一终端的信号;
- 每个进程组有唯一的进程组ID,每一个进程组有一个进程组组长。如何判断一个进程是不是这个进程组的组长?通常进程ID等于进程该进程组ID,那么该进程就是该进程组的组长。
会话组概念:
- 会话是有一个或者多个进程组组成的集合
- 一个会话可以有一个终端,建立与控制终端连接的会话首进程被成为控制进程,一个会话的几个进程组可以分为前台进程和后台进程,而这些进程组的控制终端相同,也就是sesion
id是一样的。当用户使用Ctrl + c 产生SIGINT信号时,内核会发送信号给相应的前台进程组的所有进程。- 如果运行一个程序,我想把它放到后台运行,可以在可执行程序后面加一个&;
- 如果想把后台进程提到前台,可以使用fg
- jobs指令可以查看当前会话的后台进程
- 将前台进程放到后台,Ctrl + z | bg + 任务编号
PGID:表示当前进程所属的进程组
SID: 表示当前进程所属的会话
将三个sleep放到后台运行,可以看到三个进程所属同一个进程组,进程组ID是以第一个进程的ID定义,所以在一起创建多个进程时,第一个创建的进程就是组长,组内成员的进程组ID以组长PID定义。
我们可以看到进程的所属组4334指的就是bash进程。
我们可以验证:使用不同客户端登录服务器,创建出来的会话不同。
我们可以通过指令jobs查看当前会话的任务
使用指令fg + 编号将后台进程提到前台
当我们把后台进程提到前台时,发现不能去执行我们别的任务,由此得出,同一时刻,一个会话只允许一个前台进程组进行运行。
像平时,当我们觉得Windows卡顿的时候,我们可能会重新注销一下。注销就是让用户退出登录后再重新登陆,那么此时就相当于给你新建一个会话。卡顿是因为你本次登陆过程中启动了很多任务,且都属于同一个会话,注销本质就是把你内部会话的所有进程组删掉。
我们这里有三种方式让自己的进程守护进程化:
- 自己写daemon函数(推荐)
- 用系统的daemon函数
- nohup命令
TCP网络程序(socket编程会自己实现)是在前台运行的,但是实际上服务器并不是在前台运行的,而是在后台运行的。所以现在对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:
#pragma once
#include
#include
#include
#include
#include
#include
#include
#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);
}
TCP实现多个客户端可与服务端进行互通 gitee仓库地址
我们启动服务器:
可以看到服务器进程已经是守护进程了,守护进程的本质就是孤儿进程。
守护进程是一直在服务器上运行的。我们可以使用kill指令杀死守护进程