进程组
什么是进程组
在Linux操作系统中,进程组(Process Group)是一个相关联的进程集合,一个进程组可以包含多个进程。每个进程组都有一个唯一的进程组ID(PGID),这个 PGID 类似于进程 ID, 同样是一个正整数, 可以存放在 pid_t 数据类型中,用于实现作业控制(Job Control),即对一组相关进程进行协调和管理。
// 我们在命令行输入
sleep 10000 | sleep 20000 | sleep 30000
在另一个终端输入ps axj | head -1 && ps axj | grep sleep,查询sleep的运行状态:
我们发现这是三个进程,并且互相是兄弟关系,因为它们的PPID都是一样的,这个PPID是谁啊,就是bash!
上面的PGID字段就是进程组,仔细观察可以发现,进程组ID和第一个进程的PID是一样的,它们又有什么关系呢?请继续往下看。
组长进程
每一个进程组都有一个组长进程。 第一个进程就是组长进程,其进程组 ID 与组长进程 ID 保持一致。比如说上例中pid为1188890的进程就是组长进程,也是进程组ID。
- 进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
- 进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。
注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关。
会话
什么是会话
当我们客户端登陆Linux的时候会向Linux服务器发送请求,当Linux服务器收到请求会在系统中创建一个终端文件和一个bash进程(也是一个进程组,为我们提供命令行解析的服务),并将该文件与bash进程相关联。在系统层面上,创建终端文件和bash进程我们叫做构建了一个会话。
会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。 每一个会话也有一个会话 ID(SID)。进程组,无论是前台还是后台,都属于同一个会话。
我们再来研究上面这张图,上面说过会话也有ID(SID),从上图可以看出上面四个进程的SID都是一样的,并且与PPID相同(bash),说明它们在同一个会话中;而它们的PGID却不一样,说明同一个会话中有多个进程组!
如何创建会话
可以调用 setsid 函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。
// 功能: 创建会话
// 返回值: 创建成功返回 SID, 失败返回-1
#include <unistd.h>
pid_t setsid(void);
// 1. 让自己不要成为组长
if (fork() > 0)
exit(0);
// 2. 设置让自己成为一个新的会话, 后面的代码其实是子进程在走
setsid();
// 后续代码...
作业控制
补充:前台进程与后台进程
当我们在命令行执行可执行程序时,启动的进程默认是前台进程。
在前台进程的情况下,当我们在命令行执行命令,命令行不会有任何反应(因为bash进程自动变为后台进程),而且ctrl + c可以终止掉该进程!终止掉之后,bash进程又自动变为前台进程,命令行就可以继续正常使用了。所以在前后台进程中,只有前台进程可以从标准输入中获取数据。
同一个会话中,可以运行同时存在多个进程组。但是,任何时刻,只允许一个前台进程(组),可以允许多个后台进程组。
当我们在命令行执行可执行程序时,在后面加上&,则启动的进程是后台进程。
什么是作业(job)和作业控制(Job Control)?
作业是针对用户来讲, 用户完成某项任务而启动的进程, 一个作业既可以只包含一个进程, 也可以包含多个进程, 进程之间互相协作完成任务, 通常是一个进程管道。
Shell 分 前后台 来控制的不是进程而是作业 或者进程组。 一个前台作业可以由多个进程组成, 一个后台作业也可以由多个进程组成, Shell 可以同时运⾏一个前台作业和任意多个后台作业, 这称为作业控制。
例如下列命令就是一个作业, 它包括两个命令, 在执行时 Shell 将在前台启动由两个进程组成的作业。
Shell
[node@localhost code]$ sleep 100 | sleep 200
作业号
放在后台执行的程序或命令称为后台命令, 可以在命令的后面加上&符号从而让Shell 识别这是一个后台命令, 后台命令不用等待该命令执行完成, 就可立即接收新的命令, 另外后台进程执行完后会返回一个作业号以及一个进程号(PID) 。
例如下面的命令在后台启动了一个作业, 该作业由两个进程组成, 两个进程都在后台运行:
Shell
[node@localhost code]$ sleep 100 | sleep 200 &
1表示作业号, 1230531表示进程 ID
我们使用jobs命令可以查到正在运行的进程
使用 fg + 作业号 的方式把一个后台进程放到前台。
使用 CTRL + z 组合键,可以将正在运行的前台进程暂停,当前进程就变成了后台进程(Linux不允许处于暂停状态的进程是前台进程);之后使用 bg + 作业号 的方法将后台进程启动。
守护进程
守护进程(Daemon)是一种在后台运行的特殊进程,它独立于控制终端(即与控制终端不属于同一个会话),并周期性地执行某些任务或等待某些事件的发生。其本质是,将老会话中的某进程组独立出来形成新的会话,该进程组与老会话的关系由包含变为并列,此时新会话里面的进程组就叫做守护进程!守护进程是一个孤儿进程。
// Daemon.hpp模块
#pragma once
#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_RDONLY);
if(fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
::close(fd);
}
}
}
#include <iostream>
#include <string>
#include <unistd.h>
#include "Daemon.hpp"
int main()
{
Daemon(false, false);
// 下面的代码都是子进程在执行
while(true)
{
sleep(1); // 模拟任务
}
return 0;
}
此时要杀掉该进程只能使用 killall test 命令。test为可执行程序名。