APUE 3 -- 信号 (signal)<II>: 可靠信号

  一个事件可以使一个信号发送给一个进程,这个事件可以是硬件异常,可以是软件条件触发,可以是终端产生信号,也可以是一个kill函数调用。当信号产生后,内核通常会在进程表中设置某种形式的标志(flag)。我们可以认为当进程中的信号处理函数被触发的时候认为信号下达到了(delivered)这个进程。从信号产生到信号下达到进程这段期间,信号被认为是挂起状态(pending)。进程拥有阻塞信号下达的选项。如果一个阻塞信号要发送给一个进程,而且信号的处理方式是默认处理或者被进程捕获,那么这个信号将一直处于挂起状态(pending)直到这个进程将信号设置成非阻塞状态或将信号的处理方式改为忽略。操作系统在信号下达(delivered)的时候决定如何处理这个阻塞信号,而不是在信号产生的时候。这样做允许进程在信号下达前更改信号的处理方式。

  如果一个阻塞信号在进程解除它的阻塞前产生多次,那么unix内核也仅仅会向进程下发一次这个信号。POSIX并没有规定信号下发到进程的顺序,然而与进程当前状态相关的信号会较先到达进程。

  每个进程都有一个信号掩码(signal mask),它定义了一个当前被阻塞发送到该进程的信号的集合。我们可以认为这个掩码对于每个可能下发到该进程的信号有一个与之对应的bit位,对于一个给定的信号来说,如果与之对应的bit位是打开状态,那么意味着这个信号当前应处于阻塞状态。进程可以通过sigprocmask来检查并更改他当前的信号掩码。因为信号的数量是有可能超过一个整数正bit位位数的,所有POSIX规范定义了一个叫做sigset_t的数据类型,它包含了所有信号集。信号掩码就是存储在这其中一个信号集中的。

发送信号

kill & raise

可以使用kill函数向一个进程或进程组发送信号。raise函数允许进程给他自己发送信号。

1 #include <signal.h>
2
3 /* return 0 if OK, -1 on error */
4 int kill(pid_t pid, int signo);
5
6 /* return 0 if OK, -1 on error */
7 int raise(int signo);

函数调用 raise(signo);  等同于 kill(getpid(), signo); 。

对于kill函数,pid有以下四种不同的选择:

  1. pid > 0: 信号被发送给进程号为pid的进程
  2. pid == 0: 信号被发送给所有进程组Id等于发送者进程组的进程(即发送给发送进程所属进程组下的所有进程),前提是发送进程有权限将信号发送给该进程并且该进程不是系统进程。
  3. pid < 0: 信号被发送给进程组Id为pid绝对值的进程组下的所有进程,前提是发送进程有权限发送信号到该进程并且该进程不是系统进程。
  4. pid == -1: 信号被发送给发送进程有权限发送的所有进程,但不包含系统进程。

正如之前我们提到的,进程需要足够的权限才能向另一个进程发送信号,超级用户可以向任何进程发送信号。对于其他用户,可以发送信号的基本准则是:发送进程的真实用户Id(real user Id)或有效用户Id(effective user Id)必须等于接收进程的真实用户Id或有效用户Id。如果实现支持 _POSIX_SAVED_IDS的话,系统会检查接收进程的saved set-user-ID而不是有效用户Id。关于发送信号权限的一个特殊情景是:如果待发送信号是SIGCONT,那么发送进程可以将信号发送给他所属会话下的任何进程。

POSIX规范定义信号值为0的信号为空信号(null signal)。它可以用于通过kill函数来检测某一进程是否存在。kill函数在收到值为0的信号后会进程正常的错误检查,但是不会发送此信号。因此我们可以通过 kill(pid, 0) 来判断进程id为pid的进程是否存在。然而, UNIX系统在一定时间后会循环使用进程 IDs,所以通过pid检查出来的进程未必真的是你认为的那个进程(即函数调用时,通过pid查询到的进程未必会是你认为的那个进程)。另外kill函数不是原子的,当kill函数返回时,有可能被发送信号的进程已经结束。

alarm & pause

alarm函数允许我们设置一个定时器,这个定时器在未来某个时间点触发,这个定时器触发后会产生一个SIGALRM信号,此信号的默认处理方式是结束进程。

1 #include <unistd.h>
2
3 /* 如果之前没有设置alarm返回0,否则返回之前
4 设置的alarm所剩余的秒数 */
5 unsigned int alarm(unsigned int seconds);

一旦到了alarm所设置的时间点,内核就会发送alarm信号,而由于处理器调度延时进程此时可能还无法获取到此信号处理的控制权。每个进程中只有一个alarm时钟。当我们调用alarm时,如果当前进程之前注册的闹钟还未到期,那么此函数返回之前闹钟距到期剩余的秒数,并且之前注册的闹钟会被这个新的闹钟值取代。另外,如果之前注册的闹钟还未到期并且新注册的闹钟值为0的话,那么之前注册的闹钟会被取消。

pause函数的调用会阻塞调用进程直到调用进程捕获到一个信号。

1 #include <unistd.h>
2
3 /*Returns: -1 with errno set to EINTR*/
4 int pause(void);

信号集

像我们之前提到的,不同信号的数量可能会超过一个整数的bit位所能表示的信号数量。POSIX规范定义了sigset_t类型用来表示信号集,并使用下面的5个函数来管理信号集:

 1 #include <unistd.h>
 2
 3 /*All four return 0 if OK, -1 on error*/
 4
 5 /*清空set信号集中的所有信号*/
 6 int sigemptyset(sigset_t* set);
 7 /*使set包含所有信号*/
 8 int sigfillset(sigset_t* set);
 9 /*将信号signo加入到信号集set中*/
10 int sigaddset(sigset_t* set, int signo);
11 /*从信号集set中删除signo*/
12 int sigdelset(sigset_t* set, int signo);
13
14 /*Returns 1 if true, 0 if false, -1 on error*/
15 int sigismember(const sigset_t* set,  int signo);

sigprocmask

一个进程的信号掩码是指被阻止下发到此进程的所有信号的集合。进程可以检查并更改他的信号掩码。

#include <signal.h>

/*  如果oset不为空,那么此进程信号掩码之前的值会被复制到oset中。  如果set为空,那么此进程的信号掩码不会被更改,而信号掩码的当前值  也不会复制到oset中。*/
int sigprocmask(int how, const sigset_t* restrict set,
    sigset_t* restrict oset);

how参数的值指明如何修改信号掩码:

  • SIG_BLOCK: set包含另外的我们想阻塞的信号
  • SIG_UNBLOCK:set包含我们想要解除阻塞的信号
  • SIG_SETMASK:使用set替代进程当前信号掩码

sigprocmask 不支持多线程环境。

sigpending

#include <unistd.h>

/*通过set返回发送给当前进程但被阻塞的信号*/
int sigpending(sigset_t* set);

sigaction

我们可以通过sigaction方法检查并修改特定信号的处理方式(action)。他是早期sinal函数的取代版本。

#include <unistd.h>

/*若oact不为空,函数通过oact返回当前signo的action
 * 若act不为空,则修改signo的当前action*/
int sigaction(int signo, const struct sigaction* restrict act,
struct sigaction* restrict oact);

struct sigaction{
   void (*sa_handler)(int);    /*addr of signal handler,                                  or SIG_IGN or SIG_DFL */
   sigset_t sa_mask;           /*additional signals to block*/
   int sa_flag;                /*signal options*/

   /*alternate handler*/
   void (*sa_sigaction)(int,siginfo_t *, void *);
};                                    

当使用sigaction改变signo的action时,如果sa_handler指向了一个信号处理函数(SIG_IGN和SIG_DFL除外),sigaction函数会将sa_mask指向的信号集合在这个信号处理函数(sa_handler)被调用前加入到当前进程掩码中,当信号处理函数返回时,进程的信号掩码会恢复为他原来的值。这样,使我们能够在信号处理函数被调用是阻塞一部分信号的到达。一旦我们为一个信号安装了action, 那么对于这个信号这个action将一直处于安装状态,除非我们使用sigaction方法明确的更改可它。

sa_flags:

sigsuspend

等待信号到达的一个整洁而可靠地方式是先阻塞这个信号然后使用sigsuspend。

#include <signal.h>

/*
   将当前进程信号掩码设置为sigmask,
   函数返回后将进程掩码恢复为调用前
   的值, 该函数总是返回-1,并设置
   errno 为 -1
*/
int sigsuspend(const sigset_t* sigmask);

sigsuspend 可用于等待指定信号的到达,他的常用用法如下:

 1 sigset_t mask, oldmask;
 2
 3 …
 4
 5 /* Set up the mask of signals to temporarily block. */
 6 sigemptyset (&mask);
 7 sigaddset (&mask, SIGUSR1);
 8
 9 …
10
11 /* Wait for a signal to arrive. */
12 sigprocmask (SIG_BLOCK, &mask, &oldmask);
13 while (!usr_interrupt)
14   sigsuspend (&oldmask);
15 sigprocmask (SIG_UNBLOCK, &mask, NULL);

通过user_interrupt 判断是否等待的SIGUSR1信号已到达,sigsuspend再返回时将进程信号掩码设置为他被调用前的值,因此我们最后需要将添加mask移除掉。

Signal Names and Numbers


数组sys_siglist可以帮助我们匹配信号与信号名:

1 extern char* sys_siglist[]; 数组索引为信号值, 数组元素值为信号名。

信号与信号名的转换:

 1 #include <signal.h>
 2
 3 /* 如果msg不为空,则向stderr 输出msg紧跟一个冒号加一个空着在加信号描述;如果msg为空则只向stderr输出信号描述*/
 4 void psignal(int signo, const char* msg);
 5
 6 void psiginfo(const siginfo_t info, const char* msg);
 7
 8 /*获取信号描述*/
 9 char* strsignal(int signo);
10
11 void sig2str(int signo, char* str);
12 void str2sig(const char* str, int* signop);

总结

  信号通常用于一些相对复杂的程序, 理解如何及为何处理信号对于UNIX高级编程是必要的。

时间: 2024-10-09 08:19:01

APUE 3 -- 信号 (signal)<II>: 可靠信号的相关文章

Linux信号实践(4) --可靠信号

Sigaction #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); 功能: sigaction函数用于改变进程接收到特定信号后的行为. 参数 第一个参数为信号的值,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误) 第二个参数是指向结构sigaction的指针,在结构 si

APUE学习笔记——10.可靠信号与不可靠信号

首先说明:现在大部分Unix系系统如Linux都已经实现可靠信号. 1~31信号与SIGRTMIN-SIGRTMAX之间并不是可靠信号与不可靠信号的区别,在大多数系统下他们都是可靠信号. 只不过: 1~31信号                              --不支持排队,为普通信号. SIGRTMIN-SIGRTMAX信号 --支持排队,实时信号 不可靠信号 什么是不可靠信号: 不可靠的意思是信号可能丢失或者被错误处理. 在早起系统中,信号存在两大缺陷,导致了信号不可靠. 缺陷一:

apue 第10章 信号signal

每种信号都有名字,都是以SIG开头 信号机制最简单的接口是signal函数 #include <signal.h> typedef void (*sighandler_t)(int); sighandler_t signal(int signum, sighandler_t handler); 返回值:成功以前的信号处理配置,出错SIG_ERR kill函数将信号发送给进程或进程组.raise函数则允许进程向自身发送信号 #include <sys/types.h> #includ

APUE学习笔记——10 信号(一)——信号介绍

信号的基本概念 信号是软件中断,信号提供了解决异步时间的方法. 每一中信号都有一个名字,信号名以SIG开头. 产生信号的几种方式 很多条件可以产生信号: 终端交互:用户按下某一些按键,如ctl+c,会产生信号. 硬件异常:如除数为0,内存引用错误.    kill(2)函数:将信号发送到一个进程或者进程组   kill(1)命令:该命令为kill(2)函数的接口.用于终止失控的后台in成.  检测到某软件条件发生:如网络连接上传来外数据(产生SIGURG信号),闹钟超时(产生SIGALRM信号)

使用 sigaction 函数实现可靠信号

前言 在前文中,讲述了一个可靠信号的示例.它分成几个步骤组成( 请参考前文 ).在 Linux 系统编程中,有个方法可以将这些步骤给集成起来,让我们使用起来更加的方便.那就是调用 sigaction 函数. sigaction 函数 原型:int sigaction (int signo, const struct sigaction * restrict act, struct sigaction *restrict oact) 作用:将信号及其处理函数关联起来,但这个注册函数中,信号处理函数

apue学习笔记(第十章 信号)

本章先对信号机制进行综述,并说明每种信号的一般用法. 信号概念 每个信号都有一个名字,这些名字都以3个字符SIG开头.在头文件<signal.h>中,信号名都被定义为正整形常量. 在某个信号出现时,可以按下列3种方式之一进行处理: 1 忽略该信号.大多数信号都可以使用这种方式进行处理,但有两种信号却决不能被忽略:SIGKILL和SIGSTOP(只能执行系统默认动作). 2 捕获信号.通知内核在某信号发生时,调用一个用户函数对这种时间进行处理. 3 执行系统默认动作.对于大多数信号的系统默认动作

信号 signal sigaction补充

目前linux中的signal()是通过sigation()函数实现的. 由signal()安装的实时信号支持排队,同样不会丢失. 先看signal 和 sigaction 的区别: 关键是 struct sigaction act; 里面有三个部分,除了 signal函数会关注的 sa_handler 之外, 还有 sa_mask,这里面可以提供阻塞功能(类似于sigprocmask) sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask, 

[学习笔记]可靠信号、不可靠信号

不可靠信号PK可靠信号 q  linux信号机制基本上是从unix系统中继承过来的.早期unix系统中的信号机制比较简单和原始,后来在实践中暴露出一些问题,它的主要问题是: q  进程每次处理信号后,就将对信号的响应设置为默认动作.在某些情况下,将导致对信号的错误处理:因此,用户如果不希望这样的操作,那么就要在信号处理函数结尾再一次调用signal(),重新安装该信号. q  早期unix下的不可靠信号主要指的是进程可能对信号做出错误的反应以及信号可能丢失. q  linux支持不可靠信号,但是

Linux信号signal用法详解及注意事项

信号是软件中断,是一种异步通信方式,处理异步的事件.例如我们在终端中运行程序,通过按下键盘"Ctrl+c",可以发出一个SIGINT中断信号去停止程序运行.信号的处理有3种方法:1.  忽略该信号,大多数信号都可以如此处理.但是SIGKILL和SIGSTOP除外,决不能被忽略.2.  捕获信号,用户自定义一个信号处理函数,当信号发生时,就会触发调用该自定义信号函数.信号SIGKILL和SIGSTOP不能被捕获.3.  系统默认动作, 大多数信号的默认动作是终止该进程.有些信号会&quo