2.3、进程通信之信号通信

1、信号概述 

  信号是在软件层次上对中断机制的一种模拟。在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。信号可以直接进行用户空间进程和内核进程之间的交互,内核进程也可以利用它来通知用户空间进程发生了哪些系统事件。它可以在任何时候发给某一进程,而无须知道该进程的状态。如果该进程当前并未处于执行态,则该信号就由内核保存起来,直到该进程恢复执行再传递给它为止;如果一个信号被进程设置为阻塞,则该信号的传递被延迟,直到其阻塞被取消时才被传递给进程。

  信号是进程间通信机制中唯一的异步通信机制,可以看做是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX 实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。

  信号事件的发生有两个来源:硬件来源(如我们按下了键盘上的按钮或者出现其他硬件故障);软件来源,常用发送信号的系统函数有kill()、raise()、alarm()、setitimer()和sigqueue()等,软件来源还包括一些非法运算等操作。

  进程可以通过3种方式来响应一个信号:

  (1)忽略信号:忽略信号即对信号不做任何处理,其中,有两个信号不能忽略:SIGKILL和SIGSTOP。

  (2)捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数。

  (3)执行默认操作:Linux对每种信号都规定了默认操作,如下表所示。

信 号 名 含 义 默 认 操 作
SIGHUP 该信号在用户终端连接(正常或非正常)结束时发出,通常是在终端的控制进程结束时,通知同一会话内的各个进程与控制终端不再关联 终止进程
SIGINT 该信号在用户输入INTR字符(通常是Ctrl+C)时发出,终端驱动程序发送此信号并送到前台进程中的每一个进程 终止进程
SIGQUIT 该信号和SIGINT类似,但由QUIT字符(通常是Ctrl+\)来控制 终止进程
SIGILL 该信号在一个进程企图执行一条非法指令时(可执行文件本身出现错误,或者试图执行数据段、堆栈溢出时)发出 终止进程
SIGFPE 该信号在发生致命的算术运算错误时发出。这里不仅包括浮点运算错误,还包括溢出及除数为0等其他所有的算术错误 终止进程
SIGKILL 该信号用来立即结束程序的运行,并且不能被阻塞、处理和忽略 终止进程
SIGALRM 该信号当一个定时器到时的时候发出 终止进程
SIGSTOP 该信号用于暂停一个进程,且不能被阻塞、处理或忽略 暂停进程
SIGTSTP 该信号用于交互停止进程,用户在输入SUSP字符时(通常是Ctrl+Z)发出这个信号 停止进程
SIGCHLD 子进程改变状态时,父进程会收到这个信号 忽略

 

   一个完整的信号生命周期可以分为3个重要阶段,这3个阶段由4个重要事件来刻画的:信号产生、信号在进程中注册、信号在进程中注销、执行信号处理函数。这里信号的产生、注册、注销等是指信号的内部实现机制,而不是信号的函数实现。因此,信号注册与否与本节后面讲到的发送信号函数(如kill()等)及信号安装函数(如signal()等)无关,只与信号值有关。

  相邻两个事件的时间间隔构成信号生命周期的一个阶段。要注意这里的信号处理有多种方式,一般是由内核完成的,当然也可以由用户进程来完成,故在此没有明确指出。

  信号相关的函数包括信号的发送、捕获及信号的处理,它们有各自相对应的常见函数。

  (1)发送信号的函数:kill()、raise()。

  (2)捕捉信号的函数:alarm()、pause()。

  (2)处理信号的函数:signal()、sigaction()。

  信号的使用场所:

  后台需要使用信号,如xinetd;

  如果两个进程没有亲缘关系,无法使用无名管道;

  如果两个通信进程之一只能使用标准输入和标准输出,则无法使用FIFO。

2、信号发送与设置

  2.1、信号发送:kill()和raise()(给进程发信号)

  kill()函数同读者熟知的kill系统命令一样,可以发送信号给进程或进程组(实际上,kill系统命令只是kill()函数的一个用户接口)。这里需要注意的是,它不仅可以中止进程(实际上发出SIGKILL信号),也可以向进程发送其他信号。

  与kill()函数不同的是,raise()函数允许进程向自身发送信号。

头文件

#include <signal.h>

#include <sys/types.h>

函数原型

int kill(pid_t pid, int sig);

作用

发送信号给进程或进程组

参数 

pid:pid用来决定将信号发给谁

正数:发送信号给进程号为pid的进程

0:信号被发送到所有和当前进程在同一个进程组的进程

1:信号发给进程表中所有的进程(除了进程号大的进程外)

<-1:信号发送给进程组号为-pid的每一个进程

sig

信号类型,用来操作进程,比如终止进程,暂停进程

返回值

成功

0

失败

-1

 

头文件

#include <signal.h>

#include <sys/types.h>

函数原型

int raise(int sig);

作用

该函数只允许进程向自身发送信号

参数

sig

信号类型

返回值

成功

0

失败

-1

  下面的示例首先使用fork()创建了一个子进程,接着为了保证子进程不在父进程调用kill()之前退出,在子进程中使用raise()函数向自身发送SIGSTOP信号,使子进程暂停。接下来在父进程中调用kill()向子进程发送信号,在该示例中使用的是SIGKILL,读者可以使用其他信号进行练习。

/* kill_raise.c */
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
  pid_t pid;
  int ret;

  /* 创建一个子进程 */
  if ((pid = fork()) < 0)
  {
    printf("Fork error\n");
    exit(1);
  }

  if (pid == 0) /* 子进程 */
  {
    /* 在子进程中使用raise()函数发出SIGSTOP信号,使子进程暂停 */     printf("Child(pid : %d) is waiting for any signal\n", getpid());     raise(SIGSTOP);     exit(0);   }   else   {     /* 在父进程中收集子进程发出的信号,并调用kill()函数进行相应的操作 */     if ((waitpid(pid, NULL, WNOHANG)) == 0)     {       if ((ret = kill(pid, SIGKILL)) == 0)       {         printf("Parent kill %d\n",pid);       }     }     waitpid(pid, NULL, 0);     exit(0);   } }
该程序运行结果如下:
$ ./kill_raise
Child(pid : 4877) is waiting for any signal
Parent kill 4877

 

  2.2、定时器信号:alarm()、pause(),用来在设定时间内终止进程

  alarm()也称为闹钟函数,它可以在进程中设置一个定时器,当定时器指定的时间到时,它就向进程发送SIGALARM信号。要注意的是,一个进程只能有一个闹钟时间,如果在调用alarm()之前已设置过闹钟时间,则任何以前的闹钟时间都被新值所代替。

  pause()函数用于将调用进程挂起直至捕捉到信号为止。这个函数很常用,通常可以用于判断信号是否已到。

头文件

#include <unistd.h>

函数原型

unsigned int alarm(unsigned int seconds);

作用

在指定时间内向进程发送终止SIGALRM信号

参数

seconds

指定秒数,系统经过seconds秒后向该进程发送SIGALRM信号

返回值

成功

如果调用此alarm()前进程中已经设置了闹钟时间,则返回上一个闹钟时间的剩余时间,否则返回0

失败

-1

 

头文件

#include <unistd.h>

函数原型

int pause(void);

作用

pause()使调进程(或线程)进入休眠状态,直到传递的信号终止进程或导致调用信号捕获函数。

参数

返回值

只返回-1,并且把error值设为EINTR

错误代码:EINTR 有信号到达中断了此函数

 

以下实例实际上已完成了一个简单的sleep()函数的功能,由于SIGALARM默认的系统动作为终止该进程,因此程序在打印信息前就会被结束了,代码如下:

/* alarm_pause.c */
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main()
{
  /* 调用alarm定时器函数 */
  int ret = alarm(5);
  pause();
  printf("I have been waken up.\n",ret); /* 此语句不会被执行 */
}
$./alarm_pause
Alarm clock

 

  2.3、信号的设置(处理):signal()和sigaction()

  信号处理的方法主要有两种,一种是使用signal()函数,另一种是使用信号集函数组。下面分别介绍这两种处理方式。

  (1)使用signal()函数

  使用signal()函数处理时,只需指定要处理的信号和处理函数即可。它主要用于前32种非实时信号的处理,不支持信号传递信息,但是由于使用简单、易于理解,因此也受到很多程序员的欢迎。Linux还支持一个更健壮更新的信号处理函数sigaction(),推荐使用该函数。

  特定的信号是与相应的事件相联系的。一个进程可以设定对信号的响应方式。

  这里需要对该函数原型进行说明。这个函数原型有点复杂:首先该函数原型整体指向一个无返回值并且带一个整型参数的函数指针,也就是信号的原始配置函数;接着该原型又带有两个参数,其中第2个参数可以是用户自定义的信号处理函数的函数指针。

头文件

#include <signal.h>

函数原型

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

作用

设置如何处理相应的信号

参数

signum

指定信号代码,该信号属于命令信号,要处理的的信号

handler:处理信号

SIG_IGN:忽略该信号

SIG_DFL:采用系统默认方式处理信号

自定义的信号处理函数指针

返回值

成功

以前的信号处理函数

失败

-1

  (2)sigaction()函数

   sigaction()函数相对于signal()函数更加健全、功能更全,建议使用该函数。

头文件

#include <signal.h>

函数原型

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

作用

处理信号

参数

signum

信号代码,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号

act

指向结构sigaction的一个实例的指针,指定对特定信号的处理

oldact

保存原来对相应信号的处理

返回值

成功

0

失败

-1

 

 

   首先给出了sigaction的定义,代码如下: 这里要说明的是sigaction()函数中第2和第3个参数用到的sigaction结构,这是一个看似非常复杂的结构,希望读者能够慢慢阅读此段内容。

struct sigaction 
{ 
  void (*sa_handler)(int signo); 
  sigset_t sa_mask; 
  int sa_flags; 
  void (*sa_restore)(void); 
}

  sa_handler是一个函数指针,指定信号处理函数,这里除可以是用户自定义的处理函数外,还可以为SIG_DFL(采用默认的处理方式)或SIG_IGN(忽略信号)。它的处理函数只有一个参数,即信号值。

  sa_mask是一个信号集,它可以指定在信号处理程序执行过程中哪些信号应当被屏蔽,在调用信号捕获函数前,该信号集要加入到信号的信号屏蔽字中。

  sa_flags中包含了许多标志位,是对信号进行处理的各个选择项。它的常见可选值如下表所示。

信 号 含 义
SA_NODEFER / SA_NOMASK 当捕捉到此信号时,在执行其信号捕捉函数时,系统不会自动屏蔽此信号
SA_NOCLDSTOP 进程忽略子进程产生的任何SIGSTOP、SIGTSTP、SIGTTIN和SIGTTOU信号
SA_RESTART 令重启的系统调用起作用
SA_ONESHOT / SA_RESETHAND 自定义信号只执行一次,在执行完毕后恢复信号的系统默认动作

 

  以下实例表明了如何使用signal()函数捕捉相应信号,并做出给定的处理。这里,my_func就是信号处理的函数指针,读者还可以将其改为SIG_IGN或SIG_DFL查看运行结果。第2个实例是用sigaction()函数实现同样的功能。

以下是使用signal()函数的示例:
/*
signal.c */ #include <signal.h> #include <stdio.h> #include <stdlib.h> /* 自定义信号处理函数 */ void my_func(int sign_no) {   if (sign_no == SIGINT)   {     printf("I have get SIGINT\n");   }   else if (sign_no == SIGQUIT)   {     printf("I have get SIGQUIT\n");   } } int main() {   printf("Waiting for signal SIGINT or SIGQUIT...\n");   /* 发出相应的信号,并跳转到信号处理函数处 */   signal(SIGINT, my_func);   signal(SIGQUIT, my_func);   pause();   exit(0); }

 

运行结果如下:
$ ./signal 
Waiting for signal SIGINT or SIGQUIT...
I have get SIGINT (按Ctrl+c 组合键)
$ ./signal 
Waiting for signal SIGINT or SIGQUIT...
I have get SIGQUIT (按Ctrl+\ 组合键)

 

 以下是用sigaction()函数实现同样的功能,只列出了更新的main()函数部分。

/* sigaction.c */
/* 前部分省略 */
int main()
{
  struct sigaction action;
  printf("Waiting for signal SIGINT or SIGQUIT...\n");

  /* sigaction结构初始化 */
  action.sa_handler = my_func;
  sigemptyset(&action.sa_mask);
  action.sa_flags = 0;

  /* 发出相应的信号,并跳转到信号处理函数处 */
  sigaction(SIGINT, &action, 0);
  sigaction(SIGQUIT, &action, 0);
  pause();
  exit(0);
}

  2.4、信号集函数组

  使用信号集函数组处理信号时涉及一系列的函数,这些函数按照调用的先后次序可分为以下几大功能模块:创建信号集、注册信号处理函数及检测信号。

  其中,创建信号集主要用于处理用户感兴趣的一些信号,其函数包括以下几个。

  A、 sigemptyset():将信号集初始化为空。

  B、sigfillset():将信号集初始化为包含所有已定义的信号集。

  C、sigaddset():将指定信号加入到信号集中。

  D、sigdelset():将指定信号从信号集中删除。

  E、sigismember():查询指定信号是否在信号集中。

  注册信号处理函数主要用于决定进程如何处理信号。这里要注意的是,信号集里的信号并不是真正可以处理的信号,只有当信号的状态处于非阻塞状态时才会真正起作用。因此,首先使用sigprocmask()函数检测并更改信号屏蔽字(信号屏蔽字是用来指定当前被阻塞的一组信号,它们不会被进程接收),然后使用sigaction()函数来定义进程接收到特定信号后的行为。

    检测信号是信号处理的后续步骤,因为被阻塞的信号不会传递给进程,所以这些信号就处于“未处理”状态(也就是进程不清楚它的存在)。sigpending()函数允许进程检测“未处理”信号,并进一步决定对它们做何处理。

    首先介绍创建信号集的函数格式,下表列举了这一组函数的语法要点。

头文件

#include <signal.h>

函数原型

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(sigset_t *set, int signum);

作用

创建信号集

 

set

信号集

signum

指定信号代码

返回值

成功

0 (sigismember成功返回1)

失败

-1 (sigismember失败返回0)

 

头文件

#include <signal.h>

函数原型

int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

作用

用于打开系统记录的文件

参数

how: 决定函数的操作方式

SIG_BLOCK:增加一个信号集到当前进程的阻塞集中

SIG_UNBLOCK:从当前的阻塞集中删除一个信号集

SIG_SETMASK:将当前的信号集设置为信号阻塞集

set

指定信号集,若set是一个非空指针,则参数how表示函数的操作方式;若how为空,则表示忽略此操作。

oset

信号屏蔽字

返回值

成功

0

失败

-1

 

头文件

#include <signal.h>

函数原型

int sigpending(sigset_t *set);

作用

用于打开系统记录的文件

参数 

set

要检测的信号集

返回值

成功

0

失败

-1

 

总之,在处理信号时,一般遵循如下图所示的操作流程。

定义信号处理函数,当信号发生时,执行相应的处理函数。

posted @ 2020-02-07 11:43  孤情剑客  阅读(585)  评论(0)    收藏  举报