守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待 处理某些发生的事件。守护进程是一种很有用的进程。
Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。 同时,守护进程完成许多系统任务。比如,作业规划进程crond,
打印进程lpd等。守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同, 造成不同 Unix环境下守护进程的编程规则并不一致。
基本概念 .进程 .每个进程都有一个父进程 .当子进程终止时,父进程会得到通知并能取得子进程的退出状态。 .进程组 .每个进程也属于一个进程组 .每个进程主都有一个进程组号,该号等于该进程组组长的PID号 .一个进程只能为它自己或子进程设置进程组ID号 .会话期 .对话期(session)是一个或多个进程组的集合。 .setsid()函数可以建立一个对话期: 如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。 (1)此进程变成该对话期的首进程 (2)此进程变成一个新进程组的组长进程。 (3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。 如果该进程是一个进程组的组长,此函数返回错误。 (4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行, 子进程继承了父进程的进程组ID,但是进程PID却是新分配的,所以不可能是新会话的进程组的PID。 从而保证了这一点。 if((pid=fork())>0) //parent exit(0); else if(pid==0){ //th1 child setsid(); //th1是成为会话期组长 if(fork() ==0){ //th2不会是会话期组长(变成孤儿进程组) ... } } 一. 守护进程及其特性 (1)守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。 (2)其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端, 会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell) 中继承下来的。 (3)最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动, 可以由作业规划进程crond启动,还可以由用户终端(通常是 shell)执行。 总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。 因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。 二. 守护进程的编程要点 前面讲过,不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样, 区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。 同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下; 1. 在后台运行。 为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止, 让Daemon在子进程中后台执行。 if(pid=fork()) exit(0); //是父进程,结束父进程,子进程继续 2. 脱离控制终端,登录会话和进程组 进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。 这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。 我们的目的就是要摆脱它们,使之不受它们的影响。 方法是在第1点的基础上,调用setsid()使进程成为会话组长: setsid(); 说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。 setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。 由于会话过程对控制终端的独占性,进程同时与控制终端脱离。 3. 禁止进程重新打开控制终端 现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。 可以通过使进程不再成为会话组长来禁止进程重新打开控制终端: if(pid=fork()) exit(0); //结束第一子进程,第二子进程继续(第二子进程不再是会话组长) 4. 关闭打开的文件描述符 进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源, 造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们: for(i=0;i 关闭打开的文件描述符close(i);> 5. 改变当前工作目录 进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。 对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 /tmpchdir("/") 6. 重设文件创建掩模 进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。 为防止这一点,将文件创建掩模清除:umask(0); 7. 处理SIGCHLD信号 处理SIGCHLD信号并不是必须的。 但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。 如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。 如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。 在Linux下可以简单地将 SIGCHLD信号的操作设为SIG_IGN。 signal(SIGCHLD,SIG_IGN); 这样,内核在子进程结束时不会产生僵尸进程。 这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。
/**************************************************************************************/
C,实现Linux守护进程步骤
a.创建子进程,终止父进程
b.子进程中创建新会话
c.更改工作目录
d.重设文件创建掩码
e.关闭文件描述符
主要写法如下:
#include < unistd.h > #include < signal.h > #include < sys/param.h > #include < sys/types.h > #include < sys/stat.h > void init_daemon( ) { pid_t pid; int i; /*第一次,脱离主进程的进程组*/ if((pid = fork()) < 0) { exit(1);//fork失败,退出 } else if(pid > 0) { /*主进程退出*/ /*exit(0);*/ exit(0); } /*创建session前进程继承父进程的进程组*/ /*创建一个新的会话session后*/ /*当前进程变为新的会话的会话头进程*/ /*以及新的进程组的进程组头进程*/ if(setsid() < 0) { exit(1); } /*当会话头进程(即首次fork产生的子进程)终止时*/ /*其会话中的所有进程(即再次fork产生的子进程)*/ /*都会收到SIGHUP信号*/ signal(SIGHUP, SIG_IGN); /*第二次fork的目的*/ /*新的子进程不在是一个会话头进程*/ /*确保子进程将来即使打开一个终端设备*/ /*也不会自动获得控制终端*/ if((pid = fork()) < 0) { exit(1); //fork失败,退出 } else if(pid > 0) { exit(0); //是第一子进程,结束第一子进程 } //是第二子进程,继续 //第二子进程不再是会话组长 for(i=0;i< NOFILE;++i) //关闭打开的文件描述符 close(i); chdir("/tmp"); //改变工作目录到/tmp umask(0); //重设文件创建掩模 }
比较好的博客【转】如下 :
bool AlreadyRunning() { int fdLockFile; char szPid[32]; struct flock fl; /* 打开锁文件*/ fdLockFile = open(LOCK_FILE, O_RDWR| O_CREAT, LOCK_FILE_MODE); if (fdLockFile< 0) { ErrorLog("AlreadyRunning open"); exit(EXIT_FAILURE); } /* 对整个锁文件加写锁*/ fl.l_type = F_WRLCK; //记录锁类型:独占性写锁 fl.l_whence = SEEK_SET; //加锁区域起点:距文件开始处l_start个字节 fl.l_start = 0; fl.l_len = 0; //加锁区域终点:0表示加锁区域自起点开始直至文件最大可能偏移量为止, //不管写入多少字节在文件末尾,都属于加锁范围 if (fcntl(fdLockFile, F_SETLK,&fl)< 0) { if (EACCES== errno|| EAGAIN == errno) //系统中已有该守护进程的实例在运行 { close(fdLockFile); return true; } ErrorLog("AlreadyRunning fcntl"); exit(EXIT_FAILURE); } /* 清空锁文件,然后将当前守护进程pid写入锁文件*/ ftruncate(fdLockFile, 0); sprintf(szPid,"%ld",(long)getpid()); write(fdLockFile, szPid, strlen(szPid)+ 1); return false; }
/* * test.c * * Created on: 2011-04-23 * Author: lingdxuyan */ #include <stdio.h> /* 标准输入输出定义*/ #include <stdlib.h> /* 标准函数库定义*/ #include <unistd.h> /* Unix 标准函数定义*/ #include <sys/types.h> #include <sys/stat.h> #include <sys/wait.h> #include <fcntl.h> /* 文件控制定义*/ #include <errno.h> /* 错误号定义*/ #include <signal.h> /* 信号定义*/ #include <time.h> /* 定时器定义*/ #include <stdarg.h> /* 可变参数定义*/ #include <syslog.h> /* syslog定义*/ #include <string.h> #include <fcntl.h> #define true 1 #define false 0 typedef unsigned char BYTE; typedef BYTE bool; typedef BYTE byte; #define MAX_BUF_SIZE 1024 #define CONFIG_FILE "/etc/daemon.conf" #define LOG_FILE "/tmp/daemon.log" #define LOCK_FILE "/var/run/daemon.pid" #define LOCK_FILE_MODE (S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) static volatile sig_atomic_t g_nUpdateParameter = 1; static volatile sig_atomic_t g_nServer = 0; //static volatile int g_nUpdateParameter = 1; //static volatile int g_nServer = 0; /* * 功能: 写日志 */ void vWriteLog(int nPriority,const char*fmt, va_list va) { #ifdef USE_SYSLOG vsyslog(LOG_DAEMON | nPriority, fmt, va); #else FILE *stream; if (nPriority& LOG_ERR) stream = stderr; else stream = stdout; vfprintf(stream, fmt, va); fflush(stream); #endif } void WriteLog(int nPriority,const char*fmt,...) { va_list va; va_start(va, fmt); vWriteLog(nPriority, fmt, va); va_end(va); } /* * 功能: 写错误日志,用法类似perror */ void ErrorLog(const char*str) { WriteLog(LOG_ERR,"%s: %s\n", str, strerror(errno)); } /* * 功能: 写错误日志,用法类似于printf */ void ErrorLogFmt(const char*fmt,...) { va_list va; va_start(va, fmt); vWriteLog(LOG_ERR, fmt, va); va_end(va); } /* * 功能: 写日志,用法类似printf */ void InfoLog(const char*fmt,...) { va_list va; va_start(va, fmt); vWriteLog(LOG_INFO, fmt, va); va_end(va); } /* * 功能: 重定向标准输入输出 */ void RedirectStdIO(char *szInFile, char *szOutFile, char *szErrFile) { int fd; if (NULL!= szInFile) { fd = open(szInFile, O_RDONLY| O_CREAT, 0666); if (fd> 0) { // 标准输入重定向 if (dup2(fd, STDIN_FILENO)< 0) { ErrorLog("RedirectStdIO dup2 in"); exit(EXIT_FAILURE); } close(fd); } else ErrorLogFmt("RedirectStdIO open %s: %s\n", szInFile, strerror(errno)); } if (NULL!= szOutFile) { fd = open(szOutFile, O_WRONLY| O_CREAT| O_APPEND/*| O_TRUNC*/, 0666); if (fd> 0) { // 标准输出重定向 if (dup2(fd, STDOUT_FILENO)< 0) { ErrorLog("RedirectStdIO dup2 out"); exit(EXIT_FAILURE); } close(fd); } else ErrorLogFmt("RedirectStdIO open %s: %s\n", szOutFile, strerror(errno)); } if (NULL!= szErrFile) { fd = open(szErrFile, O_WRONLY| O_CREAT| O_APPEND/*| O_TRUNC*/, 0666); if (fd> 0) { // 标准错误重定向 if (dup2(fd, STDERR_FILENO)< 0) { ErrorLog("RedirectIO dup2 error"); exit(EXIT_FAILURE); } close(fd); } else ErrorLogFmt("RedirectStdIO open %s: %s\n", szErrFile, strerror(errno)); } } /* * 功能: 读取配置文件SIGHUP信号处理函数 */ void UpdateHandler(int nSigNum) { g_nUpdateParameter = 1; } /* * 功能: 折行服务SIG_USR1信号处理函数 */ void ServerHandler(int nSigNum) { g_nServer = 1; } /* * 功能:确保任一时刻系统中只有一个该守护进程实例在运行 */ bool AlreadyRunning() { int fdLockFile; char szPid[32]; struct flock fl; /* 打开锁文件*/ fdLockFile = open(LOCK_FILE, O_RDWR| O_CREAT, LOCK_FILE_MODE); if (fdLockFile< 0) { ErrorLog("AlreadyRunning open"); exit(EXIT_FAILURE); } /* 对整个锁文件加写锁*/ fl.l_type = F_WRLCK; //记录锁类型:独占性写锁 fl.l_whence = SEEK_SET; //加锁区域起点:距文件开始处l_start个字节 fl.l_start = 0; fl.l_len = 0; //加锁区域终点:0表示加锁区域自起点开始直至文件最大可能偏移量为止,不管写入多少字节在文件末尾,都属于加锁范围 if (fcntl(fdLockFile, F_SETLK,&fl)< 0) { if (EACCES== errno|| EAGAIN == errno) //系统中已有该守护进程的实例在运行 { close(fdLockFile); return true; } ErrorLog("AlreadyRunning fcntl"); exit(EXIT_FAILURE); } /* 清空锁文件,然后将当前守护进程pid写入锁文件*/ ftruncate(fdLockFile, 0); sprintf(szPid,"%ld",(long)getpid()); write(fdLockFile, szPid, strlen(szPid)+ 1); return false; } /* * 功能:设置信号nSigNum的处理函数为handler,在调用该信号处理函数前.若handler不为SIG_DEF或SIG_IGN,则系统会将该信号添加到信号屏蔽字中;信号处理函数返回后,信号屏蔽字恢复到原先值.这样,可保证在处理指定信号时,如果该信号再次发生,那么它会被阻塞到上一信号处理结束为止.不过,要是此时信号发生了多次,在对该信号解除阻塞后,也只会调用一次信号处理函数 */ typedef void (*sighandler)(int); sighandler MySignal(int nSigNum, sighandler handler) //void ( *Signal(int nSigNum, void(*handler)(int)))(int) { struct sigaction saNew, saOld; saNew.sa_handler = handler; sigemptyset(&saNew.sa_mask); if (SIG_DFL!= handler&& SIG_IGN != handler) sigaddset(&saNew.sa_mask, nSigNum); saNew.sa_flags = 0; if (SIGALRM== nSigNum) { //不重启该信号中断的系统调用 #ifdef SA_INTERRUPT saNew.sa_flags |= SA_INTERRUPT; #endif } else { //重启该信号中断的系统调用 #ifdef SA_RESTART saNew.sa_flags |= SA_RESTART; #endif } if (sigaction(nSigNum,&saNew,&saOld) < 0) return SIG_ERR; return saOld.sa_handler; } /* * 功能: 将普通进程改造成守护进程 */ void InitDaemon() { pid_t pid; int fd, fdTableSize; /* 1、屏蔽控制终端操作信号 */ MySignal(SIGTTOU, SIG_IGN); MySignal(SIGTTIN, SIG_IGN); MySignal(SIGTSTP, SIG_IGN); MySignal(SIGHUP, SIG_IGN); /* 2、忽略子进程结束信号 */ #ifdef IGN_SIGCHLD signal(SIGCHLD, SIG_IGN); //忽略子进程结束信号,避免僵死进程产生 #endif /* 3、使守护进程后台运行 * 父进程直接退出,子进程继续运行(让守护进程在子进程中后台运行) */ pid = fork(); if (pid> 0) //父进程终止运行;子进程过继给init进程,其退出状态也由init进程处理,避免了产生僵死进程 exit(EXIT_SUCCESS); else if(pid< 0) { ErrorLog("InitDaemon fork(parent)"); exit(EXIT_FAILURE); } /* 4、脱离控制终端,登录会话和进程组 * 调用setsid()使子进程成为会话组长 */ setsid(); /* 5、禁止进程重新打开控制终端 * 通过使守护进程不再成为会话组长来禁止进程重新打开控制终端 */ pid = fork(); if (pid> 0) //子进程终止运行;孙进程过继给init进程,其退出状态也由init进程处理,避免了产生僵死进程 exit(EXIT_SUCCESS); else if(pid< 0) { ErrorLog("InitDaemon fork(child)"); exit(EXIT_FAILURE); } /* 6、重设文件创建掩模 */ umask(0); /* 7、关闭打开的文件描述符 */ RedirectStdIO("/dev/null", LOG_FILE, LOG_FILE); //重定向标准输入输出 fdTableSize = getdtablesize(); for (fd=3; fd<fdTableSize; fd++) close(fd); /* 8、改变当前工作目录 */ chdir("/tmp"); } /* * 功能: 读取守护进程的配置文件,并将获取到的信息保存在szParameter中 */ void ReadConfigFile(char *szParameter) { FILE *stream; int nRet; InfoLog("------ ReadConfigFile ------\n"); stream = fopen(CONFIG_FILE,"r"); if (NULL!= stream) { nRet = fread(szParameter, sizeof(char), MAX_BUF_SIZE, stream); if (nRet>= 0) szParameter[nRet - 1] ='\0'; fclose(stream); InfoLog("ReadConfigFile sucesss!\n"); } else ErrorLogFmt("ReadConfigFile fopen %s: %s\n", CONFIG_FILE, strerror(errno)); } /* * 功能: 执行守护进程的服务,也就是将szParameter用echo打印出来 */ void Server(char *szParameter) { int nStatus; pid_t pid; InfoLog("------ Server ------\n"); pid = vfork(); //生成子进程 #ifdef IGN_SIGCHLD InfoLog("ignore child SIGCHLD signal!\n"); if (0== pid) //子进程 { if (execlp("echo","echo", szParameter,NULL)< 0) { ErrorLog("Server execlp"); exit(EXIT_FAILURE); } } else if(pid< 0) { ErrorLog("Server vfork(parent)"); } #else if (pid> 0) //父进程 { waitpid(pid,&nStatus, 0); //等待子进程结束,否则子进程会成为僵死进程,一直存在,即便子进程已结束执行 } else if(0== pid) //子进程 { pid = vfork(); //生成孙进程 if (pid> 0) { exit(EXIT_SUCCESS); //子进程退出,孙进程过继给init进程,其退出状态也由init进程处理,与原有父进程无关 } else if(0== pid) //孙进程 { if (execlp("echo","echo", szParameter,NULL)< 0) { ErrorLog("Server execlp"); exit(EXIT_FAILURE); } } else { ErrorLog("Server vfork(child)"); } } else { ErrorLog("Server vfork(parent)"); } #endif } int main() { time_t t; sigset_t sigNewMask, sigOldMask; char szParameter[MAX_BUF_SIZE]; //将普通进程改造成守护进程 InitDaemon(); if (AlreadyRunning()) //若系统中已有该守护进程的实例在运行,则退出 { ErrorLogFmt("Daemon already running!\n"); exit(EXIT_FAILURE); } //阻塞SIGHUP信号和SIGUSR1信号 sigemptyset(&sigNewMask); sigaddset(&sigNewMask, SIGHUP); sigaddset(&sigNewMask, SIGUSR1); if (sigprocmask(SIG_BLOCK,&sigNewMask,&sigOldMask)< 0) { ErrorLog("main sigprocmask"); exit(EXIT_FAILURE); } //为SIGHUP信号和SIGUSR1信号添加信号处理函数 MySignal(SIGHUP, UpdateHandler); MySignal(SIGUSR1, ServerHandler); t = time(NULL); InfoLog("Daemon %d start at %s", getpid(), ctime(&t)); //读取守护进程配置文件 ReadConfigFile(szParameter); while(1) { sigsuspend(&sigOldMask);//将进程的信号屏蔽字暂时替代为sigOldMask并挂起进程,直到捕捉到一个信号并从其信号处理函数返回,sigsuspend才返回并将信号屏蔽字恢复为调用它之前的值;若捕捉到的是终止进程信号,sigsuspend不返回,进程直接终止 if (1== g_nUpdateParameter) //读取配置文件 { ReadConfigFile(szParameter); g_nUpdateParameter = 0; } if (1== g_nServer) //执行服务 { Server(szParameter); g_nServer = 0; } } return 0; }