1. 进程组
1.1 进程组概念
- 每一个进程除了有一个进程ID(PID)之外,还属于一个进程组
- 进程组是一个或者多个进程的集合,一个进程组可以包含多个进程
- 每一个进程组也有一个唯一的进程组 ID(PGID),并且这个PGID类似于进程ID,同样是一个正整数,可以存放在pid_t数据类型中
bash
$ ps -eo pid,pgid,ppid,comm | grep test
#结果如下
PID PGID PPID COMMAND
2830 2830 2259 test
# -e 选项表⽰every的意思, 表⽰输出每⼀个进程信息
# -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列
1.2 组长进程
每一个进程组都有一个组长进程。组长进程的ID等于其进程ID
bash
[node@localhost code]$ ps -o pid,pgid,ppid,comm | cat
# 输出结果
PID PGID PPID COMMAND
2806 2806 2805 bash
2880 2880 2806 ps
2881 2880 2806 cat
- 从结果上看ps进程的PID和PGID相同,说明ps进程是该进程组的组长进程,该进程组包括ps和cat两个进程
- 进程组组长的作用:进程组组长可以创建一个进程组或者创建该组中的进程
- 进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止
- 注意:只要某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否已经终止无关
2. 会话
2.1 会话概念
- 会话(Session) 是一个或多个进程组的集合;
- 一个会话可容纳多个进程组;
- 会话拥有唯一标识:会话 ID(SID)

一条管道命令,自动形成一个进程组。管道里的所有进程,都属于同一个进程组
末尾加 & → 整个进程组丢到后台运行
bash
proc2 | proc3 &
proc2 和 proc3 用管道连在一起放进同一个进程组,这个进程组成为后台进程组
bash
ps axj | head -n1
ps axj:列出所有进程的详细信息(含进程组、会话)
a → 显示所有用户的进程
x → 显示无控制终端的进程
j → 显示作业控制 / 进程组 / 会话信息
|:管道,把左边的输出传给右边
head -n1:只取第一行
2.2 如何创建会话
可以调用setseid函数来创建一个会话,前提是调用进程不能是一个进程组的组长
cpp
#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回SID, 失败返回-1
*/
pid_t setsid(void);
调用setsid()的进程,会一口气完成 3 个操作:
- 创建新会话,自己成为会话首进程: 新建一个全新的会话, 自己成为这个会话的首进程, 此时会话里只有它一个进程
- 创建新进程组,自己成为进程组组长,新进程组 ID = 自己的 PID
- 脱离控制终端 (最重要):调用后彻底断开终端联系, 变成没有终端的后台进程
如果调用进程本身就是组长进程,setsid () 会直接失败!为什么?系统规定:组长进程不能创建新会话
标准解决方法:先 fork,再让子进程调用:子进程继承父进程的进程组 ID**,** PID 是新的,子进程一定不是组长进程
2.3 会话ID(SID)
会话 ID (SID) = 会话首进程的 PID = 会话首进程的进程组 ID (PGID)
**3.**控制终端
3.1 什么是控制终端?
用户登录用的终端 / 伪终端 ,就是 Shell 进程的控制终端
- 打开的命令行窗口、SSH 登录,都是控制终端
- 它是进程和用户交互的入口:键盘输入、屏幕输出
- 终端信息存在进程 PCB 里,子进程会继承父进程的控制终端
3.2 核心特性
默认标准输入输出都指向它
fork 子进程会继承控制终端
3.3 终端 + 会话 + 进程组 的绑定规则
- 一个会话最多有一个控制终端
- 会话首进程 = 控制进程
- 一个会话分两种进程组:前台进程组(1 个) 独占终端输入输出,**后台进程组(N 个)**不能直接读终端
- 有控制终端,就一定有前台进程组
- 终端按键信号 → 发给前台进程组所有进程:
Ctrl+C(中断)Ctrl+\(退出 - 终端断开连接 → 信号发给会话首进程
4. 作业控制
4.1 什么是作业,作业控制
- 在 Shell 里输入的一整条命令,就是一个作业
- 一个作业 = 一个进程组
- 一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成,Shell可以同时运行一个前台作业和任意多个后台作业,这称为作业控制
4.2 作业号
放在后台执行的程序或命令称为后台命令,可以在命令的后面加上 & 符号从而让Shell识别这是一个后台命令,后台命令不用等待该命令执行完成,就可立即接收新的命令,另外后台进程执行完后 会返回一个作业号以及一个进程号(PID)
默认作业:
- + :当前默认作业
- - :候补默认作业(即将成为默认作业
- 无符号:普通普通非默认作业
4.3 前后台任务管理命令
|------------|-------------------------------------------------|
| jobs | 查看当前用户终端 下,所有后台 / 暂停的任务列表,同时会显示任务的+、-标记 |
| fg 任务号 | foreground,把指定后台任务,拉回到前台运行,接管终端交互 |
| Ctrl+C | 只能终止当前正在前台运行的任务;后台任务不受这个按键影响 |
| Ctrl+Z | 暂停当前前台正在运行的进程,将它挂起放到后台暂停状态,终端会立刻恢复 bash 命令行 |
| bg 任务号 | background,让刚才被Ctrl+Z暂停的后台任务,在后台继续运行 |
5. 守护进程(Daemon)
守护进程,就是真正脱离终端、常驻系统后台运行的特殊进程,也叫精灵进程
怎么手动把普通程序变成守护进程?
- fork () 创建子进程,父进程直接退出
- setsid () 创建新会话(最关键!脱离终端)
- chdir ("/") 把工作目录改成根目录
- umask (0) 重置文件权限(可选)
- 关闭 / 重定向 0、1、2 标准文件描述符
Deamon.hpp
cpp
#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. 修改进程的工作目录为根目录
if (ischdir)
chdir(root);
// 5. 处理标准输入、输出、错误
if (isclose)
{
close(0);
close(1);
close(2);
}
else
{
// 推荐方式:重定向到 /dev/null
int fd = open(dev_null, O_RDWR);
if (fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd);
}
}
}
cpp
dup2(旧的文件描述符, 新的文件描述符);
意思:把【旧的】复制到【新的】,让新的指向旧的
dup2 (谁复制,复制给谁);
6. 如何将服务守护进程化
cpp
// ./server port
int main(int argc, char *argv[])
{
if (argc != 2)
{
std::cout << "Usage : " << argv[0] << " port" << std::endl;
return 0;
}
uint16_t localport = std::stoi(argv[1]);
// 将程序设置为守护进程
Daemon(false, false);
std::unique_ptr<TcpServer> svr(new TcpServer(localport, HandlerRequest));
svr->Loop();
return 0;
}