TypeCodes

UNIX环境高级编程(APUE)之单实例守护进程

在UNIX环境高级编程(APUE)中提到了守护进程的创建方法,思路很清晰,所以这里通过代码具体研究下。

UNIX环境高级编程(APUE)之单实例守护进程

1 完整程序:单实例守护进程

根据APUE的介绍,创建守护进程基本需要如下7个步骤。需要注意的是由于守护进程没有TTY(控制终端),所以代码中部分特意写上去的printf语句是不会输出到终端界面上的。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
/**
 * @FileName    daemon_process.c
 * @Describe    A simple example for creating a single object of daemon process in linux.
 * @Author      vfhky 2016-03-14 17:52 https://typecodes.com/cseries/apuesingledaemonprocess.html
 * @Compile     gcc daemon_process.c -o daemon_process
 * @Reference   program list 13-1 in APUE.
 */
#include <stdio.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <syslog.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/resource.h>
#include <errno.h>

#define PRINT_PID() printf( "Row[%d]: getpid=[%d].\n", __LINE__, getpid() )
//守护进程对应的用户必须对该文件具有访问权限
#define LOCK_FILE "/home/vfhky/daemon_process.pid"
#define MAXLINE 1024


/**
 * Print a message and return to caller.
 * Caller specifies "errnoflag".
 */
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap)
{
    char    buf[MAXLINE];

    vsnprintf(buf, MAXLINE, fmt, ap);
    if (errnoflag)
        snprintf(buf+strlen(buf), MAXLINE-strlen(buf), ": %s",
                 strerror(error));
    strcat(buf, "\n");
    fflush(stdout);     /* in case stdout and stderr are the same */
    fputs(buf, stderr);
    fflush(NULL);       /* flushes all stdio output streams */
}

/**
 * Fatal error unrelated to a system call.
 * Print a message and terminate.
 */
void err_quit(const char *fmt, ...)
{
    va_list     ap;

    va_start(ap, fmt);
    err_doit(0, 0, fmt, ap);
    va_end(ap);
    exit(1);
}


void daemonize(const char *cmd)
{
    int                 i, fd0, fd1, fd2;
    pid_t               pid;
    struct rlimit       rl;
    struct sigaction    sa;

    /**
     * 第一步:设置文件模式屏蔽字为0
     * Clear file creation mask.
     */
    umask(0);

    /**
     * 获取最大的文件描述符数目
     * Get maximum number of file descriptors.
     */
    if (getrlimit(RLIMIT_NOFILE, &rl) < 0)
        err_quit("%s: can't get file limit", cmd);

    /**
     * 第二步:创建一个子进程,使父进程退出
     * Become a session leader to lose controlling TTY.
     */
    if ((pid = fork()) < 0)
        err_quit("%s: can't fork", cmd);
    else if (pid != 0) /* parent */
        exit(0);

    PRINT_PID();

    /**
     * 第三步:创建一个新的会话ID
     */
    setsid();

    /**
     * Ensure future opens won't allocate controlling TTYs.
     */
    sa.sa_handler = SIG_IGN;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    if ( sigaction(SIGHUP, &sa, NULL) < 0 )
        err_quit( "%s: can't ignore SIGHUP" );
    if( ( pid = fork() ) < 0 )                  //再次创建一个子进程,同样使父进程退出
        err_quit("%s: can't fork", cmd);
    else if( pid != 0 ) /* parent */
        exit(0);

    PRINT_PID();

    /**
     * 第四步:切换当前工作目录到根目录
     * Change the current working directory to the root so
     * we won't prevent file systems from being unmounted.
     */
    if (chdir("/") < 0)
        err_quit("%s: can't change directory to /");

    /**
     * 第五步:关闭所有打开的文件描述符
     * Close all open file descriptors.
     */
    if (rl.rlim_max == RLIM_INFINITY)
        rl.rlim_max = 1024;
    for (i = 0; i < rl.rlim_max; i++)
        close(i);

    /**
     * 第六步:使/dev/null具有文件描述符0,1,2.
     * Attach file descriptors 0, 1, and 2 to /dev/null.
     */
    fd0 = open("/dev/null", O_RDWR);
    fd1 = dup(0);
    fd2 = dup(0);

    /**
     * Initialize the log file.
     * @para-in:    cmd: the identifier in the log.
     */
    openlog( cmd, LOG_CONS, LOG_DAEMON );
    if( fd0 != 0 || fd1 != 1 || fd2 != 2 )
    {
        syslog( LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2 );
        exit(1);
    }

    /**
     * 第七步:通过文件锁避免重复运行多个守护进程
     */
    int lockfd = open( LOCK_FILE, O_RDWR );
    if( lockfd < 0 )
    {
        syslog( LOG_ERR, "Cannot lock file[%s], aborting[%s].\n", LOCK_FILE, strerror(errno) );
        //下面这一行无法打印到控制台,项目上应该打印到日志文件中
        printf( "Cannot lock file[%s], aborting[%s].\n", LOCK_FILE, strerror(errno) );
        exit(-1);
    }
    if( lockf(lockfd,F_TLOCK,0) < 0 )
    {
        syslog( LOG_ERR, "Daemon process is already running[%s].\n", strerror(errno) );
        //下面这一行无法打印到控制台,项目上应该打印到日志文件中
        printf( "Daemon process is already running[%s].\n", strerror(errno) );
        exit(-2);
    }
}

int main( int argc, char **argv )
{
    PRINT_PID();
    daemonize( "Daemon test." );
    //由于父进程退出,所有只有最后一个子进程执行下面的语句(休眠)
    printf( "This line will not be print for the daemon process has no terminate.\n" );
    while(1)
        sleep(120);
    return 0;
}

2 程序编译

使用《Linux C/C++工程中可生成ELF、动/静态库文件的通用Makefile》一文中的Makefile文件进行程序编译,当然也可以使用命令进行编译gcc daemon_process.c -o daemon_process

3 创建第一个守护进程

如下图所示,程序先执行第178行main函数中的打印语句,输出当前第一个进程的PID值为25872;然后由于在daemonize函数中第一个进程(PID:25872)退出,所以它的子进程(PID:25873)执行第101行的打印语句;接着由于第二个进程(PID:25873)退出,那么它的子进程(PID:25874)执行第121行的打印语句;在关闭了所有文件描述符后,该子进程(PID:25874)打开标准输入/输出/错误流,最后该子进程成为由Linux系统init进程托管的孤儿进程,没有终端terminal,这也就是守护进程。

创建第一个守护进程

其中使用ps -axj|head -n 1; ps -axj|grep daemon_process命令发现子进程(PID:25874)的父进程为1进程(init进程),终端TTY为空。

接着使用命令pstree -pul查看当前用户的所有进程情况,如下图所示,再次说明守护进程(PID:25874)创建成功了。

使用命令pstree -pul查看用户进程

4 创建第二个守护进程

如果尝试再次创建一个同样的守护进程,如下图所示。执行命令ps -axj|head -n 1; ps -axj|grep daemon_process,发现仍然只有一个守护进程(PID:25874),也就是创建第二个守护进程失败。

创建第二个守护进程失败

这时使用cat /var/log/message命令查看进程在Linux系统日志文件中打印的内容,如下图所示:

查看/var/log/message日志内容

很显然程序执行到第160行,由于第一个守护进程对LOCK_FILE文件加锁的缘故而无法获取该文件的访问权限最终导致子进程(PID:25909)终止。于是,第二次创建守护进程失败了。

5 附录

关于openlogsyslog函数的使用方法,可以通过命令man 3 syslog查看,大概就是根据日志标识符(ident)和日志level(LOG_EMERG、LOG_ERR、LOG_WARNING等)和日志文件类型facility(LOG_CRON、LOG_MAIL、LOG_SYSLOG和默认的LOG_USER等)把进程的内容输出到Linux系统某一类型的日志文件中。

SYNOPSIS
    #include <syslog.h>

    void openlog(const char *ident, int option, int facility);
    void syslog(int priority, const char *format, ...);
    void closelog(void);

DESCRIPTION
    closelog()  closes  the descriptor being used to write to the system logger.
    The use of closelog() is optional.

    openlog() opens a connection to the system logger for a program.  The string
    pointed  to  by ident is prepended to every message, and is typically set to
    the  program  name.   If  ident  is  NULL,  the  program   name   is   used.
    (POSIX.1-2008 does not specify the behavior when ident is NULL.)

    The option argument specifies flags which control the operation of openlog()
    and subsequent calls to  syslog().   The  facility  argument  establishes  a
    default  to  be  used  if none is specified in subsequent calls to syslog().
    Values for option and facility are given below.  The  use  of  openlog()  is
    optional; it will automatically be called by syslog() if necessary, in which
    case ident will default to NULL.

    syslog() generates a log message, which will be distributed  by  syslogd(8).
    The  priority  argument is formed by ORing the facility and the level values
    (explained below).  The remaining arguments are a format,  as  in  printf(3)
    and  any  arguments  required  by  the format, except that the two character
    sequence %m will be replaced by the error message string strerror(errno).  A
    trailing newline may be added if needed.

Comments »