守护进程编写规则与出错记录
程序员文章站
2022-07-01 09:17:18
...
在编写守护进程程序时需要遵循以下一些基本规则,以防产生不必要的交互作用:
(1)应先调用 umask 将文件模式创建屏蔽字设置为一个已知值(通常是 0),因为由继承得来的文件模式屏蔽字可能会被设置为拒绝某些权限。而另一方面,如果守护进程调用的库函数创建了文件,那么将文件模式创建屏蔽字设置为一个限制性更强的值(如 007)可能更明智,因为库函数可能不允许调用者通过一个显示的函数参数来设置权限。
(2)调用 fork,然后使父进程 exit。这就保证了子进程不是一个进程组的组长进程,这是下一步调用 setsid 的先决条件。
(3)调用 setsid 创建一个新会话,这会使调用进程:a、成为新会话的首进程,b、成为一个新进程组的组长进程,c、没有控制终端(在基于 System V 的系统中,有人建议在此时再次调用 fork,终止父进程,这就保证了该守护进程不是会话首进程,于是按照 System V 规则可以防止它取得控制终端。另外,打开一个终端设备时指定 O_NOCTTY 标志也可以避免取得控制终端)。
(4)将当前工作目录更改为根目录。从父进程继承的当前工作目录可能在一个挂载的文件系统中。因为守护进程通常在系统引导之前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载(不过也有些守护进程可能会把当前工作目录更改到某个指定位置来进行全部工作,例如行式打印机假脱机守护进程就可能将其工作目录更改到它们的 spool 目录上)。
(5)关闭不需要的文件描述符。这使守护进程不再持有从父进程继承来的文件描述符。可以使用 open_max 函数或 getrlimit 函数来判定最高文件描述符,并关闭直到该值的所有描述符。
(6)某些守护进程打开 /dev/null 使其具有文件描述符 0、1 和 2,这样任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。
守护进程存在的一个问题是如何处理出错消息。因为它本就不应该有控制终端,所以不能只是简单地写到标准错误上。而把每个守护进程的错误消息写到一个单独的文件中管理起来又比较麻烦,因此需要有一个集中的守护进程出错记录设施,这也是 syslog 设施得以广泛应用的原因。下图显示了 syslog 设施的详细组织结构。
从图中可见,有以下 3 中产生日志消息的方法。
(1)内核例程调用 log 函数。任何一个用户进程都可以通过打开 /dev/klog 设备来读取这些消息。
(2)大多数用户进程(守护进程)调用 syslog 函数来产生日志消息,这使消息被发送至 UNIX 域数据报套接字 /dev/log。
(3)无论一个用户进程是在此主机上,还是在通过 TCP/IP 网络连接到此主机的其他主机上,都可将日志消息发向 UDP 的 514 端口(syslog 函数从不产生这些 UDP 数据报,它们要求产生此日志消息的进程进行显示的网络编程)。
通常 syslogd 守护进程读取所有这 3 种格式的日志消息,它在启动时读一个一般叫 /etc/syslog.conf 的配置文件,该文件决定了不同种类的消息应发送向何处。
syslog 设施的接口是 syslog 函数。
其中,调用 openlog 是可选的,如果没调用,则在第一次调用 syslog 时会自动调用。closelog 调用也是类似,因为它只是关闭曾被用于与 syslogd 守护进程进行通信的描述符。
调用 openlog 时可以指定一个 ident(一般是程序的名称,如 cron 等),它会被加到每则日志消息中。option 参数用于指定各种选项的位屏蔽,下表介绍了可用的 option。
设置 openlog 的 facility 参数可以让配置文件说明,来自不同设施的消息将以不同的方式进行处理。如果不调用 openlog,或者设置 facility 为 0,那么在调用 syslog 时,可将 facility 作为 priority 参数的一部分来进行说明。下表是 facility 的可选值。
调用 syslog 产生一个日志消息,其 priority 参数是 facility 和 level 的组合。其中 facility 的取值见上图,而 level 值按优先级从高到低排列如下表。
syslog 的 format 参数以及其他所有参数都会被传至 vsprintf 函数以便格式化。在该参数中出现的每个 %m 字符都会代换成与 errno 值对应的出错消息字符串(strerror)。
setlogmask 函数用于设置进程的记录优先级屏蔽字,它返回调用它之前的屏蔽字。当设置了记录优先级屏蔽字时,各条消息除非已在记录优先级屏蔽字中进行了设置,否则将不被记录(注意,试图将记录优先级屏蔽字设置为 0 并不会有什么作用)。
大多数 syslog 实现将使消息短时间处于队列中,如果在此段时间中有重复消息到达,syslog 守护进程不会把它写到日志记录中,而是会打印输出一条类似于“上一条消息重复了 N 次”的消息。
此外,很多系统也将 logger 程序作为向 syslog 设施发送日志消息的方法,logger 命令是专门为以非交互方式运行的需要产生日志消息的 shell 脚本设计的。
除了 syslog,很多平台还提供它的一种变体 vsyslog 来处理可变参数列表,不过如果要使它的声明对应用程序可见,可能需要定义一个额外的符号,例如,FreeBSD 中要定义 __BSD_VISIBLE,Linux 中要定义 __USE_BSD。
根据以上所述,下面提供了一个可由一个想要初始化为守护进程的程序调用的实例。
运行结果如下:
可见,该守护进程被正确地初始化了,它已没有控制终端,第二个 fork 也使其不再是会话首进程。
(1)应先调用 umask 将文件模式创建屏蔽字设置为一个已知值(通常是 0),因为由继承得来的文件模式屏蔽字可能会被设置为拒绝某些权限。而另一方面,如果守护进程调用的库函数创建了文件,那么将文件模式创建屏蔽字设置为一个限制性更强的值(如 007)可能更明智,因为库函数可能不允许调用者通过一个显示的函数参数来设置权限。
(2)调用 fork,然后使父进程 exit。这就保证了子进程不是一个进程组的组长进程,这是下一步调用 setsid 的先决条件。
(3)调用 setsid 创建一个新会话,这会使调用进程:a、成为新会话的首进程,b、成为一个新进程组的组长进程,c、没有控制终端(在基于 System V 的系统中,有人建议在此时再次调用 fork,终止父进程,这就保证了该守护进程不是会话首进程,于是按照 System V 规则可以防止它取得控制终端。另外,打开一个终端设备时指定 O_NOCTTY 标志也可以避免取得控制终端)。
(4)将当前工作目录更改为根目录。从父进程继承的当前工作目录可能在一个挂载的文件系统中。因为守护进程通常在系统引导之前是一直存在的,所以如果守护进程的当前工作目录在一个挂载文件系统中,那么该文件系统就不能被卸载(不过也有些守护进程可能会把当前工作目录更改到某个指定位置来进行全部工作,例如行式打印机假脱机守护进程就可能将其工作目录更改到它们的 spool 目录上)。
(5)关闭不需要的文件描述符。这使守护进程不再持有从父进程继承来的文件描述符。可以使用 open_max 函数或 getrlimit 函数来判定最高文件描述符,并关闭直到该值的所有描述符。
(6)某些守护进程打开 /dev/null 使其具有文件描述符 0、1 和 2,这样任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。
守护进程存在的一个问题是如何处理出错消息。因为它本就不应该有控制终端,所以不能只是简单地写到标准错误上。而把每个守护进程的错误消息写到一个单独的文件中管理起来又比较麻烦,因此需要有一个集中的守护进程出错记录设施,这也是 syslog 设施得以广泛应用的原因。下图显示了 syslog 设施的详细组织结构。
从图中可见,有以下 3 中产生日志消息的方法。
(1)内核例程调用 log 函数。任何一个用户进程都可以通过打开 /dev/klog 设备来读取这些消息。
(2)大多数用户进程(守护进程)调用 syslog 函数来产生日志消息,这使消息被发送至 UNIX 域数据报套接字 /dev/log。
(3)无论一个用户进程是在此主机上,还是在通过 TCP/IP 网络连接到此主机的其他主机上,都可将日志消息发向 UDP 的 514 端口(syslog 函数从不产生这些 UDP 数据报,它们要求产生此日志消息的进程进行显示的网络编程)。
通常 syslogd 守护进程读取所有这 3 种格式的日志消息,它在启动时读一个一般叫 /etc/syslog.conf 的配置文件,该文件决定了不同种类的消息应发送向何处。
syslog 设施的接口是 syslog 函数。
#include <syslog.h> void openlog(const char *ident, int option, int facility); void syslog(int priority, const char *format, ...); void closelog(void); int setlogmask(int maskprl); /* 返回值:当前日志记录优先级屏蔽字值 */
其中,调用 openlog 是可选的,如果没调用,则在第一次调用 syslog 时会自动调用。closelog 调用也是类似,因为它只是关闭曾被用于与 syslogd 守护进程进行通信的描述符。
调用 openlog 时可以指定一个 ident(一般是程序的名称,如 cron 等),它会被加到每则日志消息中。option 参数用于指定各种选项的位屏蔽,下表介绍了可用的 option。
设置 openlog 的 facility 参数可以让配置文件说明,来自不同设施的消息将以不同的方式进行处理。如果不调用 openlog,或者设置 facility 为 0,那么在调用 syslog 时,可将 facility 作为 priority 参数的一部分来进行说明。下表是 facility 的可选值。
调用 syslog 产生一个日志消息,其 priority 参数是 facility 和 level 的组合。其中 facility 的取值见上图,而 level 值按优先级从高到低排列如下表。
syslog 的 format 参数以及其他所有参数都会被传至 vsprintf 函数以便格式化。在该参数中出现的每个 %m 字符都会代换成与 errno 值对应的出错消息字符串(strerror)。
setlogmask 函数用于设置进程的记录优先级屏蔽字,它返回调用它之前的屏蔽字。当设置了记录优先级屏蔽字时,各条消息除非已在记录优先级屏蔽字中进行了设置,否则将不被记录(注意,试图将记录优先级屏蔽字设置为 0 并不会有什么作用)。
大多数 syslog 实现将使消息短时间处于队列中,如果在此段时间中有重复消息到达,syslog 守护进程不会把它写到日志记录中,而是会打印输出一条类似于“上一条消息重复了 N 次”的消息。
此外,很多系统也将 logger 程序作为向 syslog 设施发送日志消息的方法,logger 命令是专门为以非交互方式运行的需要产生日志消息的 shell 脚本设计的。
除了 syslog,很多平台还提供它的一种变体 vsyslog 来处理可变参数列表,不过如果要使它的声明对应用程序可见,可能需要定义一个额外的符号,例如,FreeBSD 中要定义 __BSD_VISIBLE,Linux 中要定义 __USE_BSD。
#include <syslog> #include <stdarg.h> void vsyslog(int priority, const char *format, va_list arg);
根据以上所述,下面提供了一个可由一个想要初始化为守护进程的程序调用的实例。
#include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <syslog.h> #include <signal.h> #include <unistd.h> #include <sys/stat.h> #include <sys/resource.h> void daemonize(const char *cmd){ /* Clear file creation mask. */ umask(0); /* Become a session leader to lose controlling TTY. */ pid_t pid; if((pid=fork()) < 0){ printf("%s: can't fork\n", cmd); exit(1); }else if(pid != 0) // parent exit(0); setsid(); /* Ensure future opens won't allocate controlling TTYs. */ struct sigaction sa; sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; sigaction(SIGHUP, &sa, NULL); if((pid=fork()) < 0){ printf("%s: can't fork\n", cmd); exit(1); }else if(pid != 0) // parent exit(0); /* Change the current working directory to the root. */ chdir("/"); /* Close all open file descriptors. */ struct rlimit rl; getrlimit(RLIMIT_NOFILE, &rl); // maximum number of file descriptors if(rl.rlim_max == RLIM_INFINITY) rl.rlim_max = 1024; int i; for(i=0; i<rl.rlim_max; i++) close(i); /* Attach file descriptors 0, 1 and 2 to /dev/null. */ int fd0 = open("/dev/null", O_RDWR); int fd1 = dup(0); int fd2 = dup(0); /* Initialize the log file. */ 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 main(void){ daemonize("myDeamon"); sleep(60); exit(0); }
运行结果如下:
$ ./daemonize.out $ ps -efj | grep daemonize UID PID PPID PGID SID TTY CMD lei 94709 1 94708 94708 ? ./daemonize.out
可见,该守护进程被正确地初始化了,它已没有控制终端,第二个 fork 也使其不再是会话首进程。
上一篇: 三星Galaxy S21系列不标配充电器和耳机 官方解释原因
下一篇: POSIX 异步 I/O
推荐阅读