Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1965362
  • 博文数量: 498
  • 博客积分: 2078
  • 博客等级: 大尉
  • 技术积分: 1645
  • 用 户 组: 普通用户
  • 注册时间: 2008-11-18 22:43
个人简介

安大

文章分类

全部博文(498)

文章存档

2017年(1)

2016年(2)

2015年(21)

2014年(90)

2013年(101)

2012年(267)

2011年(16)

分类: LINUX

2015-10-08 13:58:39

原文地址:linux守护进程编程[转] 作者:vicegod

守护进程的编程规则:

1、屏蔽一些有关控制终端操作的信号
防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起。

2、脱离控制终端,登录会话和进程组
登录会话可以包含多个进程组,这些进程组共享一个控制终端,这个控制终端通常是创建进程的登录终端。控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。
其方法是在fork()的基础上,调用setsid()使进程成为会话组长。调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
setsid()实现了以下效果:
(a) 成为新对话期的首进程
(b) 成为一个新进程组的首进程
(c) 没有控制终端。
3、禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端,再fork()一次。
4、关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在地文件系统无法卸下以及无法预料的错误。一般来说, 必要的是关闭0、1、2三个文件描述符,即标准输入、标准输出、标准错误。因为我们一般希望守护进程自己有一套信息输出、输入的体系,而不是把所有的东西 都发送到终端屏幕上。
5、改变当前工作目录
将当前工作目录更改为根目录。从父进程继承过来的当前工作目录可能在一个装配的文件系统中。因为守护进程通常在系统重启之前是一直存在的,所以如果守护进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被拆卸。
另外,某些守护进程可能会把当前工作目录更改到某个指定位置,在此位置做它们的工作。例如,行式打印机假脱机守护进程常常将其工作目录更改到它们的spool目录上。
6、重设文件创建掩码
将文件方式创建屏蔽字设置为0:umask(0)。 由继承得来的文件方式创建的屏蔽字可能会拒绝设置某些许可权。例如,若守护进程要创建一个组可读、写的文件,而继承的文件方式创建屏蔽字,屏蔽了这两种许可权,则所要求的组可读、写就不能起作用。
7、处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程, 特别是服务器进程往往在请求到来时fork子进程出来处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)而仍占用系统资源。如 果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在系统V下可以简单地将SIGCHLD信号的操作设为SIG_IGN,即忽略 掉。这样,内核在子进程结束时不会产生僵尸进程,这一点与BSD4不同,在BSD4下必须显示等待子进程结束才能释放僵尸进程。
8、记录信息
在Linux/Unix下有个syslogd的守护进程,向用户提供了syslog()系统调用。任何程序都可以通过syslog记录事件。

源码实现及分析:


#include<stdio.h>
#include<signal.h>
#include<syslog.h>
#include<unistd.h>
#include<fcntl.h>
#include<time.h>
#include<sys/file.h>
#include<sys/ioctl.h>
#include<stdlib.h>

int main(int argc,char *argv[])
{
    time_t now;
    int childpid,fd,fdtablesize;
//    int error,in,out;

    int fp;
    
    //屏蔽一些有关控制终端操作的信号。防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起,此处忽略了终端I/O信号、STOP信号

    signal(SIGTTOU,SIG_IGN);
    signal(SIGTTIN,SIG_IGN);
    signal(SIGTSTP,SIG_IGN);
    signal(SIGHUP,SIG_IGN);

    //由于子进程会继承父进程的某些特性,如控制终端、登录会话、进程组等,而守护进程最终要脱离控制终端到后台去运行,所以必须把父进程杀掉,以确保子进程不是进程组长,这也是成功调用setsid()所要求的。

    if (fork() != 0)
    {
        exit(1);
    }
        
    //脱离了控制终端还要脱离登录会话和进程组,这里可以调用setsid()函数,调用成功后程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

/*
    if (setsid() < 0)
    {
        exit(1);
    }
*/

    //要达到setsid()函数的功能也可以用如下处理方法。"/dev/tty"是一个流设备,也是终端映射,调用close()函数将终端关闭。

    if ((fp=open("/dev/tty",O_RDWR)) >= 0)
    {
        ioctl(fp,TIOCNOTTY,NULL);
        close(fp);
    }

    //进程已经成为无终端的会话组长,但它可以重新申请打开一个新的控制终端。可以通过不再让进程成为会话组长的方式来禁止进程重新打开控制终端,需要再次调用fork函数。

    if (fork() != 0)
    {
        exit(1);
    }
    
    //从父进程继承过来的当前工作目录可能在一个装配的文件系统中。因为守护进程通常在系统重启之前是一直存在的,所以如果守护进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被卸载。比如说从父进程继承的当前目录是/mnt下面的一个被挂载的目录。

    if (chdir("/tmp") == -1)
    {
        exit(1);
    }

    //关闭打开的文件描述符,或重定向标准输入、标准输出和标准错误输出的文件描述符。进程从创建它的父进程那里继承了打开的文件描述符。如果不关闭,将会浪费系统资源,引起无法预料的错误。getdtablesize()返回某个进程所能打开的最大的文件数。

    for (fd=0,fdtablesize=getdtablesize();fd<fdtablesize;fd++)
    {
        close(fd);
    }

    //有的程序有些特殊的需求,还需要将这三者重新定向。

/*
    error=open("/tmp/error",O_WRONLY|O_CREAT,0600);
    dup2(error,2);
    close(error);
    in=open("/tmp/in",O_RDONLY|O_CREAT,0600);
    if(dup2(in,0)==-1) perror("in");
    close(in);
    out=open("/tmp/out",O_WRONLY|O_CREAT,0600);
    if(dup2(out,1)==-1) perror("out");
    close(out);
*/


    //由继承得来的文件方式创建的屏蔽字可能会拒绝设置某些权限,所以要重新赋于所有权限。

    umask(0);

    //如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源,如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。

    signal(SIGCHLD,SIG_IGN);

    //守护进程不属于任何终端,所以当需要输出某些信息时,它无法像一般程序那样将信息直接输出到终端,可以使用linux中自带的syslogd守护进程,它向用户提供了syslog()系统调用函数。信息都保存在/var/log/syslog文件中。

    syslog(LOG_USER|LOG_INFO,"守护进程测试!\n");

    //每隔60秒钟就向/var/log/syslog日志文件里写入一次syslog()发出的信息。

    while (1)
    {
        //time(time_t *t)返回从1970年1月1日0时0分0秒到现在的所有秒数。

        time(&now);
            
        //获得父进程ID和子进程ID。

        syslog(LOG_USER|LOG_INFO,"PPID: %d PID: %d\n",getppid(),getpid());

        //ctime(const time_t *)返回当前时间的一个char型指针。

        syslog(LOG_USER|LOG_INFO,"当前时间:%s\n",ctime(&now));

        //睡眠60秒

        sleep(60);
    }
    return 0;
}


守护进程的单实例实现

为了正常工作,守护进程应该实现为单实例的,也就是在任一时刻只运行该守护进程的一个副本,因为这个守护进程要排它的访问一个设备。这里需要用到文 件锁的机制,如果守护进程创建一个文件,并且在整个文件上加上一把锁,那就只允许创建一把这样的写锁,在此之后如果试图再创建一把这样的写锁就将失败,以 此向续守护进程副本指明已经有一个副本正在运行。而这个锁文件通常都放在/var/run目录中,锁文件的名字通常是name.pid,其中name是此 守护进程的名字。注意,守护进程可能需要有root权限才能在此目录下创建文件。

代码实现及分析:

#include<stdio.h>
#include<signal.h>
#include<syslog.h>
#include<unistd.h>
#include<fcntl.h>
#include<time.h>
#include<errno.h>
#include<string.h>
#include<sys/stat.h>
#include<sys/file.h>
#include<sys/ioctl.h>
#include<stdlib.h>

#define LOCKFILE "/var/run/test.pid"
#define LOCKMODE (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)

//成功返回0,若有错误返回-1,错误原因存于errno。

int lockfile(int ff)
{
    struct flock fl;
    
    //l_type是设置锁定的状态。它有三种状态:F_RDLCK是建立一个供读取用的锁定;F_WRLCK是建立一个供写入用的锁定;F_UNLCK是删除之前建立的锁定。

    fl.l_type = F_WRLCK;

    //l_start是设置锁定区域的开头位置

    fl.l_start = 0;

    //l_whenec是决定l_start的位置。它有三种状态:SEEK_SET是以文件开头为锁定的起始位置;SEEK_CUR是以目前文件读写位置为锁定的起始位置;SEEK_END是以文件结尾为锁定的起始位置。

    fl.l_whence = SEEK_SET;

    //l_len是设置锁定区域的大小

    fl.l_len = 0;
    //还有一个l_pid是设置锁定动作的进程

    
    //fcntl()是唯一的符合POSIX标准的文件锁实现,所以也是唯一可移植的,功能比较强大。

    //F_SETLK的作用:在 l_whence、l_start、l_len等都设置好的情况下,如果l_type被设置为F_RDLCK或F_WRLCK就分配一个锁,如果 l_type被设置为F_UNLCK就释放一个锁。如果失败就返回-1,并把errno的错误设为EACCES或EAGAIN。

    return(fcntl(ff,F_SETLK,&fl));
}

void already_running(void)
{
    int fd;
    char buf[16]    ;

    //此处的open是系统调用函数,第一个参数是打开的文件路径;第二个参数是对文件操作的权限,此处是可读写,可创建(如果文件不存在就会自动创建它);第三个参数是文件被创建后给文件的权限,比如S_IRUSR相当于400,权此用户有读权限。

    fd = open(LOCKFILE,O_RDWR|O_CREAT,LOCKMODE);

    if (fd < 0)
    {
        //strerror(),返回一个合适的string型的错误

        syslog(LOG_ERR,"can't open %s: %s",LOCKFILE,strerror(errno));
        exit(1);
    }

    if (lockfile(fd) < 0)
    {
        if (errno == EACCES || errno == EAGAIN)
        {
            close(fd);
            exit(1);
        }
        syslog(LOG_ERR,"can't lock %s: %s",LOCKFILE,strerror(errno));
        exit(1);
    }
    //把fd指定的文件大小改为0。参数fd为已打开的文件描述词,而且必须是以写入模式打开的文件。之所以要把文件长度截短为0,是因为上一个进程的ID字符串可以长于当前进程的ID字符串。

    ftruncate(fd,0);

    //把整型转换成字符串

    sprintf(buf,"%d",getpid());

    //把转换成字符串的进程ID号写入文件/var/run/*.pid中

    write(fd,buf,strlen(buf));
}

int main(int argc,char *argv[])
{
//    time_t now;

    int childpid,fd,fdtablesize;
//    int error,in,out;

    int fp;
    
    //屏蔽一些有关控制终端操作的信号。防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起,此处忽略了终端I/O信号、STOP信号

    signal(SIGTTOU,SIG_IGN);
    signal(SIGTTIN,SIG_IGN);
    signal(SIGTSTP,SIG_IGN);
    signal(SIGHUP,SIG_IGN);

    //由于子进程会继承父进程的某些特性,如控制终端、登录会话、进程组等,而守护进程最终要脱离控制终端到后台去运行,所以必须把父进程杀掉,以确保子进程不是进程组长,这也是成功调用setsid()所要求的。

    if (fork() != 0)
    {
        exit(1);
    }
        
    //脱离了控制终端还要脱离登录会话和进程组,这里可以调用setsid()函数,调用成功后程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。

/*
    if (setsid() < 0)
    {
        exit(1);
    }
*/

    //要达到setsid()函数的功能也可以用如下处理方法。"/dev/tty"是一个流设备,也是终端映射,调用close()函数将终端关闭。

    if ((fp=open("/dev/tty",O_RDWR)) >= 0)
    {
        ioctl(fp,TIOCNOTTY,NULL);
        close(fp);
    }

    //进程已经成为无终端的会话组长,但它可以重新申请打开一个新的控制终端。可以通过不再让进程成为会话组长的方式来禁止进程重新打开控制终端,需要再次调用fork函数。

    if (fork() != 0)
    {
        exit(1);
    }
    
    //从父进程继承过来的当前工作目录可能在一个装配的文件系统中。因为守护进程通常在系统重启之前是一直存在的,所以如果守护进程的当前工作目录在一个装配文件系统中,那么该文件系统就不能被卸载。比如说从父进程继承的当前目录是/mnt下面的一个被挂载的目录。

    if (chdir("/tmp") == -1)
    {
        exit(1);
    }

    //关闭打开的文件描述符,或重定向标准输入、标准输出和标准错误输出的文件描述符。进程从创建它的父进程那里继承了打开的文件描述符。如果不关闭,将会浪费系统资源,引起无法预料的错误。getdtablesize()返回某个进程所能打开的最大的文件数。

    for (fd=0,fdtablesize=getdtablesize();fd<fdtablesize;fd++)
    {
        close(fd);
    }

    //有的程序有些特殊的需求,还需要将这三者重新定向。

/*
    error=open("/tmp/error",O_WRONLY|O_CREAT,0600);
    dup2(error,2);
    close(error);
    in=open("/tmp/in",O_RDONLY|O_CREAT,0600);
    if(dup2(in,0)==-1) perror("in");
    close(in);
    out=open("/tmp/out",O_WRONLY|O_CREAT,0600);
    if(dup2(out,1)==-1) perror("out");
    close(out);
*/


    //由继承得来的文件方式创建的屏蔽字可能会拒绝设置某些权限,所以要重新赋于所有权限。

    umask(0);

    //如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源,如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。

    signal(SIGCHLD,SIG_IGN);

    //守护进程不属于任何终端,所以当需要输出某些信息时,它无法像一般程序那样将信息直接输出到终端,可以使用linux中自带的syslogd守护进程,它向用户提供了syslog()系统调用函数。信息都保存在/var/log/syslog文件中。

    syslog(LOG_USER|LOG_INFO,"守护进程测试!\n");

    //每隔60秒钟就向/var/log/syslog日志文件里写入一次syslog()发出的信息。


/*    while (1)
    {
        //time(time_t *t)返回从1970年1月1日0时0分0秒到现在的所有秒数。
        time(&now);
            
        //获得父进程ID和子进程ID。
        syslog(LOG_USER|LOG_INFO,"PPID: %d PID: %d\n",getppid(),getpid());

        //ctime(const time_t *)返回当前时间的一个char型指针。
        syslog(LOG_USER|LOG_INFO,"当前时间:%s\n",ctime(&now));

        //睡眠60秒
        sleep(60);
    }
*/

    while (1)
    {
        already_running();
        sleep(60);
    }
    return 0;
}

守护进程配置文件的创建及读取

由于守护进程的复杂化决定了它不能使用单一的代码实现方式,所以创建一个配置文件以便针对不同的需求来进行不同的设置,以达到守护进程的人性化、智能化。守护进程的配置文件通常都放在/etc目录下,它的名字通常是name.conf,其中name是守护进程的名字。

配置文件模板:

#mysql用户名
mysql_user=study

#mysql用户名密码
mysql_password=123456

#主机地址
host=192.168.0.172

#连接mysql后打开的库名
database_name=hardware

#进行数据操作的表名
table_name=information


以下是我写的一个简陋的代码,供大家参考:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<syslog.h>

#define MAX_NUM 254
char s[MAX_NUM];
char key[10][MAX_NUM];
char value[10][MAX_NUM];
#define FILENAME "/etc/main.conf"    //配置文件的路径及文件名



//消除一行中的所有空格

void trim(char p[MAX_NUM])
{
    //原理是逐个字符的检查,只要不是空格就把这个字符重新赋给一个字符型数组,最后这个新的字符数组就得到了一行没有空格的字符串

    int i,n=0;
    for (i=0;i<MAX_NUM;i++)
    {
        if (p[i] != 32)
        {
            s[n] = p[i];
            n++;
        }
    }
}

int main(void)
{
    char r[MAX_NUM];
    FILE *fp;
    int i;
    int row = 0;
    int vol = 0;
    int denghao = 0;

    //打开配置文件并把文件描述符赋给fp

    if ((fp=fopen(FILENAME,"r")) == NULL)
    {
        syslog(LOG_ERR,"can't open file %s\n",FILENAME);
        exit(1);
    }

    //逐行读取配置文件,并把每行的键值和数据分离出来

    while (fgets(r,MAX_NUM,fp))
    {
        trim(r);    //先调用trim()函数把每行的空格去掉


        //判断这一行是否为空行

        if (strlen(s) < 2)
        {
            continue;
        }

        //判断这一行是否为注释行

        if (s[0] == 35)
        {
            continue;
        }

        //逐个字符的读取和检测,以等号为分水线把键值和数据分离出来

        for (i=0;i<strlen(s);i++)
        {
            //判断这个字符是否为等号

            if (s[i] == 61)    
            {
                i++;    //这值为等号的这个字符隔过

                denghao = 1;    
            }

            //当denghao为0时把键值分离出来,为1时把数据分离出来

            if (denghao)
            {
                value[row][vol] = s[i];
                vol++;
            }
            else
            {
                key[row][i] = s[i];
            }
        }
        row++;
        vol = 0;
        denghao = 0;
    }
    fclose(fp);

    //打印键值和数据

    for (i=0;i<row;i++)
    {
        printf("key:%s\n",key[i]);

        //fgets()函数在读取一行数据时把回车符\n也读取了,所以要得到真正的数据要把这个回车符分割掉。strtok()函数以第二个参数为分割线把第一个参数分为两部分,只返回前半部分,并以\0结尾。

        printf("value:%s\n",strtok(value[i],"\n"));
    }
    return 0;
}

阅读(1140) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~