守护进程

本文介绍了Linux守护进程的特征,包括没有控制终端、在系统启动时启动等,并详细讲解了两种编写守护进程的方法,包括改变文件模式创建屏蔽字、创建新会话、改变工作目录、关闭文件描述符等步骤。此外,还提到了处理SIGCHLD信号以避免僵尸进程,以及实现单实例守护进程的方法,如使用记录锁。最后,文章解释了为何守护进程需要进行两次fork操作,以确保完全脱离终端并防止误操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

守护进程特征:

  • 没有控制终端
  • 经常在系统引导启动时启动,仅在系统关闭时终止
  • 守护进程的父进程是1号进程,在ubuntu 14.4上面用户守护进程的父进程不一定是1号进程,系统给用户空间分配了一个类似于1号进程的init–>user进程,进程号不一定

编写守护进程

方式一:

  1. 首先调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0)

    因为继承而来的文件模式创建屏蔽字很可能会被设置为拒绝某些权限。如果守护进程要创建文件,那么它可能需要设置特定的权限。

  2. 然后调用fork,然后使父进程exit。这样做实现了下面几点:

    • 如果该守护进程是作为一条简单shell命令启动的,那么父进程终止会让shell认为这条命令已经执行完毕,不占用终端。

    • 子进程继承了父进程的进程组ID,获得了一个新的进程ID,这就保证了子进程不是一个进程组的组长ID,这就是后面的setsid调用的先决条件

  3. 然后调用setsid创建一个新会话。执行完setsid之后,调用进程将成为:

    • 新会话的首进程,

    • 是一个新进程组的组长进程,

    • 同时没有控制终端

  4. 将当前工作目录改为根目录/

    这是因为守护进程要求在系统重启/关闭之前是一直存在的。所以如果守护进程的当前工作目录在一个挂载的文件系统中,则该文件系统就不能被卸载。而从父进程中继承过来的当前工作目录可能就在一个挂在的文件系统中。

    • 当然某些守护进程可能会将当前工作目录更改到某个指定位置(不一定是/
  5. 关闭不再需要的文件描述符。这使得守护进程不再持有从其父进程继承而来的任何文件描述符。

    • 可以先判断最高文件描述符值,然后关闭直到该值的所有描述符

    • 使用getrlimit获得进程能够打开的最大文件数。

  6. 某些守护进程打开/dev/null/使其具有文件描述符0,1,2。这样任何试图读标准输入、写标准输出、写标准错误的库例程都不会产生任何效果。

    因为守护进程并不与任何终端关联,所以其输出无处显示,也无法获得用户的输入。

  7. 处理一些特殊的信号:SIGHUP,SIGTTIN,SIGTTOU,SIGCHLD,SIGTSTP

    守护进程一文中:
    但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源(关于僵尸进程的更多详情,请看《僵尸进程》)。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在 Linux 下可以简单地将 SIGCHLD 信号的操作设为 SIG_IGN 。关于信号的更详细用法,请看《信号中断处理》。
    注:通过忽略信号SIGCHLD,不关心子进程在结束的时候发出的信号,交由内核处理,不会产生僵死进程。

//只fork了一次
#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>
int main(){
    pid_t pid;
    struct rlimit r;

    umask(0);
    if((pid = fork()) < 0){
        _exit(1);
    }
    else if(pid > 0){
        _exit(0);
    }
    int i, openedfile,fd0,fd1,fd2;

    setsid(); //新建会话,成为首进程
    chdir("/"); //更改目录

    signal(SIGHUP,SIG_IGN);
    signal(SIGTTOU,SIG_IGN);
    signal(SIGTTIN,SIG_IGN);
    signal(SIGTSTP,SIG_IGN);
    signal(SIGCHLD,SIG_IGN);

    /* 获取进程能打开的最大文件数 */
    if(getrlimit(RLIMIT_NOFILE,&r) != 0){
        _exit(-1);
    }
    if(RLIMIT_NOFILE == RLIM_INFINITY){
        openedfile = 1024;
    }
    else
        openedfile = RLIMIT_NOFILE;
    /* 关闭所有文件 */
    for(i = openedfile; i >= 0;--i){
        close(i);
    }
    /*设置文件描述符0 1 2  对应 /dev/null */
    fd0 = open("/dev/null",O_RDWR);
    fd1 = dup(fd0);
    fd2 = dup(fd0);
    /* 守护进程中要做的事情 */
    int fd;
    if((fd = open("my", O_CREAT|O_RDWR|O_APPEND,0666)) < 0){
            exit(-1);
        }

    while(1){
        char buf[100];
        sprintf(buf,"process id: %d\n",getpid());
        write(fd,buf,strlen(buf));
        sleep(2);
    }
}

方式二:

调用库函数:

#include <unistd.h>
int daemon(int nochdir,int noclose);
  • nochdir如果是0,则表示守护进程的工作目录修改为根目录(“/”),否则不修改工作目录。
  • noclose如果是0,则表示重定向标准输入,标准输出、标准错误到dev/null
    ,否则不做任何修改。
  • 调用成功,返回0,失败返回-1,并且设置errno。
#include <unistd.h>
#include <stdio.h>
#include <syslog.h>

int main(){
    if(daemon(1,1) < 0)
        syslog(LOG_DAEMON, "daemon error.");

    while(1){
        /*守护进程要做的事情*/
    }
}

单实例守护进程

单实例守护进程:就是同一时刻只能够运行守护进程程序的一个副本,即不能同时运行一样的两个守护进程。实现方式是用记录所,这只写锁。

#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>

int lockfile(int fd)
{
    struct flock fl;

    fl.l_type = F_WRLCK;
    fl.l_start = 0;
    fl.l_whence = SEEK_SET;
    fl.l_len = 0;
    return(fcntl(fd, F_SETLK, &fl));
}

int main(){

    pid_t pid;
    struct rlimit r;
    umask(0);

    if((pid = fork()) < 0){
        _exit(1);
    }
    else if(pid > 0){
        _exit(0); //父进程退出
    }
    //第一个子进程
    int i, openedfile,fd0,fd1,fd2;
    setsid();
    chdir("/");

    signal(SIGHUP,SIG_IGN);
    signal(SIGTTOU,SIG_IGN);
    signal(SIGTTIN,SIG_IGN);
    signal(SIGTSTP,SIG_IGN);
    signal(SIGCHLD,SIG_IGN);

    //关闭所有文件
    if(getrlimit(RLIMIT_NOFILE,&r) != 0){
        _exit(-1);
    }
    if(RLIMIT_NOFILE == RLIM_INFINITY){
        openedfile = 1024;
    }
    else
        openedfile = RLIMIT_NOFILE;

    for(i = openedfile; i >= 0;--i){
        close(i);
    }
    //重定向三个标准文件描述符
    fd0 = open("/dev/null",O_RDWR);
    fd1 = dup(fd0);
    fd2 = dup(fd0);

    if(fork() != 0 )
        _exit(0); //第一个子进程退出
    //第二个子进程开始

        int fd; //创建文件
        if((fd = open("my", O_CREAT|O_RDWR|O_APPEND,0666)) < 0){
            exit(-1);
        }
        //给文件加锁,实现单实例
        if(lockfile(fd) < 0){
            exit(-1);
            write(fd,"lock failed.\n",13);
        }
        else
            write(fd,"lock success.\n",14);

    //主要的任务
    while(1){
        char buf[100];
        sprintf(buf,"process id: %d\n",getpid());
        write(fd,buf,strlen(buf));
        /*千万不能关闭fd,关闭之后文件记录所也会解除,不能做到单实例*/
        // close(fd);
        sleep(2);
    }
}

为了结构更加清晰可以这样写:

#include <unistd.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <string.h>
#include <stdio.h>

int lockfile(int fd)
{
    struct flock fl;

    fl.l_type = F_WRLCK;
    fl.l_start = 0;
    fl.l_whence = SEEK_SET;
    fl.l_len = 0;
    return(fcntl(fd, F_SETLK, &fl));
}


int init_daemon(void){

    pid_t pid;
    struct rlimit r;
    // struct sigaction sa;
    umask(0);

    if((pid = fork()) < 0){
        _exit(1);
    }
    else if(pid > 0){
        _exit(0);
    }

    int i, openedfile,fd0,fd1,fd2;
    setsid();
    chdir("/");

    signal(SIGHUP,SIG_IGN);
    signal(SIGTTOU,SIG_IGN);
    signal(SIGTTIN,SIG_IGN);
    signal(SIGTSTP,SIG_IGN);
    signal(SIGCHLD,SIG_IGN);



    if(getrlimit(RLIMIT_NOFILE,&r) != 0){
        _exit(-1);
    }

    if(RLIMIT_NOFILE == RLIM_INFINITY){
        openedfile = 1024;
    }
    else
        openedfile = RLIMIT_NOFILE;

    for(i = openedfile; i >= 0;--i){
        close(i);
    }

    fd0 = open("/dev/null",O_RDWR);
    fd1 = dup(fd0);
    fd2 = dup(fd0);

    if(fork() != 0 )
        _exit(0);

    return 0;
}

int main(){

    int fd;
    init_daemon();
    if((fd = open("my", O_CREAT|O_RDWR|O_APPEND,0666)) < 0){
        exit(-1);
    }
    if(lockfile(fd) < 0){
        exit(-1);
        write(fd,"lock failed.\n",13);
    }
    else
        write(fd,"lock success.\n",14);
    /* 守护进程中做的事 */
    while(1){

        char buf[100];
        sprintf(buf,"process id: %d\n",getpid());
        write(fd,buf,strlen(buf));

        // close(fd);
        sleep(2);
    }
}

杂项

fork两次的原因:

  • 调用一次fork的作用:
    第一次fork的作用是让shell认为这条命令已经终止,不用挂在终端输入上,还有就是为了后面的setsid服务,因为调用setsid函数的进程不能是进程组组长,如果不fork出子进程,则此时的父进程是进程组组长,就无法调用setsid。当子进程调用完setsid函数之后,子进程是会话组长也是进程组组长,并且脱离了控制终端,此时,不管控制终端如何操作,新的进程都不会收到一些信号使得进程退出。

  • 第二次fork的作用:
    虽然当前关闭了和终端的联系,但是后期可能会误操作打开了终端。
    只有会话首进程能打开终端设备,也就是再fork一次,再把父进程退出,再次fork的子进程作为守护进程继续运行,保证了该精灵进程不是对话期的首进程。

  • 第二次不是必须的,是可选的。

/proc/进程号/目录下面是关于进程使用的资源的信息。例如:ls -al /proc/进程号/fd显示进程打开文件:
这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值