进程与信号(四)


线程

Linux进程可以协作,可以发送消息,也可以中断另一个进程。他们甚至可以在彼此之间共享内存段,但是在操作系统内部他们却是完全不同的实体。他们并不能共享变量。

在许多Unix系统与Linux系统还有另一类名为线程的进程。线程在某些程序中具有巨大的价值,例如多线程数据库服务器,然而线程很难进行编程。Linux上的线程编程并不如使用多进程那样常见,因为Linux的进程是轻量级的,而且多个协作进程的编程要比线程编程容易得多。我们会在第12章讨论线程。

信号

信号是由Unix与Linux系统响应某些条件,依据哪一个进程按顺序执行某些动作而产生的事件。我们使用术语"raise"来表示一个信号的生成,而使用术语"catch"来表示信号的接收。信号是由一些错误条件所产生的,例如内存段错误,浮点处理器错误,或是非法指令。

他们是由shell或是终端处理器所产生来引起中断,也可以显示的由一个进程发住另一个进程作为传递信息或是修改行为的一种方法。在所有这些情况下,编程接口是相同的。信号可以产生,捕获,响应或是忽略。

信号的名字是由所包含的头文件signal.h来定义的。他们以"SIG"开头,如下表所示:

信号名        描述
SIGABORT    处理失败
SIGALRM        报警时钟
SIGFPE        浮点异常
SIGHUP        挂起
SIGILL        非法指令
SIGINT        终端中断
SIGKILL        Kill(不能被捕获或是忽略)
SIGPIPE        写入没有读端的管道
SIGQUIT        终端退出
SIGSEGV        非法的内存段访问
SIGTERM        终止
SIGUSR1        用户定义的信号1
SIGUSR2        用户定义的信号2

如果进程接收一个这样的信号,但是却并没有在第一时间捕获信号,进程就会立即终止。通常,会创建一个核心映像文件(core dump)。这个文件名为core,并且位于当前目录下,这是一个进程映像文件,并且在调试中是非常有用的。

其他的信号包含在下表中。

信号        描述
SIGCHLD        子进程已停止或退出
SIGCONT        如果停止,继续执行
SIGSTOP        停止执行(不能被捕获或是忽略)
SIGTSTP        终止停止信号
SIGTTIN        后台进程尝试读
SIGTTOU        后台进程尝试写

SIGCHLD对于管理子进程十分有用。默认情况下这个信号会被忽略。其余的信号会使得接收到他们的进程停止,除了SIGCONT,这个信号会使得进程重新运行。他们被shell程序用于工作控制,而很少为其他的程序所使用。

我们会在稍后详细的讨论第一组进程。就现在,我们只需要知道如果shell与终端驱动器被正常配置,在键盘上输入中断字符会产生SIGINT信号发送到前台进程,也就是当前运行的进程就足够了。这会使得程序终止,除非已经设定动作来捕获这个信号。

如果我们希望向一个进程而不是当前前端任务发送信号,我们使用kill命令。这个命令会以一个可选的信号标识号或是名字,以及要发住的PID作为参数。例如,如果向运行在另一个终端上的,PID为512的shell发送一个"hangup"信号,我们可以使用下面的命令:

kill –HUP 512

另一个有用的kill命令的变体就是killall命令,这会允许我们向过行一个指定命令的所有进程发送一个信号。但不是所有的Unix都会支持这个命令,而Linux通常都会支持。当我们不知道PID,或是当我们希望向执行相同命令的多个不同进程发送信号时是十分有用的。一个通常的用法就是通知inetd程序重新读取其配置选项。要这样做,我们可以使用下面的命令:

killall –HUP inetd

程序可以使用信号库函数来处理信号。

#include
void (*signal(int sig, void (*func)(int)))(int);

这个复杂的声明一signal是一个需要两个参数的函数,sig与func。要捕获或是忽略的信号是由sig参数来指定的。当接收到指定的信号时要调用的函数是由func来指定的。这个函数必须是带有一个int作为参数(接收到的信号),并且其类型为void。信号函数本身返回一个相同类型的函数,这是函数设置的要处理的信号标识号,或是下面两个特殊值中的一个:

SIG_IGN        忽略这个信号
SIG_DFL        重新载入默认行为

例子会使得我们的解释更为清晰。下面我们来编写一个程序,ctrlc.c,这会输出一条合适的信息而是结束来响应Ctrl+C。第二次按下Ctrl+C会结束这个程序。

试验--信号处理

函数ouch会响应作为参数sig传递的信号。这个函数会在信号发生时调用。他会输出一条信息,然而重新设置SIGINT的信号处理为默认行为。

#include
#include
#include
void ouch(int sig)
{
    printf(“OUCH! - I got signal %d/n”, sig);
    (void) signal(SIGINT, SIG_DFL);
}

main函数必须解释当我们输入Ctrl+C时所产生的SIGINT信号。在其余的时间,他只是无限循环,每一秒输出一条信息。

int main()
{
    (void) signal(SIGINT, ouch);
    while(1) {
        printf(“Hello World!/n”);
        sleep(1);
    }
}

第一次输入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。我们不要依赖于信号的传统值;在新的程序中总是使用信号名。

在信号处理器的内部并不是调用所有的函数都是安全的,例如printf。一个有用的技巧就是使用信号处理器来设置一个标记,然后在主程序中检测这个标记并输出所需要的信息。在本章的结束处,我们会看到一个在信号处理器内部可以安全调用的函数列表。

工作原理

当我们通过Ctrl+C来传送SIGINT信号时,程序会调用ouch函数。在中断函数ouch结束之后,程序会继续执行,但是信号的行为已经恢复为默认行为。当他接收到第二个SIGINT信号时,程序会采用默认行为,这个行为会终止程序。

如果我们希望重新获得信息处理器并且继续响应Ctrl+C,我们需要通过再次调用signal函数来重新建立。这会造成一小段信号没有被处理的时间,由中断函数开始到重新建立信号处理器。在这段时间也许会接收到第二个信号,而程序就会并非如我们所愿的终止。

注:我们并不推荐使用signal接口。我们在这里介绍这个函数是我们会发现在许多老的程序中会用到这个函数。我们会在后面看到sigaction函数定义了一个更为清晰与可靠的接口,我们应在所有的新程序中使用这个接口。

signal函数会返回一个用于特定信号的信号处理器的值,否则会返回SIG_ERR,在这种情况下,errno会被设置为一个负值。如果并没有指定一个正确的值或是尝试生成一个不会被捕获或是忽略的信号时,例如SIGKILL,errno会被设置为EINVAL。

发送信号

一个进程会通过调用kill向其他进程,包括其本身发送一个信号。如果程序并没有权限向另一个进程发送信号时,调用就会失败,这通常是因为目标进程是为另一个用户所拥有的。这与具有相同名字的shell命令等同。

#include
#include
int kill(pid_t pid, int sig);

kill函数会具有pid进程标识符的进程发送一个指定的信号sig。如果成功,会返回0。要发送信号,发送进程必须具有相应的权限。通常,这意味着两个进程必须具有相同的用户ID(也就是说,我们只能向我们所拥有的进程发送信号,尽管超级用户可以任何进程发送信号)。

如果kill失败则会返回-1并且设置errno。这通常是因为所指定的信号并不是一个可用的信号(errno设置为EINVAL),或是没有相应的权限(EPERM),或是指定的进程不存在(ESRCH)。

信号为我们提供了一个非常有用的报警时钟程序。alarm函数可以为一个进程所调用来在将来的某个时间调度一个SIGALRM信号。

#include
unsigned int alarm(unsigned int seconds);

alarm函数会延时SIGALRM信号的传递seconds秒。事实上,报警会因为处理时延与调度的不确定性,会在稍后传输。值0会关闭所有的外部报警请求。在一个信号接收之间调用alarm会使得报警重新调度。每一个进程只能有一个外部警报。alarm函数会返回在外部警报可以发送之是所剩余的秒数,如果调用失败则会返回-1。

要了解alarm是如何工作的,我们可以使用fork,sleep与signal来模拟其效果。一个程序可以在某段时间之后为发送信号的角色目的重新启动一个进程。

试验--警报时钟

在alarm.c中,第一个函数ding模拟一个警报时钟。

#include
#include
#include
#include
static int alarm_fired = 0;
void ding(int sig)
{
alarm_fired = 1;
}

在main函数中,我们通知子进程在向其父进程发送SIGALRM信号之间等待5秒。

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);
    }
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);
    }

父进程安排一个函数来捕获SIGALRM信号,并且进行必须的等待。

/* if we get here we are the parent process */
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,他只是简单的使得程序执行挂起,直到产生一个信号。当他接收到一个信号时,就会运行所建立的处理器函数,并且如通常一样继续执行。其声明如下:

#include
int pause(void);

如果被一个信号中断时,此函数会返回-1并且设置errno为EINTR。通常当等待信号时,更为常见的用法是使用sigaction,我们会在本章的稍后进行讨论。

工作原理

警报时钟模拟程序通过fork启动一个新进程。子进程会休眠5秒,然后向其父进程发送SIGALRM信号。父进程会捕获SIGALRM信号,并且暂停直到接收信号。我们并没有在信号处理函数内部直接调用printf;相反,我们设置一个标识,并且在后面检测这个标记。

使用信号与挂起执行是Linux编程的一个重要部分。他意味着一个程序并不需要全时段运行。他可以等待一个事件的发生,而不是在一个循环中运行来检测一个事件是否发生了。这在多用户环境中是十分重要的,因为多用户环境的处理共享一个处理器,而这种频繁的等待对于系统的性能有巨大的影响。信号的一个特殊问题就是我们绝不会知道"在一个系统调用中间如果一个信号发生会发生什么?"。通常情况下,我们只需要担心"缓慢"的系统调用,例如终端读取,在这种情况下,当他等待时,发生一个信号,系统调用就会返回一个错误。如果我们开始在我们的程序中使用信号,那么我们必须清楚如果信号接收到一个在添加信号处理之间我们并没有考虑到的错误条件,系统调用就会失败。

我们必须小心的编写我们的信号程序,因为在使用信号的程序中会有许多"竞争条件"发生。例如,如果我们调用pause来等待一个信号,而那个信号却是在调用pause之间发生,那么我们的程序就会等待一个不会发生的事件。这些竞争条件,严格的时间问题,困扰着程序员。所以我们总是要小心的检测信号代码。

你可能感兴趣的:(LINUX,Develop)