Board logo

标题: Linux下使用daemon函数编写后台程序 [打印本页]

作者: yuyang911220    时间: 2017-1-25 19:17     标题: Linux下使用daemon函数编写后台程序

以前我们在看《unix环境高级编程》的时候,有专门的整章详细介绍如何编写一个后台daemon程序(精灵程序),主要涉及到创建会话组,切换工 作目录,设置文件屏蔽字,关闭不必要的描述符等多个操作。这些操作对于每一个后台程序来说都是类似的。
Linux中专门提供了一个函数来完成这个daemon化的过程,这个函数的原型如下
int daemon (int __nochdir, int __noclose);

如果__nochdir的值为0,则将切换工作目录为根目录;如果__noclose为0,则将标准输入,输出和标准错误都重定向到/dev /null。
经过这个函数调用后的程序将运行在后台,成为一个daemon程序,而linux下大多的服务都是以此方式运行的。
我们来看一个简单的例子。例如编写例子程序test.c
#include <unistd.h>#include <stdio.h> int do_sth(){    //Add what u want    return 0;}int main(){    daemon(0,0);    while ( 1 )    {        do_sth();        sleep(1);    }}

编译并运行
[leconte@localhost daemon]$ gcc -o test test.c[leconte@localhost daemon]$ ./test

程序进入了后台,通过ps查看进程情况,可以看到进程的父进程id为1,即init进程

用lsof查看test进程所打开的文件,可以看到文件描述符0,1,2都被重定向到/dev/null

并且能够看到,进程的当前工作目录(cwd)为根目录/,daemon函数已经帮我们完成了daemon化的过程,接下来我们只需要关注于程序功能 的实现了。

摘要:针对Linux环境下的守护进程daemon,分析了一般性守护进程的编写方法,并提出若干见解,通过总结归纳进而为设计和开发守护进程提供了有意的参考,给出了基于Linux守护进程实现的主要思想。

关键词: 守护进程;信号量;控制终端

  
  1 引言
  Linux在启动时需要启动很多系统服务,它们向本地和网络用户提供了Linux的系统功能接口,直接面向应用程序和用户。提供这些服务的程序是由运行在后台的守护进程(Daemons)来执行的。
  编写守护进程实际上是把一个普通进程按照守护进程的特性进行改造。比如,网络通信服务中的守护进程需要能同时接受多个请求,它不断地在侦听端等待远程的连接请求,收到请求后,创建一个子进程,让其负责与远端的通信,而自己则继续返回侦听。子进程和父进程间的通信采用消息机制,因此守护进程的开发涉及到子进程、进程组、会晤期、信号量、文件权限、目录和控制终端等多个概念。本文主要分析守护进程的概念、实现原理以及编写守护进程的方法。
  
  2 Linux进程结构
  2.1 进程的创建
  Linux使用fork()函数来创建一个子进程,当fork()调用失败时系统返回-1。一旦子进程被创建,父子进程一起从fork()处继续执行,相互竞争系统的资源。如果希望子进程继续执行,而父进程阻塞直到子进程完成任务,这时候可以调用wait()或者waitpid()系统调用。
  pid_t wait(int *stat_loc);
  pid_t waitpid(pid_t pid,int *stat_loc,int options);
  wait()系统调用会使父进程阻塞直到一个子进程结束或者是父进程接受到了一个信号。如果没有父进程或者没有子进程或者它的子进程已经结束了,wai()会立即返回。成功时(因一个子进程结束)wait()将返回子进程的ID,否则返回-1。
  2.2 守护进程的创建
  守护进程最重要的特性是后台运行,因此守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符、控制终端、会话和进程组、工作目录以及文件创建掩码等。这些环境是守护进程从执行它的父进程(特别是Shell)中继承下来的。以下程序使用一个INIT_DAEMON宏来实现守护进程的初始化工作。
  #define INIT_DAEMON

  {
             if (fork()>0)
             exit(0);// 是父进程,结束父进程
        else if(fork()<0)exit(1);// fork失败,退出
        setsid(); // 第一子进程成为新的进程组长,并与控制终端分离
         if(fork()>0) exit(0);//是第一子进程,结束第一子进程
         else if(fork<0) exit(1);// fork失败,退出。
  } //是第二子进程,继续,第二子进程不再是会话组长
  第一次调用fork函数,为避免挂起,控制终端将守护进程放入后台执行,然后调用setsid()函数脱离控制终端和进程组,使该进程成为会话组长,并与原来的登录会话和进程组脱离。此时进程已经成为无终端的会话组长,但它可以重新申请打开一个控制终端。为了避免这种情况,可以通过使进程不再成为会话组长来禁止进程重新打开控制终端,这就需要第二次调用fork()函数。父进程(会话组长)退出,子进程继续执行,并不再拥有打开控制终端的能力。在正在执行的进程中调用INIT_DAEMON后,进程将成为守护进程,脱离控制终端进入后台执行。
  2.3 信号量机制
  为了防止在守护进程没有正常运转起来时,控制终端受到干扰退出或挂起,需要将以下有关控制终端操作的信号屏蔽。
  signal(SIGTTOU, SIG_IGN);// 后台进程写控制终端
  signal(SIGTTIN, SIG_IGN);// 后台进程读控制终端
  signal(SIGTSTP, SIG_IGN);// 终端挂起
  signal(SIGHUP, SIG_IGN);// 进程组长退出时向所有会议成员发出
  当信号出现时,开发人员可以要求系统进行以下三种操作:
  (1)忽略信号。大多数信号都是采取这种方式进行处理,但是对SIGKILL和SIGSTOP信号不能做忽略处理。
  (2)捕捉信号。最常见的情况是,如果捕捉到SIGCHID信号,则表示子进程已经终止。可在此信号的捕捉函数中调用waitpid()函数取得该子进程的进程ID和它的终止状态。
(3)执行系统的默认动作。对绝大多数信号而言,系统的默认动作都是终止该进程。
  
  3 守护进程开发准则
  3.1 控制终端
  首先使用ps命令打印系统中各个进程的状态。
  所有守护进程都以超级用户(用户ID为0)的优先权运行。没有一个守护进程具有控制终端,终端名称设置为问号(?)、终端前台进程组ID设置为-1。缺少控制终端是守护进程调用了setsid()的结果。
  3.2 进程组
  进程组是一个或多个进程的集合。进程组ID类似于进程ID,可存放在 pid_t 数据类型中。每个进程组有一个组长进程,组长进程的标识是其进程组ID等于其进程ID。进程组组长可以创建一个进程组,创建该组中的进程,然后终止。只要在某个进程组中有一个进程存在,则该进程就存在,这与其组长进程是否终止无关。
  3.3 会话期
  会话期(Session)是一个或多个进程组的集合。在一个会话期中有3个进程组,通常是有Shell的管道线将几个进程编成一组。一个会话期可以有一个单独的控制终端,即在其上登录的终端设备(终端登录)或伪终端设备(网络登录),但这个控制终端并不是必需的。如果一个会话期有一个控制终端,则它有一个前台进程组,其他进程组为后台进程组。
  3.4 脱离控制终端,登录会话和进程组
  登录会话可以包含多个进程组,这些进程组共享一个控制终端,这个控制终端通常是创建进程的控制终端。登录会话和进程组通常是从父进程继承下来的。需要说明的是,当进程是会话组长时,setsid()调用将失败,2.2已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。具体是操作就是:
  (1)成为新对话期的首进程;
  (2)成为一个新进程组的首进程;
  (3)没有控制终端。
  守护进程要摆脱从父进程继承下来的控制终端它们影响,可以使用setsid()函数设置新会话的领头进程,并与原来的登录会话和进程组脱离。这只是其中的一种方法,还可以使用如下处理方法:
  if ((fd = open("/dev/tty",O_RDWR)) >= 0)
  {ioctl(fd,TIOCNOTTY,NULL);
  // 其中/dev/tty是一个流设备,也是终端映射
  close(fd); } // 调用close()函数将终端关闭
  3.5 关闭文件描述符
  进程从创建它的父进程那里继承了打开的文件描述符,如果守护进程留下一个处于打开状态的普通文件,将阻止该文件被任何其他进程从文件系统中删除。一般来说,必要的是关闭0、1、2三个文件描述符,即标准输入(STDINT)、标准输出(STDOU)、标准错误(STDERR)。关闭不必要的连接甚至更为重要,因为在该终端上的用户退出系统后,将执行vhangup()系统调用,守护进程访问该终端的权利于是被撤消。这表示守护进程虽有它以为处于打开状态的文件描述符,事实上已不再能通过这些文件描述符访问该终端。关闭三者的代码如下:
  for (fd = 0, fdtablesize = getdtablesize(); fd < fdtablesize; fd++)
  // fdtablesize是一个进程一次可以打开进程的最大数
  close(fd); // 关闭打开的文件描述符,包括标准输入、标准输出和标准错误输出
  如果程序想保留0、1、2三个文件描述符,那么循环应绕过这三者。实现代码如下:
  for (fd =3, fdtablesize = getdtablesize();fd < fdtablesize; fd++)
  close(fd);
  
  4 结束语
  守护进程广泛应用于Linux/Unix环境下的系统管理、网络通信以及嵌入式应用等领域。本文分析了Linux守护进程的结构与实现原理,所欠缺的是构建程序的通用化,原因是存在不同环境之间切换并执行不同的任务,同时还必须考虑其他系统之间的所有差异,今后的工作主要集中在标准化以及简化异构环境中的管理任务的方法上。




欢迎光临 电子技术论坛_中国专业的电子工程师学习交流社区-中电网技术论坛 (http://bbs.eccn.com/) Powered by Discuz! 7.0.0