一、 进程组
1.1 什么是进程组?
之前我们提到了进程的概念,其实每一个进程除了有一个进程ID(PID)之外还属于一 个进程组。进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。每一 个进程组也有一个唯一的进程组ID(PGID),并且这个PGID类似于进程ID,同样是 一个正整数,可以存放在pid_t数据类型中。
进程是以进程组的形式来完成对应任务的
sleep 1000 、sleep 2000和sleep 3000是兄弟进程,它们的ppid是2166629也就是父进程bash(如上图),它们三个的PGID都是2168307表示它们属于同一个进程组
1.2 组长进程
每一个进程组都有一个组长进程。组长进程的ID等于其进程ID。我们可以通过ps命 令看到组长进程的现象
sleep 1000的PID等于PGID它就是这个进程组的组长
- 进程组组长的作用:进程组组长可以创建一个进程组或者创建该组中的进程
- 进程组的生命周期:从进程组创建开始到其中最后一个进程离开为止。注意: 主要某个进程组中有一个进程存在,则该进程组就存在,这与其组长进程是否已经终 止无关。
二、会话
2.1 什么是会话?
刚刚我们谈到了进程组的概念,那么会话又是什么呢?
会话其实和进程组息息相关, 会话可以看成是一个或多个进程组的集合,一个会话可以包含多个进程组。每一个会话也有一个会话ID(SID)
通常我们都是使用管道将几个进程编成一个进程组。如上图的进程组2和进程组3可 能是由下列命令形成的:
Shell
[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
# &表示将进程组放在后台执行
2.2如何创建会话
可以调用setseid函数来创建一个会话,前提是调用进程不能是一个进程组的组长
#include<unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回SID,失败返回-1
*/
pid_tsetsid(void);
该接口调用之后会发生:
- 调用进程会变成新会话的会话首进程。此时,新会话中只有唯一的一个进程
- 调用进程会变成进程组组长。新进程组ID就是当前调用进程ID
- 该进程没有控制终端。如果在调用setsid之前该进程存在控制终端,则调用之后会切断联系
需要注意的是:这个接口如果调用进程原来是进程组组长,则会报错,为了避免这种情况,我们通常的使用方法是先调用fork创建子进程,父进程终止,子进程继续执行,因为子进程会继承父进程的进程组ID,而进程ID则是新分配的,就不会出现错误的情况
2.3 会话ID(SID)
上边我们提到了会话ID,那么会话ID是什么呢?
我们可以先说一下会话首进程,会话首进程是具有唯一进程ID的单个进程,那么我们可以将会话首进程的进程ID当做是会话ID。
注意:会话ID在有些地方也被称为会话首进程的进程组ID,因为会话首进程总是一个进程组的组长进程,所以两者是等价的。
三、控制终端
先说一下什么是控制终端?
在UNIX系统中,用户通过终端登录系统后得到一个Shell进程,这个终端成为Shell 进程的控制终端。控制终端是保存在PCB中的信息,我们知道fork进程会复制PCB 中的信息,因此由Shell进程启动的其它进程的控制终端也是这个终端。默认情况下 没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:
- 一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。
- 建立与控制终端连接的会话首进程被称为控制进程。
- 一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。
- 如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。
- 无论何时进入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。
- 如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给 控制进程(会话首进程)
这些特性的关系如下图所示:
用户终端影响服务器
1. 进程收到挂起信号(SIGHUP)
现象:当用户关闭终端(或 SSH 断开)时,内核会给该终端控制的所有前台/后台进程发送
SIGHUP
。影响:如果服务器进程没有脱离终端,它会收到
SIGHUP
并退出,导致服务中断。例子:
在终端运行
python3 -m http.server 8080
,然后关闭 SSH,会话断开后服务就停止了。如果使用
nohup python3 -m http.server 8080 &
或让它以守护进程方式运行,它就不会受影响。
2. 输入输出绑定在终端
现象:如果服务器进程的
stdin/stdout/stderr
仍然绑定在终端设备上,终端关闭会导致 I/O 出错。影响:
程序可能因为写入失败(Broken pipe)而异常退出。
日志和错误信息混入用户终端,既不安全,也不利于集中管理。
例子:
某个服务在终端启动后不断往
stdout
打日志,当你关闭终端时,日志无法输出,进程报错。
3. 信号传播与作业控制
现象:终端还负责作业控制(job control),用户输入
Ctrl+C
(SIGINT)、Ctrl+Z
(SIGTSTP)等信号时,会发给终端关联的前台进程。影响:如果服务器进程直接运行在用户终端里,可能会被用户无意中终止或挂起。
例子:
直接在终端里运行
mysqld
,一旦误操作Ctrl+C
,数据库就被停掉了。
4. 会话生命周期依赖
现象:普通进程的生命周期与用户会话(session)绑定。用户退出登录后,进程也会被内核清理掉。
影响:导致服务器进程不能做到“长时间运行”,失去作为后台服务的意义。
例子:
在 Linux 上登录一个账户,启动服务,退出登录后,进程也随之消失。
四、守护进程(精灵进程)
为什么要设置为守护进程?
长期运行
服务器程序(如 Web 服务器、数据库、消息队列)需要 24/7 持续提供服务,不依赖某个用户是否登录。守护进程可在系统启动时自动运行,并常驻后台。不依赖用户会话
如果进程依赖用户的终端(TTY),一旦该用户注销、关闭终端,进程会收到SIGHUP
信号而退出。守护进程通过与终端分离,避免被意外终止。后台服务模式
它不需要和用户交互界面,而是通过网络端口、消息队列、管道等方式对外提供服务。
与用户控制终端的关系
在 UNIX/Linux 中,每个进程可能和某个控制终端(Controlling Terminal)关联。
普通进程:通常继承父进程的终端(比如你在 Shell 启动一个程序,它的控制终端就是这个 Shell 的终端)。
守护进程:在启动时会主动与控制终端 脱离(detach),通常通过以下步骤:
调用
fork()
,让父进程退出,子进程被init
或systemd
收养。调用
setsid()
,新建一个会话,脱离原有控制终端。将标准输入、输出、错误(stdin, stdout, stderr)重定向到
/dev/null
或日志文件。
这样守护进程就不会再受到用户终端状态的影响,也不会因为用户退出而被杀死。
代码如下:
#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "Log.hpp"
#include "Common.hpp"
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);
}
}
}
五、 如何将服务守护进程化
#include "NetCal.hpp"
#include "Protocol.hpp"
#include "TcpServer.hpp"
#include "Daemon.hpp"
#include <memory>
void Usage(std::string proc)
{
std::cerr << "Usage: " << proc << " port" << std::endl;
}
// ./tcpserver 8080
int main(int argc, char *argv[])
{
if (argc != 2)
{
Usage(argv[0]);
exit(USAGE_ERR);
}
std::cout << "服务器已经启动,已经是一个守护进程了" << std::endl;
Daemon(0, 0);
// daemon(1, 1);
// Enable_Console_Log_Strategy();
Enable_File_Log_Strategy();
// 1. 顶层
std::unique_ptr<Cal> cal = std::make_unique<Cal>();
// 2. 协议层
std::unique_ptr<Protocol> protocol = std::make_unique<Protocol>([&cal](Request &req)->Response{
return cal->Execute(req);
});
// 3. 服务器层
std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(std::stoi(argv[1]),
[&protocol](std::shared_ptr<Socket> &sock, InetAddr &client){
protocol->GetRequest(sock, client);
});
tsvr->Start();
// sleep(5);
return 0;
}