【Linux应用开发】之守护进程

来源:互联网 发布:软件工程项目有哪些 编辑:程序博客网 时间:2024/05/22 04:33

守护进程概述

   守护进程,又叫daemon进程,是Linux中的后台服务进程。他是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或者等待处理某些发生的事件。守护进程常常在系统引导载入时启动,在系统关闭时终止。Linux有很多系统哦服务,大多数服务都是通过守护进程实现的。同时,守护进程还能完成许多系统任务,例如,作业规划进程cronf、打印进程lqd等(这里的结尾字母 d 就是 daemon的意思)。

   在Linux中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端称为这些进程的控制终端,当控制终端关闭时,相应的进程都会自动关闭。但是守护进程却能够突破这种限制,它从被执行开始运转,直到接收到某种信号或者整个系统关闭时才退出。如果想让某个进程不因为用户、终端或者其它的变化而受到影响,那么就必须把这个进程变成一个守护进程。可见,守护进程是非常重要的。

编写守护进程步骤

守护进程的最大特点就是脱离了终端,Linux提供了一个系统调用daemon(),要想自定义实现的话,主要包括以下五个步骤:

1.第一步是使用umask函数,把所有的文件屏蔽字置0。文件屏蔽字是可以继承的,当你有相关操作时,如果你要创建一个文件,继承过来的屏蔽字可能阻止你创建相关属性的文件。比如:如果你明确的创建一个文件为组可读,组可写。如果你没有把屏蔽字清零,那么继承过来的屏蔽字可能不允许你添加这两个属性。
2.第二步,创建一个子进程,并且令父进程退出。这样做有以下几个好处:一,如果守护进程是一个简单的shell命令启动的,那么父进程的终止可以使shell认为这个命令已经执行结束了。二,子进程继承了父进程的组ID,但又有自己的进程ID,所以我们可以保证目前的子进程不是进程组长。这一步也是我们接下来要用到的setid函数之前的必要条件。
3.使用setsid函数创建一个新的对会话。首先,该进程变为一个新的会话组的会话头。其次,成为了新的进程组的组长。最后该进程不再控制终端。在system V 下,一些人建议在此时重新fork一次,并且令父进程退出。第二个子进程仍然是一个守护进程。这样做可以保证当前进程不是一个会话组的组长,这样就可以防止他获得控制终端的能力。作为选择,为了防止获得终端的控制权,确定打开终端驱动时明确设置O_NOCTTY。
4.把当前工作目录变为根目录。当前的工作目录是继承父进程的。守护进程是一直存在的,除非你重启计算机。如果你的守护进程是挂载到文件系统上的,那这个文件系统就不能卸载掉。
5.不需要的文件描述符应当关掉。这样可以防止守护进程持有从父进程继承过来的文件描述符。我们可以获取最大的文件描述符,或者使用getrlimit函数来决定最大的文件描述符的值。并且全部关闭。(非必要)

    注意:一些守护进程把0,1,2这三个文件描述符指向/dev/null,这样的话,当库函数试图通过标准输入输出,标准错误时是没有效果的。当一个守护进程脱离了终端时,就没有地方打印信息;也没有地方接收来自用户的交互式输入。甚至当一个守护进程从一个交互式的会话开始,守护进程在后台运行,登陆会话关闭也不会影响到守护进程。如果其他用户用同样的终端登陆,我们不用设想从守护进程打印信息到终端,也别指望用户读取守护进程。

具体分析:

1、创建子进程,父进程退出。

   这是编写守护进程的第一步。由于守护进程是脱离控制终端的,因此,完成第一步后就会在shell终端造成一种程序已经运行完毕的假象,之后的所有工作都在子进程中完成,而用户在shell终端则可以执行其他的命令,从而在形式上做到与控制终端的脱离。

   但是,父进程创建了子进程后退出,此时该子进程不就没有父进程了吗?守护进程中确实会出现这么一个有趣的现象:由于父进程已经先于子进程退出,就会造成子进程没有父进程,从而变成一个孤儿进程。在Linux中,每当系统发现一个孤儿进程时,就会自动由1号进程(也就是 init 进程)收养它,这样原先的子进程就会变成 init 进程的子进程。

2、在子进程中创建新会话   

   这个步骤是创建守护进程最重要的一步,虽然实现非常简单,但意义却非常重大。在这里使用的是系统函数 setsid(),在具体介绍 setsid()之前,先了解以下两个概念:进程组和会话期。

   ●  进程组。进程组是一个或多个进程的集合。进程组由进程组ID来唯一标识。除了进程号PID之外,进程组ID也是一个进程的必备属性。每隔进程组都有一个组长进程,其组长进程的进程号PID等于进程组ID,且该进程组ID不会因为组长进程的退出而受到影响。(组长没了,再找个组员来担任组长呗)

   ●  会话期。会话组是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。进程组和会话期之间的关系如图1所示:

    

   接下来具体介绍 setsid()的相关内容。

   ① setsid()函数的作用。setsid()函数用于创建一个新的会话组,并让执行此函数的进程担任该会话组的组长。调用setsid()有以下3个作用:

         ●   让进程摆脱原会话的控制

         ●   让进程摆脱原进程组的控制

         ●   让进程摆脱原控制终端的控制

    那么,回过头来想想,在创建守护进程时为什么要调用 setsid()函数呢?是这样的,在创建守护进程的第一步中,调用了fork()函数创建子进程再令父进程退出。由于在调用 fork()函数时,子进程全盘复制了父进程的会话期、进程组和控制终端等,虽然父进程退出了,但原先的会话期、进程组和控制终端等并没有改变,因此,还不是真正意义上的独立。而setsid()函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

    ② setsid函数格式

   

3、改变当前目录为根目录

   这一步也是必要的步骤。使用fork()创建的子进程继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统(如“/mnt/usb”等)是不能卸载的,这对以后的使用会造成诸多的麻烦(如系统由于某种原因需要进入单用户模式)。因此,通常的做法是让“/”作为守护进程的当前工作目录,这样就可以避免上述问题。当然,如有特殊需要,也可以把当前工作目录换成其他的路径,如/tmp。改变工作目录的常见函数是chdir()。

4、重设文件权限掩码

  文件权限掩码是指屏蔽掉文件权限中的对应位。例如,有一个文件权限掩码是050,它就屏蔽了文件组拥有者的可读与可执行权限。由于使用fork()函数新建的子进程继承了父进程的文件权限掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件权限掩码设置为0,可以大大增强该守护进程的灵活性。设置文件权限掩码的函数是umask()。通常的使用方法为umask(0)。

5、关闭文件描述符

   同文件权限掩码一样,用fork()函数新建的子进程会从父进程那里继承一些已经打开的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法被卸载。

   事实上,在上面的第2步之后,守护进程已经与所属的控制终端失去了联系,因此,从终端输入的字符不可能达到守护进程,守护进程中用常规方法(如printf())输出的字符也不可能在终端上显示出来。所以文件描述符为0,1和2的3个文件(常说的输入/输出和报错这3个文件)已经失去了存在的价值,也应该被关闭。

   到这里,一个简单的守护进程就建立起来了。创建守护进程的流程图如图所示:

  

基础实验

   本实验按照以上的创建流程建立了一个守护进程,然后让守护进程每隔10s向日志文件/home/song/tmp/daemon.log


使用命令编译:gcc dameon.c -o daemon

    然后执行命令: ./daemon  你可以看到此时没有看到有什么变化

使用命令:ps -ef|grep ./daemon 利用ps中的关键字来查看系统当前正在运行的进程中,有没有咱们的daemon进程

    

    可以看到咱们的守护进程已经在运行了,再来看看/tmp目录下的内容

        

    可以看到,已经有daemon.log日志文件了。

    然后使用命令:tail -f /tmp/daemonl.log  ,可以看到该程序每隔10s就会在对应的文件中输入相关的内容

   

   到这里,这个实验就已经结束了,通过前边使用命令:ps -ef|grep ./daemon可以看到咱们这个进程的进程号是3346,现在使用命令:kill -9  3346将这个进程杀死,同时也把/tmp中的daemon.log文件页删除,方便咱们下边的实验。

守护进程的出错处理

   在编写守护进程的具体调试过程中会发现,由于守护进程完全脱离了控制终端,因此,不能像其他普通进程一样,将错误信息输出到控制终端来通知程序员,即使使用gdb也无法正常调试。那么,守护进程的进程要如何调试呢?一种通用的方法是使用 syslog 服务,将程序中的出错信息输入到系统日志文件中(如“/var/log/messages”),从而可以直观地看到程序的问题所在(“/var/log/message”系统日志文件只能由拥有root权限的超级用户查看。在不同的Linux发行版本中,系统日志文件路径全名可能有所不同,例如,我的ubuntu中路径就是“/var/log/syslog”)。

    syslog 是Linux中的系统日志管理服务,通过守护进程 syslogd 来维护。该守护进程在启动时会读一个配置文件“/etc/syslog.conf”,该文件决定了不同种类的消息会发送到何处。例如,紧急消息可被送到系统管理员并在控制台上显示,而警告消息则可被记录到一个文件中。

    该机制提供了3个syslog相关函数,分别为 openlog()、syslog()和closelog(),下面就分别介绍这3个函数。

函数说明

    openlog()函数用于打开系统日志服务的一个链接;syslog()函数用于向日志文件中写入信息,在这里可以设定消息的优先级、消息输出格式等;closelog()函数用于关闭系统日志服务的链接。


代码:


咱们可以尝试用普通身份执行程序(redhat中不要用root,ubuntu正常运行就可以)。由于这里的open()函数必须具有root权限,因此,syslog 会将错误信息写入到系统日志文件("如/var/log/syslog")中,结果如下图

     


主要的实现使用守护进程加上文件锁的形式保证系统中只有一个可执行实例在运行。

本文主要包括三个部分:
    一是如何实现一个守护进程,二是如何检测一个进程是否活着,三是保证某一执行文件只有一个实例在运行。
/*
 * 1.守护进程:前面已经介绍。
 */
/*
 * 2.如何检查一个进程是否活着
 */

    判断一个进程是否活着,我们主要是通过kill这一系统调用来完成,先看一下kill的manual page:

[cpp] view plain copy
  1. #include <sys/types.h>    
  2. #include <signal.h>    
  3. int kill(pid_t pid, int sig)    
  4.     DESCRIPTION    
  5.     The  kill()  system  call can be used to send any signal to any process    
  6.     group or process.    
  7.     If pid is positive, then signal sig is sent to pid.    
  8.     If pid equals 0, then sig is sent to every process in the process group    
  9.     of the current process    
  10.     If pid equals -1, then sig is sent to every process for which the call-    
  11.     ing process has permission  to  send  signals,  except  for  process  1    
  12.     (init), but see below.    
  13.     If  pid  is less than -1, then sig is sent to every process in the pro-    
  14.     cess group -pid.    
  15.     If sig is 0, then no signal is sent, but error checking is  still  per-    
  16.     formed.    
  17.     if you can send a signal to PID, and returns 1 if you can't (don't have access or invalid PID)。   

所以kill(pid,0)可以用于检测一个为pid的进程是否还活着[在shell下面可以用ps来查找],基本逻辑如下:

[cpp] view plain copy
  1. if(kill(pid,0)!=0)    
  2.     it's dead.    
  3. else    
  4.     it's alive.  

/*
 *3.保证某一执行文件只有一个实例在运行
 */
    这样的需求主要是解决保证只有同时只有一个这样的进程在运行,像MySQL都这样处理:
    1.启动进程后,先检查pid文件是否存在,存在则读取之前写入的pid,然后用上面的kill(pid,0);来检查是否活着,
    2.活着则退出进程,不允许再启动一个进程,否则启动并将当前的pid写入pid文件。写入的时候要锁住文件,避免
    其他进程也往里面写,主要是lockf这个系统调用,方法是:

[cpp] view plain copy
  1.     /**  
  2.      * @Brief  write the pid into the szPidFile  
  3.      *  
  4.      * @Param szPidFile name of pid file  
  5.      */    
  6. void writePidFile(const char *szPidFile)    
  7. {    
  8.     /*open the file*/    
  9.     char            str[32];    
  10.     int lfp = open(szPidFile, O_WRONLY|O_CREAT|O_TRUNC, 0600);    
  11.     if (lfp < 0) exit(1);    
  12.     
  13.     /*F_LOCK(block&lock) F_TLOCK(try&lock) F_ULOCK(unlock) F_TEST(will not lock)*/    
  14.     if (lockf(lfp, F_TLOCK, 0) < 0) {    
  15.         fprintf(stderr, "Can't Open Pid File: %s", szPidFile);    
  16.         exit(0);    
  17.     }    
  18.     
  19.     /*get the pid,and write it to the pid file.*/    
  20.     sprintf(str, "%d\n", getpid()); // \n is a symbol.    
  21.     ssize_t len = strlen(str);    
  22.     ssize_t ret = write(lfp, str, len);    
  23.     if (ret != len ) {    
  24.         fprintf(stderr, "Can't Write Pid File: %s", szPidFile);    
  25.         exit(0);    
  26.     }    
  27.     close(lfp);    
  28. }