序言
 当我们使用指令 ps 查看进程的相关信息时,在以前我们只是关注该进程的 PID(该进程的标识符) , PPID(其父进程的标识符) 以及 STAT(该进程的状态)。
  那 PGID 和 SID 又是什么?有什么作用呢?
1. 进程组
1.1 什么是进程组?
当我们启动程序执行相应的任务时,我们的任务可能只是创建了一个进程:
 1 #include <iostream>
   2 #include <unistd.h>
   3 
   4 int main()
   5 {
   6     while(1)
   7     {
   8         std::cout << "I am running, my pid is " << getpid() << std::endl;
   9         sleep(1);
  10     }
  11     return 0;
  12 }
我们使用指令 ps 查看进程信息:
 
在这里的 PGID 就是代表进程组,进程组的 id 和 PID 保持一致。当我们的进程组只包含一个进程时,进程的 ID 等于其进程 ID。
 那如果我们的任务包含多个进程呢?举个栗子:
 
 当一个进程组包含多个进程时,进程组的 ID 和第一个创建的进程的 ID 保持一致。
 总结一下,进程组是一个或者多个进程的集合, 一个进程组可以包含多个进程。
 
1.2 组长进程
 每一个进程组都包含一个组长进程,组长进程的 ID 就是该进程组的 ID。根据上面代码的举例,我们不难得出以下结论:
- 当一个进程组只有一个进程时,该进程就是组长进程
- 当一个进程组包含多个进程时,首先创建的进程为组长进程
一个进程组的生每周期取决于最后终止的进程而非是组长进程。
2. 会话
2.1 什么是会话
 会话可以看成是 一个或多个进程组的集合, 一个会话可以包含多个进程组。每一个会话也有一个会话 ID(SID)。
  创建一个新的会话时可以简单理解为 创建终端文件和启动 bash 进程:
- 终端:终端是用户与操作系统进行交互的界面。
- bash:- bash是- Linux上最常用的- Shell之一,- Shell是运行在终端上的程序,它提供了用户与操作系统交互的接口。
怎么来证明呢?现在我在 XShell 上只是启动一个会话,查看我们的终端文件和 bash 进程:
 
 
可以看到只存在一个终端文件和 bash 进程,那我们再创建一个回话呢:
 

 现在就变成了两个终端文件和两个 bash 进程。
 所以在每一次登录时,都会为我们自动建立一次会话。会话的 id 和第一个创建的进程的 id 保持一致,在大多数情况下都是我们的 bash,除非是我们手动创建的会话。
 
2.2 创建一个会话
 可以调用 setsid 函数来创建一个会话, 前提是 调用进程不能是一个进程组的组长。
  大家都知道可以使用 ctrl + c 来终止当前程序(需要是前台的程序,后面会说)的执行吧,但是该程序必须是当前会话下的程序。那不是废话吗,我在我会话下启动的程序肯定就是啊,难不成跑到别处去了?
  没认识 setsid 之前你的话是对的,但是认识之后就不一定了,举个栗子:
   1 #include <iostream>
   2 #include <unistd.h>
   3 
   4 int main()
   5 {
   6     // child
   7     if(fork() ==  0)
   8     {
   			 // 创建新的会话
   9         setsid();
  10         while(1)
  11         {
  12             std::cout << "I am child process, my pid is " << getpid() << std::endl;
  13             sleep(2);
  14         }
  15     }
  16     // parent
  17     else
  18     {
  19         while(1)
  20         {
  21             std::cout << "I am parent process, my pid is " << getpid() << std::endl;
  22             sleep(2);
  23          }
  24     }
  25 
  26     return 0;
  27 }
现在我们运行这段程序:
 
 程序正常运行,但是终止进程后子进程依然执行,这是因为子进程属于其他会话,不归当前会话管。那除了重启大法没办法终止他了吗?肯定不是,我们还有 kill 指令。
3. 前后台任务
3.1 前台任务
 前台任务会占据终端的输入输出,即它会接收你通过键盘输入的命令或数据,并将它的输出结果直接显示在终端上。前台任务会阻塞终端,直到它完成或者被你明确地放到后台执行。简而言之,前台任务会占有终端文件! 比如:
   1 #include <iostream>
   2 #include <unistd.h>
   3 
   4 int main()
   5 {
   6     while(true) sleep(1);
   7     return 0;
   8 } 
现在我们运行该程序,并向终端输入指令:
 
 可以看到并没有任何结果,这是因为我们输入的指令都是被 bash 指令接受之后创建子进程执行的,但是现在终端文件被该进程占有了,自然 bash 收不到了。
 
3.2 后台任务
 后台任务是指那些在终端之外运行的任务,它们 不会直接占据终端的输入输出。后台任务可以在你执行其他任务或关闭终端时继续运行。要将一个任务放到后台执行,你可以在命令的末尾加上 & 符号。
  还是上一段程序,但是在运行时在最后加上 &:
 
可以看到,我们指令的执行并没有受到干扰。
 那我怎么查看我后台任务的执行情况呢,使用指令 jobs [-l]:
 
 
3.3 后台任务切回前台
 只需要使用指令 fg n,n 代表该任务的编号:
 
 
3.4 前台任务切回后台
 首先我们需要使用指令 ctrl + z 将该任务暂停,之后使用指令 bg n 将该任务切换到后台:
 
4. 守护进程
 我们运行一个普通的进程时,不管是前台还是后台,当我们一退出,会话一结束。我们执行的进程也会随之终止,但是在很多应用场景下,服务是不能停的!不可能程序员一下班,我们的应用就罢工了吧!
  所以有了守护进程,守护进程通常用于 提供需要持续运行的服务,如网络服务(Web服务器、FTP服务器等)、数据库服务等。这些服务在系统运行期间 一直保持运行状态,确保用户可以随时访问。
  那我们如何创建一个守护进程呢,关键是 创建一个新的会话,当我们的会话结束时,该会话不受影响!但是只是靠一个进程是做不到的,因为 调用 setsid 的函数不能是进程组长!包含一个进程的进程组,该进程就是组长!解决方法也很简单,一个进程不行那就创建一个子进程嘛,创建的过程如下:
#pragma once
#include <unistd.h>
#include <signal.h>
#include <string>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
void Daemon(const std::string &newpath = "")
{
    // 防止一些异常退出信号
    signal(SIGCHLD, SIG_IGN);
    // 创建子进程,父进程退出
    if (fork() > 0)
        exit(0);
    // 设置一个新的会话
    setsid();
    // 关闭原来的文件描述符
    int fd = open("/dev/null", O_RDWR);
    if (fd > 0)
    {
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
    // 是否更改工作路径
    if (!newpath.empty())
    {
        chdir(newpath.c_str());
    }
}
 父进程的作用就是创建一个子进程,之后父进程的生命周期就结束了。子进程创建了一个新的会话,脱离了原来的会话。
  在这里为什么需要关闭原来的文件描述符呢?这是因为现有的文件描述符还指向原来会话的文件,这是不严谨的,因为我们当前已经脱离了原来的会话。在这里没有直接的关闭,因为 考虑到后续场景可能使用到读写操作。所以我们让他指向一个空的文件(类似于空指针)。是否需要切换路径和使用场景相关。
5. 总结
在这篇文章中,我们介绍了进程的组以及会话的概念,还实现了一下守护进程功能的函数。



















