1.守护进程创建步骤
守护进程是没有终端的进程, 运行在后台, 常在系统引导时启动. 那么如何创建守护进程呢?
参照APUE 13.3, 创建守护进程步骤:
- 调用umask设置进程创建文件的权限屏蔽字(umask), 便于守护进程创建文件
umask通常设为0, 如果调用库函数创建文件, 可设置为007 - 调用fork, 父进程exit
因为要调用setsid创建会话, 需要确保调用进程(子进程)不是进程组组长, fork子进程可以确保这点.
PS:不确定程序以何种方式启动,有可能是进程组组长,也有可能不是。 - 调用setsid创建新会话
子进程调用setsid, 成为新会话首进程, 新进程组组长, 断开终端连接。
PS:对进程组组长调用setsid,会调用失败,返回-1,errno被设置。因此,必须先通过fork+父进程exit,这样子进程会成为孤儿进程,被init收养,从而确保子进程不是进程组长。 - 再次调用fork, 父进程exit(可选)
非必须, 主要是为了确保进程无法通过open /dev/tty 再次获得终端, 因为调用open时, 系统会默认为会话首进程创建控制终端。 - 调用chdir, 将当前工作目录更改为根目录
守护进程一般长期存在, 守护进程存在时, 无法卸载工作目录. 为避免这种情况, 更改当前工作目录为根目录("/"). - 调用close, 关闭所有不需要的文件描述符
open_max, getrlimit, sysconf(_SC_OPEN_MAX) 这3个函数都可以获得文件描述符最高值 - 打开/dev/null文件, 让文件描述符0,1,2指向该文件
可以有效防止产生意外效果
2. 创建守护进程代码
自定义函数daemonize将一个进程转化为守护进程
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <errno.h>
#include <unistd.h>
#include <syslog.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdarg.h>
#define MAXLINE 200
static void err_doit(int errnoflag, int error, const char *fmt, va_list ap) {
char buf[MAXLINE];
// 将格式化串fmt (参数ap) 转换成字符串存放到buf
vsnprintf(buf, MAXLINE - 1, fmt, ap);
if (errnoflag) {
// snprintf最后一个参数是const char*, vsnprintf最后一个参数是va_list, 功能一样
snprintf(buf + strlen(buf), MAXLINE - strlen(buf) - 1, ": %s", strerror(error));
}
strcat(buf, "\n"); // buf末尾粘贴 "\n", 并以'\0'结束
fflush(stdout); // 以防stdout, stderr是相同设备, 先冲刷stdout
fputs(buf, stderr);
}
/**
* 与系统调用相关的致命错误
* 打印消息和终止程序
*/
static void err_quit (const char *fmt, ...) {
va_list ap;
// va_start, va_end 配对获取可变参数..., 存放到va_list ap中
va_start(ap, fmt);
err_doit(0, 0, fmt, ap);
va_end(ap);
exit(1);
}
/**
* 将一个进程转换为守护进程
* 步骤:
* 1. 调用umask设置创建文件权限的umask
* 2. 调用fork, 让父进程退出, 子进程成为孤儿进程
* 3. 调用setsid, 创建新会话, 子进程成为新会话首进程以及进进程组组长, 断开控制终端
* 4. 再次调用fork, 并让父进程退出, 子进程不是会话首进程(防止再次获得控制终端)
* 5. 将当前工作目录修改为根目录, 防止无法卸载目录
* 6. 关闭不需要的文件描述符
* 7. 打开/dev/null, 使其具有文件描述符0,1,2
*/
void daemonize(const char *pname) {
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
// 1. 调用umask修改创建文件权限的umask
umask(0);
if (getrlimit(RLIMIT_NOFILE, &rl) < 0) {
// <=> sysconf(_SC_OPEN_MAX);
err_quit("%s: getrlimit error", pname);
}
// 2. 调用fork, 父进程退出, 子进程称为孤儿被init进程收养
if ((pid = fork()) < 0)
err_quit("%s: fork error", pname);
else if (pid > 0) { // 父进程
exit(0);
}
// 3. 调用setsit, 创建新会话, 子进程成为首进程
setsid();
/* 发送SIGHUP信号情形:
1)终端关闭时, 信号被发送到session首进程, 以及作为job提交的进程(shell 以&方式运行的进程)
2)session首进程退出时, 该信号被发送到同session的所有前台进程
3)若父进程退出, 导致进程组成为孤儿进程组, 且该进程组中有进程处于停止状态(收到SIGSTOP信号或SIGSTP), 该信号会被发送到进>程组每个成员
*/
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", pname);
// 4. 再次调用fork
if ((pid = fork()) < 0)
err_quit("%s: fork error", pname);
else if (pid > 0)
exit(0);
// 5. 改变工作目录为 "/"(根目录)
if (chdir("/") < 0)
err_quit("%s: can't change directory to /", pname);
// 6. 关闭不需要的文件描述符
if (rl.rlim_max == RLIM_INFINITY)
rl.rlim_max = 1024;
for (i = 0; i < rl.rlim_max; ++i)
close(i);
#if 1
// 7. 附加文件描述符0,1,2到 /dev/null
// 因为前面已经关闭了所有文件描述符, 因此重新open, dup得到的文件描述符是递增的
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
#else
open("/dev/null", O_RDONLY);
open("/dev/null", O_RDWR);
open("/dev/null", O_RDWR);
#endif
/* 建立到syslog的连接
LOG_CONS: 若无法发送到syslogd守护进程则登记到控制台
LOG_DAEMON: 标识消息发送进程的类型为 系统守护进程 */
openlog(pname, LOG_CONS, LOG_DAEMON);
/* 文件描述符异常检查: 正常情况fd0, fd1, fd2应该分别是0,1,2 */
if (fd0 != 0 || fd1 != 1 || fd2 != 2) {
syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
exit(1);
}
}
main函数
#include <stdio.h>
#include <unistd.h>
#include "daemonize.h"
int main() {
char *s = "mydaemonize";
printf("ready to convert a normal process into a daemonize process\n");
daemonize(s);
while(1)
sleep(1);
return 0;
}
检查守护进程
编译, 运行新建的守护进程a.out
$ ./a.out
$ ps -efj
UID PID PPID PGID SID C STIME TTY TIME CMD
martin 12596 1614 12595 12595 0 23:27 ? 00:00:00 ./a.out
$ ps -efj | grep 12595
martin 12596 1614 12595 12595 0 23:27 ? 00:00:00 ./a.out
终结守护进程
用kill -9命令
$ kill -9 12596