一、进程组
学习过进程,我们知道进程有进程PID、状态等这些属性;
每个进程除了有自己的PID之外,每个进程还属于一个进程组。
进程组是一个或者多个进程的集合,一个进程组可以包含多个进程。
进程组中也可能只存在一个进程
每一个进程组还存在一个唯一的进程组
id:PGID

组长进程:
每一个进程组都存在一个组长进程,进程组PGID就等于组长进程的PID:

二、会话
1. 什么是会话
了解了进程组,那会话又是什么呢,二者又存在什么关系呢?
在刚看到进程信息中,处理父进程id、进程id、进程组id外,还存在一个信息SID;而SID就是会话id

会话 :就是一组进程为了完成同一项任务而组成的"班组",系统用它来做权限隔离、资源统计和终端管理。

在刚才查询到到会话id是5960,且sleep 100的父进程id也是5960,那5960是什么呢?

可以看到5960是bash进程。
- 在登录
shell时,操作系统会为我们创建一个bash进程,同时也会创建一个新会话(session) - 每一个会话都有唯一的
SID。 
2. 创建会话
简单了解了什么是会话,那我们可不可以通过代码(系统调用)来创建一个新的会话呢?
当然是可以的,我们可以调用setsid来创建一个独立的会话;调用setsid的前提是调用进程不是一个进程组组长。
            
            
              c
              
              
            
          
                 pid_t setsid(void);
        - 调用进程会变成新会话的会话首进程;
 - 新会话中只有唯一的一个进程,就会变成进程组组长;新进程组的
ID就是当前调用进程的ID。 - 该进程没有控制终端,如果调用
setsid之前存在控制终端,则调用之后会切断联系。 
注意:调用setsid的进程不能是进程组组长,通常情况下,先调用fork创建子进程,再让子进程调用setsid,父进程直接退出。
三、控制终端
控制终端是这一切的"调度中心",信号、输入、输出都通过它分发。
- 会话(session)
一次"登录"就诞生一个会话,内核用 session ID 标记所有相关进程。
会话首进程(session leader)是第一个打开该终端的进程,PID 即成 SID。 - 控制终端(controlling terminal)
一旦终端设备被会话首进程打开,它就升级为整个会话的控制终端 ,信息记录在每份 PCB 里,fork 时自动继承。
因此后代进程无论多少层,缺省的 stdin/stdout/stderr 都指向同一终端。 - 控制进程(controlling process)
就是"会话首进程"本人;终端断开时,内核把 SIGHUP 发给它,由它负责清理整个会话。 - 进程组(process group)
会话内部再细分为若干进程组,每个组有唯一 PGID,方便一次性信号投递。
任意时刻只有一个组是前台进程组,其余全是后台进程组。 - 前台/后台规则
- 终端输入只送给前台组;
 - Ctrl-C 产生 SIGINT、Ctrl-\ 产生 SIGQUIT,只发给前台组全体成员;
 - 后台组若尝试读终端,会被 SIGTTIN 暂停;写终端则可能被 SIGTTOU 暂停(取决于 tostop 设置)。
 
 - 挂断与回收
调制解调器掉线、网络 SSH 断连、窗口关闭,终端驱动检测到载波丢失,立即向控制进程 发 SIGHUP;
会话首进程收到后通常终止,内核随之向会话内所有进程再广播 SIGHUP,实现"一断全清"。 
简单来说就是:"登录→创建会话→打开终端→终端成控制终端→首进程成控制进程→会话分前台/后台组→终端只理前台组→断线先杀控制进程→会话全灭。
四、作业控制
1. 作业
作业(job) 就是一次在 shell 命令行上提交给内核的一个或一组相连进程"

例如,这里启动的一个进程组,它就是一个作业。(启动的一个进程,也是一个进程组,也是一个作业)。
作业控制,简单来说就是:
使用用 Ctrl-Z、bg、fg 等命令,把正在跑的进程组(作业)随时暂停、扔进后台或再拉回前台继续运行的一套终端多任务切换机制。
2. 作业号
放在后台运行的程序,称之为后台进程。例如在启动程序时带上 &,让它在后台执行:

可以看到,启动sleep 1000 &后,获得了[1] 6512,一个是作业号、一个是进程id。
使用jobs命令可以查看所有后台作业。
使用jobs不仅可以查看到作业号,我们还能发现存在一个Running字段;它表示进程正在运行。
常见的作业状态:
除此之外,在[1]后面还存在一个+,这个表示什么意思呢?

多启动一些进程,我们会发现,除了存在一个+之外,还存在一个-
默认作业:在使用
fg命令,不指明作业号时,就会将默认作业变成前台作业
+: 表示该作业默认作业-: 表示该作业即将称为默认作业- 其他作业
 当默认作业变成前台进程、或者执行结束后,带
-的作业就会变成默认作业。
3. 作业挂起与切回
这里作业挂起和切回,和之前前台进程和后台进程一样。
作业挂起
当一个作业正在前台运行,键盘按下Ctrl + z,当前作业就暂停,并且变成后台作业。
前台进程获取键盘输入,不能被暂停。

作业切回
要将一个后台作业变成前台作业,就要使用fg命令了。
把"后台/已暂停"的作业重新切到前台运行,并把它对应的进程组绑定为终端的当前前台进程组。
使用格式 : fg 作业号

注意:如果直接使用fg命令,不跟作业号就会将默认作业切到前台运行。
4. 查看后台作业
要查看后台作业要使用的命令:jobs。
-l: 显示后台作业的所有信息。-p: 只查看作业的pid。
5. 相关信号
这里键盘按下Ctrl + Z,将前台作业挂到后台,本质上是通过信号SIGTSEP完成的。
像Ctrl + C就是像向台作业发送SIGINT信号。
Ctrl +\就是向前台作业发送SIGQUIT信号。
五、守护进程
简单了解了进程组、会话和作业;现在来看什么是守护进程。
首先,对于我们之前所实现了UDP、TCP通信,在服务器上所部署的服务,它都是属于bash会话的;
而bash会话是登录Shell时,操作系统自动给我们创建的,当退出Shell时,bash会话就退出了,我们所部署的服务也就终止了。
但是,我们想要的服务是一种存在的,不会受到我们的登录和退出的影响。
守护进程(daemon)就是脱离终端、在后台长期运行 、专门负责某项系统任务的进程。
简单来说就是:常驻后台、无终端、为系统/用户提供持续服务
所以,对于我们之前实现的Tcp通信,server服务要守护进程化。
要将一个进程变成守护进程,要进行以下处理:
- 忽略信号
 - 创建新会话
 - 修改工作路径
 - 输入输出重定向
 
1. 忽略信号
对于一个守护进程,不希望因为某些信号,导致进程退出;
这里就要忽略掉某些信号,避免因为信号导致进程异常退出。
这里像SIGCHLD、SIGPIPE信号都要忽略掉。(这里就简单忽略掉这两个信号)
            
            
              cpp
              
              
            
          
          void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);
}
        2. 创建新会话
对于一个守护进程,它不希望受到我们Shell登录和退出的影响,所以就要让该守护进程成为一个新的会话。
调用setsid可以创建一个新会话,调用进程成为会话首进程。
注意:调用setsid的进程,不能是进程组组长。
所以,调用
setsid,要先创建子进程,让子进程去调用setsid,父进程退出。这样子进程就变成了孤儿进程,被操作系统领养,进程退出时自动回收。
守护进程也是孤儿进程
            
            
              cpp
              
              
            
          
          void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);
    // 2. 创建新会话
    if(fork() != 0)
        exit(0);//成功父进程退出、创建子进程失败也直接退出
    setsid();
}
        3. 修改工作路径
这里可能会感觉到很奇怪,为什么要修改工作路径呢?
守护进程要进行输入输出重定向到
/dev/null,所有输出信息都被丢弃了;而我们想要知道该服务的运行情况,就只能去看日志。
而将服务的工作路径修改到
/中,这样就让服务以绝对路径的方式写入日志信息
防止它偶然占用任何可能后来被卸载的文件系统,导致管理员无法换盘、无法升级、甚至无法关机
修改工作路径,直接调用chdir即可:
            
            
              cpp
              
              
            
          
          const char* root = "/";
void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);
    // 2. 创建新会话
    if(fork() != 0)
        exit(0);//成功父进程退出、创建子进程失败也直接退出
    setsid();
    // 3. 修改工作路径
    chdir(root);
}
        4. 输入输出重定向
在守护进程中,可能存在从标准输入中获取、向标准输出、标准错误中输出数据;
这里就可以将进程的0、1、2(标准输入、标准输出、标准错误)重定向到/dev/null中。
/dev/null文件:
- 写进去就消失------返回成功,但数据立即丢弃,磁盘不增一寸。
 - 读出来永远 EOF------立即返回 0 字节,不阻塞、不等待。
 
这样进程输出信息就会被丢弃,从标准输入中获取为空。
            
            
              cpp
              
              
            
          
          const char* root = "/";
const char* dev_null = "/dev/null";
void Daemon()
{
    // 1. 忽略信号
    signal(SIGCHLD,SIG_IGN);
    signal(SIGPIPE,SIG_IGN);
    // 2. 创建新会话
    if(fork() != 0)
        exit(0);//成功父进程退出、创建子进程失败也直接退出
    setsid();
    // 3. 修改工作路径
    chdir(root);
    // 4. 重定向
    int fd = open(dev_null,O_RDWR);//以读写方式打开
    if(fd > 0)
    {
        dup2(fd,0);
        dup2(fd,1);
        dup2(fd,2);
    }
}
        5. 系统调用daemon
这里我们自己简单实现了一个Daemon,让进程守护进程化。
当然,也存在现成的daemon供我们使用:

这里daemon存在两个参数:
nochdir:
简单来说就是:如果传递
0,就将进程的工作目录修改为/。
noclose:
简单来说:如果传递
0,就表示将标准输入、输出和错误重定向到/dev/null;否则就只关闭标准输入、输出和错误。
六、服务守护进程化
对于之前实现了Tcp网络版本计算器,这里将它守护进程化:
(这里就不修改工作路径了)
详细代码:linux: linux学习
            
            
              cpp
              
              
            
          
          //tcpserver.cc
int main(int agrc, char *argv[])
{
    if (agrc != 2)
    {
        std::cout << "err usage : " << argv[0] << " port" << std::endl;
        exit(1);
    }
    int port = std::stoi(argv[1]);
    daemon(1,0);// 不修改工作路径 输入输出重定向
    //Daemon();
    ENABLEFILE();
    NetCal cal;
    std::unique_ptr<Protocol> pro = std::make_unique<Protocol>([&cal](Request &res) -> Responce
                                                               { return cal.Handler(res); });
    std::unique_ptr<TcpServer> tsvr = std::make_unique<TcpServer>(port, [&pro](std::shared_ptr<Socket> fd, InetAddr &client)
                                                                  { pro->GetRequest(fd, client); });
    tsvr->Start();
    return 0;
}
        此外,守护进程通常以d结尾
这里也进行简单修改,编译形成tcpserverd可执行程序。

此时,我们再使用客户端去访问服务器,也是能够获得回应的。

本篇文章到这里就结束了,感谢支持
我的博客即将同步至腾讯云开发者社区,邀请大家一同入驻:https://cloud.tencent.com/developer/support-plan?invite_code=2oul0hvapjsws


