Unix环境高级编程——守护进程记录总结(从基础到实现)

时间:2021-09-20 16:17:52

一、概念及其特征

守护进程是系统中生存期较长的一种进程,常常在系统引导装入时启动,在系统关闭时终止,没有控制终端,在后台运行。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。

在这里,我们在Linux2.6内核的centos中,ps -ef |awk '{print $1"\t "$2"\t "$3"\t  "$8}'看到:PPID=0的进程有两个,分别是PID=1的/sbin/init进程和PID=2的[kthreadd]进程。

其中,[kthreadd]为内核进程,由它fork出来的子进程都是内核进程,并且内核守护进程的名字出现在方括号中,对于需要在进程上下文执行工作但却不被用户层进程(init)上下文调用的每一个内核组件,通常有它自己的内核守护进程。

而对于init进程,它是一个由内核在引导装入时启动的用户层次的命令,属于用户级守护进程,主要负责启动各运行层次特定系统服务。这些服务通常是在它们自己拥有的守护进程的帮助下实现的。用户层守护进程缺少控制终端可能是守护进程调用了setsid的结果。大多数用户层守护进程都是进程组的组长进程以及会话的首进程,而且是这些进程组和会话中的唯一进程。

守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。此外,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建屏蔽字等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。

二、编程规则(细节可参考Unix环境高级编程)

1、调用umask将文件模式创建屏蔽字设置为一个已知值(通常是0)。如前所述,由继承得来的文件模式创建屏蔽字可能会被设置为拒绝权限。我们可以根据我们的具体需求设定特定的权限。

2、调用fork,然后使父进程exit。这样做,使得当我们以./的shell命令启动守护进程时,父进程终止会让shell认为此命令已经执行完毕,而且,这也使子进程获得了一个新的进程ID。此外,让父进程先于子进程exit,会使子进程变为孤儿进程,这样子进程成功被init这个用户级守护进程收养。

3、调用setsid创建一个新会话。这在setsid函数中有介绍,调用setsid,会使这个子进程成为(a)新会话的首进程,(b)成为一个新进程组的组长进程,(c)切断其与控制终端的联系,或者就是没有控制终端。至此,这个子进程作为新的进程组的组长,完全脱离了其他进程的控制,并且没有控制终端。

4、将当前工作目录更改为根目录(或某一特定目录位置)。这是为了保证守护进程的当前工作目录在一个挂载的文件系统中,该文件系统不能被卸载。

5、关闭不再需要的文件描述符。根据具体情况来定。

6、某些守护进程可以打开/dev/null使其具有文件描述符0、1、2,这使任何一个试图读标准输入、写标准输出或标准错误的库例程都不会产生任何效果。

7、忽略SIGCHLD信号

这一步并非必须的,只对需要创建子进程的守护进程才有必要,很多服务器守护进程设计成通过派生子进程来处理客户端的请求,如果父进程不对SIGCHLD信号进行处理的话,子进程在终止后变成僵尸进程,通过将信号SIGCHLD的处理方式设置为SIG_IGN可以避免这种情况发生。

8、用日志系统记录出错信息

因为守护进程没有控制终端,当进程出现错误时无法写入到标准输出上,可以通过调用syslog将出错信息写入到指定的文件中。该接口函数包括openlog、syslog、closelog、setlogmask,具体可参考13.4节出错记录。

9、守护进程退出处理

当用户需要外部停止守护进程运行时,往往会使用 kill命令停止该守护进程。所以,守护进程中需要编码来实现kill发出的signal信号处理,达到进程的正常退出。可用如下信号函数处理:

signal(SIGTERM, sigterm_handler);

voidsigterm_handler(int arg)

{

//进行相应处理的函数

}


三、一个简单的守护进程实例

按照上面的编程规则,结合书中示例,写了一个简单的守护进程,可以测试通过。其中为了方便查看出错记录的过程,将syslog函数注释掉,改在/tmp文件下记录日志。此外,想了一下,由于在子进程中已经将守护进程的所有标准IO加入到/dev/null中,因此我们无法通过printf函数来交互了,所以程序中还是有很多瑕疵的,我们只能将所有信息写入日志中,后续还需多考虑改进,信号处理函数有问题,还需要加一段信号屏蔽字的处理函数,防止收到的信号进行默认动作处理,也就是这个原因,使我下面这段程序无法触发往/tmp/daemon.txt文件中记录退出信息。

<span style="font-size:18px;">#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h> void daemonize(const char *cmd)
{
pid_t pid;
int i, fd0, fd1, fd2;
struct rlimit r1; umask(0); //第一步:将文件模式创建屏蔽字设置为0 /*
* 第二步:fork子进程,并且使子进程成为会话首进程,脱离终端控制
*/
if ((pid = fork()) < 0)
printf("ERROR:can't fork a son process");
else if (pid != 0) //父进程
exit(0);
setsid(); /*
* 第三步:改变当前工作目前为根目录,以防卸载当前文件系统所在的目录
*/
if (chdir("/") < 0)
printf("ERROR:can't change the directory to root(/)"); /*
* 先获取当前最大的文件描述符,并且关闭所有不需要的文件描述符
*/
if (getrlimit(RLIMIT_NOFILE, &r1) < 0)
printf("ERROR:can't get the maximum fd");
if (r1.rlim_max == RLIM_INFINITY)
r1.rlim_max = 1024;
for (i = 0; i < r1.rlim_max; i++)
close(i); /*
* 把fd为0、1、2三个fd加入到/dev/null中
*/
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0); /*
* 初始化日志文件,调用syslog函数的openlog
* openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2){
syslog(LOG_ERR,"unexpected fd %d %d %d", fd0,fd1,fd2);
exit(1);
}
*/ //以下代码段为了演示记录日志的效果
char *buf="this is a dameon \n";
int len, fd;
len = strlen(buf);
while(1)
{
if((fd=open("/tmp/dameon.txt",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
{
printf("open file err \n");
exit(0);
}
write(fd,buf,len+1);
close(fd);
sleep(60); //每60秒向日志中记录一次,daemon还存在
} }
void sigterm_handler(int arg)
{
//进行相应处理的函数
char *buf="I have received a signal,I will exit 3 seconds later \n";
int len, fd;
len = strlen(buf);
if((fd=open("/tmp/dameon.txt",O_WRONLY|O_APPEND,0600))<0)
{
printf("open file err \n");
exit(0);
}
write(fd,buf,len+1);
close(fd);
sleep(3);
exit(0);
} int main()
{
char *cmd;
cmd = "cron";
daemonize(cmd);
pause();
signal(SIGUSR1, sigterm_handler);
}</span>

下面是加了信号屏蔽字处理代码段的程序,这回可以将SIGUSR1信号抓取,并且写入日志了。

#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <string.h> void siguser1(int signo)
{
char *buf="I have received a signal,I will exit 3 seconds later \n";
int len, fd;
len = strlen(buf);
if((fd=open("/tmp/dameon_exit.txt",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
{
printf("open file err \n");
exit(0);
}
write(fd,buf,len+1);
close(fd);
sleep(3);
exit(0);
} void daemonize(const char *cmd)
{
pid_t pid;
int i, fd0, fd1, fd2;
struct rlimit r1;
struct sigaction sa; umask(0); //第一步:将文件模式创建屏蔽字设置为0 /*
* 第二步:fork子进程,并且使子进程成为会话首进程,脱离终端控制
*/
if ((pid = fork()) < 0)
printf("ERROR:can't fork a son process");
else if (pid != 0) //父进程
exit(0);
setsid(); /*
*对SIGUSR1信号进行处理 (注意,这个代码段必须写在第三步之前)
*/
sa.sa_handler = siguser1;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGUSR1, &sa, NULL) < 0){
syslog(LOG_ERR,"can't catch SIGUSR1");
exit(1);
} /*
* 第三步:改变当前工作目前为根目录,以防卸载当前文件系统所在的目录
*/
if (chdir("/") < 0)
printf("ERROR:can't change the directory to root(/)"); /*
* 先获取当前最大的文件描述符,并且关闭所有不需要的文件描述符
*/
if (getrlimit(RLIMIT_NOFILE, &r1) < 0)
printf("ERROR:can't get the maximum fd");
if (r1.rlim_max == RLIM_INFINITY)
r1.rlim_max = 1024;
for (i = 0; i < r1.rlim_max; i++)
close(i); /*
* 把fd为0、1、2三个fd加入到/dev/null中
*/
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0); /*
* 初始化日志文件,调用syslog函数的openlog
* openlog(cmd, LOG_CONS, LOG_DAEMON);
if (fd0 != 0 || fd1 != 1 || fd2 != 2){
syslog(LOG_ERR,"unexpected fd %d %d %d", fd0,fd1,fd2);
exit(1);
}
*/ //以下代码段为了演示记录日志的效果
char *buf="this is a dameon \n";
int len, fd;
len = strlen(buf);
while(1)
{
if((fd=open("/tmp/dameon.txt",O_CREAT|O_WRONLY|O_APPEND,0600))<0)
{
printf("open file err \n");
exit(0);
}
write(fd,buf,len+1);
close(fd);
sleep(60); //每60秒向日志中记录一次,daemon还存在
} } int main()
{
char *cmd;
cmd = "cron";
daemonize(cmd);
pause();
}