个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创进程间关系与守护进程
收录于专栏[Linux学习]
本专栏旨在分享学习Linux的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
1. 进程组
什么是进程组
组长进程
2. 会话
什么是会话
如何创建会话
会话 ID(SID)
3. 控制终端
4. 作业控制
什么是作业和作业控制?
作业号
作业状态
作业的挂起与切回
作业挂起
作业切回
查看后台执行或挂起的作业
作业控制相关的信号
5. 守护进程
1. 进程组
什么是进程组
之前我们提到了进程的概念,其实每一个进程除了有一个进程 ID(PID)之外,还属于一个进程组。进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。每一个进程组也是有唯一的进程组 ID(PGID),并且这个 PGID 类似于进程 ID,同样是一个正整数,可以存放在 pid_t 数据类型中。
-e:选项表示 every 的意思,表示输出每一个进程信息
-o:选项以逗号操作符(,)作为定界符,可以指定要输出的列
组长进程
每一个进程组都有一个组长进程。组长进程的 ID 等于其进程 ID。我们可以通过 ps 命令看到组长进程的现象:
从结果上看 ps 进程的 PID 和 PGID 相同,那就是说明 ps 进程是该进程组的组长进程,该进程包括 ps 和 cat 两个进程。
进程组组长的作用:进程组长可以创建一个进程组或者创建该组中的进程
进程组的声明周期:从进程组创建开始到其中最有一个进程离开为止。
注意:某个进程中有一个进程存在,则该进程组就存在,这与其组长进程是否已经终止无关。
2. 会话
什么是会话
刚刚我们谈到了进程组的概念,那么会话又是什么呢?会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程。每一个会话也有一个会话 ID(SID)
通常我们都是使用 管道 将几个进程编程一个进程组。如上如所示进程组2和进程组3可能是由下列命令形成的:
proc2 | proc3 &
proc4 | proc5 | proc6 &
我们举一个例子观察一下这个现象:
a 选项表示不仅当前用户的进程,也列出所有其他用户的进程
x 选项表示不仅列出有控制终端的进程,也列出所有无控制终端的进程
j 选项表示列出与作业控制相关的信息,作业控制后续再说
grep 的 -v 选项表示反向过滤,即不过滤带有 grep 字段相关的进程
从上述结果来看 3 个进程对应的 PGID 相同,即属于同一个进程组。
如何创建会话
可以调用 setseid 函数创建一个会话,前提是调用进程不能是一个进程的组长。
#include <unistd.h>
功能:创建会话
返回值:创建成功返回 SID,失败返回-1
pid_t setsid(void);
该接口调用之后会发生:
1. 调用进程会变成新会话的会话首进程。此时,新会话只有唯一的一个进程
2. 调用进程会变成进程组组长。新进程组 ID 就是当前调用进程 ID
3. 该进程没有控制终端。如果在调用 setsid 之前该进程存在控制终端,则调用之后会切断联系
需要注意的是:这个接口如果调用进程是进程组组长,则会报错,为了避免这种情况,我们通常的使用方法是先调用 fork 创建子进程,父进程终止,子进程继续执行,因为子进程会继承父进程的进程组 ID,而进程 ID 则是新分配的,就不会出现错误的情况。
会话 ID(SID)
上边我们提到了会话 ID,那么会话 ID 是什么呢?我们可以先说一下会话首进程,会话首进程是具有唯一进程 ID 的单个进程,那么我们可以将会话首进程的 ID 当做是会话 ID。注意:会话 ID 在有些地方也被称为 会话首进程的进程组 ID,因为会话首进程总是一个进程组的组长进程,所以两者是等价的。
3. 控制终端
先说一下什么是控制终端?
在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端称为 Shell 进程的控制终端。控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB 中的信息,因此 Shell 进程启动的其他进程的控制终端也是这个终端。 默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍:
一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
建立与控制终端连接的会话首进程被称为控制进程。
一个会话中的几个进程组可以分成一个前台进程组以及一个或者多个后台进程组。
如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组的所有进程。
如果终端接口检测到调制解调器(或网络)已经断开,则将挂断的信号发送给这些特性的关系如下图所示:
4. 作业控制
什么是作业和作业控制?
作业是针对用户来讲,用户完成某项任务而启动的进程,一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务,通常是一个进程管道。
Shell 分前后台控制的不是进程而是作业 或者进程组。一个前台作业可以由多个进程组成。一个后台作业也可以由多个进程组成,Shell 可以同时运行一个前台作业和任意多个后台作业,这称为作业控制
例如下列命令就是一个作业,它包括两个命令,在执行时 Shell 将在前台启动由两个进程组成的作业:
cat /etc/filesystems | head -n 5
作业号
放在后台执行的程序或命令称为后台命令,可以在命令的后面加上 & 符号从而让 Shell 识别这是一个后台命令,后台命令不用等待该命令执行完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号以及一个进程号(PID)。
例如 下面的命令在后台启动了一个作业,该作业由两个进程组成,两个进程都在后台运行:
cat /etc/filesystems | grep ext &
执行结果如下:
[1] 2202
ext4
ext3
ext2
# 按下回车
[1]+ 完成 cat /etc/filesystems | grep --
color=auto ext
第一行表示作业和进程 ID,可以看到作业号是 1,进程 ID 是2022
第 3-4 行表示该程序运行的结果,过滤 /etc/filesystems 有关 ext 的内容
第 6 行分别表示作业号、默认作业、作业状态以及所执行的命令
关于默认作业:对于一个用户来说,只能有一个默认作业(+),同时也只能有一个即将成为默认作业的作业(-),当默认作业退出后,该作业会成为默认作业。
+:表示该作业号是默认作业
-:表示该作业即将成为默认作业
无符号:表示其他作业
作业状态
常见的作业状态如下表所示:
作业的挂起与切回
作业挂起
我们在执行某个作业时,可以通过 ctrl + z 键将该作业挂起,然后 Shell 会显示相关的作业号、状态以及所执行的命令信息。
例如我们运行一个死循环的程序、通过 ctrl + Z 将该作业挂起,观察一个对应的作业状态:
#include <stdio.h>
int main()
{
while(1)
{
printf("hello\n");
}
return 0;
}
可以发现通过 ctrl + z 将作业挂起,该作业状态已经变为了停止状态
作业切回
如果想将挂起的作业切回,可以通过 fg 命令,fg 后面可以跟作业号或作业的命令名称。如果参数缺省则会默认将作业号为 1 的作业切到前台来执行,若当前系统只有一个作业在后台进行,则可以直接使用 fg 命令不带参数直接切回。具体的参数参考如下:
参数 | 含义 |
---|---|
%n | n为正整数,表示作业号 |
%string | 以字符串开头的命令所对应的作业 |
%?string | 包含字符串的命令所对应的作业 |
%+或%% | 最近提交的一个作业 |
%- | 倒数第二个提交的作业 |
例如我们把刚刚挂起来的 ./test 作业切回到前台:
运行结果为开始无限循环打印 hello,可以发现该作业已经切换到前台了。
注意:当通过 fg 命令切回作业时,若没有指定作业参数,此时会将默认作业切到前台执行,即带有 “*” 的作业号的作业
查看后台执行或挂起的作业
我们可以直接通过输入 jobs 命令查看本用户当前后台执行或挂起的作业
参数 -1 则显示作业的详细信息
参数 -p 则只显示作业的 PID
例如,我们先在后台及前台运行两个作业,并将前台作业挂起,来用 jobs 命令查看作业相关的信息:
作业控制相关的信号
上面我们提到了键入 ctrl + z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送至前台进程组作业中的所有进程,后台进程组中的作业不受影响。在 unix 系统中,存在 3 个特殊字符可以使得终端驱动程序产生信号,并将信号发送至前台进程组作业,它们分别是 :
crtl + C :中断字符,会产生 SIGINT 信号
ctrl + \ :退出字符,会产生 SIGQUIT 信号
ctrl + z :挂起字符,会产生 STGSTP 信号
终端的 I/O (即标准输入和标准输出)和终端产生的信号总是从前台进程组作业连接打破实际终端。
5. 守护进程
#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";
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);
}
}
}
这个 Daemon 函数的目的是将一个程序转化为守护进程。它完成了一下几个步骤:
1. 忽略一些信号,防止进程异常退出
2. 使用 fork 确保父进程退出,使子进程成为孤儿进程。
3. 使用 setsid 创建新的会话,并使进程不再与终端相关。
4. 可选地将当前工作目录更改为根目录 / ,避免占用启动目录。
5. 关闭或重定向标准输入输出,确保守护进程不再与终端交互
通过这些步骤,程序就能以守护进程的方式在后台运行,不再依赖用户终端或任何终端的交互。