进程间关系和守护进程
序言
当我们使用指令 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 // child7 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 // parent17 else18 {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. 总结
在这篇文章中,我们介绍了进程的组以及会话的概念,还实现了一下守护进程功能的函数。
