守护进程

在Linux/UNIX系统引导的时候会开启很多服务,这些服务称为守护进程(也叫Daemon进程)。守护进程是脱离于控制终端并且在后台周期性地执行某种任务或等待处理某些事件的进程,脱离终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的中断信息所终止。(被终端启动的进程,是会随着终端结束而结束的,详见:https://blog.csdn.net/asdfsadfasdfsa/article/details/86497621)

创建守护进程的一般步骤

(1) 创建子进程,退出父进程

为了脱离控制终端需要退出父进程,之后的工作都由子进程完成。在Linux中父进程先于子进程退出会造成子进程成为孤儿进程,而每当系统发现一个孤儿进程时,就会自动由1号进程(init)收养它,这样,原先的子进程就会变成init进程的子进程。

ps –ef | grep ProcName          通过PID/PPID查看进程的父子关系

(2) 在子进程中创建新的会话

使用系统函数setsid来完成。

man 2 setsid    查看关于setsid函数的说明

setsid – creates a session and sets theprocess group ID

#include <unistd.h>

pid_t setsid(void);

进程组:是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号PID之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID,且该进程组ID不会因组长进程的退出而受到影响。

setsid函数作用:用于创建一个新的会话,并担任该会话组的组长。调用setsid有3个作用

(a) 让进程摆脱原会话的控制;

(b) 让进程摆脱原进程组的控制;

(c) 让进程摆脱原控制终端的控制;

使用setsid函数的目的:由于创建守护进程的第一步调用了fork函数来创建子进程再将父进程退出。由于在调用fork函数时,子进程拷贝了父进程的会话期、进程组、控制终端等,虽然父进程退出了,但会话期、进程组、控制终端等并没有改变,因此,这还不是真正意义上的独立开了。使用setsid函数后,能够使进程完全独立出来,从而摆脱其他进程的控制。

(3) 改变当前目录为根目录

使用fork创建的子进程继承了父进程的当前的工作目录。由于在进程运行中,当前目录所在的文件系统是不能卸载的,这对以后的使用会造成诸多的麻烦。因此,通常的做法是让根目录”/”作为守护进程的当前工作目录。这样就可以避免上述的问题。如有特殊的需求,也可以把当前工作目录换成其他的路径。改变工作目录的方法是使用chdir函数。

(4) 重设文件权限掩码

文件权限掩码:是指屏蔽掉文件权限中的对应位。例如,有个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限(对应二进制为,rwx, 101)。由于fork函数创建的子进程继承了父进程的文件权限掩码,这就给子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0(即,不屏蔽任何权限),可以增强该守护进程的灵活性。设置文件权限掩码的函数是umask。通常的使用方法为umask(0)。

(5) 关闭文件描述符

用fork创建的子进程也会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸载。在使用setsid调用之后,守护进程已经与所属的控制终端失去了联系,因此从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf)输出的字符也不可能在终端上显示出来。所以,文件描述符为0、1、2(即,标准输入、标准输出、标准错误输出)的三个文件已经失去了存在的价值,也应该关闭。

(6) 守护进程退出处理

当用户需要外部停止守护进程时,通常使用kill命令停止该守护进程。所以,守护进程中需要编码来实现kill发出的signal信号处理,达到进程正常退出。

http://blog.csdn.net/delphiwcdj/article/details/7364343

http://www.ruanyifeng.com/blog/2016/02/linux-daemon.html

当我们只fork()一次后,存在父进程和子进程。这时有两种方法来避免产生僵尸进程:

  • 父进程调用waitpid()等函数来接收子进程退出状态。
  • 父进程先结束,子进程则自动托管到Init进程(pid = 1)。

      目前先考虑子进程先于父进程结束的情况:     

  • 若父进程未处理子进程退出状态,在父进程退出前,子进程一直处于僵尸进程状态。
  • 若父进程调用waitpid()(这里使用阻塞调用确保子进程先于父进程结束)来等待子进程结束,将会使父进程在调用waitpid()后进入睡眠状态,只有子进程结束父进程的waitpid()才会返回。 如果存在子进程结束,但父进程还未执行到waitpid()的情况,那么这段时期子进程也将处于僵尸进程状态。

      由此,可以看出父进程与子进程有父子关系,除非保证父进程先于子进程结束或者保证父进程在子进程结束前执行waitpid(),子进程均有机会成为僵尸进程。那么如何使父进程更方便地创建不会成为僵尸进程的子进程呢?这就要用两次fork()了。

      父进程一次fork()后产生一个子进程随后立即执行waitpid(子进程pid, NULL, 0)来等待子进程结束,然后子进程fork()后产生孙子进程随后立即exit(0)。这样子进程顺利终止(父进程仅仅给子进程收尸,并不需要子进程的返回值),然后父进程继续执行。这时的孙子进程由于失去了它的父进程(即是父进程的子进程),将被转交给Init进程托管。于是父进程与孙子进程无继承关系了,它们的父进程均为Init,Init进程在其子进程结束时会自动收尸,这样也就不会产生僵尸进程了

#include <stdio.h>  
#include <unistd.h>  
#include <stdlib.h>  
#include <sys/wait.h>  
  
int main(void)  
{  
    pid_t   pid;  
  
    if( ( pid = fork() ) < 0 ){  
        fprintf( stdout, "fork error!\n" );  
    } else if( pid == 0 ) {                     /*first child*/  
        if (( pid = fork()) < 0 )  
            printf( "fork error!\n" );  
        else if( pid > 0 )  
            exit( 0 );  
      
    sleep( 2 );  
    printf( "Second child , parent pid = %d\n", getppid() );  
    exit( 0 );            
    }  
  
    if( waitpid( pid, NULL, 0 ) != pid )  
        printf( "waitpid error!\n" );  
  
    printf( "father of original!\n" );  
  
    exit( 0 );  
  
  
}

附创建一个守护进程的完整代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <signal.h>

void my_daemon() {
    int pid, fd;

    // 1.转变为后台进程
    if ((pid = fork()) == -1) exit(1);
    if (pid != 0) exit(0); // 父进程(前台进程)退出

    // 2.离开原先的进程组,会话
    if (setsid() == -1) exit(1); // 开启一个新会话

    // 3.禁止再次打开控制终端
    if ((pid = fork()) == -1) exit(1);
    if (pid != 0) exit(0); // 父进程(会话领头进程)退出

    // 4.关闭打开的文件描述符,避免浪费系统资源
    for (int i = 0; i < NOFILE; i++)
        close(i);

    // 5.改变当前的工作目录,避免卸载不了文件系统
    if (chdir("/") == -1) exit(1);

    // 6.重设文件掩码,防止某些属性被父进程屏蔽
    if (umask(0) == -1) exit(1);

    // 7.重定向标准输入,输出,错误流,因为守护进程没有控制终端
    if ((fd = open("/dev/null", O_RDWR)) == -1) exit(1); // 打开一个指向/dev/null的文件描述符
    dup2(fd, STDIN_FILENO);
    dup2(fd, STDOUT_FILENO);
    dup2(fd, STDERR_FILENO);
    close(fd);

    // 8.本守护进程的子进程若不需要返回信息,那么交给init进程回收,避免产生僵尸进程
    if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) exit(1);
}

#define INTERVAL 2

int main(int argc, char *argv[]) {
    my_daemon(); // 首先使之成为守护进程

    int t = 0;
    FILE *fp = fopen("/root/tmp.txt", "a");
    fprintf(fp, "ppid = %d, pid = %d, sid = %d, pgrp = %d\n", getppid(), getpid(), getsid(0), getpgrp());
    fflush(fp);

    do { // 测试此后台进程,每INTERVAL秒打印当前时间t,30秒后退出此后台进程
        fprintf(fp, "%d\n", t);
        fflush(fp); // 输出缓冲区内容到文件中
        sleep(INTERVAL);
        t += INTERVAL;
    } while(t < 30);

    fclose(fp);

    return 0;
}

保存为daemon.c
编译命令 gcc daemon.c
运行 ./a.out
查看tmp.txt文件内容 cat /root/tmp.txt

 

posted @ 2019-07-19 21:00  車輪の唄  阅读(12)  评论(0编辑  收藏  举报  来源