文章目录
- [1. 进程组](#1. 进程组)
- [2. 会话](#2. 会话)
-
- [2.1 什么是会话](#2.1 什么是会话)
- [2.2 如何创建会话](#2.2 如何创建会话)
- [2.3 守护进程](#2.3 守护进程)
- [3. 作业控制](#3. 作业控制)

1. 进程组
我们运行下面的命令
bash
sleep 10000 | sleep 20000 | sleep 30000
然后查看进程的信息:

可以看到,其实每一个进程除了有进程PID、PPID之外,还属于一个进程组(PGID)。 进程组是一个或者多个进程的集合,一个进程组可以包含多个进程,每一个进程组也有一个唯一的进程组 ID(PGID)。
进程组的ID一般是组长的ID,即组中最先创建的进程的PID。
-
进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程
-
进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。 注意:只要某个进程组中有一个进程存在, 则该进程组就存在, 这与
其组长进程是否已经终止无关
。
2. 会话
2.1 什么是会话
系统登录时,会创建终端文件 + bash进程;会话 = bash进程 + 终端文件 (每一个新的登录,都会形成一个-bash进程)
终端文件:
/dev/pts/
向不同的终端文件中输出,就会在其会话页面中显示
- 会话其实和进程组息息相关,会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程组。
- 每一个会话也有一个会话 ID(SID,一般是会话中的第一个进程,即bash )

下面是我在同一个会话中运行了两个程序查询出来的结果

2.2 如何创建会话
那如何创建一个会话呢?
可以调用 setsid
函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。
c
#
include <unistd.h>
/*
*功能: 创建会话
*返回值: 创建成功返回 SID, 失败返回-1
*/
pid_t setsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。 此时,新会话中只有唯一的一个进程
- 调用进程会变成进程组组长。 新进程组 ID 就是当前调用进程 ID
- 该进程没有控制终端。 如果在调用 setsid 之前该进程存在控制终端,则调用之后会切断联系
需要注意的是:
- 这个接口如果调用进程原来是进程组组长,则会报错,为了避免这种情况,我们通常的使用方法是先调用 fork创建子进程,父进程终止,子进程继续执行。
- 因为子进程会继承父进程的进程组 ID,而进程 ID 则是新分配的,就不会出现错误的情况。
2.3 守护进程
守护进程(Daemon Process)是一种在后台运行的特殊进程,通常用于执行特定的任务或服务,而不需要用户直接交互。
守护进程的特点
- 后台运行:守护进程在后台运行,不会在前台显示任何界面或交互窗口。它独立于用户终端,即使用户注销,守护进程仍然可以继续运行。
- 独立于控制终端:守护进程通常与控制终端分离,这意味着它不会因终端的关闭而终止。例如,一些网络服务(如 Web服务器)会以守护进程的形式运行,确保服务的持续可用性。
那如何创建守护进程呢?
- 创建子进程并退出父进程:通过 fork() 创建子进程,父进程退出。这样可以确保子进程不会被终端挂起。
- 创建新会话:通过 setsid() 创建一个新的会话,使子进程成为会话的领导者,从而脱离控制终端。
- 改变工作目录:通常将工作目录更改为根目录(/),以避免守护进程因当前工作目录被卸载而导致问题。
- 关闭文件描述符:关闭所有打开的文件描述符,避免资源泄漏。
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);
int rid = fork(); //2.创建子进程
if(rid > 0)
{
exit(1); //父进程直接退出
}
int id = setsid(); //3.子进程调用,子进程独立成立一个会话
// 4. 每一个进程都有自己的 CWD, 是否将当前进程的 CWD 更改成为 /根目录
//以避免守护进程因当前工作目录被卸载而导致问题
if(ischdir)
chdir(root);
// 5. 已经变成守护进程啦,不需要和用户的输入输出,错误进行关联了
if(isclose)
{
close(0);
close(1);
close(2);
}
else
{
//避免后面有向0,1,2中操作的,为了防止失败
//一般将0、1、2重定向到"黑洞"(任何写入到 /dev/null 的数据都会被永久丢弃,不会保存在任何地方)
int fd = open(dev_null,O_RDWR);
if(fd > 0)
{
dup2(fd,0);
dup2(fd,1);
dup2(fd,2);
close(fd); //避免fd泄漏,重定向后关闭
}
}
}
如何将一个服务器守护进程化呢?特别简单,直接调用Daemon即可
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;
}
即使当前会话关闭了,守护进程依旧还在。它是一个独立的会话
3. 作业控制
- 什么是作业?
作业是针对用户来讲的,一个进程组要完成的任务,一般叫做作业。
一个作业既可以只包含一个进程,也可以包含多个进程,进程之间互相协作完成任务(通常是通过进程管道)。
Shell 分前后台来控制的不是进程而是作业或者进程组。 一个前台作业可以由多个进程组成,一个后台作业也可以由多个进程组成, Shell 可以同时运行一个前台作业和任意多个后台作业,这称为作业控制。
例如:下面通过管道协作的sleep命令就是一个作业
此时无论执行什么(只要会话不退出),都不会影响后台作业。
放在后台执行的程序或命令称为后台命令,可以在命令的后面加上
&
符号从而让Shell识别这是一个后台命令,后台命令不用等待该命令执⾏完成,就可立即接收新的命令,另外后台进程执行完后会返回一个作业号 以及一个进程号(PID)。
- 作业控制
将作业放回前台:fd + 作业号
如果再想将作业放回后台,要先暂停作业(Ctrl + z
),然后再bg + 作业号
,才可将作业切回到后台运行。

我们可以直接通过输入 jobs 命令查看本用户当前后台执⾏或挂起的作业
- 参数-l 则显示作业的详细信息
- 参数-p 则只显示作业的 PID

关于作业号后面的+、-号
对于一个用户来说,只能有一个默认作业(+号),同时也只能有一个即将成为默认作业的作业(-号),当默认作业退出后,该作业会成为默认作业。
- +:表示该作业号是默认作业
- -:表示该作业即将成为默认作业
- 无符号:表示其他作业
- 作业状态
常见的作业状态如下表所示:

- 作业控制相关的信号
上面我们提到了键入 Ctrl + Z 可以将前台作业挂起,实际上是将 STGTSTP 信号发送至前台进程组作业中的所有进程, 后台进程组中的作业不受影响。
在 unix系统中, 存在 3 个特殊字符可以使得终端驱动程序产生信号,并将信号发送至前台进程组作业,它们分别是:
- Ctrl + C: 中断字符, 会产生 SIGINT 信号
- Ctrl + \: 退出字符, 会产生 SIGQUIT 信号
- Ctrl + Z: 挂起字符, 会产生 STGTSTP 信号
