一、守护进程
守护进程在服务端是一种比较常用的进程。如果不在服务端,一般就是系统级的进程了,客户端用得相对少得多。顾名思义,就知道,这种进程是一种特殊的后台进程,用于执行特定的系统任务,与控制终端独立并周期的执行一些任务。它不会因为终端的关闭而终止,而是在接到指定的停止信息后才会退出。守护进程是一种特殊的后台进程,不受控制终端管理。
象一些输入法程序和一些服务器处理程序、某些数据库后台服务进程等都可以设置为守护进程,早期也有人称其为精灵进程。
使用类Unix和类Linux系统的都知道,启动程序一般是在终端中启动,然后即使未关闭程序,但只是关闭终端,终端都会提示会把当前程序关闭。换句话说,在终端中执行的程序,一般是和终端的命运绑定在一起的。终端分为好多种,如字符终端、伪终端(pts)、图像终端和网络终端等。
守护进程的实现其实非常简单,之所以复杂的原因就是因为类Unix和类Linux系统的版本太多,而同样的版本又有不同的细分,导致对守护进程的处理各有不同,在《UNIX高级编程》中,对SIGHUP或SIGCHLD等信号进行处理,但在类Linux系统中,基本不处理。
守护进程基本上都是一个孤儿进程,有兴趣的可以看看孤儿进程和僵尸进程,就明白了。
二、实现方法
说起守护进程的实现,就不得不提一下在操作系统中对进程的管理。学习过操作系统的知道,作业即是进程组,它是一组(1~N)进程的集合。进程创建出来,就会有一个进程组,创建者是父进程,而被创建者是子进程,这样便于操作系统对进程的管理。而同一个组内的进程,第一个被创建出来的即为组长。
这样,就明白了进程、进程组和组长的关系。另外还有一个会话进程(会话期Session)的概念,会话是多个进程的集合,每个进程也要属于一个会话。会话的主要作用是拥有控制终端,最多一个(可以没有)。一个会话只有一个前台进程组,只有前台进程组的进程才可以和控制终端进行交互。
明白这些基本的概念,才可以更清楚的了解守护进程的实现过程。下面还得简单介绍一下前台进程和后台进程,写过后台服务的都知道可以用“&”符号来启动时创建一个后台进程,也可以便用bg,fg命令来前后台进程转换。
创建守护进程的过程如下:
1、fork子进程并让父进程退出
2、在子进程创建新的会话(setsid)
3、将当前目录更改为根目录(防止文件系统无法卸载)
4、重设文件权限掩码
5、关闭文件描述符
6、启动守护进程并开始工作
7、退出,主要是处理各种资源机制
下面开始说一下守护进程实现的两种主要方式:
1、调用两次fork的方式,这是一种比较主流的方式
2、使用库函数daemon。
三、使用二次fork的方式
为什么要进行两次fork来产生守护进程:
1 、首闪fork目的是断开与当前终端联系,让shell以为命令完成。另外就是为setsid函数铺垫。只有非进程组组长(group leader)才可以调用setsid函数,而且只能调用一次。
2 、setsid函数非常重要,调用结果就是当前进程成为了进程组长(同时成为会话组长)并结束与原控制贩联系。 这样,原来终端在发送信号,进程就无法收到了。
值得一提的是,第二次fork不是必须的。很多的开源框架并没有进行第二次的fork。那它有什么 作用呢?再次fork的目的主要是防止进程再次打开一个控制终端(会话组长可以再次打开一个终端)。而再进行一次fork,子进程不再是当前的会话组长也就无法 打开终端了。
看下面的实现方式:
#include <argp.h>
#include <iostream>
#include <signal.h>
bool continueLoop = true;
void stopService();
//守护方式处理
void handler(int signal) {
printf("recv SIGINT ,exit!\n");
continueLoop = false;
if (isdaemon){
exit(0);
}
}
//非守护方式处理
void cmdline() {
std::cout << "exit cmd:(q) " << std::endl;
char inputChar;
std::cin >> inputChar;
switch (inputChar) {
// Quit the program
case 'q':
continueLoop = false;
break;
default:
break;
}
}
void start(const char *config, bool cmd) {
while (continueLoop) {
if (!cmd) {
sleep(1000);
printf("exit circle!\n");
} else {
cmdline();
}
}
}
int main(int argc, char **argv) {
signal(SIGINT, handler);
isdaemon = argv[1];
//创建守护进程
if (!argv[1]) {
pid_t pid;
// double fork 第一次
if ((pid = fork()) > 0) {
exit(0);
} else if (pid == 0) {
// Process detached from control terminal
if (setsid() < 0) {
printf("grand son process setsid err!\n");
exit(0);
}
// close(0);
// close(1);
// close(2);
pid_t subpid;
//第二次
if ((subpid = fork()) > 0) {
exit(0);
} else if (subpid == 0) {
start(as.args[0], as.daemon);
} else {
perror("sub process fork err!\n");
}
} else {
perror("fork err!\n");
}
} else {
start(as.args[0], as.daemon);
}
return 0;
}
上面就是一个通过fork两次实现的守护进程,它除了可以通过命令来实现守护和非守护进程。不过在实现了守护进程时,就无法用Qt等IDE进行调试了,只能gdb attach上去进行。如果是这种情况,最好还是先在非守护进程下调试成功再直接改成守护进程即可,防止思维惯性的操作。
这个代码中就没有处理上面的实现方式中的一些机制,这些机制比如有的需要root权限等,不同的系统都有不同的要求,还是要多根据具体的系统来确定,不过多写了一般也没有异常出现。
四、使用库的daemon函数
在系统库“unistd.h”提供了一个API来创建守护进程,先看一个例子:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
int main(int argc, char *argv[])
{
char path[1024];
if(daemon(1, 1) < 0)
{
exit(1);
}
sleep(5);
if(getcwd(path, 1024) == NULL)
{
exit(1);
}
return 0;
}
函数的定义如下:
int daemon (int __nochdir, int __noclose);
返回值为0表示成功,如果返回非零表示是fork或者setsid调用时的错误返回值。__nochdir为0,表示更改目录为根目录(“/”),1为保持目录不变;__noclose为0表示将标准输入等重定向到/dev/null,否则不更改。
用库的方式理论上可能会更好一些,更简单一些。但自己写fork更容易操控,至于用哪种方法,就看具体情况了。
五、总结
写守护进程使用哪种方式,看实际情况来定。永远记住一点,简单即真理。这个简单不只是技术简单,如果有熟练的使用过某种技术的开发者来说,那也是一种实现上的简单。复杂的技术和方法,一定会慢慢被简化,这是一种大趋势。