Linux网络:守护进程
- 会话
- 进程组
- 会话
- 终端
- 守护进程
- setsid
- daemon
在创建一个网络服务后,往往这个服务进程是一直运行的。但是对于大部分进程来说,如果退出终端,这个终端上创建的所有进程都会退出,这就导致进程的生命周期与终端绑定,如果终端退出,服务就无法继续运行了。
为此,创建一个服务进程后,需要让其不在受到终端的影响,就算终端退出,进程依然执行,这种进程称为守护进程
。
在讲解如何创建一个守护进程之前,先了解一些Linux
中与会话相关的知识。
会话
进程组
进程组是一个或者多个进程的集合,这些进程都有同样的进程组ID(PGID
),这个ID和PID
类似,都是一个正整数,可以放在pid_t
类型中。
往往一个进程组内部的所有进程,要共同完成一个功能,这些进程之间可以进行通信。而当在Linux
中通过管道命令创建多个进程,此时Linux
认为多个进程是共同完成一个功能的,就会把它们放到同一个进程组中。
通过管道创建三个sleep
进程:
创建进程后,通过ps -ajx
查看进行信息,可以看到三个经常的PGID
都是54844
,也就是sleep 100
这条命令的PID
。这说明这三个由管道串联起来的进程,属于同一个进程组。而sleep 100
这个进程是进程组长
。
进程组长往往是进程组中的第一个进程,其可以在当前进程组中创建新的进程,也可以创建一个新的进程组。
但是如果进程组长退出了,不代表进程组退出了,就算进程组长退出了,只要进程组中还有进程,那么这个进程组依然存在。
通过kill
杀掉组长进程后,剩余的两个进程依然存在,并且PGID
依然是54844
。
会话
一个Linux
系统,可以有多个会话,每个会话都是一个或多个进程组的集合。
创建一个新的会话,并在会话中执行三个sleep
进程:
在原先的会话中,查看bash
进程和sleep
进程的信息:
此处-E "bash|sleep"
表示查找bash
或者sleep
进程。
其中SID
表示会话ID
,可以看到第一个bash
的SID = 53792
,第二个bash
的SID = 54989
。
这两个bash
本身也是进程,它们的PID == PGID == SID
。这种PID == SID
的进程,称为话首进程
,它是会话中的第一个进程。一般而言,话首进程都是bash
。
而创建的三个sleep
进程,它们的SID = 54989
,与第二个bash
相同,说明它们属于同一个会话,而54989
会话中,包含四个进程以及两个进程组。而53792
会话中只有一个进程和一个进程组。
这说明一个会话是一个或多个进程组的集合。当一个话首进程退出,这个会话内部的所有进程都会退出。
在一个会话中创建三个sleep
进程:
随后把bash
话首进程杀掉:
可以看到,不仅仅话首进程退出了,三个sleep
进程一起退出了。
终端
一个会话不仅仅包含多个进程组,它还需要一个控制终端,来接受用户的指令。
当创建一个会话,会执行以下两步:
- 创建一个终端
- 启动一个
bash
进程(组)
终端本质上是一个Linux
的文件,存在于/dev/pts
目录下。
当前我创建了两个会话,那么就有两个终端,这两个终端分别对应0
和1
这两个文件。
如果尝试删除这些文件会被拒绝,哪怕你是root
用户,因为这些文件是由内核管理的。
除去两个终端文件,还有一个ptmx
,这个文件是用于创建终端的文件,如果这个文件丢失,可能就无法创建新的终端了。
在一个会话中,包含多个进程组,这些进程组分为两类:
前台进程组
:一个会话只有一个,这个进程组独占终端的输入输出流后台进程组
:一个会话可以有多个,无法接收到来自终端的数据,在后台运行
比如向终端输入ctrl + c
,就是一个中断信号,此时这个信号会发送给前台进程,导致前台进程退出。
但是并不是所有的信号都直接发送给前台进程组,因为有一个特殊的bash
进程,如果某些信号的功能是直接挂断整个会话,那么这个信号会发送给bash
,哪怕bash
不是当前的前台进程组。
至此,可以这样理解Linux
中的会话机制:
- 一个进程组管理多个进程,这些进程往往共同完成一个功能
- 一个会话管理多个进程组,当会话退出,该会话下的所有进程组都退出
- 终端是会话的对外表现,一个会话只有一个进程组可以占用终端
刚才提到,一个终端本质就是/dev/pts
下的一个文件,其实与终端交互,就是进程在读写这个文件。那么一个进程如何知道要读取/dev/pts
下的哪一个文件?会话又是如何保证其下的所有进程都使用同一个终端的?
终端的信息存储在进程的PCB
中,先前说过会话创建包括一个终端和一个bash
,而这个过程中,会把这个终端的信息存储到bash
的PCB
中。
而在一个会话中创建的所有进程,都是bash
的后代进程,会继承来自bash
的PCB
,也就会继承到PCB
中的终端信息,从而保证同一会话的所有进程最后都操控同一个终端。
守护进程
了解Linux
的会话机制后,就可以谈一谈守护进程的问题了。
先前说过,当一个会话的话首进程退出,那么该会话的所有进程都会退出,因此一个守护进程一定不能属于其它会话,而是自己创建一个会话,自己就是话首进程。
setsid
setsid
可以创建一个新会话并把自己变成话首进程,需要头文件<sys/types.h>
和<unistd.h>
。
函数声明:
pid_t setsid(void)
调用该函数有一个前提,该进程不能是进程组长。
但是对于一个新创建的进程,它自成一个进程组,自己就是进程组长,如何才能让它不是进程组长?
尝试以下代码:
#include <unistd.h>int main()
{fork();while (true){}return 0;
}
这个代码中,通过fork
创建了一个子进程,随后父子进程同时陷入死循环。
运行代码:
此时终端被阻塞,切换到另一个终端,查看test.exe
这个进程的信息。
此时查询到了两个进程,一个是父进程,另一个是子进程,重点在于它们的GPID
都是52746
,这说明通过fork
创建的父子进程属于同一个进程组。
而frok
创建的子进程会继承来自父进程的所有代码,PCB
等信息。因此如果想要让一个进程调用setsid
创建一个新会话,只需要通过fork
创建一个子进程,子进程继承父进程的所有信息,但又不是进程组长,可以调用setsid
。
因此setsid
的常见写法如下:
pid_t id = fork();
if (id > 0)exit(0);setsid();
这段代码中,通过fork
创建一个子进程,此时父子进程属于同一个进程组,父进程是组长。如果id > 0
说明是父进程,父进程直接退出,一个进程组中,进程组长退出,进程组其他进程继续运行,不会退出。因此子进程不会退出,继承了父进程的所有信息,并且还不是进程组长。
于是子进程调用setsid
,自己创建一个会话,自己做话首进程,至此子进程就是一个守护进程了!
运行以下代码:
#include <unistd.h>
#include <sys/types.h>int main()
{pid_t id = fork();if (id > 0)exit(0);setsid();while (true){}return 0;
}
这个代码在子进程变为守护进程后,执行一个死循环。
执行./test.exe
后,查看bash
和test
进程的信息,发现test.exe
这个进程PID=PGID=SID=58278
,并且不和任何一个bash
重复。这说明test.exe
已经自己创建一个会话,自己做话首进程,成为了一个守护进程了。
但是还有一个问题,那就是它的终端没有切断:
可以看到它的三个标准流,都指向/dev/pts/0
,这就是当前的终端文件。如果当前终端退出,那么如果这个守护进程还向终端输出内容,就会导致错误,因此还要改变它的输出流:
// 关闭标准输入、输出和错误
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);// 打开/dev/null作为标准输入、输出和错误
open("/dev/null", O_RDONLY);
open("/dev/null", O_WRONLY);
open("/dev/null", O_WRONLY);
此处的/dev/null
是一个Linux
提供的文件,它可以接收任何输入,但是不论输入什么都会被系统丢弃。因此如果一个程序有输出,但是又不希望接收到它的输出时,就可以把输出重定向到/dev/null
下。
daemon
setsid
的用法还是有点复杂了,为此Linux
提供了另一个系统调用daemon
,它封装了上述所有过程,可以快速创建一个守护进程。需要头文件<unistd.h>
。
函数声明:
int daemon(int nochdir, int noclose)
参数:
nochdir
:改变工作目录- 传入
0
:改变工作目录为根目录 - 传入
1
:保持当前工作目录
- 传入
nclose
:改变输入输出流- 传入
0
:输入输出流重定向到/dev/null
- 传入
1
:不改变输入输出流
- 传入
原先的代码就可以变成:
#include <unistd.h>
#include <sys/types.h>int main()
{daemon(0, 0);while(true){}return 0;
}
这就是一个功能全面的守护进程了。