超出能力之外的事,
如果永远不去做,
那你就永远无法进步。
--- 乌龟大师 《功夫熊猫》---
进程间关系与守护进程
- [1 进程组](#1 进程组)
- [2 会话](#2 会话)
- [3 控制终端](#3 控制终端)
- [4 作业控制](#4 作业控制)
- [5 守护进程](#5 守护进程)
1 进程组
之前我们提到了进程的概念, 其实每一个进程除了有一个进程 ID(PID)之外 ,还属于一个进程组。 进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。 每一个进程组也有一个唯一的进程组 ID(PGID), 并且这个 PGID 类似于进程 ID, 同样是一个正整数, 可以存放在 pid_t 数据类型中!
我们现在启动一些程序sleep 1000 | sleep 2000 | sleep 3000
,可以来看看:
这三个sleep
进程就属于同一个进程组,进程组PGID是34379!他们的PPID是bash,都是34345!
同样的通过fork创建的父子进程也是属于同一个进程组:
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int n = fork();
if(n == 0)
{
while(true)
{
std::cout << "I am Child process ,PID: "<< getpid() <<std::endl;
sleep(1);
}
}
std::cout << "I am Parent process ,PID: "<< getpid() <<std::endl;
sleep(100);
return 0;
}
每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID。 我们可以通过 ps 命令看到组长进程!
- 进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
- 进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。 注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。
2 会话
提到了会话,我们先来谈一谈我们平时是怎么通过Xshell进行登录的。每当我们通过Xshell客户端正确的登录到Linux系统后,系统会给我们创建一个终端文件,并且配套一个bash进程(进程组的形式)!我们写的命令写入到终端文件,然后通过bash进程执行在返回结果。
我们来看看系统是不是会在我们登录时创建一个终端文件,并且配套一个bash进程
可以看到我们每次打开一个新的的会话,就会产生一个对应的bash文件!终端文件也是如此:
刚刚我们谈到了进程组的概念, 那么会话又是什么呢? 会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。 每一个会话也有一个会话 ID(SID),一般是会话的第一个进程操PID。
通常我们都是使用管道将几个进程编成一个进程组
可以调用 setseid 函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。
cppinclude <unistd.h> //功能: 创建会话 //返回值: 创建成功返回 SID, 失败返回-1 pid_t setsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。 此时,新会话中只有唯一的一个进程
- 调用进程会变成进程组组长。 新进程组 ID 就是当前调用进程 ID
- 该进程没有控制终端。 如果在调用setsid 之前该进程存在控制终端, 则调用之后会切断联系
上边我们提到了会话 ID, 那么会话 ID 是什么呢? 我们可以先说一下会话首进程, 会话首进程是具有唯一进程 ID 的单个进程, 那么我们可以将会话首进程的进程 ID 当做是会话 ID。
注意: 会话 ID 在有些地方也被称为 会话首进程的进程组 ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的。
3 控制终端
先说一下什么是控制终端?
在 UNIX 系统中, 用户通过终端登录系统后得到一个 Shell 进程, 这个终端成为 Shell进程的控制终端。 控制终端是保存在 PCB 中的信息, 我们知道 fork 进程会复制 PCB中的信息, 因此由 Shell 进程启动的其它进程的控制终端也是这个终端。 默认情况下没有重定向, 每个进程的标准输入、 标准输出和标准错误都指向控制终端, 进程从标准输入读也就是读用户的键盘输入, 进程往标准输出或标准错误输出写也就是输出到显示器上。 另外会话、 进程组以及控制终端还有一些其他的关系。
我们在下边详细介绍一下:
- 一个会话可以有一个控制终端, 通常会话首进程打开一个终端(终端设备或伪终端设备) 后, 该终端就成为该会话的控制终端。
- 建立与控制终端连接的会话首进程被称为控制进程。
- 一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。
- 如果一个会话有一个控制终端, 则它有一个前台进程组, 会话中的其他进程组则为后台进程组。
- 无论何时进入终端的中断键(ctrl+c) 或退出键(ctrl+\) , 就会将中断信号发送给前台进程组的所有进程。
- 如果终端接口检测到调制解调器(或网络) 已经断开, 则将挂断信号发送给控制进程(会话首进程) 。
通常我们执行程序,都是在前台进行运行的。当我们在启动程序后加入&
就会在后台运行程序。
在同一个会话中可以运行同时存在多个进程组,但是,任何时刻,只允许一个前台进程组,可以运行多个后台进程组!需要注意的是只有前台进程组可以获取到标准输入!后台不能获取标准输入!
4 作业控制
作业在Linux环境中,是指为完成用户指定任务而启动的一组进程。一个作业可能仅包含单一进程,也可能由多个相互协作的进程构成,这些进程通常通过管道机制进行通信。
在Shell的管理下,控制单元并非单个进程,而是作业或进程组。前台作业可能由多个进程联合执行,同样,后台作业也可以由一系列进程共同构成。Shell能够同时管理一个前台作业和多个后台作业,这种能力我们称之为作业控制。通过这种方式,用户可以在不中断前台操作的前提下,有效地调度和监控后台任务。
我们来看:[作业号] 进程组长pid
我们来看看详细的信息,可以看到正在Runing。
- 我们可以通过
fg 作业号
将后台作业移动到前台
- 放到后台,首先需要将前台作业暂停,又因为Linux系统不允许前台有暂停的作业,系统就会把其移动到后台。所以我们通过
ctrl + z
暂停进程就将其放回到后台了,然后再通过bg 作业号
启动就可以了!
来看一下作业的状态:
状态名称 | 描述 |
---|---|
运行中 Running | 作业正在执行。 |
暂停 Suspended | 作业被挂起,等待继续执行。 |
停止 Stopped | 作业已经结束执行。 |
后台运行 Background | 作业在后台执行,不占用命令行界面。 |
前台运行 Foreground | 作业在前台执行,用户必须等待其完成后才能进行其他操作。 |
已完成 Completed | 作业成功执行完毕。 |
已终止 Terminated | 作业因错误或其他原因被强制终止。 |
等待中 Waiting | 作业正在等待系统资源或其他作业的完成。 |
在Linux中,作业状态的产生如下:
- 运行中 (Running):作业启动后立即执行。如果作业是前台作业,它将直接占用命令行界面。如果作业是后台作业,它将在后台运行,不占用命令行界面。
- 暂停 (Suspended) :通过
Ctrl+Z
暂停前台作业。暂停的作业可以通过bg命令将其放入后台,或者通过fg命令将其恢复到前台运行。 - 停止 (Stopped):作业自然完成或因错误结束。
- 后台运行 (Background) :命令后加
&
或使用bg
命令。 - 前台运行 (Foreground) :默认启动方式或使用
fg
命令。 - 已完成 (Completed):作业成功执行完毕。在这个状态下,作业已经结束,不再运行。
- 已终止 (Terminated):作业由于接收到终止信号(如SIGTERM或SIGKILL)而被强制结束。
- 等待中 (Waiting):作业等待资源或事件。
5 守护进程
守护进程,又称为Daemon:守护进程是一种在操作系统后台运行的进程,它通常在系统启动时开始运行,并在系统关闭时终止。它独立于任何控制终端,不会因为用户登录或注销而受到影响。守护进程通常用于执行系统级别的任务,如网络服务、系统监控、日志记录等,它们默默地工作,不需要用户直接交互,确保了系统服务的持续性和稳定性。
那么守护进程是怎么实现的?
- 首先,我们通过Xshell连接终端时,会产生新的会话,我们创建所有进程组也一定属于这个会话!进程组无论是前台还是后台,都是属于同一个会话!
- 然后,只有是一个会话内的进程组,就会收到用户登录或注销而受到影响。而守护进程想要不受影响就要单独创建一个会话!
- 形成独立的会话之后,这个会话里只有这一个进程组,那么其他用户的登录和注销就不会影响了!
对于守护进程,每一个程序员实现守护进程的方法可能都不太一样,这里只展示最基础的实现方法:
cpp
const char *root = "/";
const char *dev_null = "/dev/null";//这类似一个黑洞 不会储存信息!
void Daemon(bool ischdir, bool isclose)
{
// 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);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
这样就设置好了守护进程!!!