守护进程的建立
在介绍守护进程的建立之前,首先来看一下下面的这个例程 daemon_init() ,它演示了 建立一个守护进程的全部过程: #include #include #include #include #define MAXFD 64 void daemon_init(const char *pname, int facility) { int i: pid_t pid; /* fork,终止父进程 */ if (pid=fork()) exit(0); /* 第一子进程 */ setsid(); signal(SIGHUP,SIG_IGN); /* fork,终止第一子进程 */ if (pid=fork()) exit(0); /* 第二子进程 */ daemon_proc=1; /* 将工作目录设定为"/" */ chdir("/"); /* 清除文件掩码 */ umask(0); /* 关闭所有文件句柄 */ for (i=0;i<MAXFD;I++) close(i); 打开 log openlog(pname,LOG_PID,facility); 看过了上面的程序,下面我们就来讲讲建立一个守护进程需要进行哪些操作:
第三章 进程控制
- 49 -
1.fork 首先需要 fork 一个子进程并将父进程关闭.如果进程是作为一个 shell 命令在命令行上 前台启动的,当父进程终止时,shell 就认为该命令已经结束.这样子进程就自动称为了后 台进程.而且,子进程从父进程那里继承了组标识符同时又拥有了自己的进程标识符,这 样保证了子进程不会是一个进程组的首进程.这一点是下一步 setsid 所必须的. 2.setsid setsid()调用创建了一个新的进程组,调用进程成为了该进程组的首进程.这样,就使 该进程脱离了原来的终端,成为了独立于终端外的进程. 3.忽略 SIGHUP 信号,重新 fork 这样使进程不在是进程组的首进程,可以防止在某些情况下进程意外的打开终端而重 新与终端发生联系. 4.改变工作目录,清除文件掩码 改变工作目录主要是为了切断进程与原有文件系统的联系.并且保证无论从什么地方 启动进程都能正常的工作.清除文件掩码是为了消除进程自身掩码对其创建文件的影响. 5.关闭全部已打开的文件句柄 这是为了防止子进程继承了在父进程中打开的文件而使这些文件始终保持打开从而产 生某些冲突. 6.打开 log 系统 以上就是建立一个守护进程的基本步骤.当然,一个实际的守护进程要比这个例子复 杂许多,但是万变不离其宗,原理都是相同的.通过上面几步,我们可以正确的建立自己 的守护进程.
3.5 本章小结
进程是 UNIX/Linux 系统的基本核心概念,具有十分重要的地位和作用. 在本章中,我们详细介绍了进程的建立和使用以及相关的问题.熟练的掌握这些知识, 是 Linux 编程所必须的.希望读者能用心体会.
- 50 -
Linux 网络编程
第四章
进程间通信
网络程序设计中通常包括两个或更多的需要互相对话(interprocess communications ) 的进程,因此进程通信的方法在网络程序设计中是极为重要的.网络程序设计在这个方面 不同于一般程序设计通常所使用的方法.一个传统的程序可以通过全局变量或函数调用和 不同的模块(甚至同一机器上的其它应用程序)对话,但是在网络上却不行. 网络程序设计的一个重要的目标是保证进程间不互相干涉,否则系统可能被挂起或自 锁,因此,进程间必须使用简洁有效的方法进行通信,在此方面,Linux 具有非常显著的 兼容性.因为 Linux 的许多基本性能如管道,队列等都非常适合网络. 在这一章中,我们将详细的介绍各种进程间通信的方法及其使用方式.
4.1 进程间通信的一些基本概念
下面是一些在学习进程间通信时会遇到的基本概念: l 进程阻塞 当一个进程在执行某些操作的条件得不到满足时,就自动放弃 CPU 资源而进入休眠状 态,以等待条件的满足.当操作条件满足时,系统就将控制权返还给该进程继续进行未完 的操作. l 共享资源 因为计算机的内存,存储器等资源是有限的,无法为每一个进程都分配一份单独的资 源.所以系统将这些资源在各个进程间协调使用,称为共享资源. l 锁定 当某个进程在使用共享资源使用,可能需要防止别的进程对该资源的使用.比如,一 个进程在对某个文件进行读操作时,如果别的进程也在此时向文件中写入了内容,就可能 导致进程读入错误的数据.为此,Linux 提供一些方法来保证共享资源在被某个进程使用 时,别的进程无法使用.这就叫做共享资源的锁定.
4.2 信号
信号是 UNIX 系统所使用的进程通信方法中,最古老的一种.系统使用它来通知一个 或多个进程异步事件的发生,比如键盘上某个键被按下,或者计时器到达了某个特定的事 件.系统也用信号来处理某种严重的错误.比如,一个进程试图向一块不存在的虚拟内存 写入数据,或者某个进程试图执行一条非法指令. 信号不但能从内核发往一个进程,也能从一个进程发往另一个进程.例如,用户在后 台启动了一个要运行较长时间的程序,如果想中断其执行,可以用 kill 命令把 SIGTERM 信号发送给这个进程,SIGTERM 将终止此进程的执行. 信号还提供了向 UNIX 系统进程传送软中断的简单方法.信号可以中断一个进程,而 不管它正在作什么工作.由于信号的特点,所以不用它来作进程间的直接数据传送,而把 它用作对非正常情况的处理. 由于信号本身不能直接携带信息,这就限制了它作为一项通用的进程通信机制.但是,
第四章
进程间通信
- 51 -
每种信号都有其特定的含义,并由其名字所指示.在 Linux 系统库 bits/signum.h 中对这些 信号名作了定义,每个名字代表一个正整数.例如: #define SIGHUP 1 /* Hangup (POSIX). */ 定义了信号 SIGHUP. Linux 提供的大多数信号类型是供内核使用的,只有少数的几种信号可以用作在进程 之间传送.下面给出常用的信号和它们的意义: l SIGHUP 当终止一个终端时,内核就把这一种信号发送给该终端所控制的所有进程.通常情况 下,一个进程组的控制终端是该用户拥有的终端,但不完全是如此.当进程组的首进程结 束时,就会向该进程组的所有进程发送这种信号.这就可以保证当一个用户退出使用时, 其后台进程被终止,除非有其它方面的安排. l SIGINT 当一个用户按了中断键(一般为 Ctrl+C)后,内核就向与该终端有关联的所有进程发 送这种信号.它提供了中止运行程序的简便方法. l SIGQUIT 这种信号与 SIGINT 非常相似,当用户按了退出键时(为 ASCII 码 FS,通常为 Ctrl+\) , 内核就发送出这种信号.SIGQUIT 将形成 POSIX 标准所描述的非正常终止.我们称这种 UNIX 实现的实际操作为核心转贮(core dump) ,并用信息"Quit (core dump)"指出这一操 作的发生.这时,该进程的映象被转贮到一个磁盘文件中,供调试之用. l SIGILL 当一个进程企图执行一条非法指令时,内核就发出这种信号.例如,在没有相应硬件 支撑的条件下,企图执行一条浮点指令时,则会引起这种信号的发生.SIGILL 和 SIGQUIT 一样,也形成非正常终止. l SIGTRAP 这是一种由调试程序使用的专用信号.由于他的专用行和特殊性,我们不再对它作进 一步的讨论.SIGTRAP 也形成非正常终止. l SIGFPE 当产生浮点错误时(比如溢出) ,内核就发出这种信号,它导致非正常终止. l SIGKILL 这是一个相当特殊的信号,它从一个进程发送到另一个进程,使接收到该信号的进程 终止.内核偶尔也会发出这种信号.SIGKILL 的特点是,它不能被忽略和捕捉,只能通过 用户定义的相应中断处理程序而处理该信号.因为其它的所有信号都能被忽略和捕捉,所 以只有这种信号能绝对保证终止一个进程. l SIGALRM 当一个定时器到时的时候,内核就向进程发送这个信号.定时器是由改进程自己用系 统调用 alarm()设定的. l SIGTERM 这种信号是由系统提供给普通程序使用的,按照规定,它被用来终止一个进程. l SIGSTOP 这个信号使进程暂时中止运行,系统将控制权转回正在等待运行的下一个进程. l SIGUSR1 和 SIGUSR2 和 SIGTERM 一样,这两种信号不是内核发送的,可以用于用户所希望的任何目的. l SIGCHLD 子进程结束信号.UNIX 中用它来实现系统调用 exit()和 wait().执行 exit()时,就向子
- 52 -
Linux 网络编程
进程的父进程发送 SIGCHLD 信号,如果这时父进程政在执行 wait(),则它被唤醒;如果这 时候父进程不是执行 wait(),则此父进程不会捕捉 SIGCHLD 信号,因此该信号不起作用, 子进程进入过渡状态(如果父进程忽略 SIGCHLD,子进程就结束而不会进入过渡状态) . 这个机制对大多数 UNIX 程序员来说是相当重要的. 对于大多数情况来说,当进程接收到一个信号时,它就被正常终止,相当于进程执行 了一个临时加入的 exit()调用.在这种情况下,父进程能从进程返回的退出状态中了解可能 发生的事情,退出状态的低 8 位含有信号的号码,其高 8 位为 0. 信号 SIGQUIT,SIGILL,SIGTRAP,SIGSYS 和 SIGFPE 会导致一个非正常终止,它 们将发生核心转贮,即把进程的内存映象写入进程当前目录的 core 文件之中.core 文件中 以二进制的形式记录了终止时程序中全部变量之值,硬件寄存器之值和内核中的控制信息. 非正常终止进程的退出状态除了其低端第 7 位被置位外,其它均与通过信号正常终止时一 样. Linux 的调试程序 gdb 知道 core 文件的格式,可以用它们来观察进程在转贮点上的状 态.这样,就可以用 gdb 正确的定出发生问题的位置. 这里再介绍一下系统调用 abort(),它在 Linux 系统库 stdlib.h 中定义: void abort(void); abort()向调用进程发送一个信号,产生一个非正常终止,即核心转贮.由于它能够使 一个进程在出错时记录进程的当前状态,所以可以用它来作为调试的辅助手段.这也说明 了进程可以向自己发送信号这一事实.
4.2.1
信号的处理
几乎所有的信号都将终止接收到该信号的进程.对于一些简单的程序,这完全能满足 要求.用户按了中断或者退出键,就可以停止一个有问题的程序的运行.但是在大型的程 序中,一些意料之外的信号会导致大问题.例如,正当在对一个重要的数据库进行修改期 间,由于不小心碰到了中断键,而使程序被意外的终止,从而产生严重的后果. UNIX 的系统调用 signal()用于接收一个指定类型的信号,并可以指定相应的方法.这 就是说,signal()能够将指定的处理函数与信号向关联.它在 Linux 系统库 signal.h 中的函 数声明如下: int signal (int sig, __sighandler_t handler); Signal()有两个参数: 第一个参数 sig 指明了所要处理的信号类型,它可以取除了 SIGKILL 和 SIGSTOP 外 的任何一种信号.参数 handler 描述了与信号关联的动作,它可以取以下三种值: l 一个返回值为整数的函数地址. 此函数必须在 signal()被调用前声明,handler 中为这个函数的名字.当接收到一个类型 为 sig 的信号时,就执行 handler 所指定的函数.这个函数应有如下形式的定义: int func(int sig); sig 是传递给它的唯一参数.执行了 signal()调用后,进程只要接收到类型为 sig 的信号, 不管其正在执行程序的哪一部分,就立即执行 func()函数.当 func()函数执行结束后,控制 权返回进程被中断的那一点继续执行. l SIG_IGN 这个符号表示忽略信号.执行了相应的 signal()调用好,进程会忽略类型为 sig 的信号. l SIG_DFL 这个符号表示恢复系统对信号的默认处理.
第四章
进程间通信
- 53 -
函数如果执行成功,就返回信号在此次 signal()调用之前的关联. 如果函数执行失败,就返回 SIG_ERR.通常这种情况只有当 sig 参数不是有效的信号 时才会发生.函数不对 handler 的有效性进行检查. 下面我们来看几个例子. 首先,下面的这段代码则将使进程忽略 SIGINT 信号: #include #include #include int main(void) { signal(SIGINT,SIG_IGN); /*告诉进程将 SIGINT 信号忽略*/ printf("xixi\n"); sleep(10); /*系统函数 sleep()使进程休眠指定的时间(以秒为单位)*/ printf("end\n"); return; } 如果在程序中需要重新恢复系统对信号的缺省处理,就使用下面的语句: signal(SIGINT, SIG_DFL); 在 Linux 程序中常常利用 SIG_IGN 和 SIG_DFL 屏蔽 SIGINT 和 SIGQUIT 来保证执行 重要任务的程序不会被意外的中止. 在 shell 中也是利用这一技术来确保用户按中断键时,不中断后台程序的运行.因为被 一个进程忽略的信号,在进程执行 exec()调用后,仍然被忽略,所以 shell 能够调用 signal() 来保证 SIGQUIT 和 SIGINT 被忽略,然后用 exec 执行新程序.但是要注意到,在父进程 中设定的信号和函数的关联关系会被 exec()调用自动用 SIG_DFL 恢复成系统的缺省动作, 这是因为在 exec 的子进程中没有父进程的函数映象. 再让我们来看看下面这段捕捉 SIGINT 的代码: #include #include #include int catch(int sig); int main(void) { signal(SIGINT,catch); /* 将 SIGINT 信号与 catch 函数关联 */ printf("xixi\n"); sleep(10); printf("end\n"); return; } int catch(int sig) {
- 54 -
Linux 网络编程
printf("Catch succeed!\n"); return 1; } 当程序运行时我们按下中断键(Ctrl+C) ,进程被中断,函数 catch 就被执行.它执行 完毕后,进程回到中断点继续执行. 如果我们希望一个进程被信号终止前能够完成一些处理工作,如删除工作中使用的临 时文件等,就可以设计一个信号处理函数来完成工作.比如可以有这样的一个处理函数: int catch(int sig) { printf("Catch succeed!\n"); exit(1); } 这个函数在最后调用 exit()函数来使进程结束运行.这样就保留了信号原有的中断进程 的功能. 这里需要指出一点,当程序把一个信号处理函数与 SGINT 和 SIGQUIT 联系起来后, 如果该程序在后台执行,那么由于 shell 的作用,会使得 SIGINT 和 SIGQUIT 被忽略.这 样后台程序就不会被 SIGINT 和 SIGOUT 所中止. 前面曾经提过,signal()调用返回原先与指定信号相关联的处理函数,这样,我们就可 以保存和恢复原来对指定信号的处理动作.下面的代码说明这一技术: int (*oldptr)(),newcatch(); /* 设定 SIGINT 的关联,同时保存原来的关联*/ oldptr=singal(SIGINT,newcatch); /* 工作代码段 */ … .. /* 恢复原来的关联 */ signal(SIGINT,oldptr);
4.2.2
信号与系统调用的关系
当一个进程正在执行一个系统调用时,如果向该进程发送一个信号,那么对于大多数 系统调用来说,这个信号在系统调用完成之前将不起作用,因为这些系统调用不能被信号 打断.但是有少数几个系统调用能被信号打断,例如:wait(),pause()以及对慢速设备(终端, 打印机等)的 read(),write(),open()等.如果一个系统调用被打断,它就返回-1,并将 errno 设为 EINTR.可以用下列代码来处理这种情况: if (wirte(tfd,buf,SIZE)<0) { if (errno==EINTR) { warn(" Write interrupted." ); … … } }
第四章
进程间通信
- 55 -
4.2.3
信号的复位
在 Linux 中,当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信 号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用 相应的处理函数.下面的程序演示了这一点: #include int interrupt() { printf(" Interrupt called\n" ); sleep(3); printf(" Interrupt Func Ended.\n" ); } main() { signal(SIGINT,interrupt); printf(" Interrupt set for SIGINT\n" ); sleep(10); printf(" Program NORMAL ended.\n" ); return; } 执行它,结果如下: Interrupt set for SIGINT Interrupt called Func Ended Interrupt called Func Ended Program NORMAL ended. 但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中 断: #include int interrupt() { printf(" Interrupt called\n" ); sleep(3); printf(" Interrupt Func Ended.\n" ); } int catchquit() {
- 56 -
Linux 网络编程
printf(" Quit called\n" ); sleep(3); printf(" Quit ended.\n" ); } main() { signal(SIGINT,interrupt); signal(SIGQUIT,catchquit); printf(" Interrupt set for SIGINT\n" ); sleep(10); printf(" Program NORMAL ended.\n" ); return; } 执行这个程序的结果如下: Interrupt set for SIGINT Interrupt called Quit called Quit ended. Interrupt Func Ended. Program NORMAL ended. 还要注意的是,在 Linux 系统中同种信号是不能积累的.比如我们执行上面的代码: Interrupt set for SIGINT Interrupt called Func Ended Interrupt called Func Ended Program NORMAL ended. 而且如果两个信号同时产生,系统并不保证进程接收它们的次序.以上的两个缺点影 响了信号作为进程通信手段的可靠性,因为一个进程不能保证它发出的信号不被丢失. 当某个信号未被处理的时候,如果对该信号执行 signal 调用,那么该信号将被注销.
4.2.4
在进程间发送信号
一个进程通过对 signal()的调用来处理其它进程发送来的信号.同时,一个进程也可以 向其它的进程发送信号.这一操作是由系统调用 kill()来完成的. kill()在 linux 系统库 signal.h 中的函数声明如下: int kill(pid_t pid, int sig); 参数 pid 指定了信号发送的对象进程:它可以是某个进程的进程标识符(pid),也可以 是以下的值: 如果 pid 为零,则信号被发送到当前进程所在的进程组的所有进程;
第四章
进程间通信
- 57 -
如果 pid 为-1,则信号按进程标识符从高到低的顺序发送给全部的进程(这个过程受 到当前进程本身权限的限制,请看后面的解释) ; 如果 pid 小于-1,则信号被发送给标识符为 pid 绝对值的进程组里的所有进程. 需要说明的是,一个进程并不是向任何进程均能发送信号的,这里有一个限制,就是 普通用户的进程只能向具有与其相同的用户标识符的进程发送信号.也就是说,一个用户 的进程不能向另一个用户的进程发送信号.只有 root 用户的进程能够给任何线程发送信号. 参数 sig 指定发送的信号类型.它可以是任何有效的信号. 由于调用 kill()的进程需要直到信号发往的进程的标识符,所以这种信号的发送通常只 在关系密切的进程之间进行,比如父子进程之间. 下面是一个使用 kill()调用发送信号的例子.这个程序建立两个进程,并通过向对方发 送信号 SIGUSR1 来实现它们之间的同步.这两个进程都处于一个死循环中,在接收对方发 送的信号之前,都处于暂停等待中.这是通过系统调用 pause()来实现的,它能够使一个程 序暂停,直至一个信号到达,然后进程输出信息,并用 kill 发送一个信号给对方.当用户 按了中断键,这两个进程都将终止. #include int ntimes=0; main() { int pid,ppid; int p_action(), c_action(); /* 设定父进程的 SIGUSR1 */ signal(SIGUSR1,p_action); switch(pid=fork()) { case -1: /*fork 失败*/ perror("synchro"); exit(1); case 0: /*子进程模块*/ /* 设定子进程的 SIGUSR1 */ signal(SIGUSR1,c_action); /* 获得父进程的标识符 */ ppid=getppid(); for(;;) { sleep(1); kill(ppid,SIGUSR1); pause(); }
- 58 -
Linux 网络编程
/*死循环*/ break; default: /*父进程模块*/ for (;;) { pause(); sleep(1); kill(pid,SIGUSR1); } /*死循环*/ } } p_action() { printf("Patent caught signal #%d\n",++ntimes); } c_action() { printf("Child caught signal #%d\n",++ntimes); } 程序运行结果如下: Patent caught signal #1 Child caught signal #1 Patent caught signal #2 Child caught signal #2 Patent caught signal #3 Child caught signal #3 Patent caught signal #4 Child caught signal #4 这里顺便介绍一下 kill 命令,它是一个对系统调用 kill()的命令层接口.kill 命令用于 向一个运行进程发送信号,它发送的信号默认为 SIGTERM,但是也可以指定为其它信号. 我们可以直接用信号的号码来指定 kill 命令所发送信号之类型,也可以用符号名指定.比 如可以用下面的命令来完成向进程标识符为 1234 的进程发送 SIGINT 信号: kill – SIGINT 1234 s
4.2.5
系统调用 alarm()和 pause()
1.系统调用 alarm() alarm()是一个简单而有用的系统调用,它可以建立一个进程的报警时钟,在时钟定时
第四章
进程间通信
- 59 -
器到时的时候,用信号向程序报告.alarm()系统调用在 Linux 系统函数库 unistd.h 中的函数 声明如下: unsigned int alarm(unsigned int seconds); 函数唯一的参数是 seconds,其以秒为单位给出了定时器的时间.当时间到达的时候, 就向系统发送一个 SIGARLM 信号.例如: alarm(60); 这一调用实现在 60 秒后发一个 SIGALRM 信号.alarm 不会象 sleep 那样暂停调用进 程的执行,它能立即返回,并使进程继续执行,直至指定的延迟时间到达发出 SIGALRM 信号.事实上,一个由 alarm()调用设置好的报警时钟,在通过 exec()调用后,仍将继续有 效.但是,它在 fork()调用后中,在子进程中失效. 如果要使设置的报警时钟失效,只需要调用参数为零的 alarm(): alarm(0) alarm()调用也不能积累.如果调用 alarm 两次,则第二次调用就取代第一次调用.但 是,alarm 的返回值柜橱了前一次设定的报警时钟的剩余时间. 当需要对某项工作设置时间限制时,可以使用 alarm()调用来实现.其基本方法为:先 调用 alarm()按时间限制值设置报警时钟,然后进程作某一工作.如果进程在规定时间以内 完成这一工作,就再调用 alarm(0)使报警时钟失效.如果在规定时间内未能完成这一工作, 进程就会被报警时钟的 SIGALRM 信号中断,然后对它进行校正. 下面这个程序使用上述方法来强制用户作出回答.在其中包括一个 quickreply()函数, 它有一个参数 prompt,它是一个指向提示字符串的指针.quickreply 的返回值也是一个指 针.它指向含有输入行信息的字符串.这个例行程序在试作五次之后,如果仍未得到输入 信息,就返回一个 null 指针.每当 quickreply 要提醒用户时,它就向终端发送 ASCII 码 007, 这会使终端响铃. quickreply 调用了标准 I/O 库中的例行程序 gets().gets()把标准输入上的下一行信息存 入一个字符型数组,它返回一个指向该数组的指针.当到达文件末或出错时,gets 则返回 一个 null 指针.函数 catch 是信号 SIGALRM 的关联函数,它完成对此信号的处理.catch 设置了一个 timed_out 标志,在 quickreply 中对这个标志进行检查,看它是否超过了规定的 时限. #include #include #define TIMEOUT 5 #define MAXTRIES 5 #define LINESIZE 100 #define BELL '\007' #define TRUE 1 #define FALSE 0 /* 判断超时是否已经发生的标志 */ static int time_out; static char inputline[LINESIZE]; char* quickreply (char* prompt);
- 60 -
Linux 网络编程
main() { printf("%s\n",quickreply("Input")); } char* quickreply (char* prompt) { int (*was)(),catch(),ntries; char* answer; /* 设定捕捉 SIGALRM 的的关联并保存原有关联 */ was=signal(SIGALRM,catch); for (ntries=0;ntries",prompt); /* 设定定时器 */ alarm(TIMEOUT); /* 获取输入 */ answer=gets(inputline); /* 关闭定时器 */ alarm(0); if (!time_out) break; } /* 恢复原有的 SIGALRM 关联 */ signal(SIGALRM,was); return (time_out ((char*) 0):answer); } /* SIGALRM 信号处理函数 */ catch() { /* 设定超时标志 */ time_out=TRUE; /* 响铃警告 */ putchar(BELL);
第四章
进程间通信
- 61 -
} 2.系统调用 pause() 系统调用 pause()能使调用进程暂停执行,直至接收到某种信号为止.pause()在 Linux 系统函数库 unistd.h 中的函数声明如下: int pause(void); 该 调 用 没 有 任 何 的 参 数 . 它 的 返 回 始 终 是 -1 , 此 时 errno 被 设 置 为 ERESTARTNOHAND. 下面这个程序为了在规定时间显示一个消息,使用了 alarm 和 pause.对它的调用方法 如下: $tml minutes message-text & 第一个参数为时间数,第二个参数为显示的消息. #include #include #define TRUE 1 #define FALSE 0 #defi
在介绍守护进程的建立之前,首先来看一下下面的这个例程 daemon_init() ,它演示了 建立一个守护进程的全部过程: #include #include #include #include #define MAXFD 64 void daemon_init(const char *pname, int facility) { int i: pid_t pid; /* fork,终止父进程 */ if (pid=fork()) exit(0); /* 第一子进程 */ setsid(); signal(SIGHUP,SIG_IGN); /* fork,终止第一子进程 */ if (pid=fork()) exit(0); /* 第二子进程 */ daemon_proc=1; /* 将工作目录设定为"/" */ chdir("/"); /* 清除文件掩码 */ umask(0); /* 关闭所有文件句柄 */ for (i=0;i<MAXFD;I++) close(i); 打开 log openlog(pname,LOG_PID,facility); 看过了上面的程序,下面我们就来讲讲建立一个守护进程需要进行哪些操作:
第三章 进程控制
- 49 -
1.fork 首先需要 fork 一个子进程并将父进程关闭.如果进程是作为一个 shell 命令在命令行上 前台启动的,当父进程终止时,shell 就认为该命令已经结束.这样子进程就自动称为了后 台进程.而且,子进程从父进程那里继承了组标识符同时又拥有了自己的进程标识符,这 样保证了子进程不会是一个进程组的首进程.这一点是下一步 setsid 所必须的. 2.setsid setsid()调用创建了一个新的进程组,调用进程成为了该进程组的首进程.这样,就使 该进程脱离了原来的终端,成为了独立于终端外的进程. 3.忽略 SIGHUP 信号,重新 fork 这样使进程不在是进程组的首进程,可以防止在某些情况下进程意外的打开终端而重 新与终端发生联系. 4.改变工作目录,清除文件掩码 改变工作目录主要是为了切断进程与原有文件系统的联系.并且保证无论从什么地方 启动进程都能正常的工作.清除文件掩码是为了消除进程自身掩码对其创建文件的影响. 5.关闭全部已打开的文件句柄 这是为了防止子进程继承了在父进程中打开的文件而使这些文件始终保持打开从而产 生某些冲突. 6.打开 log 系统 以上就是建立一个守护进程的基本步骤.当然,一个实际的守护进程要比这个例子复 杂许多,但是万变不离其宗,原理都是相同的.通过上面几步,我们可以正确的建立自己 的守护进程.
3.5 本章小结
进程是 UNIX/Linux 系统的基本核心概念,具有十分重要的地位和作用. 在本章中,我们详细介绍了进程的建立和使用以及相关的问题.熟练的掌握这些知识, 是 Linux 编程所必须的.希望读者能用心体会.
- 50 -
Linux 网络编程
第四章
进程间通信
网络程序设计中通常包括两个或更多的需要互相对话(interprocess communications ) 的进程,因此进程通信的方法在网络程序设计中是极为重要的.网络程序设计在这个方面 不同于一般程序设计通常所使用的方法.一个传统的程序可以通过全局变量或函数调用和 不同的模块(甚至同一机器上的其它应用程序)对话,但是在网络上却不行. 网络程序设计的一个重要的目标是保证进程间不互相干涉,否则系统可能被挂起或自 锁,因此,进程间必须使用简洁有效的方法进行通信,在此方面,Linux 具有非常显著的 兼容性.因为 Linux 的许多基本性能如管道,队列等都非常适合网络. 在这一章中,我们将详细的介绍各种进程间通信的方法及其使用方式.
4.1 进程间通信的一些基本概念
下面是一些在学习进程间通信时会遇到的基本概念: l 进程阻塞 当一个进程在执行某些操作的条件得不到满足时,就自动放弃 CPU 资源而进入休眠状 态,以等待条件的满足.当操作条件满足时,系统就将控制权返还给该进程继续进行未完 的操作. l 共享资源 因为计算机的内存,存储器等资源是有限的,无法为每一个进程都分配一份单独的资 源.所以系统将这些资源在各个进程间协调使用,称为共享资源. l 锁定 当某个进程在使用共享资源使用,可能需要防止别的进程对该资源的使用.比如,一 个进程在对某个文件进行读操作时,如果别的进程也在此时向文件中写入了内容,就可能 导致进程读入错误的数据.为此,Linux 提供一些方法来保证共享资源在被某个进程使用 时,别的进程无法使用.这就叫做共享资源的锁定.
4.2 信号
信号是 UNIX 系统所使用的进程通信方法中,最古老的一种.系统使用它来通知一个 或多个进程异步事件的发生,比如键盘上某个键被按下,或者计时器到达了某个特定的事 件.系统也用信号来处理某种严重的错误.比如,一个进程试图向一块不存在的虚拟内存 写入数据,或者某个进程试图执行一条非法指令. 信号不但能从内核发往一个进程,也能从一个进程发往另一个进程.例如,用户在后 台启动了一个要运行较长时间的程序,如果想中断其执行,可以用 kill 命令把 SIGTERM 信号发送给这个进程,SIGTERM 将终止此进程的执行. 信号还提供了向 UNIX 系统进程传送软中断的简单方法.信号可以中断一个进程,而 不管它正在作什么工作.由于信号的特点,所以不用它来作进程间的直接数据传送,而把 它用作对非正常情况的处理. 由于信号本身不能直接携带信息,这就限制了它作为一项通用的进程通信机制.但是,
第四章
进程间通信
- 51 -
每种信号都有其特定的含义,并由其名字所指示.在 Linux 系统库 bits/signum.h 中对这些 信号名作了定义,每个名字代表一个正整数.例如: #define SIGHUP 1 /* Hangup (POSIX). */ 定义了信号 SIGHUP. Linux 提供的大多数信号类型是供内核使用的,只有少数的几种信号可以用作在进程 之间传送.下面给出常用的信号和它们的意义: l SIGHUP 当终止一个终端时,内核就把这一种信号发送给该终端所控制的所有进程.通常情况 下,一个进程组的控制终端是该用户拥有的终端,但不完全是如此.当进程组的首进程结 束时,就会向该进程组的所有进程发送这种信号.这就可以保证当一个用户退出使用时, 其后台进程被终止,除非有其它方面的安排. l SIGINT 当一个用户按了中断键(一般为 Ctrl+C)后,内核就向与该终端有关联的所有进程发 送这种信号.它提供了中止运行程序的简便方法. l SIGQUIT 这种信号与 SIGINT 非常相似,当用户按了退出键时(为 ASCII 码 FS,通常为 Ctrl+\) , 内核就发送出这种信号.SIGQUIT 将形成 POSIX 标准所描述的非正常终止.我们称这种 UNIX 实现的实际操作为核心转贮(core dump) ,并用信息"Quit (core dump)"指出这一操 作的发生.这时,该进程的映象被转贮到一个磁盘文件中,供调试之用. l SIGILL 当一个进程企图执行一条非法指令时,内核就发出这种信号.例如,在没有相应硬件 支撑的条件下,企图执行一条浮点指令时,则会引起这种信号的发生.SIGILL 和 SIGQUIT 一样,也形成非正常终止. l SIGTRAP 这是一种由调试程序使用的专用信号.由于他的专用行和特殊性,我们不再对它作进 一步的讨论.SIGTRAP 也形成非正常终止. l SIGFPE 当产生浮点错误时(比如溢出) ,内核就发出这种信号,它导致非正常终止. l SIGKILL 这是一个相当特殊的信号,它从一个进程发送到另一个进程,使接收到该信号的进程 终止.内核偶尔也会发出这种信号.SIGKILL 的特点是,它不能被忽略和捕捉,只能通过 用户定义的相应中断处理程序而处理该信号.因为其它的所有信号都能被忽略和捕捉,所 以只有这种信号能绝对保证终止一个进程. l SIGALRM 当一个定时器到时的时候,内核就向进程发送这个信号.定时器是由改进程自己用系 统调用 alarm()设定的. l SIGTERM 这种信号是由系统提供给普通程序使用的,按照规定,它被用来终止一个进程. l SIGSTOP 这个信号使进程暂时中止运行,系统将控制权转回正在等待运行的下一个进程. l SIGUSR1 和 SIGUSR2 和 SIGTERM 一样,这两种信号不是内核发送的,可以用于用户所希望的任何目的. l SIGCHLD 子进程结束信号.UNIX 中用它来实现系统调用 exit()和 wait().执行 exit()时,就向子
- 52 -
Linux 网络编程
进程的父进程发送 SIGCHLD 信号,如果这时父进程政在执行 wait(),则它被唤醒;如果这 时候父进程不是执行 wait(),则此父进程不会捕捉 SIGCHLD 信号,因此该信号不起作用, 子进程进入过渡状态(如果父进程忽略 SIGCHLD,子进程就结束而不会进入过渡状态) . 这个机制对大多数 UNIX 程序员来说是相当重要的. 对于大多数情况来说,当进程接收到一个信号时,它就被正常终止,相当于进程执行 了一个临时加入的 exit()调用.在这种情况下,父进程能从进程返回的退出状态中了解可能 发生的事情,退出状态的低 8 位含有信号的号码,其高 8 位为 0. 信号 SIGQUIT,SIGILL,SIGTRAP,SIGSYS 和 SIGFPE 会导致一个非正常终止,它 们将发生核心转贮,即把进程的内存映象写入进程当前目录的 core 文件之中.core 文件中 以二进制的形式记录了终止时程序中全部变量之值,硬件寄存器之值和内核中的控制信息. 非正常终止进程的退出状态除了其低端第 7 位被置位外,其它均与通过信号正常终止时一 样. Linux 的调试程序 gdb 知道 core 文件的格式,可以用它们来观察进程在转贮点上的状 态.这样,就可以用 gdb 正确的定出发生问题的位置. 这里再介绍一下系统调用 abort(),它在 Linux 系统库 stdlib.h 中定义: void abort(void); abort()向调用进程发送一个信号,产生一个非正常终止,即核心转贮.由于它能够使 一个进程在出错时记录进程的当前状态,所以可以用它来作为调试的辅助手段.这也说明 了进程可以向自己发送信号这一事实.
4.2.1
信号的处理
几乎所有的信号都将终止接收到该信号的进程.对于一些简单的程序,这完全能满足 要求.用户按了中断或者退出键,就可以停止一个有问题的程序的运行.但是在大型的程 序中,一些意料之外的信号会导致大问题.例如,正当在对一个重要的数据库进行修改期 间,由于不小心碰到了中断键,而使程序被意外的终止,从而产生严重的后果. UNIX 的系统调用 signal()用于接收一个指定类型的信号,并可以指定相应的方法.这 就是说,signal()能够将指定的处理函数与信号向关联.它在 Linux 系统库 signal.h 中的函 数声明如下: int signal (int sig, __sighandler_t handler); Signal()有两个参数: 第一个参数 sig 指明了所要处理的信号类型,它可以取除了 SIGKILL 和 SIGSTOP 外 的任何一种信号.参数 handler 描述了与信号关联的动作,它可以取以下三种值: l 一个返回值为整数的函数地址. 此函数必须在 signal()被调用前声明,handler 中为这个函数的名字.当接收到一个类型 为 sig 的信号时,就执行 handler 所指定的函数.这个函数应有如下形式的定义: int func(int sig); sig 是传递给它的唯一参数.执行了 signal()调用后,进程只要接收到类型为 sig 的信号, 不管其正在执行程序的哪一部分,就立即执行 func()函数.当 func()函数执行结束后,控制 权返回进程被中断的那一点继续执行. l SIG_IGN 这个符号表示忽略信号.执行了相应的 signal()调用好,进程会忽略类型为 sig 的信号. l SIG_DFL 这个符号表示恢复系统对信号的默认处理.
第四章
进程间通信
- 53 -
函数如果执行成功,就返回信号在此次 signal()调用之前的关联. 如果函数执行失败,就返回 SIG_ERR.通常这种情况只有当 sig 参数不是有效的信号 时才会发生.函数不对 handler 的有效性进行检查. 下面我们来看几个例子. 首先,下面的这段代码则将使进程忽略 SIGINT 信号: #include #include #include int main(void) { signal(SIGINT,SIG_IGN); /*告诉进程将 SIGINT 信号忽略*/ printf("xixi\n"); sleep(10); /*系统函数 sleep()使进程休眠指定的时间(以秒为单位)*/ printf("end\n"); return; } 如果在程序中需要重新恢复系统对信号的缺省处理,就使用下面的语句: signal(SIGINT, SIG_DFL); 在 Linux 程序中常常利用 SIG_IGN 和 SIG_DFL 屏蔽 SIGINT 和 SIGQUIT 来保证执行 重要任务的程序不会被意外的中止. 在 shell 中也是利用这一技术来确保用户按中断键时,不中断后台程序的运行.因为被 一个进程忽略的信号,在进程执行 exec()调用后,仍然被忽略,所以 shell 能够调用 signal() 来保证 SIGQUIT 和 SIGINT 被忽略,然后用 exec 执行新程序.但是要注意到,在父进程 中设定的信号和函数的关联关系会被 exec()调用自动用 SIG_DFL 恢复成系统的缺省动作, 这是因为在 exec 的子进程中没有父进程的函数映象. 再让我们来看看下面这段捕捉 SIGINT 的代码: #include #include #include int catch(int sig); int main(void) { signal(SIGINT,catch); /* 将 SIGINT 信号与 catch 函数关联 */ printf("xixi\n"); sleep(10); printf("end\n"); return; } int catch(int sig) {
- 54 -
Linux 网络编程
printf("Catch succeed!\n"); return 1; } 当程序运行时我们按下中断键(Ctrl+C) ,进程被中断,函数 catch 就被执行.它执行 完毕后,进程回到中断点继续执行. 如果我们希望一个进程被信号终止前能够完成一些处理工作,如删除工作中使用的临 时文件等,就可以设计一个信号处理函数来完成工作.比如可以有这样的一个处理函数: int catch(int sig) { printf("Catch succeed!\n"); exit(1); } 这个函数在最后调用 exit()函数来使进程结束运行.这样就保留了信号原有的中断进程 的功能. 这里需要指出一点,当程序把一个信号处理函数与 SGINT 和 SIGQUIT 联系起来后, 如果该程序在后台执行,那么由于 shell 的作用,会使得 SIGINT 和 SIGQUIT 被忽略.这 样后台程序就不会被 SIGINT 和 SIGOUT 所中止. 前面曾经提过,signal()调用返回原先与指定信号相关联的处理函数,这样,我们就可 以保存和恢复原来对指定信号的处理动作.下面的代码说明这一技术: int (*oldptr)(),newcatch(); /* 设定 SIGINT 的关联,同时保存原来的关联*/ oldptr=singal(SIGINT,newcatch); /* 工作代码段 */ … .. /* 恢复原来的关联 */ signal(SIGINT,oldptr);
4.2.2
信号与系统调用的关系
当一个进程正在执行一个系统调用时,如果向该进程发送一个信号,那么对于大多数 系统调用来说,这个信号在系统调用完成之前将不起作用,因为这些系统调用不能被信号 打断.但是有少数几个系统调用能被信号打断,例如:wait(),pause()以及对慢速设备(终端, 打印机等)的 read(),write(),open()等.如果一个系统调用被打断,它就返回-1,并将 errno 设为 EINTR.可以用下列代码来处理这种情况: if (wirte(tfd,buf,SIZE)<0) { if (errno==EINTR) { warn(" Write interrupted." ); … … } }
第四章
进程间通信
- 55 -
4.2.3
信号的复位
在 Linux 中,当一个信号的信号处理函数执行时,如果进程又接收到了该信号,该信 号会自动被储存而不会中断信号处理函数的执行,直到信号处理函数执行完毕再重新调用 相应的处理函数.下面的程序演示了这一点: #include int interrupt() { printf(" Interrupt called\n" ); sleep(3); printf(" Interrupt Func Ended.\n" ); } main() { signal(SIGINT,interrupt); printf(" Interrupt set for SIGINT\n" ); sleep(10); printf(" Program NORMAL ended.\n" ); return; } 执行它,结果如下: Interrupt set for SIGINT Interrupt called Func Ended Interrupt called Func Ended Program NORMAL ended. 但是如果在信号处理函数执行时进程收到了其它类型的信号,该函数的执行就会被中 断: #include int interrupt() { printf(" Interrupt called\n" ); sleep(3); printf(" Interrupt Func Ended.\n" ); } int catchquit() {
- 56 -
Linux 网络编程
printf(" Quit called\n" ); sleep(3); printf(" Quit ended.\n" ); } main() { signal(SIGINT,interrupt); signal(SIGQUIT,catchquit); printf(" Interrupt set for SIGINT\n" ); sleep(10); printf(" Program NORMAL ended.\n" ); return; } 执行这个程序的结果如下: Interrupt set for SIGINT Interrupt called Quit called Quit ended. Interrupt Func Ended. Program NORMAL ended. 还要注意的是,在 Linux 系统中同种信号是不能积累的.比如我们执行上面的代码: Interrupt set for SIGINT Interrupt called Func Ended Interrupt called Func Ended Program NORMAL ended. 而且如果两个信号同时产生,系统并不保证进程接收它们的次序.以上的两个缺点影 响了信号作为进程通信手段的可靠性,因为一个进程不能保证它发出的信号不被丢失. 当某个信号未被处理的时候,如果对该信号执行 signal 调用,那么该信号将被注销.
4.2.4
在进程间发送信号
一个进程通过对 signal()的调用来处理其它进程发送来的信号.同时,一个进程也可以 向其它的进程发送信号.这一操作是由系统调用 kill()来完成的. kill()在 linux 系统库 signal.h 中的函数声明如下: int kill(pid_t pid, int sig); 参数 pid 指定了信号发送的对象进程:它可以是某个进程的进程标识符(pid),也可以 是以下的值: 如果 pid 为零,则信号被发送到当前进程所在的进程组的所有进程;
第四章
进程间通信
- 57 -
如果 pid 为-1,则信号按进程标识符从高到低的顺序发送给全部的进程(这个过程受 到当前进程本身权限的限制,请看后面的解释) ; 如果 pid 小于-1,则信号被发送给标识符为 pid 绝对值的进程组里的所有进程. 需要说明的是,一个进程并不是向任何进程均能发送信号的,这里有一个限制,就是 普通用户的进程只能向具有与其相同的用户标识符的进程发送信号.也就是说,一个用户 的进程不能向另一个用户的进程发送信号.只有 root 用户的进程能够给任何线程发送信号. 参数 sig 指定发送的信号类型.它可以是任何有效的信号. 由于调用 kill()的进程需要直到信号发往的进程的标识符,所以这种信号的发送通常只 在关系密切的进程之间进行,比如父子进程之间. 下面是一个使用 kill()调用发送信号的例子.这个程序建立两个进程,并通过向对方发 送信号 SIGUSR1 来实现它们之间的同步.这两个进程都处于一个死循环中,在接收对方发 送的信号之前,都处于暂停等待中.这是通过系统调用 pause()来实现的,它能够使一个程 序暂停,直至一个信号到达,然后进程输出信息,并用 kill 发送一个信号给对方.当用户 按了中断键,这两个进程都将终止. #include int ntimes=0; main() { int pid,ppid; int p_action(), c_action(); /* 设定父进程的 SIGUSR1 */ signal(SIGUSR1,p_action); switch(pid=fork()) { case -1: /*fork 失败*/ perror("synchro"); exit(1); case 0: /*子进程模块*/ /* 设定子进程的 SIGUSR1 */ signal(SIGUSR1,c_action); /* 获得父进程的标识符 */ ppid=getppid(); for(;;) { sleep(1); kill(ppid,SIGUSR1); pause(); }
- 58 -
Linux 网络编程
/*死循环*/ break; default: /*父进程模块*/ for (;;) { pause(); sleep(1); kill(pid,SIGUSR1); } /*死循环*/ } } p_action() { printf("Patent caught signal #%d\n",++ntimes); } c_action() { printf("Child caught signal #%d\n",++ntimes); } 程序运行结果如下: Patent caught signal #1 Child caught signal #1 Patent caught signal #2 Child caught signal #2 Patent caught signal #3 Child caught signal #3 Patent caught signal #4 Child caught signal #4 这里顺便介绍一下 kill 命令,它是一个对系统调用 kill()的命令层接口.kill 命令用于 向一个运行进程发送信号,它发送的信号默认为 SIGTERM,但是也可以指定为其它信号. 我们可以直接用信号的号码来指定 kill 命令所发送信号之类型,也可以用符号名指定.比 如可以用下面的命令来完成向进程标识符为 1234 的进程发送 SIGINT 信号: kill – SIGINT 1234 s
4.2.5
系统调用 alarm()和 pause()
1.系统调用 alarm() alarm()是一个简单而有用的系统调用,它可以建立一个进程的报警时钟,在时钟定时
第四章
进程间通信
- 59 -
器到时的时候,用信号向程序报告.alarm()系统调用在 Linux 系统函数库 unistd.h 中的函数 声明如下: unsigned int alarm(unsigned int seconds); 函数唯一的参数是 seconds,其以秒为单位给出了定时器的时间.当时间到达的时候, 就向系统发送一个 SIGARLM 信号.例如: alarm(60); 这一调用实现在 60 秒后发一个 SIGALRM 信号.alarm 不会象 sleep 那样暂停调用进 程的执行,它能立即返回,并使进程继续执行,直至指定的延迟时间到达发出 SIGALRM 信号.事实上,一个由 alarm()调用设置好的报警时钟,在通过 exec()调用后,仍将继续有 效.但是,它在 fork()调用后中,在子进程中失效. 如果要使设置的报警时钟失效,只需要调用参数为零的 alarm(): alarm(0) alarm()调用也不能积累.如果调用 alarm 两次,则第二次调用就取代第一次调用.但 是,alarm 的返回值柜橱了前一次设定的报警时钟的剩余时间. 当需要对某项工作设置时间限制时,可以使用 alarm()调用来实现.其基本方法为:先 调用 alarm()按时间限制值设置报警时钟,然后进程作某一工作.如果进程在规定时间以内 完成这一工作,就再调用 alarm(0)使报警时钟失效.如果在规定时间内未能完成这一工作, 进程就会被报警时钟的 SIGALRM 信号中断,然后对它进行校正. 下面这个程序使用上述方法来强制用户作出回答.在其中包括一个 quickreply()函数, 它有一个参数 prompt,它是一个指向提示字符串的指针.quickreply 的返回值也是一个指 针.它指向含有输入行信息的字符串.这个例行程序在试作五次之后,如果仍未得到输入 信息,就返回一个 null 指针.每当 quickreply 要提醒用户时,它就向终端发送 ASCII 码 007, 这会使终端响铃. quickreply 调用了标准 I/O 库中的例行程序 gets().gets()把标准输入上的下一行信息存 入一个字符型数组,它返回一个指向该数组的指针.当到达文件末或出错时,gets 则返回 一个 null 指针.函数 catch 是信号 SIGALRM 的关联函数,它完成对此信号的处理.catch 设置了一个 timed_out 标志,在 quickreply 中对这个标志进行检查,看它是否超过了规定的 时限. #include #include #define TIMEOUT 5 #define MAXTRIES 5 #define LINESIZE 100 #define BELL '\007' #define TRUE 1 #define FALSE 0 /* 判断超时是否已经发生的标志 */ static int time_out; static char inputline[LINESIZE]; char* quickreply (char* prompt);
- 60 -
Linux 网络编程
main() { printf("%s\n",quickreply("Input")); } char* quickreply (char* prompt) { int (*was)(),catch(),ntries; char* answer; /* 设定捕捉 SIGALRM 的的关联并保存原有关联 */ was=signal(SIGALRM,catch); for (ntries=0;ntries",prompt); /* 设定定时器 */ alarm(TIMEOUT); /* 获取输入 */ answer=gets(inputline); /* 关闭定时器 */ alarm(0); if (!time_out) break; } /* 恢复原有的 SIGALRM 关联 */ signal(SIGALRM,was); return (time_out ((char*) 0):answer); } /* SIGALRM 信号处理函数 */ catch() { /* 设定超时标志 */ time_out=TRUE; /* 响铃警告 */ putchar(BELL);
第四章
进程间通信
- 61 -
} 2.系统调用 pause() 系统调用 pause()能使调用进程暂停执行,直至接收到某种信号为止.pause()在 Linux 系统函数库 unistd.h 中的函数声明如下: int pause(void); 该 调 用 没 有 任 何 的 参 数 . 它 的 返 回 始 终 是 -1 , 此 时 errno 被 设 置 为 ERESTARTNOHAND. 下面这个程序为了在规定时间显示一个消息,使用了 alarm 和 pause.对它的调用方法 如下: $tml minutes message-text & 第一个参数为时间数,第二个参数为显示的消息. #include #include #define TRUE 1 #define FALSE 0 #defi