1. daemon 是后台进程
后台进程有些是内核的(kernel daemons),通常他们的父进程号是0,他们在系统启动阶段启动;有些是用户层的(user-level daemons),所有用户层后台程序是 process group leaders 和 session leaders,而且进程组和会话中只有它一个进程,它们中大部分的父进程是init;
在Ubuntu 10.10 下,ps -efjc 查看运行中的进程,可以看到像kswapd这样的内核后台进程,它们的父进程号是2,而不是0,进程号为2的进程就是kthreadd,如下图:
daemon 没有控制终端,显示为?
init 进程号是1,是第一个启动用户进程,用于启动系统服务;(具体参考LSAH p32)
initd 用于启动网络服务,不过不是在系统初始化时启动,而是延迟到有网络连接请求的时候在启动相应的服务,它会帧听配置文件中的所有网络端口,所以它也叫super-server;linux下的inet是xinetd,即extended inet;(具体参考man xinet)
2. 编程规则
1. 调用 umask 将文件模式创建屏蔽字设置为0,不屏蔽任何权限;
2. 调用 fork,任何使父进程退出;
执行 shell 命令的时候,shell 会 fork 一个进程,如果是通过 shell 命令来启动该 daemon 的,那么 shell 认为这条命令已经执行完毕了,而 daemon 程序已经在后台执行了;
子进程继承了父进程的进程组ID,但不是进程组的 leader,这是调用 setsid,并成为 session leader 的前提;
3. 调用 setsid 创建一个新会话。那么这个 daemon 程序就成为了新进程组的leader,而且没有控制终端,如果调用之前有,也会被解除关联。(控制终端,参考 9.6)
4. 当前工作目录改为根目录。daemon 的父进程的当前目录可以是一个 mounted 的目录,如果daemon继承后不改变的话,那么这个文件系统不能在 daemon 运行的时候不能被卸载,这不是我们希望的,而根文件系统随系统一直存在;
一些特殊的 daemon 也会设置到其他目录;
5. 关闭不需要的文件描述符;
6. 在/dev/null 上 打开文件描述符 0, 1, 2,这么做是为了关闭不必要的交互功能,因为有些例程试图读取标准输入/输出/错误;因为没有控制终端,所以 daemon 的任何输出,不会出现在认识显示设备上,也会从交互设备上的到任何输入(键盘等);同时,会话的关闭也不会影响 daemon ;
下面是一个书中 daemon 初始化的代码,关注第二个 fork:
#include "apue.h" #include <syslog.h> #include <fcntl.h> #include <sys/resource.h> void daemonize(const char *cmd) { int i, fd0, fd1, fd2; pid_t pid; struct rlimit rl; struct sigaction sa; /* * 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 */ // 结束 shell 命令父进程 exit(0); 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) // 保证 daemon 不是 seesion leader,不会有机会得到控制终端,参考 1.3.3 ruler 3,9.6 err_quit("%s: can't fork", cmd); else if (pid != 0) /* parent */ exit(0); /* * 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); /* * 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. */ 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); } }
3. 日志
daemon 不能写写终端,它通过日志记录出错信息;
产生日志消息的三种方式:
内核调用 log 函数
大多数用户进程调用 syslog 函数
通过网络编程,发送 UDP 数据包到端口 514
上面三种方式产生的日志,都会被 syslogd 读取,syslogd 相关资料参考 LAH 第10章
用户进程调用的 syslog 函数及相关函数原型如下:
indent 指向的字符串会被加入到日志消息中去,一般是写 程序名称;#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 maskpri);
option 指示日志消息处理方式,LOG_CONS 必要是发送到终端;LOG_NDELAY 直接打开用于发送日志消息的 socket ,而不能等到写入第一个消息的时候才产生,和LOG_ODELAY相反;LOG_NOWAIT 和等待子进程的 wait 函数相关;LOG_PERROR 不仅把日志消息发送非 syslogd ,还发送到标准输出上;LOG_PID 消息中包含进程ID
facility 表示消息的来源,或者类型
Figure 13.4. The facility argument for openlog
level 指示日志消息的重要程度,这里可以了解一些单词的用法:emergency > alert > critical > error > warning > notice > information > debug
例子:
syslog (LOG_ERR | LOG_LPR, "open error for %s: %m", filename);
syslog 之前没有 openlog的话,可以通过上面的方式来指定 priority 类型;
4. daemon 有时候需要保证只有一个例程在运行,这是需要用到文件和记录锁机制:daemon 创建一个文件,然后给这个文件加一把写入锁。-->这个文件一般就是 /var/run/ 下面的 *.pid 文件;
5. Daemon Conventions
1. 锁文件通常是/var/run/*.pid;
2. 配置文件通常在/etc/,名字为name.conf,如/etc/syslog.conf;
3. 守护进程可用命令行启动,通常由系统初始化脚本( /etc/rc* 或者 /etc/inittab )启动。如果需要在守护进程终止是,能自动重启它,需要在 /etc/inittab 中为该守护进程添加 _respawn 记录项;
4. 守护进程可以通过捕捉 SIGHUP 信号,来重新读取配置文件;