关键词:daemon、fork、setsid、umask、chdir、syslog等等。
本章主要包括两个方面:daemon进程特征以及如何创建一个daemon进程;如何使用syslog工具记录消息。
1. 创建daemon以及注意点
1.1 daemon进程特征
生命周期很长,在系统启动时候被创建并一直运行直至系统被关闭。
在后台运行并且不拥有控制终端。控制终端的缺失确保了内核永远不会为daemon自动生成任何任务控制信号以及终端相关的信号(SIGINT、SIGTSTP、SIGHUP)。
1.2 创建一个daemon(P629-P631)
要变成一个daemon,一个应用需要完成下面步骤:
1. 执行一个fork(),之后父进程退出,子进程继续执行。结果是daemon成为了init进程的子进程。因为:
1.1 假设daemon是从命令行启动的,父进程的终止会被shell发现,shell在发现之后会显示出另一个shell提示符并让子进程继续在后台运行。
1.2 子进程被确保不会成为一个进程组首进程,因为它从其父进程那里继承了进程组ID并且拥有了自己的唯一的进程ID,而这个进程ID与继承而来的进程组ID是不同的。
2. 子进程调用setsid()开启一个新会话并释放它与控制终端之间的所有关联关系。
3. 如果daemon从来没有打开过终端设备,那么就无需担心daemon会重新请求一个控制终端了。如果daemon后面可能会打开一个终端设备,那么必须采取措施确保这个设备不会成为控制终端。
3.1 在所有可能应用到一个终端设备上的open()调用中指定O_NOCTTY标记。
3.2 在setsid()调用之后执行第二个fork(),然后再次让父进程退出并让孙子进程继续执行。这样就确保了子进程不会成为会话组长。
4. 清除进程的umask(),以确保当daemon创建文件和目录时拥有所需的权限。
5. chdir()修改进程的当前工作目录,通常会改为根目录(/)。这样做是因为,如果daemon工作目录不是根目录的某个文件系统下,那么就可能无法卸载该文件系统。
6. 关闭daemon从其父进程继承而来的所有打开着的文件描述符。一是因为daemon失去了控制终端,0/1/2文件描述符毫无意义;二是文件描述符也是系统有限资源。
7. 在关闭了文件描述符0/1/2之后,daemon通常会打开/dev/null并使用dup2()使所有这些描述符指向这个设备。
7.1 它确保了当daemon调用这些描述符执行io库函数时不会出乎意料地失败。
7.2 它防止了daemon后面使用描述符1或2打开一个文件的情况。
下面创建becomeDaemon()函数完成上面步骤。
/* Bit-mask values for 'flags' argument of becomeDaemon() */ #define BD_NO_CHDIR 01 /* Don't chdir("/") */ #define BD_NO_CLOSE_FILES 02 /* Don't close all open files */ #define BD_NO_REOPEN_STD_FDS 04 /* Don't reopen stdin, stdout, and stderr to /dev/null */ #define BD_NO_UMASK0 010 /* Don't do a umask(0) */ #define BD_MAX_CLOSE 8192 /* Maximum file descriptors to close if sysconf(_SC_OPEN_MAX) is indeterminate */ /* Listing 37-2 */ /* become_daemon.c A function encapsulating the steps in becoming a daemon. */ #include <sys/stat.h> #include <fcntl.h> #include "become_daemon.h" #include "tlpi_hdr.h" int /* Returns 0 on success, -1 on error */ becomeDaemon(int flags) { int maxfd, fd; 1. switch (fork()) { /* Become background process */ case -1: return -1;--------------------------------------------------------------fork()返回-1表示失败,0表示进入子进程,其他表示父进程。 case 0: break; /* Child falls through... */-----------------处于子进程。 default: _exit(EXIT_SUCCESS); /* while parent terminates */----------------处于父进程,然后退出父进程。 } 2. if (setsid() == -1) /* Become leader of new session */----------调用setsid()成功的进程变成新会话的首进程,即新创建的子进程变成了会话首进程,并且没有控制终端。 return -1; 3. switch (fork()) { /* Ensure we are not session leader */ case -1: return -1; case 0: break;-----------------------------------------------------------------进入孙子进程。 default: _exit(EXIT_SUCCESS); } 4. if (!(flags & BD_NO_UMASK0)) umask(0); /* Clear file mode creation mask */--------umask(mask)则后面文件模式mask取(mask&0777),表示此进程创建的文件不能具备mask指定权限。mask=0表示可取所有权限。 5. if (!(flags & BD_NO_CHDIR)) chdir("/"); /* Change to root directory */-------------更改工作目录到根目录。 6. if (!(flags & BD_NO_CLOSE_FILES)) { /* Close all open files */ maxfd = sysconf(_SC_OPEN_MAX);---------------------------------------------_SC_OPEN_MAX表示进程可以打开文件最大数目。 if (maxfd == -1) /* Limit is indeterminate... */ maxfd = BD_MAX_CLOSE; /* so take a guess */ for (fd = 0; fd < maxfd; fd++) close(fd);-------------------------------------------------------------逐个关闭文件描述符。 } 7. if (!(flags & BD_NO_REOPEN_STD_FDS)) { close(STDIN_FILENO); /* Reopen standard fd's to /dev/null */ fd = open("/dev/null", O_RDWR); if (fd != STDIN_FILENO) /* 'fd' should be 0 */--------------------确保STDIN_FILENO指向/dev/null。 return -1; if (dup2(STDIN_FILENO, STDOUT_FILENO) != STDOUT_FILENO)-------------------使STDOUT_FILENO和STDERR_FILENO同样指向/dev/null。 return -1; if (dup2(STDIN_FILENO, STDERR_FILENO) != STDERR_FILENO) return -1; } return 0; }
1.3 编写daemon指南
在系统关闭的时候init进程会向所有子进程发送SIGTERM信号,init在发完SIGTERM信号5秒后会发送一个SIGKILL信号。
SIGTERM默认情况下会终止一个进程。如果daemon在终止之前需要做些清理工作,那么就需要为SIGTERM注册一个处理器。这个处理器必须尽快完成,因为5秒后将收到SIGKILL。
由于daemon长时间运行,所以还需要确保没有内存泄漏和文件描述符泄漏等异常。
daemon还可以通过SIGHUP信号,实现一种机制让daemon通过获取配置文件并重新打开所使用的所有日志文件来重新初始化自身。
2. syslog记录
更全面的消息参考《Busybox的syslogd认识与使用》。
从下面架构图可以看出syslog工具主要分为两部分:syslogd daemon和syslog()函数。
syslogd从socket /dev/log获取内核(klogd)或者应用(syslog())日志,根据syslog.conf的配置输出到不同设备上。