Linux系统编程学习笔记(十一)守护进程
守护进程:
守护进程是生存时间比较长的一种进程.它们常常在系统自举时启动,仅在系统关闭时才终止.因为它们没有控制终端,
所以说它们是在后台运行的.先来介绍一些Linux系统常见的守护进程:
init:它的pid为1,是系统守护进程,负责启动系统服务,这些服务通常自己也拥有守护进程.
keventd:为在内核中运行计划执行的函数提供进程上下文.
kapmd:对计算机系统中具有的高级电源管理提供支持.
kswapd:页面调出守护进程(PageoutDaemon),它通过将脏页面(DirtyPage)以低速写到磁盘上,使这些页面在需要时仍然可以回收使用,这种方式支持虚存子系统.
bdflush:当可用内存达到上限时,将脏缓冲区从缓冲池中冲洗到磁盘上.
kupdated:将脏页面写到到磁盘上.
portmap:提供将RPC(RemoteProcedureCall,远程过程调用)程序号映射到网络端口号的服务.
syslogd:提供把系统消息记入日志的接口,供需要的程序使用.可以打印到termino也可以写到文件.
inetd:侦听系统网络接口,以便取得来自网络的各种网络服务请求.
crond:在指定的日期和时间执行指定的命令.使定期地执行相关程序得意实现.
cupsd:打印假脱机进程,它处理对系统提出的所有打印请求.
nfsd,lockd,rpciod:提供对网络文件系统的支持(NetworkFileSystem).
2.编程规则:
1)要调用umask将文件模式创建屏蔽字设置为0.
2)调用fork,然后使父进程退出.这样做为了实现下面几点:
3)如果该守护进程是作为一条简单shell命令启动的,那么父进程终止使得shell认为这条命令已经执行完毕.3)
4)子进程继承了父进程的进程组ID,但具有一个新的进程ID,这就保证了子进程不是一个进程组的组长进程.这对于第三步的setsid调用是不要的前提.
5)调用setsid以创建一个新会话,使得调用进程:
成为新会话的首进程.
成为一个新进程组的组长进程.
没有控制终端.
6)将当前工作目录更改为根目录,这是为了防止当前工作目录在mount的文件系统中,而UNIX可以确保共同都有的目录是根目录
7)关闭不再需要的文件描述符.
某些守护进程打开/dev/null,并dup其为filedes012,使标准输入、标准输出、错误输出都不会产生任何效果.
例子:
#include <stdio.h> #include <stdlib.h> #include <syslog.h> #include <fcntl.h> #include <signal.h> #include <sys/resource.h> #include <sys/types.h> void demonize(const char *cmd){ int i,fd0,fd1,fd2; pid_t pid; struct rlimit r1; struct sigaction sa; /* clear the creation mask */ umask(0); /* Get maximum number of file descriptors */ if(getrlimit(RLIMIT_NOFILE,&r1) < 0){ perror("getrlimit"); exit(1); } /*Become a session leader to lose controlling tty */ if((pid = fork()) < 0){ perror("fork"); exit(1); }else if(pid != 0){//parent exit exit(0); } setsid(); /* Ensure future opens won't allocate controlling tty */ sa.sa_handler = SIG_IGN; sigemptyset(&sa.sa_mask); sa.sa_flags = 0; if(sigaction(SIGHUP,&sa,NULL) < 0){ perror("sigaction"); exit(1); } /* 第二次fork */ if((pid = fork()) < 0){ perror("fork"); exit(1); }else if(pid != 0){/*parent exit*/ exit(0); } /* Change the current working directory to root so we won't prevent file systems from being unmounted. */ if(chdir("/") < 0){ perror("chdir"); exit(0); } /* Close all open file descriptors */ if(r1.rlimit_max == RLIM_INFINITY) rl.rlim_max = 1024; for(i = 0; i < r1.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); } }
你可能会对程序的第二次的fork感到疑惑,原因是:在基于系统V的系统中,有些人建议在此时再次调用fork,使父进程终止。第二个子进程(其实是孙子进程)
作为守护进程继续运行。这就保证了该守护进程不是会话首进程,于是按照系统V规则(见APUE9.6节)可以防止它取得控制终端。避免取得控制终端的另一种方法
是,无论何时打开一个终端设备都一定要指定O_NOTTY。
我们可以看一下Memcached的Daemon.c源代码,这个代码和书上的例子类似:
#include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include "memcached.h" int daemonize(int nochdir, int noclose) { int fd; switch (fork()) { case -1: return (-1); case 0: break; default: _exit(EXIT_SUCCESS); } if (setsid() == -1) return (-1); if (nochdir == 0) { if(chdir("/") != 0) { perror("chdir"); return (-1); } } if (noclose == 0 && (fd = open("/dev/null", O_RDWR, 0)) != -1) { if(dup2(fd, STDIN_FILENO) < 0) { perror("dup2 stdin"); return (-1); } if(dup2(fd, STDOUT_FILENO) < 0) { perror("dup2 stdout"); return (-1); } if(dup2(fd, STDERR_FILENO) < 0) { perror("dup2 stderr"); return (-1); } if (fd > STDERR_FILENO) { if(close(fd) < 0) { perror("close"); return (-1); } } } return (0); }
3、错误日志:
守护进程是没有控制终端的,所以不能简单的向标准错误输出.我们也不想每一个daemon进程都有自己的错误日志,这样会使管理员去跟踪那个
进程写入了那个日志文件感到头痛,在Unix中,有一个集中的守护进程出错记录设施,它就是syslog。
有3种方法产生日志消息:
1)内核例程可以调用log函数.任何一个用户进程通过读取/dev/klog就可以获得这些信息,当然你要先打开这个设备.
2)大多数用户进程(守护进程)调用syslog函数产生日志消息.这使消息发送至UNIX域UDPSocket:/dev/log.
3)在此主机上的一个用户进程,或通过TCP/IP网络连接到此主机上的一个用户进程可将日志消息发向UDP端口514.
注意,syslog并不产生这些UDP数据报,而是要求产生此日志消息的进程进行显示的网络编程.
下面看一下syslog的函数:
#include<syslog.h> void openlog(const char *ident, int option, int facility); void syslog(int priority, const char *format, ...); void closelog(); int setlogmask(int maskpri); //返回之前日志记录优先级掩码值.
说明:
openlog调用是可选择的,如不调用,在第一次调用syslog时会自动调用openlog
closelog调用也是可选择的,它只关闭曾用于与syslogd守护进程通信的描述符.
参数:
ident:log会把它加到每则日志消息中,它一般是程序的名称(如inetd).
option:指定位屏蔽选项.
LOG_CONS:若日志消息不能通过UNIX域数据报送至syslogd,则该消息写至控制台.
LOG_NDELAY:立即打开至syslogd的UNIX域数据报socket,而不等记录第一条消息.
LOG_NOWAIT:不等待在将消息记入日志过程中可能创建的子进程.阻塞了与捕捉SIGCHLD信号的应用程序冲突.
LOG_ODELAY:在记录第一条消息之前延迟打开至syslogd的连接.
LOG_PERROR:除了将日志消息发送给syslogd外,还将它写到标准出错.
LOG_PID:每条消息都包含进程ID.
facility:让配置文件说明,来自不同设施的消息将以不同的方式进程处理.
LOG_AUTH:授权程序:login,su,getty等.
LOG_AUTHPRIV:与LOG_AUTH相同,但写日志文件时具有权限限制.
LOG_CRON:cron和at.
LOG_DAEMON:系统守护进程:inetd,routed等.
LOG_FTP:FTP守护进程(ftpd).
LOG_KERN:内核产生的消息.
LOG_LOCAL0-9:保留由本地使用.
LOG_LPR:行打印系统:lpd,lpc等.
LOG_MAIL:邮件系统.
LOG_NEWS:Usenet网络新闻系统.
LOG_SYSLOG:syslogd守护进程.
LOG_USER:来自其他用户进程的消息(默认).
LOG_UUCP:UUCP系统.
priority:这个参数是facility和level的组合,可以是(由高到低):
LOG_EMERG:紧急状态(系统不可使用).
LOG_ALERT:必须立即修复的状态.
LOG_CRIT:严重状态.
LOG_ERR:出错状态.
LOG_WARNING:警告状态.
LOG_NOTICE:正常,但重要的状态.
LOG_INFO:信息性消息.
LOG_DEBUG:调试消息.
此外还有一个变体函数:
#include <syslog.h> #include <stdarg.h> void vsyslog(int priority, const char *format, va_list arg);
例子:
openlog("lpd", LOG_PID, LOG_LPR); syslog(LOG_ERR, "open error for %s: %m", filename);
等同于:
syslog(LOG_ERR | LOG_LPR, "open error for %s: %m", filename);
不过更推荐用第一种方式,有open有close,更规矩更清晰.
4、单例的守护进程:
一些守护进程对于一些操作实现为只有一个守护进程副本可以运行。比如守护进程cron,如果多个实例运行,每一个副本可能去开启一个调度操作,
那么将导致重复操作,并可能产生错误。
如果一个守护进程需要反问一个设备,而设备驱动可能组织多次打开/dev下的设备节点,这个也限制了我们一次只能运行一个守护进程,但是没有提供种设施,所以需要我们自己写。
文件和记录锁给我们提供了保证只能一个守护进程副本可以运行的一个方法。如果每一个守护进程都创建一个文件,并且设置一个写锁来锁住整个文件,只有一个这样的写锁可以被创建。后续创建写锁都会导致失败,这对后续创建守护进程可以指示已经存在一个守护进程正在运行。
例子:
#include <unistd.h> #include <stdlib.h> #include <fcntl.h> #include <syslog.h> #include <string.h> #include <errno.h> #include <stdio.h> #include <sys/stat.h> #define LOCKFILE "/var/run/daemon.pid" #define LOCKMODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) extern int lockfile(int); int already_running(void){ int fd; char buf[16]; fd = open(LOCKFILE,O_RDWR | O_CREAT, LOCKMODE); if(fd < 0){ syslog(LOG_ERR,"can't open %s: %s", LOCKFILE, strerror(errno)); exit(1); } if(lockfile(fd) < )){ if(errno == EACCES || errno == EAGAIN ){ close(fd); return 1;: } syslog(LOG_ERR,"can't lock %s : %s",LOCKFILE,strerror(errno)); exit(1); } ftruncate(fd,0); sprintf(buf,"%ld",(long)getpid()); write(fd,buf,strlen(buf)+1); return 0; }
5、守护进程惯例:
1)如果一个守护进程使用了文件锁,文件通常存储在/var/run目录下,守护进程可能需要超级用户权限来创建它,名字通常是name.pid,name通常是守护进程或服务
的名字。
2)如果守护进程支持配置选项,它们通常存储在/etc下,配置文件通常是name.conf,name是守护进程的名字。
3)守护进程可以通过命令行启动,但是他们通常在系统初始化脚本(/etc/rc*或者/etc/init.d/*)。如果守护进程需要自动启动,我们需要安排init去重新启动它,
我们在/etc/initab中添加一个respawn的项。
4)若守护进程有一配置文件,那么当更改了配置文件后,守护进程可能需要被停止,然后再启动,以使配置文件的更改生效。为避免这种麻烦,某些守护进程将捕捉SIGHUP信号,当它们接收到该信号时,重读配置文件。
参考:
《AdvancedProgramminginUnixEnvironment》