第十一章 进程和信号(二)

来源:互联网 发布:恶搞p图软件 编辑:程序博客网 时间:2024/06/05 06:08
信号 

      信号是UNIX与Linux系统响应某些条件而产生的一个事件。接收到该信号的进程会相应地采取一些行动。我们用术语(raise)表示一个信号的产生,使用术语(catch)来表示接收到一个信号。信号是由于某些错误条件而生成的,如内存段冲突、浮点处理器错误或非法指令。他们由 shell 和终端处理器生成来引起中断,它们还可以作为在进程间传递消息或修改行为的一种方式,明确地由一个进程发送给另一个进程。无论何种情况,它们的编程接口都是相同的。信号可以被生成、捕获、响应或忽略。

      信号的名字是在头文件 signal.h 中定义的。他们以 "SIG" 开头,见表:

信号名称说明SIGABORT
*进程异常终止SIGALRM
超时警告SIGFPE
*浮点运算异常SIGHUP
连接挂断SIGILL 
*非法指令SIGINT
终端中断SIGKILL
终止进程(此信号不能被捕获或忽略)SIGPIPE
向无读进程的管道写数据SIGQUIT
终端退出SIGSEGV
*无效内存段访问SIGTERM
终止SIGUSR1 
用户定义信号1SIGUSR2 
用户定义信号2             *  系统对信号的响应视具体实现而定。

         如果进程接收到这些信号中的一个,但事先没有安排捕获它,进程将会立刻终止。通常,系统将生成核心转储文件 core ,并将其放在当前目录下。该文件是进程在内存中的映像,它对程序的调试很有用处。

           其它信号见表
信号名称说明SIGCHLD
子进程已经停止或退出SIGCONT 
继续执行暂停进程SIGSTOP
停止执行(此信号不能被捕获或忽略)SIGTSTP
终端挂起SIGTTIN
后台进程尝试读操作SIGTTOU
后台进程尝试写操作

        SIGCHLD 信号对于管理子进程很有用。默认情况下,它是被忽略的。其余的信号会使接收它们的进程停止运行,但 SIGCONT 是个另外,它的作用是让进程恢复并继续执行。shell 脚本通过它来控制作业,但用户程序很少会用到它。

        现在,我们只需要知道如果 shell 和终端驱动程序是按通常情况配置的话,在键盘上敲入中断字符(通常是Ctrl+C组合键)就会向前台进程(即当前正在运行的程序)发送 SIGINT 信号,这将引起该程序的终止,除非它事先安排了捕获这个信号。

        如果想发送一个信号给进程,而该进程并不是当前的前台进程,就需要用 kill 命令。该命令需要有一个可选的信号代码或信号名称和一个接收信号的目标进程的PID(这个PID一般需要用 ps 命令查出来)。例如,如果要向运行在另一个终端上的 PID 为512的进程发送 “挂断” 信号,可以使用如下命令:

$ kill -HUP 512

       kill 命令有一个有用的变体叫 killall,它可以给运行着某一命令的所有进程发送信号。并不是所有的Unix系统都支持它,但 Linux 系统一般都有该命令。如果不知道某个进程的 PID,或者想给执行相同命令的许多不同的进程发送信号,这条命令就很有用了。一种常见的用法是,通知 inetd 程序重新读取它的配置选项,要完成这一工作,可以使用下面这条命令:

$ killall -HUP inetd

程序可以用 signal 库函数来处理信号,它的定义如下:
#include <signal.h>
void ( *signal ( int sig, void ( *func ) ( int ) ) ) ( int );

       这个相当复杂的函数定义说明,signal 是一个带有 sig 和 func 两个参数的函数。准备捕获或忽略的信号由参数 sig 给出,接收到指定的信号后将要调用的函数由参数 func 给出。信号处理函数必须有一个 int 类型参数(即接收到的信号代码)并且返回类型为 void 。signal 函数本身也返回一个同类型的函数,即先前用来处理这个信号的函数,或者也可以用下表中的两个特殊值之一来代替信号处理函数。

SIG_IGN忽略信号SIG_DFL恢复默认行为示例:

#include <stdio.h>#include <unistd.h>#include <signal.h> void ouch(int sig){    printf("OUCH! - I got signal %d\n", sig);    (void) signal(SIGINT, SIG_DFL);}int main(){    (void) signal(SIGINT, ouch);        while(1)    {        printf("Hello World\n");sleep(1);    }}

       main函数的作用是,截获按下 Ctrl+C 组合键时产生的 SIGINT 信号。没i有信号出现时,它会在一个无限循环中每隔一秒打印一条消息。

       第一次按入 Ctrl+C 组合键会让程序做出响应,然后程序继续执行。再次按下 Ctrl+C 组合键时,程序结束运行,因为 SIGINT 信号的处理方式已恢复为默认行为 --- 终止程序的运行。

$ ./ctrlc1
Hello World!
Hello World!
Hello World!
Hello World!
^C
OUCH! - I got signal 2
Hello World!
Hello World!
Hello World!
Hello World!
^C
$

       在此例中我们可以看到,信号处理函数使用了一个单独的整数参数,它就是引起该函数被调用的信号代码。如果需要在同一个函数中处理多个信号,这个参数就很有用。在本例中,我们打印出 SIGINT 的值,它的值在这个系统中恰好是 2 ,但你不能过分依赖传统的信号数字值,而应该在新的程序中总是使用信号的名字。

实验解析

       程序中安排函数 ouch 来处理在按下 Ctrl+C 组合键时所产生的 SIGINT 信号。程序会在中断函数 ouch 处理完毕后继续执行,但信号处理方式已经恢复为默认行为(不同版本的 UNIX 系统,特别是从 Berkley UNIX 衍生出来的那些版本,在对信号的处理方式上从历史上就有些细微的不同。如果想让信号的处理方式在信号发生后恢复到i其默认行为,最好的方法就是自己写出具体的信号处理代码)。当它接收到第二个 SIGINT 信号后,程序将采取默认的行动,即终止程序的运行。
       如果想保留信号处理函数,让它继续响应用户的 Ctrl+C 组合键,我们就需要再次调用 signal 函数来重新建立它。这会使信号在一段时间内无法得到处理,这段时间从调用中断函数开始,到信号处理函数的重建为止。如果在这段时间内程序接收到第二个信号,它就会违背我们的意愿终止程序的运行。
      注:我们不推荐大家使用 signal 接口。之所以会在这里介绍它,是因为你可能会在许多老程序中看到它的应用。稍后我们会介绍一个定义更清晰、执行更可靠的函数 sigaction ,在所有的新程序中都应该使用这个函数。
      signal 函数返回的是先前对指定信号进行处理的信号处理函数的函数指针,如果未定义信号处理函数,则返回 SIG_ERR并设置 errno 为一个正数值。如果输出的是一个无效的信号,或者尝试处理的信号是不可捕获或不可忽略的信号(如SIGKILL),errno将被设置为 EINVAL。


发送信号 
       进程可以通过调用 kill 函数向包括它本身在内的其它进程发送一个信号。如果程序没有发送该信号的权限,对 kill 函数的调用就将失败,失败的常见原因是目标进程由另一个用户所拥有。这个函数和同名的 shell 命令完成相同的功能,它的定义如下:
#include <sys/types.h>
#include <signal.h>
int kill( pid_t pid, int sig );
       kill 函数把参数 sig 给定的信号发送给由参数 pid 给出的进程号所指定的进程,成功时它返回 0 。要想发送一个信号,发送进程必须拥有相应的权限。这通常意味着两个进程必须拥有相同的用户 ID (即你只能发送信号给属于自己的进程,但超级用户可以发送信号给任何进程)。
       kill 调用会在失败时返回 -1 并设置 errno 变量。失败的原因可能是:给定的信号无效(errno设置为EINVAL);发送进程权限不够(errno 设置为 EPERM);目标进程(errno 设置为 ESRCH)。
       信号为我们提供了一个有用的闹钟功能。进程可以通过调用 alarm 函数在经过预定时间后发送一个 SIGALRM 信号。

#include <unistd.h>
unsigned int alarm ( unsigned int seconds );
      alarm 函数用来在 seconds 秒之后安排发送一个 SIGALARM 信号。但由于处理的延时和时间调度的不确定性,实际闹钟时间将比预先安排的要稍微拖后一点。把参数 seconds 设置为 0 将取消所有已设置的闹钟请求。如果在接收到 SIGALARM 信号之前再次调用 alarm 函数,则闹钟开始重新计时。每个进程只能有一个闹钟时间。alarm 函数的返回值是以前设置的闹钟时间的余留秒数,如果调用失败则返回 -1.。
      为了说明 alarm 函数的工作情况,我们通过使用 fork,sleep 和 signal 来模拟它的效果。程序可以启动一个新的进程,它专门用于在未来的某一时刻发送一个信号。

实验  模拟一个闹钟

        alarm.c程序里的第一个函数 ding 的作用是模拟一个闹钟。

        在main函数中,我们告诉子进程在等待 5 秒后发送一个 SIGALRM 信号给它的父进程。

        父进程通过一个 signal 调用安排好捕获 SIGALRM 信号的工作,然后等待它的到来。

/*  In alarm.c, the first function, ding, simulates an alarm clock.  */#include <signal.h>#include <stdio.h>#include <unistd.h>#include <stdlib.h>static int alarm_fired = 0;void ding(int sig){    alarm_fired = 1;}/*  In main, we tell the child process to wait for five seconds    before sending a SIGALRM signal to its parent.  */int main(){    pid_t pid;    printf("alarm application starting\n");    pid = fork();    switch(pid) {    case -1:      /* Failure */      perror("fork failed");      exit(1);    case 0:      /* child */        sleep(5);        kill(getppid(), SIGALRM);        exit(0);    }/*  The parent process arranges to catch SIGALRM with a call to signal    and then waits for the inevitable.  */    printf("waiting for alarm to go off\n");    (void) signal(SIGALRM, ding);    pause();    if (alarm_fired)        printf("Ding!\n");    printf("done\n");    exit(0);}

        运行这个程序时,它会暂停 5 秒,等待模拟闹钟的闹响。

$ ./alarm
alarm application starting
waiting for alarm to go off
<5 second pause>
Ding!
done
$

        这个程序用到了一个新的函数 pause,它的作用很简单,就是把程序的执行挂起直到有一个信号出现为止。当程序接收到一个信号时,预先设定好的信号处理函数开始运行,程序也将恢复正常的执行。pause 函数的定义如下所示:
#include <unistd.h>
int pause(void);
        当它被一个信号中断时,将返回 -1 (如果下一个接收到的信号没有导致程序终止的话)并把 errno 设置为 EINTR。当需要等待信号时,一个更常见的方法是使用稍后将要介绍的 sigaction 函数。

实验解析

       闹钟模拟程序通过 fork 调用启动新的程序。这个子进程休眠 5 秒后向其父进程发送一个 SIGALRM 信号。父进程在安排好捕获 SIGALRM 信号后暂停运行,直到接收到一个信号为止。我们并未在信号处理函数中直接调用 printf ,而是通过在该函数中设置标志,然后在 main 函数中检查该标志来完成消息的输出。
       使用信号并挂起执行是 Linux 程序设计的一个重要部分。这意味着程序不需要总是在执行着。程序不必在一个循环中无休止地检查某个事件是否已经发生,相反,它可以等待事件的发生。这在只有一个 CPU 的多用户环境中尤其重要,进程共享着一个处理器,繁忙的等待将会对系统的性能造成极大的影响。程序中信号的使用将带来一个特殊问题:“如果信号出现在系统调用的执行过程中会发生什么情况?”答案是相当让人不满意的“视情况而定”。一般来说。你只需要考虑系统调用,例如从终端读取数据,如果在这个系统调用等待数据时出现一个信号,它就会返回一个错误。如果你开始在自己的程序中使用信号,就需要注意一些系统调用会因为接收到了一个信号而失败,而这种错误情况可能是你在添加信号处理函数之前没有考虑到的。
        在编写程序中信号处理部分的代码时必须非常的小心,因为在使用信号的程序中会出现各种各样的"竞态条件"。例如,如果想调用 pause 等待一个信号,可信号出现在 pause 之前,就会使程序无限期地等待一个不会发生的事件。这些竞态条件都是一些对时间要求很苛刻的问题,许多编程新手都有这方面的烦恼,所以在检查和信号相关的代码时总是要非常小心。

原创粉丝点击