1 完整程序:单实例守护进程
根据APUE的介绍,创建守护进程基本需要如下7个步骤。需要注意的是由于守护进程没有TTY(控制终端),所以代码中部分特意写上去的printf语句是不会输出到终端界面上的。
/**
* @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)创建成功了。
4 创建第二个守护进程
如果尝试再次创建一个同样的守护进程,如下图所示。执行命令ps -axj|head -n 1; ps -axj|grep daemon_process
,发现仍然只有一个守护进程(PID:25874),也就是创建第二个守护进程失败。
这时使用cat /var/log/message
命令查看进程在Linux系统日志文件中打印的内容,如下图所示:
很显然程序执行到第160行,由于第一个守护进程对LOCK_FILE文件加锁的缘故而无法获取该文件的访问权限最终导致子进程(PID:25909)终止。于是,第二次创建守护进程失败了。
5 附录
关于openlog
和syslog
函数的使用方法,可以通过命令man 3 syslog
查看,大概就是根据日志标识符(ident)和日志level(LOG_EMERG、LOG_ERR、LOG_WARNING等)和日志文件类型facility(LOG_CRON、LOG_MAIL、LOG_SYSLOG和默认的LOG_USER等)把进程的内容输出到Linux系统某一类型的日志文件中。