一、进程组
1.1 概念
平时提到的进程,它属于进程组,进程组的作用是解决某一任务。进程组由一个或多个进程组成,一般来说第一个启动的进程一般就是进程组组长。
注意:进程组的生命周期与进程组长无关!当进程组最后的进程退出时,该进程组才消失。
1.2 实例
测试命令:sleep 1000 | sleep 2000 | sleep 3000,下面是结果:

PGID 是进程组的编号,也就是组长的 PID 即第一个启动进程的 id。PPID 是父进程,这里其实就是bash。

进程在创建进程时,创建出来的进程依然属于这个进程组(代码我放到了文章末尾):

二、会话
会话与进程组息息相关,一个会话中会有多个进程组。会话是把一次登录里的所有进程组织在一起,统一管理。
当远端登录(像用 Xshell 登录云服务器)时,系统会给我们分配一个 bash 进程和一个伪终端文件。bash 进程和伪终端文件都在服务器上,Xshell会模拟一个终端并且它还是 SSH 客户端,用户输入数据后加密完通过SSH协议交给服务器,然后通过 sshd 服务转发给伪终端文件,最后bash进程去处理。
会话也是有 id 叫 SID,它一般是第一个进程组的组长的 id,也就是bash。

可以看到,上面红框表示会话id,篮框表示启动进程的父id(我是启动了一个bash进程,所以 SID 是 bash的父进程id,这个id在去查就是 sh,sh 就是这个会话的第一个进程)。
三、控制终端
通过控制终端就可以管理进程(或进程组、会话),它是属性存在 PCB 里(具体来说存储的是该会话 tty 设备的指针)。当新建一个会话后会主动打开并绑定一个 tty,这时tty就是控制终端。当收到数据时通过 tty 来对前台作业进行操作。
上面我说的伪终端文件就是 tty 设备文件的一种,是通过软件模拟出来的。用于远程登录等。
补充:一个会话只能绑定一个控制终端; 后台作业默认会被 tty 的读 / 写信号阻塞,只有前台进程组能直接读写控制终端。对于守护进程来说他没有前后台一说,因为没有控制终端。通过 tty 命令可以查看本次会话的空控制终端。

四、作业控制
4.1 介绍
作业是面向用户的,用户为了完成某项任务而启动的进程。Shell控制的前后台不是进程而是作业或进程组。前台作业或后台作业可以由多个进程组成。前台作业只能有一个(只有它才能接收到键盘的信息),后台作业可以由多个。
4.2 作业号
想启动后台作业,在启动命令时加 & 即可:

1是作业号,2648606是 pid,不过它是进程组的最后一个进程。通过 jobs 可以查看后台进程:

jobs参数:
- -l: 显示作业详细信息
- -p: 只显示作业的 PID
对于一个用户来说只能有一个默认作业,用 + 表示。还有一种是 -,他表示即将称为默认作业的作业。区别是默认作业对于像bg、fg等操作可以不写作业号。Running表示正在运行,另外还有Done、Stopped等。
4.3 作业切换
4.3.1 前台变后台
用 ctrl + z :

这时会处于暂停状态,然后用 bg 作业号(默认作业可不写作业号):

4.3.2 后台变前台
用 fg 作业号(默认作业可不写作业号):

五、守护进程
要部署一个服务时,如果放到当前会话中,那么当这次会话结束的时候,你部署的任务可能会直接被强制结束,也可能不会但一定会受到影响。为了解决该问题,可以把该服务的进程放到一个新会话中,让系统去管理它。守护进程是没有 tty 的。
通过 setsid() 可以新建一个会话,不过有个要求就是不能是组长去新建会话(这是为了避免混淆)。下面是实现守护进程:
cpp
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
void Deamon()
{
signal(SIGCHLD, SIG_IGN);
signal(SIGPIPE, SIG_IGN);
if(fork() > 0)
exit(0);
setsid();
// 修改进程的cwd
chdir("/");
// 关闭标准输入和标准输出(一般不直接关,可能会有后续问题)
int fd = open("/dev/null", O_RDWR);
if(fd > 0)
{
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
close(fd); // 只是关掉 "临时用的文件描述符",完全不影响 0、1、2 它们依然是向 null 文件输入输出
// 这时 null 文件的引用计数是3
}
}
把服务器变成守护进程化很简单,调用它即可:
cpp
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();
std::unique_ptr<TcpServer> svr(new TcpServer(localport,HandlerRequest));
svr->Loop();
return 0;
}
六、代码
cpp
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
std::cout << "子进程创建失败" << std::endl;
exit(-1);
}
else if(id == 0)
{
int cnt = 5;
while(cnt--)
{
std::cout << "我是子进程, pid:" << getpid() << std::endl;
sleep(1);
}
}
std::cout << "我是父进程, pid:" << getpid() << std::endl;
wait(NULL);
return 0;
}