守护进程(daemon)

——Linux学习笔记

Posted by Samuel on August 3, 2017

目录

概述

守护进程(daemon)是在后台运行不受终端控制的进程(如输入、输出等),一般的网络服务都是以守护进程的方式运行。

守护进程脱离终端的主要原因:

  1. 用来启动守护进程的终端在启动守护进程之后,需要执行其他任务。(如其他用户登录该终端后,以前的守护进程的错误信息不应出现)
  2. 由终端上的一些键所产生的信号(如中断信号)不应对以前从该终端上启动的任何守护进程造成影响。

守护进程与后台运行程序(即加&启动的程序)的区别:

  1. 守护进程已经完全脱离终端控制台了,而后台程序并未完全脱离终端,在终端未关闭前还是会往终端输出结果
  2. 守护进程在关闭终端控制台时不会受影响,而后台程序会随用户退出而停止,需要在以nohup command & 格式运行才能避免影响
  3. 守护进程的会话组和当前目录,文件描述符都是独立的。后台运行只是终端进行了一次fork,让程序在后台执行,这些都没改变。

守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录以及文件创建掩码等。这些环境通常是守护进程从执行它的父进程(特别是shell)继承下来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,也可以由作业控制进程crond启动,还可以由用户终端(通常是shell)执行。

启动守护进程的方法

  1. 在系统启动是由系统初始化脚本启动,这些脚本一般在/etc或/etc/rc开头的目录。如inet超级服务器,web服务器等;
  2. 许多网络服务器是由inet超级服务器启动的,如Telnetd,FTPd等;
  3. cron守护进程按一定的规则执行一些程序,由它启动的程序也以守护进程的方式运行。
  4. 守护进程可以在用户终端上启动,这是测试守护进程或重新启动守护进程常用的方法。

创建步骤

创建子进程,终止父进程

由于守护进程是脱离控制终端的,因此首先创建子进程,终止父进程,使得程序在shell终端里造成一个已经运行完毕的假象(形成孤儿进程)。之后所有的工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而使得程序以僵尸进程形式运行,在形式I上做到了与控制终端的脱离。实现的语句如下:if(pid=fork()){exit(0);}是父进程就结束,然后子进程继续执行。

在子进程中创建新会话(脱离控制终端)

这个步骤是创建守护进程中最重要的一步,在这里使用的是系统函数setsid。

setsid函数用于创建一个新的会话,并担任该会话组的组长。调用setsid有三个作用:让进程摆脱原会话的控制、让进程摆脱原进程组的控制和让进程摆脱原控制终端的控制。

在调用fork()函数时,子进程全盘拷贝父进程的会话期(session,是一个或多个进程组的集合)、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

进程组与会话期

  1. 进程组:是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程组ID不会因组长进程的退出而受到影响。
  2. 会话周期:会话期是一个或者多个进程的集合。通常一个会话开始于用户的登录,终止于用户的退出,在此期间该用户运行的所有进程都属于这个会话期。

Setsid()函数:

  1. setsid()函数的作用:创建一个新的会话,并且担任该会话组的组长。具体作用包括:让一个进程摆脱原会话的控制,让进程摆脱原进程的控制,让进程摆脱原控制终端的控制。
  2. 创建守护进程要调用setsid()函数的原因:由于创建守护进程的第一步是调用fork()函数来创建子进程,再将父进程退出。由于在调用了fork()函数的时候,子进程拷贝了父进程的会话期、进程组、控制终端等资源、虽然父进程退出了,但是会话期、进程组、控制终端等并没有改变,因此,需要用setsid()函数来时该子进程完全独立出来,从而摆脱其他进程的控制。

改变工作目录(改变当前目录为根目录)

使用fork创建的子进程也继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统不能卸载,因此,把当前工作目录换成其他的路径,如“/”或“/tmp”等。改变工作目录的常见函数是chdir。

重设文件创建掩码

文件创建掩码是指屏蔽掉文件创建时的对应位。由于使用fork函数新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。设置文件创建掩码的函数是umask,通常的使用方法为umask(0)。

关闭文件描述符

同文件权限码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开了的文件。这些文件被打开的文件可能永远不会被守护进程读写,如果不进行关闭的话将会浪费系统的资源,造成进程所在的文件系统无法卸下以及引起预料的错误。按照如下方法关闭它们:

for (i = 0; i < NOFILE; ++i) //关闭打开的文件描述符
    close(i);

守护进程的退出

上面建立了守护进程,当用户需要外部停止守护进程运行时,往往需要使用kill命令来停止该守护进程,所以守护进程中需要编码来实现kill发出的signal信号处理,达到进程的正常退出。实现该过程的函数是signal函数:

signal(SIGTERM, sigterm_handler);
void sigterm_handler(int arg){
//进行相应处理的函数
}

功能是:将一个给定的函数和一个特定的信号联系起来,即在收到特定的信号的时候执行相应的函数。

示例程序

示例一

#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<fcntl.h>// open  
#include<sys/types.h>  
#include<sys/stat.h>  
#include<unistd.h>  
#include<sys/wait.h>  
#include<signal.h>  
  
#define MAXFILE 65535  
  
volatile sig_atomic_t _running = 1;  
int fd;  
  
// signal handler  
void sigterm_handler(int arg) {  
    _running = 0;  
}  
  
int main() {  
    pid_t pid;  
    char *buf = "This is a Daemon, wcdj\n";  
  
    /* 屏蔽一些有关控制终端操作的信号 
     * 防止在守护进程没有正常运转起来时,因控制终端受到干扰退出或挂起 
     * */  
    signal(SIGINT,  SIG_IGN);// 终端中断  
    signal(SIGHUP,  SIG_IGN);// 连接挂断  
    signal(SIGQUIT, SIG_IGN);// 终端退出  
    signal(SIGPIPE, SIG_IGN);// 向无读进程的管道写数据  
    signal(SIGTTOU, SIG_IGN);// 后台程序尝试写操作 
    signal(SIGTTIN, SIG_IGN);// 后台程序尝试读操作 signal(SIGTERM, SIG_IGN);// 终止  
  
    // test  
    //sleep(20);// try cmd: ./test &; kill -s SIGTERM PID  
  
    // [1] fork child process and exit father process  
    pid = fork();  
    if(pid < 0) {  
        perror("fork error!");  
        exit(1);  
    }  
    else if(pid > 0) {  
        exit(0);  
    }  
  
    // [2] create a new session  
    setsid();  
  
    // [3] set current path  
    char szPath[1024];  
    if(getcwd(szPath, sizeof(szPath)) == NULL) {  
        perror("getcwd");  
        exit(1);  
    }  
    else {  
        chdir(szPath);  
        printf("set current path succ [%s]\n", szPath);  
    }  
  
    // [4] umask 0  
    umask(0);  
  
    // [5] close useless fd  
    int i;  
    //for (i = 0; i < MAXFILE; ++i)  
    for (i = 3; i < MAXFILE; ++i) {  
        close(i);  
    }  
  
    // [6] set termianl signal  
    signal(SIGTERM, sigterm_handler);  
  
    // open file and set rw limit  
    if((fd = open("outfile", O_CREAT|O_WRONLY|O_APPEND, 0600)) < 0) {  
        perror("open");  
        exit(1);  
    }  
  
    printf("\nDaemon begin to work..., and use kill -9 PID to terminate\n");  
  
    // do sth in loop  
    while(_running) {  
        if (write(fd, buf, strlen(buf)) != strlen(buf)) {  
            perror("write");  
            close(fd);  
            exit(1);  
        }  
        usleep(1000*1000);// 1 s  
    }  
    close(fd);  
  
    // print data  
    if((fd = open("outfile", O_RDONLY)) < 0) {  
        perror("open");  
        exit(1);  
    }  
    char szBuf[1024] = {0};  
    if(read(fd, szBuf, sizeof(szBuf)) == -1) {  
        perror("read");  
        exit(1);  
    }  
    printf("read 1024 bytes:\n%s\n", szBuf);  
    close(fd);  
    return 0;  
}  
/* 
   gcc -Wall -g -o test test.c 
   ps ux | grep -v grep | grep test 
   tail -f outfile 
   kill -s SIGTERM PID 
 */  

示例二

void init_daemon() {
    pid_t pid;
    int i = 0;
    if ((pid = fork()) > 0)
        exit(0);          //是父进程,结束父进程
    else if( pid < 0){
        exit(1);          //fork失败,退出
    }
    setsid();             //第一子进程成为新的会话组长和进程组长,并与控制终端分离
    if ((pid = fork()) > 0)
        exit(0);          //是第一子进程,结束第一子进程
    else if(pid < 0){
        exit(1);          //fork失败,退出
    }
    //是第二子进程,继续,第二子进程不再是会话组长
    for (i = 0; i < NOFILE; ++i)//关闭打开的文件描述符
        close(i);
    chdir("/tmp"); //改变工作目录
    umask(0);      //重设文件创建掩模
}