进程组
什么是进程组
之前我们提到了进程的概念, 其实每一个进程除了有一个进程 ID(PID)之外 还属于一个进程组。进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。 每一个进程组也有一个唯一的进程组 ID(PGID), 并且这个 PGID 类似于进程 ID, 同样是一个正整数, 可以存放在 pid_t 数据类型中。
C++
$ ps -eo pid,pgid,ppid,comm | grep test
#结果如下
PID PGID PPID COMMAND
2830 2830 2259 test
# -e 选项表示 every 的意思, 表示输出每一个进程信息
# -o 选项以逗号操作符(,)作为定界符, 可以指定要输出的列
组长进程
每一个进程组都有一个组长进程。 组长进程的 ID 等于其进程 ID。我们可以通过 ps 命
令看到组长进程的现象:
Shell
[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 两个进程。进程组组长的作用: 进程组组长可以创建一个进程组或者创建该组中的进程进程组的生命周期: 从进程组创建开始到其中最后一个进程离开为止。注意:主要某个进程组中有一个进程存在, 则该进程组就存在, 这与其组长进程是否已经终止无关
会话
什么是会话
刚刚我们谈到了进程组的概念, 那么会话又是什么呢? 会话其实和进程组息息相关,
会话可以看成是一个或多个进程组的集合, 一个会话可以包含多个进程组。每一个会
话也有一个会话 ID(SID)
通常我们都是使用管道将几个进程编成一个进程组。 如上图的进程组 2 和进程组 3 可
能是由下列命令形成的:
Shell
[node@localhost code]$ proc2 | proc3 &
[node@localhost code]$ proc4 | proc5 | proc6 &
# &表示将进程组放在后台执行
我们举一个例子观察一下这个现象:
# 用管道和 sleep 组成一个进程组放在后台运行
[node@localhost code]$ sleep 100 | sleep 200 | sleep 300 &
# 查看 ps 命令打出来的列描述信息
[node@localhost code]$ ps axj | head -n1
# 过滤 sleep 相关的进程信息
[node@localhost code]$ ps axj | grep sleep | grep -v grep
# a 选项表示不仅列当前⽤户的进程,也列出所有其他⽤户的进程
# x 选项表示不仅列有控制终端的进程,也列出所有⽆控制终端的进程
# j 选项表示列出与作业控制相关的信息, 作业控制后续会讲
# grep 的-v 选项表示反向过滤, 即不过滤带有 grep 字段相关的进程
从结果来看 3 个进程对应的 PGID 相同, 即属于同一个进程组。
如何创建会话
可以调用 setseid 函数来创建一个会话, 前提是调用进程不能是一个进程组的组长。
#include <unistd.h>
/*
*功能:创建会话
*返回值:创建成功返回 SID, 失败返回-1
*/
pid_t setsid(void);
该接口调用之后会发生:调用进程会变成新会话的会话首进程。此时, 新会话中只有唯一的一个进程,调用进程会变成进程组组长。 新进程组 ID 就是当前调用进程 ID,该进程没有控制终端。 如果在调用 setsid 之前该进程存在控制终端, 则调用之后会切断联系
需要注意的是: 这个接口如果调用进程原来是进程组组长, 则会报错, 为了避免这种情况, 我们通常的使用方法是先调用 fork 创建子进程, 父进程终止, 子进程继续执行, 因为子进程会继承父进程的进程组 ID, 而进程 ID 则是新分配的, 就不会出现错误的情况。
会话 ID(SID)
上边我们提到了会话 ID, 那么会话 ID 是什么呢? 我们可以先说一下会话首进程, 会话首进程是具有唯一进程 ID 的单个进程, 那么我们可以将会话首进程的进程 ID 当做是会话 ID。注意:会话 ID 在有些地方也被称为 会话首进程的进程组 ID, 因为会话首进程总是一个进程组的组长进程, 所以两者是等价的。
控制终端
先说一下什么是控制终端?
在 UNIX 系统中,用户通过终端登录系统后得到一个 Shell 进程,这个终端成为 Shell进程的控制终端。控制终端是保存在 PCB 中的信息,我们知道 fork 进程会复制 PCB中的信息,因此由 Shell 进程启动的其它进程的控制终端也是这个终端。默认情况下没有重定向,每个进程的标准输入、标准输出和标准错误都指向控制终端,进程从标准输入读也就是读用户的键盘输入,进程往标准输出或标准错误输出写也就是输出到显示器上。另外会话、进程组以及控制终端还有一些其他的关系,我们在下边详细介绍一下:
一个会话可以有一个控制终端,通常会话首进程打开一个终端(终端设备或伪终端设备)后,该终端就成为该会话的控制终端。建立与控制终端连接的会话首进程被称为控制进程。一个会话中的几个进程组可被分成一个前台进程组以及一个或者多个后台进程组。如果一个会话有一个控制终端,则它有一个前台进程组,会话中的其他进程组则为后台进程组。无论何时进入终端的中断键(ctrl+c)或退出键(ctrl+\),就会将中断信号发送给前台进程组的所有进程。如果终端接口检测到调制解调器(或网络)已经断开,则将挂断信号发送给控制进程(会话首进程)。
这些特性的关系如下图所示:
守护进程
上篇博客写了TCP,将服务器守护进程化
其余内容不变,只修改main.cc和引入封装Daemon.hpp
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_RDWR);if (fd > 0){dup2(fd, 0);dup2(fd, 1);dup2(fd, 2);close(fd);}}
}
main.cc
#include"TcpServer.hpp"
#include <memory>
#include "Protocol.hpp"
#include"NetCal.hpp"
#include "Daemon.hpp"
void Usage(std::string proc)
{std::cerr<<"Usage: "<<proc<<"prot"<<std::endl;
}// ./tcpserver
int main(int argc,char* argv[])
{if(argc !=2){Usage(argv[0]);exit(USAGE_ERR);}Daemon(false, false);//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->GetReqquest(sock,client);});tsvr->Start();return 0;
}
此时将服务器放在后台进程里键盘输入的任何消息只会发送给前台进程接收
也可以调用系统调用
deamon
#include"TcpServer.hpp"
#include <memory>
#include "Protocol.hpp"
#include"NetCal.hpp"
#include "Daemon.hpp"#include <unistd.h>
void Usage(std::string proc)
{std::cerr<<"Usage: "<<proc<<"prot"<<std::endl;
}// ./tcpserver
int main(int argc,char* argv[])
{if(argc !=2){Usage(argv[0]);exit(USAGE_ERR);}daemon(0, 0);//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->GetReqquest(sock,client);});tsvr->Start();return 0;
}
效果也是一样的