linux编程-守护进程编写
守护进程(Daemon)是运行在后台的一种特殊进程。它独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程是一种很有用的进程。
Linux的大多数服务器就是用守护进程实现的。比如,Internet服务器inetd,Web服务器httpd等。同时,守护进程完成许多系统任务。比如,作业规划进程crond,打印进程lpd等。
守护进程的编程本身并不复杂,复杂的是各种版本的Unix的实现机制不尽相同,造成不同 Unix环境下守护进程的编程规则并不一致。
需要注意,照搬某些书上的规则(特别是BSD4.3和低版本的System V)到Linux会出现错误的。
守护进程及其特性
守护进程最重要的特性是后台运行。在这一点上DOS下的常驻内存程序TSR与之相似。其次,守护进程必须与其运行前的环境隔离开来。这些环境包括未关闭的文件描述符,控制终端,会话和进程组,工作目录以及文件创建掩模等。这些环境通常是守护进程从执行它的父进程(特别是shell)中继承下来的。最后,守护进程的启动方式有其特殊之处。它可以在Linux系统启动时从启动脚本/etc/rc.d中启动,可以由作业规划进程crond启动,还可以由用户终端(通常是shell)执行。
总之,除开这些特殊性以外,守护进程与普通进程基本上没有什么区别。因此,编写守护进程实际上是把一个普通进程按照上述的守护进程的特性改造成为守护进程。如果读者对进程有比较深入的认识就更容易理解和编程了。
基本概念及特性
进程:
系统进行资源分配和CPU调度的单位.函数getpid可以得到进程的进程ID:pid_t getpid(void);函数getppid可以得到进程的父进程ID:pid_t getppid(void);
① 每个进程都有一个父进程
② 当子进程终止时,父进程会得到通知并能取得子进程的退出状态.
进程组:
进程组是一个或多个进程的集合。它们与同一作业相关联,可以接受来自同一终端的各种信号。每个进程组都有唯一的进程组ID。函数getpgrp可以得到进程的进程组ID。
pid_t getpgrp(void);
每个进程组都可以有一个组长进程。组长进程的标识是,其进程组ID等于进程ID。
① 每个进程也属于一个进程组。
② 每个进程主都有一个进程组号,该号等于该进程组组长的PID号
③ 一个进程只能为它自己或子进程设置进程组ID号
会话期:
对话期(session)是一个或多个进程组的集合。函数getsid返回会话首进程的进程组ID。此函数是Single UNIX Specification的XSI扩展。pid_t getsid(pid_t pid);
如果pid是0,返回调用进程的会话首进程的进程组ID。如果pid并不属于调用者所在的会话,那么调用者就不能得到该会话首进程的进程组ID。
① setsid()函数可以建立一个对话期:
② 如果,调用setsid的进程不是一个进程组的组长,此函数创建一个新的会话期。
(1)此进程变成该新的对话期的首进程
(2)此进程变成一个新进程组的组长进程。
(3)此进程没有控制终端,如果在调用setsid前,该进程有控制终端,那么与该终端的联系被解除。如果该进程是一个进程组的组长,此函数返回错误。
(4)为了保证这一点,我们先调用fork()然后exit(),此时只有子进程在运行,子进程继承了父进程的进程组ID,但是进程PID却是新分配的,所以不可能是新会话的进程组的PID。
控制终端:
linux是一个多用户多任务的分时操作系统,必须要支持多个用户同时登陆同一个操作系统,当一个用户登陆一次终端时就会产生一个会话,
每个会话有一个会话首进程,即创建会话的进程,建立与终端连接的就是这个会话首进程,也被称为控制进程。
pid_t tcgetpgrp(int filedes);
函数tcgetpgrp返回前台进程组的进程组ID,该前台进程组与在filedes上打开的终端相关联;如果进程有一个控制终端,则该进程可以调用tcsetpgrp将前台进程组ID设置为pgrpid,pgrpid的值应该是在同一会话中的一个进程组的ID,filedes必须引用该会话的控制终端。
下图可以表示以上四者的基本关系:
会话和进程组有一些特性:
1). 一个会话可以有一个控制终端(controlling terminal)。
2). 建立与控制终端连接的会话首进程被称为控制进程(controlling process)。
3). 一个会话中的几个进程组可被分成一个前台进程组(forkground process group)和几个后台进程组(background process group)。
4). 如果一个会话有一个控制终端,则它有一个前台进程组。
5). 无论何时键入终端的中断键(DELETE或Ctrl+C),就会将中断信号发送给前台进程组的所有进程。
6). 无论何时键入终端的退出键(Ctrl+),就会将退出信号发送给前台进程组的所有进程。
7). 如果终端检测到调制解调器(或网络)已经断开连接,则将挂断信号发送给控制进程(会话首进程)。
下边就以守护进程的实际代码运行,辅助理解。
守护进程的编程要点
前面讲过,不同Unix环境下守护进程的编程规则并不一致。所幸的是守护进程的编程原则其实都一样,区别在于具体的实现细节不同。这个原则就是要满足守护进程的特性。同时,Linux是基于Syetem V的SVR4并遵循Posix标准,实现起来与BSD4相比更方便。编程要点如下;
1. 在后台运行。
为避免挂起控制终端将Daemon放入后台执行。方法是在进程中调用fork使父进程终止,让Daemon在子进程中后台执行。
if(pid=fork())
exit(0);//是父进程,结束父进程,子进程继续
2. 脱离控制终端,登录会话和进程组
有必要先介绍一下Linux中的进程与控制终端,登录会话和进程组之间的关系:进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。
控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用setsid()使进程成为会话组长:
setsid();
说明:当进程是会话组长时setsid()调用失败。但第一点已经保证进程不是会话组长。setsid()调用成功后,进程成为新的会话组长和新的进程组长,并与原来的登录会话和进程组脱离。由于会话过程对控制终端的独占性,进程同时与控制终端脱离。
3. 禁止进程重新打开控制终端
现在,进程已经成为无终端的会话组长。但它可以重新申请打开一个控制终端。可以通过使进程不再成为会话组长来禁止进程重新打开控制终端:
if(pid=fork())
exit(0);//结束第一子进程,第二子进程继续(第二子进程不再是会话组长)
4. 关闭打开的文件描述符
进程从创建它的父进程那里继承了打开的文件描述符。如不关闭,将会浪费系统资源,造成进程所在的文件系统无法卸下以及引起无法预料的错误。按如下方法关闭它们:
for(i=0;i 关闭打开的文件描述符close(i);>
5. 改变当前工作目录
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如/tmpchdir("/")
6. 重设文件创建掩模
进程从创建它的父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0);
7. 处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN。
signal(SIGCHLD,SIG_IGN);
这样,内核在子进程结束时不会产生僵尸进程。这一点与BSD4不同,BSD4下必须显式等待子进程结束才能释放僵尸进程。
关于信号的处理此处做一些补充:参考此博客:http://www.cnblogs.com/hoys/archive/2012/08/19/2646377.html
void setupSignal(void) { signal( SIGTERM, SIG_IGN ); signal( SIGINT, SIG_IGN ); signal( SIGPIPE, SIG_IGN ); signal( SIGHUP, SIG_IGN ); signal( SIGTTOU, SIG_IGN ); signal( SIGTTIN, SIG_IGN ); signal( SIGTSTP, SIG_IGN ); signal( SIGCHLD, SIG_IGN );//设置忽略此信号,以为着子进程退出的时候,不需要父进程处理子进程的僵尸状态,而是由init进程(pid=1)负责收尸 }
#include<unistd.h> #include<signal.h> #include<stdio.h> #include<stdlib.h> #include<sys/param.h> #include<sys/types.h> #include<sys/stat.h> #include<time.h> void init_daemon() { int pid; int i; // for(i=0;i<NOFILE;i++) // close(i); printf("parent "); printf("pid[%d] ",getpid()); printf("ppid[%d] ",getppid()); printf("gid[%d] ",getpgrp()); printf("sid[%d] ",getsid(0)); printf("tcid[%d] ",tcgetpgrp(0)); printf(" "); pid=fork(); if(pid<0) exit(1);//使得子进程一定不是进程组组长,这样才能调用setsid,建立新的进程组和会话组 else if(pid>0) exit(0); printf("child 1 "); printf("pid[%d] ",getpid()); printf("ppid[%d] ",getppid()); printf("gid[%d] ",getpgrp()); printf("sid[%d] ",getsid(0)); printf("tcid[%d] ",tcgetpgrp(0)); printf(" "); else if(pid>0) exit(0); printf("child 1 "); printf("pid[%d] ",getpid()); printf("ppid[%d] ",getppid()); printf("gid[%d] ",getpgrp()); printf("sid[%d] ",getsid(0)); printf("tcid[%d] ",tcgetpgrp(0)); printf(" "); setsid(); //建立新的进程组和会话组,并成为新的进程组的组长,和回话组的组长 printf("setsid child 1 "); printf("pid[%d] ",getpid()); printf("ppid[%d] ",getppid()); printf("gid[%d] ",getpgrp()); printf("sid[%d] ",getsid(0)); printf("tcid[%d] ",tcgetpgrp(0)); printf(" "); pid=fork(); if(pid<0) exit(1); else if(pid>0) exit(0);//使得孙子进程不在是进程组的组长,即没有权限建立新的与回话组绑定的控制终端 printf("child 2 "); printf("pid[%d] ",getpid()); printf("ppid[%d] ",getppid()); printf("gid[%d] ",getpgrp()); printf("sid[%d] ",getsid(0)); printf("tcid[%d] ",tcgetpgrp(0)); printf(" "); //关闭文件描述符,这样进程不在与文件描述符传递数据,比如printf打印的数据不在现在是终端界面中 for(i=0;i<NOFILE;i++) close(i); printf("close fd "); chdir("/home/cz/Desktop/mcs/"); //切换工作目录 printf("cd "); umask(0);//清除文件掩膜 printf("umask "); } void main() { FILE *fp; time_t t; printf("%s ","start"); init_daemon();
setupSignal(); while(1) { printf("%s ","run"); sleep(1); printf("hello "); fp=fopen("test.log","a"); //if(fp>=0) //{ time(&t); printf("current time is:%s ",asctime(localtime(&t))); //} } return 0; }
cz@ubuntu:~/Desktop/mcs$ ./demaontest start parent pid[2724] ppid[1821] gid[2724] sid[1821] tcid[2724] cz@ubuntu:~/Desktop/mcs$ child 1 pid[2725] ppid[1] gid[2724] sid[1821] tcid[1821] setsid child 1 pid[2725] ppid[1] gid[2725] sid[2725] tcid[-1] child 2 pid[2726] ppid[1] gid[2725] sid[2725] tcid[-1]
由以上实验结果就可以清晰的明白,守护进程化过程中,每一步的作用。
查看/tmp下的test.log的,可以看到守护进程在不断运行。