第09天 在Linux系统中以守护进程方式运行程序
视频讲解
今天要学习的案例对应的源代码目录:src/chapter02/ks02_08。本案例不依赖第三方类库。程序运行效果如图2-52所示。
图2-52 第09天案例程序运行效果
今天的目标是掌握如下内容。
- Linux中守护进程(或称作后台服务进程)的定义。
- Linux系统中如何让程序作为守护进程运行。
今天我们一起来学习一下守护进程的开发方法。在开始之前,先看一下本节示例程序运行截图,见图2-52。因为本案例演示的是后台服务进程,这意味着该进程没有界面并且不占用终端,所以只能使用ps axj命令查看进程信息。图2-52显示的是ps axj命令的输出。其中PID列为进程Id,TPGID列表示进程连接到的终端所在的前台进程组的Id。Linux上的所有守护进程的TPGID值都是-1。可以看出,本节案例进程ks02_08_d的TPGID=-1,这说明它是一个守护进程,那么什么是守护进程呢?
1.什么是守护进程
Linux系统启动时会启动很多系统服务进程,这些进程在正常运行时,一般都以后台服务方式运行、不占用终端、无须人工干预,也具有比较好的稳定性。这种以后台服务方式运行的进程,通常也称作守护进程或精灵进程(Daemon),它们不占用终端(Shell),因此不会受终端输入或其他信号(如中断信号)的干扰。守护进程有如下特点。
(1)守护进程没有控制终端,不能直接和用户交互,不会接收终端输入或信号,也不能向终端输出信息。
(2)其他进程都是在用户登录或者运行程序时创建,在运行结束或用户注销时终止,但守护进程不受用户登录、注销的影响,它只受开机、关机的影响。
守护进程为什么不使用终端呢?假设用户A从一个终端启动一个守护进程P,然后用户B也登录到这个终端,那么进程P向终端输出的信息会被用户B看到,而且用户B在终端上的输入可能导致守护进程P退出。所以,为了避免这种情况,守护进程不应使用终端。那么守护进程和后台进程有什么区别呢?守护进程和后台进程的区别如下。
(1)守护进程是后台进程,但后台进程不一定是守护进程。
(2)守护进程运行时与终端无关,不能向终端输出消息,因此不会受终端影响,即使关闭终端或用户注销(退出操作系统登录状态),守护进程也会继续运行;后台进程并未脱离终端,后台进程可以向终端输出信息并可接收来自终端的信号(如中断信号),关闭终端会导致该终端中运行的后台进程退出,用户注销也会导致后台进程退出。
(3)守护进程的所属会话、当前目录、文件描述符都是独立的;后台进程只是终端进行了一次fork()函数,让程序在后台执行,因此后台进程的当前目录、文件描述符等都依赖所在终端。
2.如何让一个进程变成守护进程
让一个进程变成守护进程,分为如下步骤。
1)创建子进程,终止父进程
由于守护进程是脱离控制终端的,因此要先创建子进程,然后终止父进程,造成进程已经运行完毕的假象。在这之后,所有的工作都在子进程中完成,而用户在终端里可以执行其他命令,这样可以先在形式上做到与控制终端的脱离。让一个进程以后台方式运行,可以通过fork()函数实现。fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程。一个进程调用fork()函数后,系统先给新的进程分配资源(如存储数据和代码的空间),然后把原来进程的所有值都复制到新进程中,只有少数值与原进程的值不同,这相当于克隆了一个进程。新旧两个进程可以做完全相同的事,也可以做不同的事,这可以由初始参数决定。先看一下fork()函数的一个简单例子,见代码清单2-14。
代码清单2-14
如代码清单2-14所示,在标号①处,在代码processId=fork()执行之前,只有一个进程在执行这之前的代码,但在这条语句之后,就变成两个进程在执行了。在fork()函数执行完毕后,如果创建新进程成功,则出现两个进程,一个是父进程,另一个是子进程。这两个进程将要执行的下一条语句都是标号②处的if(0 == processId)。fork()函数只会把下一个要执行的代码以及之后的代码复制到新进程。fork()函数可能有以下三种不同的返回值。
- 在父进程中,调用fork()函数成功,并且新创建子进程的进程ID>0。此时输出的信息见标号③处。
- 在子进程中,fork()函数返回0。此时输出的信息见标号④处。
- 如果出现错误,则fork()函数返回一个负值。此时可以通过errno的值判断错误原因。
因此,可以通过fork()函数返回的值来判断当前进程是子进程还是父进程。通过调用fork()函数可以让新创建的子进程继续执行父进程尚未执行的代码,那么父进程就可以退出运行了。但此时的子进程仍未脱离终端,如果需要进程以后台服务方式运行,那么就需要让进程脱离终端以守护进程方式运行。
2)在子进程中创建新会话
//创建守护进程(后台进程)的函数
#include<unistd.h>
pid_t setsid(void);
这是最关键的步骤,调用setsid()函数。
setsid()函数用于创建一个新的会话,并将调用它的进程设置为该会话组的组长。调用setsid()函数有三个作用:让进程脱离原会话、让进程脱离原进程组、让进程脱离原终端。在调用fork()函数时,子进程会复制父进程的会话期(Session,是一个或多个进程组的集合)、进程组、终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上的使两者独立开来。setsid()函数能够使进程完全独立出来,从而脱离所有其他进程的控制。setsid()函数接口规定:调用setsid()函数的进程不能是进程组组长。而此时的父进程是会话组长、进程组长,所以父进程不能调用setsid()函数,即使调用也会失败。因此需要先调用fork()函数创建子进程,这样的话父进程仍是会话组长、进程组长,而子进程不是。当子进程调用完setsid()函数之后,子进程是新会话的会话组长,也是新的进程组组长,并且脱离了控制终端。此时,不管原来的终端如何操作,子进程都不会因收到信号而导致自己退出。这就是在调用setsid()函数之前先要调用fork()函数创建子进程的原因。至此,最关键的一步就执行完了。
在有些守护进程中会执行两次fork()函数,那么执行一次fork()函数和两次fork()函数有什么区别呢?执行第一次fork()函数的作用已经在前文介绍过,执行第二次fork()函数有什么作用呢?这是因为虽然已经关闭了和终端的联系,但是该子进程在后期还有可能因为误操作打开终端。因为会话期的首进程(会话组长)能够打开终端设备,而子进程在刚才调用setsid()函数之后已经是会话组长了,它就有条件打开终端设备,为了防止这种事情发生,可以再调用fork()函数一次得到子子进程,因为子进程是会话组长,所以子子进程就不是会话组长。然后把作为会话组长的子进程退出,让子子进程作为守护进程继续运行。这样保证了该守护进程(子子进程)不是对话期的首进程。第二次调用fork()函数不是必需的,是可选的,市面上有些开源项目也是调用fork()函数一次,本案例选择调用fork()函数两次,这样更加稳妥。创建守护进程的流程如图2-53所示。
为了通用,可以将设置守护进程的代码封装到一个接口中,见代码清单2-15。因为Linux与Windows的实现方式不同,为了避免代码放在一起发生混淆,特意将Linux的实现封装到api_linux.cpp,将Windows的实现封装到api_windows.cpp。本节先实现Linux版本。
代码清单2-15
图2-53 创建守护进程的关键流程
3)关闭文件描述符
通过fork()函数方式创建的子进程会从父进程那里继承一些已经打开的文件句柄。子进程可能永远不会操作这些被打开的文件,但它们却会消耗系统资源,而且可能导致文件所在的文件系统(如U盘、光盘)无法卸载。为了避免这种情况,需要关闭文件描述符,见代码清单2-16中标号①处。当关闭文件描述符之后再调用printf()之类接口时可能导致异常,这是因为printf()接口默认对终端进行操作,而此时进程已经同终端脱离了。因此应该把标准输入stdin、标准输出stdout、标准错误输出stderr进行重定向,见标号②处。
代码清单2-16
4)改变工作目录
通过fork()创建的子进程也会继承父进程的当前工作目录。如果进程运行过程中一直占用该目录,将导致当前目录所在的文件系统不能卸载,因此,应该把当前工作目录换成其他的路径,如“/”。
chdir("/");//更改目录防止占用可卸载的文件系统
5)重设文件创建掩码
通过fork()方式创建的子进程会从父进程那里继承文件创建掩码。文件创建掩码指的是屏蔽掉文件创建时对应的访问权限位。文件的访问权限共有9种,分别是r、w、x、r、w、x、r、w、x,它们分别代表用户读、用户写、用户执行、组读、组写、组执行、其他读、其他写、其他执行。可以通过umask()设置文件创建掩码,其实这个函数的作用就是为当前进程设置创建文件或者目录的最大可操作权限。比如,umask(0)的含义是0取反再与创建文件时的权限相与。如果用mode代表文件创建权限,那么umask(0)的含义是(~0)&mode,也就是八进制的777&mode。这样的话,在此之后的代码在创建文件或目录时就可以给出最大的权限,避免了创建目录或文件时权限的不确定性。
umask(0);// 重设文件创建掩码
3.在进程中调用封装的接口
把设置守护进程的接口api_start_as_service()编写完后,就可以在进程中调用了,见代码清单2-17中标号①处,这样就实现了让进程以守护进程方式运行。
代码清单2-17