Chinaunix首页 | 论坛 | 博客
  • 博客访问: 2039165
  • 博文数量: 369
  • 博客积分: 10093
  • 博客等级: 上将
  • 技术积分: 4271
  • 用 户 组: 普通用户
  • 注册时间: 2005-03-21 00:59
文章分类

全部博文(369)

文章存档

2013年(1)

2011年(2)

2010年(10)

2009年(16)

2008年(33)

2007年(146)

2006年(160)

2005年(1)

分类: LINUX

2007-03-02 23:48:39

守护(Daemon)进程又叫作“精灵进程”,虽然守护进程这个名字更为常用,但是个人感觉还是精灵进程较为机灵可爱些。服务器进程一般都是守护进程,这类进程的一个显著特点就是无交互地在后台进程。注意:这里所说的无交互并不是说真的不能和这类进程打交道,不能控制其运行,那样他们还能提供什么服务?而是说不能通过传统的终端用类似shell的交互方式控制其运行。

那么怎么创建守护进程呢?咋们就边看代码边讲解。

     1
     2  #include
     3  #include
     4  #include
     5  #include
     6  #include
     7  #include
     8  #include
     9  #include
    10  #include
    11  #include
    12  #include
    13  #include
    14  #include
    15


1-15行: 加载必要的头文件,其实这些头文件并不是随意罗列的,而是当需要时再添加,具体方法是需要调用某个库函数或者系统调用时,用man查找它被定义的头文件的路径,然后添加之。如果在编译的时候显示某个函数没有被定义的错误,这时也可以用man查找所需的头文件之所在。有的时候甚至需要用grep到/usr/include目录下面查找变量或者函数的定义。

    16  void daemonize(const char *prgname, ...)
    17  {
    18          va_list args;
    19          char buf[512];
    20          int pid, i;
    21          struct sigaction act, oldact;
    22          struct rlimit lim;
    23
    24          /* Detach controlling terminal */
    25          if ((pid = fork()) < 0)
    26                  exit(1);
    27          else if (pid > 0)
    28                  _exit(0);
    29          setsid();
    30

25-29行:从当前进程fork出一个子进程,然后当前进程退出。如果当前进程是shell从前台启动的的话,当当前进程退出的时候,子进程将变成孤儿进程,接着自动被启动进程(init)收养,当然它所在的进程组也将从前台转为后台。调用完setsid()之后,子进程将创建一个新的会话和进程组,sid和gpid都是子进程的pid,因为子进程已经和当前进程不属于一个会话,那么与会话相关联的控制终端也不复存在。如果你足够细心,你可能注意到了这段代码中用了两个进程退出函数exit和_exit,为什么要如此呢?_exit并不会执行由atexit或者on_exit注册的进程退出回调函数,除此之外,它和exit并没有区别。假设用户在调用daemonize把当前进程守护化之前注册过进程退出回调函数,如果fork成功而当前进程通过调用exit退出,那么回调函数将被执行,而这时执行回调函数也许是不当的,因为子进程并没有退出,当子进程退出的时候也许还将执行一遍回调函数。exit和_exit的选用正是为了保证进程退出回调函数被且尽被执行一次。以下对exit和_exit的选用也是基于此目的,遇到时将不再赘述。事实上,daemonize函数应该尽早调用,最好不要再其前面做太多的非必要操作,类似注册进程退出回调函数的举动应该尽量避免。

    31          /* Avoid owning controlling terminal again */
    32          memset(&act, 0, sizeof(act));
    33          act.sa_handler = SIG_IGN;
    34          sigemptyset(&act.sa_mask);
    35          sigaction(SIGHUP, &act, &oldact);
    36          if ((pid = fork()) < 0)
    37                  exit(1);
    38          else if (pid > 0)
    39                  _exit(0);
    40          /* Wait for the death of it's parent. */
    41          while (getppid() != 1)
    42                  ;
    43          sigaction(SIGHUP, &oldact, NULL);
    44

31-43行:这段代码的意义何在呢?有些UNIX操作系统(如SVR4)的会话首进程打开一个终端设备时,如果其所在会话组并没有控制终端,那么这个终端设备将自动成为这个会话组的控制终端。通过这次的fork而产生的孙子进程因为不是会话首进程,也就失去了为此会话设置控制终端的能力。当会话首进程退出的时候可能向其所在会话组的所有进程发送SIGHUP信号,而SIGHUP信号的默认处理函数是结束进程。为了防止孙子进程因此意外结束,忽略SIGHUP信号直到子进程退出,孙子进程被启动进程(init)收养。我查看了Linux内核的相关代码,发现只有当进程被SIGSTP终止时才会被发送SIGHUP和SIGCONT信号,所以此段关于信号的处理部分在Linux下是无效的,也许其他操作系统行为有异,姑且加之。

    45          /* Deal with file operations */
    46          umask(0);
    47          if (chdir("/") < 0)
    48                          exit(1);
    49          if (getrlimit(RLIMIT_NOFILE, &lim) < 0)
    50                  exit(1);
    51          if (lim.rlim_cur == RLIM_INFINITY)
    52                  lim.rlim_cur = 1024;
    53          for (i = 0; i < lim.rlim_cur; i ++) {
    54                  if (close(i) < 0 && errno != EBADF)
    55                          exit(1);
    56          }
    57          if (open("/dev/null", O_RDWR) < 0
    58                          || dup(0) < 0
    59                          || dup(0) < 0)
    60                  exit(1);
    61

45-60行:设置文件掩码为0,改变当前工作目录到系统根目录,关闭所有打开的文件描述符,并把标准输入、标准输出和标准错误输出重定向到空设备(/dev/null),使他们保持沉默。

    62          /* Ignore all traditional signals */
    63          for (i = 1; i < 32; i ++)
    64                  sigaction(i, &act, NULL);
    65

62-65行:忽略所有的传统信号,当然SIGKILL信号是无法忽略的,所以我也没有检查返回值。按照设计惯例:SIGHUP用来热更新系统配置;SIGTERM用来结束进程,这个信号一般是需要捕捉并处理的,不然被SIGKILL强制杀死的滋味可不好受哦。(修正:62-65行的操作最好免除,因为大部分信号是不希望被忽略的,如SEGV)。

    66          /* Initialize the log file */
    67          va_start(args, prgname);
    68          vsnprintf(buf, sizeof(buf), prgname, args);
    69          va_end(args);
    70          openlog(buf, LOG_CONS | LOG_PID, LOG_DAEMON);
    71  }
    72

66-71行:一般的服务器都需要用日志(log)记录守护进程的状态等信息以备分析和调试之用,这段代码就是打开到系统日志服务器(syslogd)的连接,并设置记录守护进程的进程名和pid。

至此,进程的守护化就顺利完成了。

服务器程序一般都具有排他性,换句话说就是一个操作系统上只允许有一个守护进程实例存在。以下代码实现了这个功能:

 96 int uniqued(const char *prgname)
 97 {
 98         char buf[512];
 99         int fd, retval = -1;
100
101         assert(prgname != NULL);
102         snprintf(buf, sizeof(buf), "/var/run/%s.pid", prgname);
103         if ((fd = open(buf, O_RDWR | O_CREAT)) < 0)
104                 goto out;
105         if (flock(fd, LOCK_EX | LOCK_NB) < 0) {
106                 if (errno == EWOULDBLOCK)
107                         retval = 0;
108                 else
109                         unlink(buf);
110                 goto err;
111         }
112         if (ftruncate(fd, 0) < 0)
113                 goto err;
114         snprintf(buf, sizeof(buf), "%ld\n", (long)getpid());
115         if (write(fd, buf, strlen(buf)) != strlen(buf))
116                 goto err;
117         retval = fd;
118
119 out:
120         return retval;
121
122 err:
123         while (close(fd) < 0 && errno == EINTR)
124                 ;
125         goto out;
126 }

遵从惯例,记录有守护进程进程号的文件被放在/var/run/目录下,并被命名为:守护进程名.pid。函数uniqued利用排他文件锁保证了守护进程实例的单一性。

阅读(2387) | 评论(0) | 转发(0) |
0

上一篇:春雨

下一篇:无题

给主人留下些什么吧!~~