【网络编程】四、守护进程实现 前后台作业 会话与进程组
文章目录
- Ⅰ. 守护进程的概念
- Ⅱ. 理解会话和作业
- 🎏 会话和进程组的特性小总结
- Ⅳ. 作业的前后台转换
- 1、fg 指令
- 2、bg 指令
- Ⅴ. 守护进程实现
- 1、常见接口
- ① 创建守护进程 -- daemon
- ② 自成会话函数 -- setsid
- ③ 获取会话ID函数 -- getsid
- 2、自主实现守护进程函数

Ⅰ. 守护进程的概念
守护进程(daemon
),也称 精灵进程,本质上是一个孤儿进程。是在计算机操作系统中运行的一种 特殊类型的后台进程。它们通常在系统启动时启动,并在系统关闭时终止。守护进程在后台运行,不与用户交互,通常没有控制终端。
守护进程的主要目的是执行特定的任务或提供特定的服务,而不需要用户的干预。它们可以是网络服务、系统监控程序、定时任务等。守护进程通常以超级用户(root
)权限运行,以便执行需要特权的操作。
守护进程通常会在系统启动时由启动脚本或系统配置文件启动。它们会在后台运行,并通过日志文件记录其活动。守护进程还可以通过进程间通信(
IPC
)机制与其他进程进行通信。 守护进程的一个重要特点是它们能够在系统崩溃或重启后自动重新启动,以确保服务的连续性。它们通常会监视系统状态,并在必要时采取措施来恢复服务。
我们可以通过 ps ajx | grep sshd
指令来查看当前系统中的守护进程信息:
[liren@VM-8-7-centos tcp]$ ps ajx | head -1 && ps ajx | grep sshdPPID PID PGID SID TTY TPGID STAT UID TIME COMMAND1 3461 3461 3461 ? -1 Ss 0 1:09 /usr/sbin/sshd -D
12517 5486 5485 12517 pts/6 5485 S+ 1001 0:00 grep --color=auto sshd3461 11352 11352 11352 ? -1 Ss 0 0:00 sshd: liren [priv]
11352 11368 11352 11352 ? -1 S 1001 0:00 sshd: liren@pts/43461 11510 11510 11510 ? -1 Ss 0 0:00 sshd: liren [priv]
11510 11513 11510 11510 ? -1 S 1001 0:00 sshd: liren@pts/53461 12513 12513 12513 ? -1 Ss 0 0:00 sshd: liren [priv]
12513 12516 12513 12513 ? -1 S 1001 0:01 sshd: liren@pts/6
……
其实这些守护进程和打印信息中的 SID
也是有关系,下面我们就来研究一下!
Ⅱ. 理解会话和作业
会话(session
)是一个或多个进程组的集合。比如下图:
💥注意:这里的会话和
http
中常讲到的会话是不同的概念,不要搞混了!
一个会话只有 1
个前台进程组和 n
个后台进程组,作业之间可以前后台转换。这样的任务可能会收到用户登录和注销而被清理。一般来说当我们启动云服务器之后,就会创建一个会话,会话中创建了一个前台进程组,这个前台进程组就是由 bash
为进程组组长控制的,配合其它后台进程组一起工作运转!
就继续拿上面的例子来说,进程组和会话如何看,如下图所示:
一般我们可以 用 shell
提供的管道将几个进程编成一组,如下所示:
上图在操作的时候 使用
&
符号将想要执行的命令放到后台执行,而不会阻塞终端或当前会话。下面是对**作业号后面的符号**的解释:
-
符号:表示该作业在 后台运行。当在终端中运行一个命令时,可以使用Ctrl + Z
将其暂停,并将其放入后台运行。此时,操作系统会返回一个作业号,并在作业号后面添加-
符号。+
符号:表示该作业在 前台运行。当有多个作业在后台运行时,可以使用fg
命令将指定作业调至前台运行。在作业号后面添加+
符号,表示将该作业调至前台运行。作业状态 通常用以下符号表示:
Running
(运行中):作业正在后台运行。Stopped
(已停止):作业已被暂停或停止。Done
或Exited
(已完成):作业已经完成。Terminated
(已终止):作业被终止。
下面我们看看这几个进程的 ID
情况:
我们通过 PGID
,也就是进程组标识,就能看出各自是属于哪个进程组的;而通过 SID
,也就是会话标识,就能看出是属于哪个会话的!
前两个 sleep
命令,它们 构成一个进程组,共同来完成一个作业,作业号在上上副图中,是作业二;而后三个 sleep
命令它们构成一个进程组,共同完成一个作业,它们的作业号是三号!
这些灵感其实都来源于生活,就像在工地,一个包工头将多名员工分为多个组,每个组完成不同的任务!
而 这些作业的包工头就是 bash
进程,我们可以检验一下:
此时这个关系我们可以再画一下:
这就是为什么平时我们连接服务器,打开就是一个命令行输入,其实就是默认访问会话 1
中前台进程组的 bash
进程!
🎏 会话和进程组的特性小总结
1、一个会话可以有一个控制终端(controlling terminal
),这通常是终端设备(在终端登录情况下)或者伪终端设备(在网络登录情况下)。
2、建立和控制终端连接的会话首进程被称为控制进程(controlling process
)。
3、一个会话中的几个进程组可被分为一个前台进程组和多个后台进程组。
4、如果一个会话有一个控制终端,则它有一个前台进程组,其它进程组为后台进程组。
5、无论何时键入终端的中断键(如 ctrl + c
)或者退出键(比如 ctrl + \
),都会将中断信号发送至前台进程组的所有进程。
6、如果终端接口检测到调制解调器(或网络)已经断开连接,则将挂断信号发送至控制进程,即会话首进程。
Ⅳ. 作业的前后台转换
1、fg 指令
fg
是一个在命令行中使用的 内置命令(不需要通过外部可执行文件来执行,而是由命令解释器直接处理。),用于 将一个后台运行的作业调至前台运行。
当在终端中运行多个命令时,可以 使用 &
符号将它们放入后台运行,这些后台运行的命令就成为了作业。然后再使用 fg
命令可以将指定的作业调至前台运行,使其成为当前终端会话的活动作业。
以下是
fg
命令的一些常见用法:
fg
:将最近的一个后台作业调至前台运行。fg n
:将作业号为n
的后台作业调至前台运行,其中n
是作业的编号。
调至前台运行的作业将重新获得终端的控制权,并在终端中显示其输出。在作业运行期间,终端会被该作业占用,直到作业完成或被暂停。
需要注意的是,只有在当前终端会话中的作业才能被调至前台运行。如果有多个后台作业,可以使用 jobs -l
命令查看作业列表,并确定要调至前台运行的作业的作业号。
2、bg 指令
bg
是一个在命令行中使用的 内置指令,用于 将一个已停止的作业(stopped job
)调至后台继续运行。
当在终端中运行多个命令时,可以使用 &
符号将它们放入后台运行,这些后台运行的命令就成为了作业。有时,一个作业可能会被暂停或停止,例如通过按下 Ctrl + Z
键来 暂停一个正在前台运行的作业。
然后使用 bg
命令可以将一个已停止的作业调至后台继续运行,该作业将恢复运行,并在后台继续执行。
以下是
bg
命令的一些常见用法:
bg
:将最近的一个已停止的作业调至后台继续运行。bg n
:将作业号为n
的已停止的作业调至后台继续运行,其中n
是作业的编号。
调至后台运行的作业将继续在后台执行,不会占用终端的控制权。作业的输出通常不会直接显示在终端上,但可以通过重定向输出或使用其他工具来监视作业的输出。
需要注意的是,只有在当前终端会话中的已停止的作业才能被调至后台继续运行。如果有多个已停止的作业,可以使用 jobs
命令查看作业列表,并确定要调至后台运行的作业的作业号。
下面我们来演示一下这两个指令的使用:
Ⅴ. 守护进程实现
通过上面的学习我们也知道,我们默认创建的作业和后台进程,都 bash
这个包工头指挥着,如果说我们退出了服务器,有可能因为 bash
等进程也退出了,此时我们的后台进程,就全被干掉了,这不符合我们的预期呀!
所以如果想让我们写的服务端不受用户登录注销的影响,就必须 让服务端自成会话,自成进程组,使其与终端设备的状态无关,可以一直运行的进程。这样子相当于我们就让我们想要后台执行的进程,自己当包工头!
1、常见接口
① 创建守护进程 – daemon
这是 linux
中自带的一个生成守护进程的函数!
#include <unistd.h>
int daemon(int nochdir, int noclose);
- 用途:该函数的意义在于将进程转变为守护进程,守护进程是在后台运行的进程,通常不与控制台交互,而是在后台执行某些任务,如网络服务器等。通过调用该函数,可以实现以下功能:
- 将当前进程的父进程置为
init
进程(进程id
为1
),从而脱离原有的进程组和会话。 - 将当前进程的工作目录切换到根目录下,以避免守护进程因为当前工作目录被卸载等原因导致崩溃。
- 关闭标准输入、输出和错误输出,以避免守护进程输出信息到控制台,从而影响用户体验。
- 将当前进程的父进程置为
- 参数:
nochdir
:是否改变当前工作目录。如果为0
,则将当前工作目录切换到根目录下,否则保持不变。noclose
:是否关闭标准输入、标准输出、标准错误的文件描述符。如果为0
,则不关闭,否则关闭。
- 返回值:
- 调用成功返回
0
- 调用失败返回
-1
,并设置errno
变量。
- 调用成功返回
一般来说,我们是不会选择这个函数来创建守护进程的,因为局限性就摆在这,而守护进程可能因为需求不同,是需要一直变化限制的,所以我们会用下面其它几个函数来自己写一个 daemon
函数!
② 自成会话函数 – setsid
这是一个在 Unix
和类 Unix
系统(如 Linux
)中使用的系统调用,用于创建一个新的会话,并使调用进程成为该新会话的领导者。这个函数通常在创建守护进程时使用。
#include <unistd.h>
pid_t setsid(void);
- 作用:
- 使调用进程成为一个新的会话领导者,并且不会受到终端的控制。这对于守护进程和后台进程非常有用,因为它们需要在后台运行,并且不希望受到终端的影响。调用
setsid
后,新进程组PGID
等于调用进程的PID
,该进程成为新会话的领导进程,并且不再有控制终端。 - 进程组的组长不能调用
setsid()
函数来创建一个新的会话。因此,通常在调用该函数之前,会先调用fork()
,然后在子进程中调用setsid()
,因为子进程虽然PGID
是一样的,但是因为子进程的PID
是新分配的,所以两者不可能相等!
- 使调用进程成为一个新的会话领导者,并且不会受到终端的控制。这对于守护进程和后台进程非常有用,因为它们需要在后台运行,并且不希望受到终端的影响。调用
- 返回值:
- 成功返回新会话的
SID
- 失败返回
-1
- 成功返回新会话的
当调用该函数时,会发生以下几件事情:
- 创建一个新的会话,调用进程成为新会话的领导者,也成为新进程组的领导者,并且新会话没有控制终端(如果有的话会被切断)。
- 调用进程成为新进程组的领导者。新进程组
PGID
等于调用进程的PID
。- 调用进程将与其父进程的会话和进程组脱离。
③ 获取会话ID函数 – getsid
#include <unistd.h>
pid_t getsid(pid_t pid);
- 作用:
- 获取指定进程的
SID
- 获取指定进程的
- 参数:
pid
:进程id
- 返回值:
- 如果
pid
参数为0
,则返回调用进程的SID
。 - 如果
pid
参数为一个有效的进程ID
,则返回该进程所属会话的SID
。 - 如果出现错误,返回
-1
,并设置errno
以指示错误。
- 如果
需要注意的是,只有会话的领导者进程才能调用 getsid()
函数来获取会话ID
。
2、自主实现守护进程函数
具体的步骤都在注释中:
#pragma once
#include <cstdlib>
#include <cstdio>
#include <cassert>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>const char* DEV = "/dev/null";void daemonSelf(const char* curPath = nullptr)
{// 1. 创建子进程并退出父进程// 此时就要创建一个子进程,然后将当前进程给退出,让子进程去调用即可pid_t pid = fork();if(pid > 0) {exit(0); // 退出当前父进程}else if(pid < 0){perror("fork error!");exit(1);}// 2. 子进程调用setsid()创建一个自成会话,调用setsid()的进程不能是组长,组长是之前的父进程!if(setsid() < 0) {perror("setsid error");exit(1);}// 3. 让调用进程忽略掉容易异常的信号:// 忽略SIGHUP信号的目的是为了防止守护进程在终端断开时终止// 忽略SIGPIPE信号是防止当进程向一个已经关闭写端的管道写入数据时,内核会向进程发送SIGPIPE信号,或者当进程向一个已经关闭的socket连接写入数据时,内核也会向进程发送SIGPIPE信号。signal(SIGHUP, SIG_IGN);signal(SIGPIPE, SIG_IGN);signal(SIGINT, SIG_IGN);// 4. 创建孙子进程后再次fork,以防止守护进程重新打开控制终端pid = fork();if(pid > 0) {exit(0); // 退出当前父进程}else if(pid < 0){perror("fork error!");exit(1);}// 5. 守护进程需要脱离终端,需要关闭或者重定向以前进程默认打开的文件// 很显然关闭那些文件是不太好的做法,所以优先选择重定向// 而我们可以重定向到 ‘/dev/null’ 这个文件中,相当于linux中的一个垃圾桶// 如果创建这个重定向文件失败了,我们才采用关闭文件的方式int fd = open(DEV, O_RDWR); if(fd != -1){// 打开文件成功,则重定向到/dev/null中dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}else{// 失败则直接关闭close(0);close(1);close(2);}// 6. 可选项:根据需求将进程执行路径更改if(curPath != nullptr)chdir(curPath);
}