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