【Linux】实现守护进程 | 以tcpServer为例
本文将以tcp服务器代码为基本,讲述如何将进程守护进程化,后台运行
1.守护进程
所谓守护进程,就是和其他进程没有关系的进程;其独立运行于系统后台,除非自己退出或收到信号终止,否则会一直运行下去
1.1 进程组
在我们使用的bash中,同一时刻只会有一个前台进程组
如图,当一个前台进程开始运行之后,我们没有办法在当前终端开启第二个前台进程。
在运行的命令后面加&,临时让当前进程在后台运行。注意,此时tcp虽然在后台运行了, 但对于它而言,stdin/stdout/stderr
的文件描述符依旧指向的是当前bash的输入输出,所以它的日志依旧会打印到当前终端上。
用ps
命令查看当前进程的信息,其中ppid
是当前进程的父进程,也就是当前bash,pid
是进程编号,pgid
是进程的组编号,可以看到这个组编号和grep命令的组编号是不同的。
我们用这个c语言的代码调用两次fork,相当于创建了3个子进程。
1 |
|
此时再来查看进程信息,能看到这4个进程的进程组pgid是相同的,而且和第一个test的pid相同;这说明第一个test就是父进程,后面的3个都是子进程。
1.2 进程会话
这里还有一个我们之前没有太多了解的信息,进程的sid是什么?
还是上面的例子,在图中能看到,我们执行的test和grep的sid都是相同的,而且都等于第一个test进程的ppid(bash的pid)
这表明图中的5个进程同属于一个进程会话,这个会话就是我们当前打开的bash,并用sid来表示进程会话;
这也是为什么我们登录linux的时候一定会有一个终端,linux系统就是创建会话并加载bash,来给用户提供服务的。
既然存在会话,那就肯定会有会话的资源上限。一旦满了,就会开始杀掉一些进程。
1 | ./test & |
即便我们用&让进程在后台运行,其也有可能收到会话的创建/关闭
的影响而被操作系统干掉🧐比如我们将当前正在运行进程的bash关掉,其前台进程会被直接终止,后台进程也会受到影响(有可能终止有可能不终止,取决于系统)
这和我们对tcp服务器的需求不一致:我们需要的是让tcp服务器的进程能一直稳定的在后台运行,让操作系统别去管它;除非系统内存满了,负载重到实在没有办法的时候,操作系统才能过来把他刀了。
为了不让守护进程受到进程会话的影响,我们就必须让其能够独立出来,自成一个进程组和一个新会话
👆这种独立的进程,就可以被称为守护进程/精灵进程
2.实现
2.1 自己写
别以为写这个很难哦,实际特别简单!
2.1.1 setsid
这里需要用到的setsid接口,其作用如名字一般,是设置当前进程的进程会话组
1 |
|
但是调用这个函数有一个要求:调用的进程不能是进程组的组长!
比如下图中,第一个test就是进程组的组长,它不能调用这个函数。会报错
那要怎么让自己不成为进程组的组长呢?很简单,创建一个子进程就ok了!
1 | if (fork() > 0) |
2.1.2 重定向到dev/null
如果你不知道什么是
/dev/null
,简而言之,这是一个linux下的数据垃圾桶。和windows的回收站会存放删除的资料不同,这个垃圾桶是个黑洞,丢进去的东西不会被存放,是直接丢弃的!
守护进程需要把默认的0.1.2
文件描述符都重定向到dev/null
,是因为设置成独立的进程组和进程会话了之后,当前进程是没有和bash关联的。
此时,默认这个0 1 2
所指向的bash是无效的!如果不重定向,使用cout打印的时候,就会引发异常(可以理解为往一个不存在的文件中写内容),服务器直接退出了,无法实现守护进程。
重定向了之后,所有的打印输出都会被丢到/dev/null
这个文件垃圾桶中,也就不需要担心上述的问题。
1 | if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3 |
你可能会疑惑,那日志信息也被丢到垃圾桶里面了,怎么办?
很简单,因为我们服务器的日志都统一使用了log.hpp
里面的logging
函数,所以只需要对logging
函数的输出重定向到日志文件里面,就ok了!
2.1.3 chdir(选做)
这个操作的目的是修改工作路径。作为服务器进程,很多日志信息是存放在/etc/
目录而不是当前路径下的,为了安全,也应该使用绝对路径
而不用相对路径,避免出现工作目录切换而导致的无法读写文件的问题
不过,如果使用绝对路径,即便我们不修改工作目录,也是能正常访问的;所以这个操作是选做的
2.1.4 信号捕捉
自己写这个函数有个好处,那就是我们可以在里面自定义捕捉一些信号,给这些信号 加上自己的自定义方法;
比如SIGPIPE
就是管道的信号,当管道的读端关闭的时候,写端会被终止;此时写端就会收到这个信号。如果不对这个信号进行SIG_IGN
忽略,我们的服务器会直接终止!
1 | signal(SIGPIPE, SIG_IGN); |
除了这个信号,我们还可以对2号或者3号信号进行自定义捕捉,设定退出信号,让服务器能够安全退出(保存日志信息到磁盘,释放资源等;虽然进程退出之后操作系统会帮我们干这些事,但我们这么写能让项目更规范)
2.1.5 完整代码
1 |
|
没错,就这一点点代码,就能让我们的tcp服务器变成守护进程!
此时我们的客户端依旧能正常连接服务端,获取结果
2.2 nohup
no hang up(不挂起),用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行。用nohup命令执行一个进程,就能让这个进程成为不受终端退出影响的进程
1 | nohup ./test & |
此时,nohup会在当前目录下创建一个nohup.out
文件,用于记录test进程的输出信息(如果通过了>
或>>
执行了重定向,则不会创建)
通过ps可已看到,当前test进程的进程会话还是和bash相同,但我们关闭当前bash,这个test进程依旧能正常运行,只不过父进程会变成操作系统1
,我们的目的也算是达到了
2.3 deamon接口
linux系统中有一个接口daemon
,可以帮我们实现守护进程
1 |
|
了解过守护进程的写法了之后,这两个参数的作用就很明显了
- 第一个参数nochdir表明是否需要修改工作目录;如果设置为0,则切换工作目录到
/
系统根目录 - 第二个参数noclose表明是否需要重定向基础io到
/dev/null
;设置为0则重定向
以下是man手册中的说明
1 | If nochdir is zero, daemon() changes the calling process's current working directory to the root directory ("/"); otherwise, the cur‐ |
我们直接用一个简单代码来演示
1 |
|
运行之后可以看到,这个进程的父id是操作系统,其自成一个进程组和进程会话;和我们自己写的函数作用相同
3.重定向log
因为守护进程把输入输出丢到了垃圾捅里面,所以我们就需要重定向日志的输出
1 |
|
做完这一切之后,我们运行服务器,的确创建了log.txt文件,可里面空空如也
这是因为我们的数据其实都被写道了缓冲区里面,我们需要在logging里面添加一个刷新机制,才能让数据尽快写入到硬盘中,避免日志丢失
1 | fflush(out); // 将C缓冲区中的数据刷新到OS |
此时再运行服务器,就能看到日志很快被写入文件里面了。
over
搞定啦!