《网络应用程序设计》课件第4章 进程与信号机制_第1页
《网络应用程序设计》课件第4章 进程与信号机制_第2页
《网络应用程序设计》课件第4章 进程与信号机制_第3页
《网络应用程序设计》课件第4章 进程与信号机制_第4页
《网络应用程序设计》课件第4章 进程与信号机制_第5页
已阅读5页,还剩84页未读 继续免费阅读

下载本文档

版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领

文档简介

第4章进程与信号机制

4.1概述

4.2信号4.3进程间的关系及相互制约4.4守护进程(daemonprocess)习题4.1概述

进程通信的概念最初来源于单机系统,由于每个进程都在自己的地址空间内运行,因此为保证两个相互通信的进程之间既互不干扰又能协调一致地工作,操作系统为进程通信提供了相应的应用程序接口。例如,UNIX

BSD中的管道(pipe)、命名管道(namedpipe)和软中断信号(signal),UNIX

system

V的消息(message)、共享存储区(sharedmemory)和信号量(semaphore)等,但这些都仅限于本机进程之间的通信。网间进程通信要解决的是不同主机进程间的相互通信问题(同一主机上的进程通信可以看作是其中的特例)。为此,首先,要解决的是网间进程标识问题。

同一主机上,不同进程可用进程号(process

ID)惟一标识,但在网络环境下,各主机独立分配的进程号不能惟一标识该进程。例如,主机A赋于某进程号105,在主机B中也可以存在105号进程,因此,105号进程不能将主机A的105号进程和主机B的105号进程区分开来。其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同,因此,网间进程通信还要解决多重协议的识别问题。 不管是同一台主机上的进程还是异地主机上的进程间的通信,进程是通信的主体,因此,本章我们讨论进程、进程类型,以及进程间的通信机制。4.2信号 4.2.1信号的产生 信号在下列五种情况下产生,并向有关进程传送。

(1)系统调用kill允许一个进程向另一个进程(或其自身)发一个信号。

intkill(intpid,intsig);

一个进程不能给任意的进程发信号,发送信号的进程和接收信号的进程必须具有相同的有效用户标识,或者发送信号的进程是特权用户。 当变元pid和sig值不同时,kill的信号发送情况如下: ①

若pid=0,则信号发送至发信号进程所在进程组内每一个进程。

若pid=-1,且发信号进程不是特权用户,则信号发至用户标识符等于发送者的有效用户标识符的所有进程。 ③

若pid=-1,且发信号进程是特权用户,则信号发至除系统进程以外的所有进程。 ④

若pid<0,且pid≠1,则信号发至进程组号等于

pid绝对值的所有进程。 ⑤

若sig=0,则不发信号,做差错检查。这可用来检测

pid的有效性。 (2)用kill命令发送信号,该命令是一个程序,它接收命令行参数后,发出调用kill。上面的传送特性也适用于此。

(3)特定的键盘字符能产生信号。例如,每个交互式终端都有中断字符和退出字符,中断字符CTR_C终止正在运行的进程,产生一个信号SIGINT,退出字符

CTR-backspace终止正在运行的进程,并产生该进程的存储映像,产生信号IGQUIT。终端挂起字符CTR_Z,产生信号SIGTSTP。延迟终端挂起字符CTR_Y,当进程读该字符时产生信号SIGTSTP。 这些由终端产生的信号一般由内核发送给进程,不只发给正在运行的进程,也发给所有终端控制组中的进程。 (4)在陷入处理子程序TRAP中,针对各种故障以及使用的各种陷入指令产生不同类型的信号,如SIGEMT为终止并保留存储映像,SIGFPE为浮点算术错信号。

(5)某些软件条件也产生信号。例如,一个加急数据到达管套时,将产生SIGURG信号。

4.2.2信号的类型及定义

UNIX系统各种信号及功能列于表4-1之中。所有信号均被罗列在标题文件signal.h中。表

4-1UNIX的各种信号及功能 (1)SIGALRM:通过调用

alarm函数设定报警时钟。

unsignedintalarm(unsignedintsec);

变元sec指定产生信号SIGALRM所需秒数,若此变元为

0,则该进程以前设置的所有报警时钟全部取消,该信号可以用来设定软件超时。

(2)SIGBUS:由硬件故障错误产生。

(3)SIGCHLD:进程结束时向其父进程发送的信号,若父进程未收到此信号,该信号便被丢掉。该信号还可以表示子进程的状态变化,它可以表示子进程结束,也可以表示子进程被信号 SIGSTOP、SIBTTIN、SIGTTOU、SIGTSTP中断。 (4)SIGCONT:当一个被中断的进程继续运行时,产生此信号。

(5)SIGFPE:由某种硬件条件而产生,此信号表示浮点溢出和一些整数算术运算条件的满足,如被零除。

(6)SIGHUP:当一个终端关闭时,所有以该终端为控制终端的进程均收到这个信号。当进程组头发出该信号时,该进程组中的所有进程接收到这个信号。

(7)SIGILL:由硬件条件满足而产生。

(8)SIGINT:在终端键入一中断键时,产生该信号。

(9)SIGIOT:由硬件故障产生,系统V的函数

abort把该信号送当前运行的进程。

(10)SIGIO:该信号表示可以在文件描述符上

I/O。可以利用此信号产生一种异步I/O方式。

(11)SIGKILL:利用该信号可以可靠的终止一个进程。

(12)SIGPIPE:如果一个进程向管道或FIFO写入,却没有进程读出时发该信号给写进程。当进程向一个已断开的管道写入时,也会收到此信号。

(13)SIGPROF:在4.3BSD中,提供了三个报警时钟信号:SIGALRM测量进程的实际时间,SIGVTALRM测量进程的虚拟时间,即进程的实际运行时间,SIGPROF测量虚拟时间以及内核为本进程服务所花时间。 (14)SIGQUIT:当收到键入的QUIT键时,发出该信号。

(15)SIGSEGV:由硬件故障引起,如当某进程访问一个不允许访问的地址时,产生此信号。

(16)SIGSTOP:此信号使进程停止,系统管理程序可以用此来停止一个进程,一个停止的进程得到SIGCONT时可继续执行。

(17)SIGTERM:软件终止信号,它是在执行KILL命令时,发给进程的缺省信号。

(18)SIGTRAP:利用该信号和系统调用ptrace一起来跟踪某个进程。 (19)SIGTSTP:当键入暂停键CTR_E或延时暂停键CTR_Y时,向进程发此信号,进程被暂停执行,被暂停的进程利用SIGCONT信号恢复执行。

(20)SIGTTIN:当一个后台进程试图读其控制终端时,产生此信号。多个进程从终端读时将会产生混乱。4.3BSD利用该信号可以避免这些混乱,如果进程忽略此信号,则将被终止运行。

(21)SIGTTOU:当后台进程欲写控制终端时,产生此信号。在缺省状态下允许后台进程写它的控制终端,可以定义若要写入终端,则产生此信号。 (22)SIGURG:出现紧急情况时产生此信号。

(23)SIGUSR1:该信号是用户定义的用于进程间通信的信号,它用于两个或多个进程间的通信。发送进程把此信号的类型提供给接收进程,此信号不携带其他信息,接收进程得不到发送进程的有关信息。所以该信号实际上在进程间通信中用得不多。

(24)SIGUSR2:用户定义的用于进程间通信的另一信号,性能同上。

(25)SIGVTALRM:参见信号SIGPROF。

(26)SIGWINCH:标志终端窗口大小已经改变。 (27)SIGXCPU:允许某一进程设置影响它自己和由它产生的子进程的资源限制量,这些量是: ①

每个进程允许用的CPU时间。 ②

所建文件的最大长度。 ③

进程数据段的最大长度。 ④

进程栈段的最大长度。 ⑤

可产生的存储映像的最大长度。 ⑥

一个进程可驻留空间的最大值。

(28)SIGXFSZ:该信号的出现表示进程已超出了它的文件最大长度。 4.2.3可靠信号

UNIX的早期版本提供的信号机构是不可靠的,因为系统中存在竞争条件,当某一事件出现而发出了信号时,进程却没有感知到,信号被丢失。4.3BSD和系统V对以往的信号作了些改动,提供了可靠信号机构,信号值小于SIGRTMIN(Redhat7.2中, SIGRTMIN=32,SIGRTMAX=63)的信号都是不可靠信号。改进的性能有:

(1)当一个信号发出之后,要保证信号管理函数装入,这就意味着用户信号管理函数能够再次调用SIGNAL产生某个信号之前,如果有同一信号出现就会丢失。 (2)进程可以根据需要,延迟信号的产生,防止信号发送时机不当,而删除已发信号,可以把信号记住,延迟到适当的时候再送。

(3)在一个信号发送给另一个进程的过程中,信号被阻塞。例如,当进程正在处理某个信号时,同样的信号要第二次产生,信号管理函数并不第二次调用执行,而是把第二个信号记住。当先被处理的信号正常处理完毕后,再第二次调用管理函数处理记住的第二个信号。 4.2.4实时信号与非实时信号 早期的UNIX系统只定义了32种信号,Rethat7.2支持64种信号,编号0~63(SIGRTMIN=31,SIGRTMAX=63),将来可能会进一步增加,这需要得到内核的支持。前32种信号已经有了预定义值,每个信号有确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的Ctrl+C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号,这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,可用于应用进程。非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。 4.2.5设置进程对信号的响应 如果进程要处理某一信号,那么就要在进程中设置该信号的关联动作或称对信号的响应、安装信号。设置信号的关联动作主要用来确定信号值以及进程针对该信号值与动作之间的映射关系,也就是说,进程将要处理该信号;该信号被传递给进程时,将执行何种操作。 每个信号都有一个相应的动作,当进程接收到某个信号后,将调用该动作。进程可以通过三种方式来响应一个信号:

(1)忽略信号,即对信号不做任何处理。其中,有两个信号不能忽略:SIGKILL及SIGSTOP。 (2)捕捉信号。定义信号处理函数,当信号发生时,执行相应的处理函数。用户可以用自己定义的函数来处理信号。

(3)执行缺省操作。Linux对每种信号都规定了默认操作。进程对实时信号的缺省反应是进程终止。

Linux主要有两个函数实现信号的安装:signal()和sigaction()。 1.sigaction()函数

进程通过设置信号的处理函数来捕获信号,sigaction()函数设置信号的处理函数,其定义如下:

#include<signal.h> intsigaction(intsignum,

conststructsigaction*act,

conststructsigaction*oldact)sigaction结构定义为:structsigaction{void(*sa_handler)(int); /*SIG_DFL,SIG_IGN,函数

*/sigset_tsa_mask;intsa_flags;void(*sa_handler)(void); /*

已过时,不使用

*/};

设置进程对信号的响应一般包含以下几步:

structsigactionact; /*定义sigaction的变量*/ act.sa_handler=sig_handle; /*

设置中断处理函数

*/ ...

sigaction(SIGINT,&act,NULL); /*

设置信号处理函数

*/...

voidsig_handle(intsig) /*

中断处理函数

*/{...} sigaction()函数用于改变进程接收到特定信号后的行为。函数的第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误)。第二个参数是指向sigaction结构的一个实例的指针,在sigaction结构的实例中,指定了对特定信号的处理,可以为空,进程会以缺省方式对信号处理。参数oldact指向的对象用来保存原来对相应信号的处理设定,以便以后恢复,可指定oldact为NULL。如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。第二个参数很重要,它包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等。 (1) sa_handler指定信号的动作,可取三种值: ①

当取值为SIG_DEL时,使用默认动作; ②

当取值为SIG_IGN时,表示忽略掉这个信号; ③

当取值为相应函数时,设置为使用用户设定函数。

(2)sa_mask用来指定当调用信号处理函数时,哪些信号将被屏蔽。被屏蔽的信号不被传递给进程执行。 (3) sa_flags用来设置标志: SA_ONESHOT或

SA_RESETHAND:当信号处理函数被调用之后,将这个信号的动作设置为默认动作。 SA_RESTART:使得某些系统调用在被信号中断之后能够自动重新执行。 SA_NOCLDSTOP:当参数signum设置为SIGCHLD,子进程停止时。例如,子进程收到SIGSTOP信号时,不接收通知。 SA_NOMASK或SA_NODEFER:表示信号处理函数执行过程中,不屏蔽这个信号的接收。 2.signal()函数

signal()函数是Linux系统提供的另一种设置信号处理的函数。 #include<signal.h> void(*signal(intsig,void(*func)(int)))(int);

函数的第一个参数sig用来指定信号,第二个参数设定信号处理函数。用signal()函数设置的信号处理函数在被执行之后,自动将信号的处理动作设置为默认值,这和函数sigaction设置标志SA_ONESHOT时的情况是相同的。例如,一个signal的例子如下: #include<signal.h>

#include<string.h>

#include<error.h>

voidsigint_handler(int);

intmain() {intm; charbuffer[100]; structsigactionevent;

event.sa_handler=sigint_handler; sigemptyset(&event.sa_mask); event.sa_flags=0;/*

处理完这个信号,按照默认方法处理中断进程

*/ sigaction(SIGINT,&event,NULL); m=read(0,buffer,sizeof(buffer));if(m<0){if(error==EINTR){printf("readactionisinterrupted");}elseprintf("error:%s\n",strerror(error));}write(1,buffer,m);} voidsigint_handler(intsigg){/*

具体处理内容

*/return;}

首先该程序sigaction()函数捕获信号SIGINT,如当用户在终端上使用Ctrl+C组合键时产生信号,然后,从终端读取数据。如果从键盘键入数据,则程序显示这些数据;如果用户键入Ctrl+C组合键,则捕获信号SIGINT中断,程序输出相应的提示。程序的执行结果为: $sig_exam ^C readactionisinterrupted Linux系统的信号处理机制需要注意以下几点:

(1)当一个信号处理函数被设置了,它将一直起作用,除非对这个信号作了其他的设置。

(2)在设置信号处理器时,在参数sa_mask中设定需要被屏蔽的信号,在一个信号处理器执行过程当中,新到达的信号将被屏蔽。

(3)Linux系统不将同一信号进行排队。如果一个信号被屏蔽时,多次产生这个信号,则该屏蔽被解除时,这个信号只被发送一次。4.3进程间的关系及相互制约

4.3.1进程的创建

Linux系统的进程是一种树状结构,即一个父节点可以拥有多个子节点,而一个子节点只能有一个父节点,init是这棵树的根。Linux系统为每个进程在系统进程表中添加一项记录,内核程序根据进程表中记录的信息来执行进程,并维护相应的资源。

当系统刚刚启动时,只有一个初始化进程在运行。初始化进程的进程标识符是1,它是系统的第一个真正的进程。它首先做一些系统的初始化设置(例如打开系统控制台和挂接根目录文件系统),然后执行系统初始化程序。初始化程序是/etc/init、/bin/init或/sbin/init中的一个。初始化程序使用/etc/inittab作为脚本文件来创建新的进程。这些新的进程同样可以创建其他新的进程。系统中所有的进程都是初始化进程的子进程。

进程是运行于自己虚拟地址空间的一个程序。Linux系统中包括下面几种类型的进程: ●交互进程:是由Shell控制和运行的。它即可以在前台运行,也可以在后台运行。 ●批处理进程:该进程不属于某个终端,它被提交到一个队列中,以便顺序执行。 ●守护进程:只有在需要时才被唤起在后台运行。它一般在Linux启动时开始执行。

Linux系统中的每个进程都有自己的权限和任务,某一进程的失败一般不会导致其他进程的失败,进程之间可以通过由内核控制的机制相互通信。

系统调用fork()或者clone()创建新进程,并且是在内核内部的内核方式下完成的。系统调用结束时,进程调度算法在父进程和新产生的进程间选择,新进程就可能被调度运行。用fork()创建子进程时,如果创建子进程成功,则父进程从fork()所得返回值是新的子进程标识符,子进程所得到的返回值为0。如果创建失败,则父进程得到的返回值为-1。 下面用一个例子说明fork()的作用。main(){intn;

while((n=fork())==-1){printf("forkisfail\n");

}if(n){printf("Itisparentprocess.\n");

printf("parent:

childpid=%d,parentpid=%d\n",childpid(),getpid()));

} else{printf("Itischildprocess.\n");

printf("child:

childpid=%d,parentpid=%d\n",getpid(),getppid());

}exit(0);}

执行该程序时,父进程先生成子进程,然后父、子进程接受调度,调度到父进程时,打印出如下结果:

Itisparentprocess. parent:

childpid=1573,parentpid=346

调度到子进程时,打印出如下结果:

Itischildprocess. child:childpid=2731,parentpid=1689 在生成子进程后,父、子进程除共享正文段外,其他部分占用不同存储区域。原来由父进程打开的文件在fork()调用之后被子进程共享。这个性质为父进程把一些文件和设备传给子进程提供了方便。 fork()调用后,子进程继承父进程的进程变量有: ●用户标识符; ●有效用户标识符; ●用户组号; ●有效用户组号; ●进程组号; ●终端组号; ●根目录; ●当前工作目录; ●信号管理函数设置; ●文件方式初模。

一个进程通过产生子进程,可以同时处理两项任务,这在网络应用程序中常用,当一个进程想执行另一个进程时,也用fork()创建子进程,父子进程中的某一个进程调用exec来执行新程序。 4.3.2进程的终止和父、子进程的同步

1.进程自我终止 在用户态程序中,如果一个用fork()创建的子进程希望终止自己,就利用系统调用函数exit。它使进程进入等待善后处理状态,并等待父进程作善后处理。 相应的操作内容为:

(1)如果是组长,则向所有组员发信号SIGHUP。

(2)如果是父进程,则将所有子进程的父进程改为init。

(3)如果是子进程,则向父进程发信号SIGCHLD。

(4)关闭打开的所有文件描述符。

执行该函数后,子进程向父进程发出SIGCHLD,释放资源,变为僵尸进程,它的进程表相依旧保存。 僵尸(Zombie)进程是指,已经放弃了几乎所有内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。当一个进程已退出,但其父进程还没有调用系统调用wait对其进行处理之前的这段时间里,它会一直保持僵尸状态。

收集这些信息,并终结这些僵尸进程靠waitpid调用和wait调用等方法完成。 僵尸进程虽然对其他进程几乎没有什么影响,不占用CPU时间,消耗的内存也几乎可以忽略不计,但由于Linux系统中进程数目是有限制的,在一些特殊的情况下,如果存在太多的僵尸进程,也会影响到新进程的产生。 2.父进程等待子进程终止

wait、waitpid和exit是Linux向用户态进程提供的进程之间实施同步的主要手段。系统调用wait用于父进程待它的一个子进程终止。如果在wait之前已经有一个进程结束了,则对其作善后处理并返回,如果它没有子进程,则返回-1。其格式为

intwait(intstatus)

在以下三种情况,wait返回进程号:

(1)子进程调用exit。

(2)子进程被一个信号终止。

(3)子进程被跟踪且停止。

第三种情况发生在一个进程跟踪另一个进程的执行的情况下,如调试程序用于单步执行某个进程时,这种情况不加讨论。下列说明系统调用

exit和

wait的应用:

main() { intn;

if(fork()) {n=wait();

printf("Itisparentprocess.\n");

printf("childpid:%dhasfinished.\n",n);

}else{printf("Itischildprocess.\n");

exit();

}}

执行结果:

Itischildprocess. Itisparentprocess. childpid:1236hasfinished.

函数waitpid的调用格式为

pid_twaitpid(pid_tpid,int*statloc,intoption);

系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。

(1) Pid:是一个进程ID。当pid取不同的值时,有不同的意义: ①

pid>0时,只等待进程ID等于pid的子进程,不管其他子进程是否运行结束,只要指定的子进程还没有结束,waitpid就会一直等下去。 ②pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用相同。

pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会理睬它。

pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。

(2)Options:目前在Linux中只支持WNOHANG和WUNTRACED两个选项,可以用“|”运算符把它们连接起来使用,例如:

ret=waitpid(-1,NULL,WNOHANG|WUNTRACED);

其中,WNOHANG表示即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。WUNTRACED与跟踪调试有关,极少用到。

waitpid的返回值比wait稍微复杂一些,共有三种情况:

(1)正常返回的时候,waitpid返回收集到的子进程的进程ID。

(2)如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0。 (3)若调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。例如,当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程时,waitpid就会出错返回,这时errno被设置为ECHILD。

4.3.3系统调用exec()

使用fork()创建一个进程后,程序的两个拷贝都在运行。通常一个拷贝使用exec()调用另一个拷贝。系统调用exec()负责定位可执行文件的二进制代码,并负责装入和运行。系统提供给程序员六种调用exec()的方法。它装入可执行文件的文件头,并试图执行它。 intexeclp(char*fileneme,char*arag0,char*arg1,...,charargn,(char*)0);

intexecl(char*pathname,char*arg0,char*arg1,...,

char*argn,(char*)0);

intexecle(char*pathname,char*arg0,char*arg1,...,char*argn,char**envp);

intexecvp(char*filename,char**argv);

intexecv(char*pathname,char**argv);

intexecve(char*pathname,char**argv,

char**envp);

它们的执行使当前进程的控制权转移给新程序。其中,只有execve是真正意义上的系统调用,其他都是在此基础上经过包装的库函数。

exec()函数族的作用根据指定的文件名找到可执行文件,并用它来取代调用进程的内容,即在调用进程内部执行一个可执行文件。 与一般情况不同,exec()函数族的函数执行成功后不会返回,因为调用进程的实体,包括代码段、数据段和堆栈等都已经被新的内容取代,只留下进程ID等一些表面上的信息仍保持原样。看上去还是旧的躯壳,却已经注入了新的灵魂。只有调用失败了,它们才会返回

-1,从原程序的调用点接着往下执行。

现在我们明白了在Linux下是如何执行新程序的,当有进程调用任何一个exec()时,可让自己以新的面貌重生;或者,更普遍的情况是,如果一个进程想执行另一个程序,它就可以fork()出一个新进程,然后调用任何一个exec(),这样看起来就好像通过执行应用程序而产生了一个新进程一样。

Linux专门为exec()作了优化,我们已经知道,fork会将调用进程的所有内容原封不动地拷贝到新产生的子进程中去,这些拷贝的动作很消耗时间,而如果fork完之后我们马上就调用exec,这些拷贝来的东西又会被立刻抹掉,这非常不划算,于是人们设计了一种“写时拷贝(copy-on-write)”技术,写时拷贝技术使得fork结束后并不立刻复制父进程的内容,而是到了真正实用的时候才复制,这样如果下一条语句是exec(),它就不会作无用功了,也就提高了效率。

对比以下两个函数:

intmain(intargc,

char*argv[],

char*envp[]) intexecve(constchar*path,

char*constargv[],

char*constenvp[])

这两个函数里的argv和envp是完全一一对应的关系。path是被执行应用程序的完整路径,argv就是传给被执行应用程序的命令行参数,envp是传给被执行应用程序的环境变量。

下列语句产生子进程“CHILD.EXE”及传递三个参数。

#include<process.h>

#include<stdio.h>

externchar**environ;

main()

{charargs[4];

intn;

args[0]="child";

args[1]="one"; args[2]="two";

args[3]=NULL;

n=execl("child.exe","child","one","two",NULL);

n=execle("child.exe","child","one","two",NULL,environ);

n=execlp("child.exe","chile","one","two",NULL);

n=execv("child.exe",args);

n=execve("child.exe",args,environ);

n=execvp("child.exe",args);

}4.4守护进程(daemonprocess)

守护进程用来执行一些守护任务,例如,行式打印机的守护进程一直等待打印文件的请求,守护进程一直等待某个事件的发生,或者周期性等待某个事件的发生,它是在后执行的进程。例如,在网络中,远程登录程序有一个守护进程,它一直等待用户的登录请求。 一般典型的守护进程具有如下特征:

(1)在系统初始化时,它被启动。

(2)它的生存期为系统的运行时间。

(3)它一直在等待,等到某个等待的事件发生的,它处理事件。

(4)它可以利用其他进程来处理各种请求。

一个守护进程有多种不同的起动方式:

(1)大多数系统守护进程在系统启动时由初始化文件/etc/rc启动,以这种方式启动的进程具有超级用户特权。

(2)由系统文件

/usr/lib/crontab周期性启动。以这种方式启动的进程具有超级用户特权。

(3)由AT命令的执行,它将守护进程延迟到某一时间执行。

(4)在终端上起动,常用来测试一个守护进程。 下面讨论守护进程的一些特性,然后给出编写守护进程的一些规则。要编写一个守护进程,首先要了解进程运行的前提条件,即在何种条件下激活,由谁激活;其次才是服务部分。 2.改变当前工作目录 在进程的生存期内,它的当前工作目录一直处于打开状态,这个目录所在的安装文件中,如果有一个活跃进程的当前工作目录,就不能拆卸该安装文件。为了使系统管理程序拆下该文件系统,守护进程应把其当前工作目录变为根目录。

#include<direct.h> chdir("/"); 4.后台执行

当守护进程从注册开始启动,且又不放在后台运行时,那么在它执行的过程中便与终端相连,使终端处于停顿状态,或当守护进程从shell文本启动而又不放在后台运行时,那么守护进程将使shell文本的运行停顿下来,直到守护进程终止。为了防止以上情况的发生,守护进程应用fork产生一子进程,让守护程序在子进程中执行,而原进程用exit终止。 5.与进程组脱离

一个进程有其进程组号,该进程组的有关信息对它均有影响,为了防止守护进程被影响,可以脱离原进程组,而重新构成自己的进程组。这个功能可以用下面系统调用获得:

setpgrp(0,getpid()); 它使得守护进程的进程组号与进程相等,从而使调用进程成为这个新进程组的组长。 6.忽略I/O信号

在支持作业控制的系统中,可以用stty选项控制后台作业把信息输出到控制终端上的能力,如果禁止后台写,那么,当进程写终端时便会收到信号SIGTTOU。解决该问题的办法可以使守护进程与它的控制终端脱离。如果守护进程执行时不需要把错误信息写到控制终端上,它就可以忽略这个信号。 #ifdefSIGTTOU signal(SIGTTOUSIG_IGN);

#endif

守护进程从执行它的进程处继承了终端组号。如果不想受其影响,可以利用以下方法脱离控制终端。

/*

父进程产生子进程,然后父进程终止

*/ intfd;

if(fork()!=0) exit(); /*

子进程处理组

*/ setpgrp(0,getpid());

if((fd=open("/dev/tty",O_RDWR))>=0) { ioctl(fd,TIOCNOTTY,(char*)0);

/*ioctl()通过设置信号TIOCNOTTY脱离控制终端

*/ close(fd);

} 7.防止重新获得控制终端

守护进程与控制终端脱离关系后,当它打开一个终端设备,便又重新获得控制终端。例如,守护进程把错误信息送给

/dev/tty或

/dev/console,就可以打开终端设备,即使在关闭后,内核仍可能把该终端作为守护进程的控制终端。 当守护进程调用过setpgrp且成为进程组组长后,它的进程组号不为“0”,不能获得控制状态,因为只有在一个进程的组号为“0”时,才允许获得控制终端。 8.守护进程终止

系统V与4.3BSD由init进程发出信号SIGTERM来通知每一个运行的进程,系统准备由多用户方式转为单用户方式,并等待进程的终止。如果过了一定的时间后,仍有进程没有终止,则再发信号

SIGKILL给它们,该信号是不可忽视的,守护进程是在收到

SIGTERM后终止运行的。

9.处理信号

SIGCLD

守护进程有时对其子进程调用exit函数后的终止状态不做善后处理,忽略子进程发来的信号SIGCLD。这样会使子进程浪费占据的资源。UNIX在守护进程中设置一可选函数处理SIGCLD信号。

#include"sgstype.h" #include<sys/wait.h> #include<signal.h>

sig_child() { #ifdefBSD intpid;

unionwaitstatus;

while((pid=wait3(&statusWNOHANG,(structrusage*)0))>0);

#endif } 4.4.2守护程序框架 下面的守护进程实例包括两部分:主程序test.c和初始化程序init.c。主程序每隔一分钟向/tmp目录中的日志test.log报告运行状态。 初始化程序中的init_daemon函数负责生成守护进程。可以利用init_daemon函数生成自己的守护进程。

1.init.c部分

#include<unistd.h> #include<signal.h> #include<sys/param.h> #include<sys/types.h> #include<sys/stat.h> voidinit_daemon(void){intpid;inti;if(pid=fork())exit(0); /*

是父进程,结束父进程

*/elseif(pid<0)exit(1); /*fork失败,退出

*/ /*

第一子进程,在后台继续执行

*

setsid(); /*

第一子进程成为新的会话组长和进程组长,并与控制终端分离

*/ if(pid=fork()) exit(0); /*

是第一子进程,结束第一子进程

*/ elseif(pid<0)exit(1); /*fork失败,退出

*/ /*

第二子进程,继续

*/ /*

第二子进程不再是会话组长

*/ for(i=0;i<NOFILE;++i) /*

关闭打开的文件描述符

*/ close(i);Ch dir("/tmp"); /*

改变工作目录到/tmp*/U mask(0); /*

重设文件创建掩模

*/Re turn; } 2.test.c部分

#include<stdio.h> #include<time.h> voidinit_daemon(void); /*

守护进程初始化函数

*/ main() { FILE*fp; time_tt; init_daemon(); /*

初始化为Daemon*/

while(1) /*

每隔一分钟向test.log报告运行状态

*/ { sleep(60); /*

睡眠一分钟

*/ if((fp=fopen("test.log","a"))>=0) { t=time(0); fprintf(fp,"Imhereat%sn",asctime(localtime(&t))); fclose(fp); } } }

编译:gcc-g-otestinit.ctest.c。 执行:./test。 键入ps命令查看进程:ps–ef。 从输出可以发现,test守护进程的各种特性满足上面的要求。

这个文件中的条目由下列字段组成:servicetypeprotocolwaituserservercmdline ● service:提供服务名。必须在/etc/service文件内查找这个服务名,并把它译为端口号。 ● type:指定套接字类型,要么是stream(用于面向连接的协议),要么是dgram(用于数据报传输协议)。 ● protocol:服务器所用的协议名称。这个字段值必须是一个能够在protocols文件内找到的有效协议名。

● wait:该字段只用于dgram类型的套接字。既可以是wait,也可以是nowait。如果指定的是wait,则inetd在任何时候对指定的端口都只执行一个服务器。如若不然,它就会在执行服务器之后,立即返回端口,监听接入连接。这对单线程服务器来说是非常有用的。所谓单线程服务器,是单纯地读取所有进入的数据报,直到再也没有数据报进入时才退出。多数RPC(远程进程调用)服务器都是这种类型的服务器,因此多数情况下应该指定为wait。与单线程服务器相反的是多线程服务器,它允许同时运行多个实例进程,数额不限。这类服务器较少使用,如果有,应该指定为nowait。stream套接字应该始终采用nowait。 ● user:这是用户的登录ID,进程就在这个ID下执行。其值通常是根用户,但有些服务可采用不同的账号。 ●server:指出准备执行

温馨提示

  • 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
  • 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
  • 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
  • 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
  • 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
  • 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
  • 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。

评论

0/150

提交评论