Linux 信号机制(三)

信号机制,远比想象中的复杂。

信号的本质

信号是在软件层次上对中断机制的一个模拟,原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的。 信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号什么时候到达。

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

信号的来源

信号事件的发生有两个来源: 硬件来源(比如按下键盘,或者其他硬件的故障);软件来源,最常用的发送信号的系统函数是 kill, raise, alarmsetitimer 以及 sigqueue函数,软件来源还包括一些非法运算操作。


信号的种类

从两个方面进行分类: (1) 可靠性方面: 可靠信号 与 不可靠信号
(2) 与时间的关系上: 实时信号 与 非实时信号。

可靠信号 与 不可靠信号

“不可靠信号”

Linux 的信号机制 是从Unix系统继承过来的,早起的Unix系统中信号机制十分简单和原始,后来在实践中暴露过一些问题。因此,把那些建立在早期的机制上的信号称为”不可靠信号”,信号值小于 SIGRTMIN(应该是SIGRTMIN = 32, SIGRTMAX = 63)的信号都是不可靠信号。

不可靠信号的主要问题是:

  1. 进程每次处理信号之后,就会将对信号的响应设置为默认动作。 在某些情况下导致对信号的错误处理; 因此,如果用户不希望这样的操作,那么就需要在信号处理函数结尾再次调用 signal(),重新安装该信号处理函数。
  2. 信号可能丢失。

Linux 支持不可靠信号,但是对不可靠信号的机制做了改进: 在调用完信号处理函数之后,不必重新调用该信号的安装函数。 因此 Linux 下的信号的不可靠问题主要是指 ”信号可能丢失“。

“可靠信号”

随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。所以,后来出现的各种Unix版本分别在这方面进行了研究,力图实现”可靠信号”。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。同时,信号的发送和安装也出现了新版本:信号发送函数sigqueue()及信号安装函数sigaction()。POSIX.4对可靠信号机制做了标准化。但是,POSIX只对可靠信号机制应具有的功能以及信号机制的对外接口做了标准化,对信号机制的实现没有作具体的规定。

信号值位于 SIGRTMINSIGRTMAX 之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。 Linux 支持新版本的信号安装函数 sigaction 和 信号发送函数 sigqueue()的同时,也支持早期的 信号安装函数signal() 和信号发送函数 kill()

注:不要有这样的误解:由sigqueue()发送、sigaction安装的信号就是可靠的。事实上,可靠信号是指后来添加的新信号(信号值位于SIGRTMIN及SIGRTMAX之间);不可靠信号是信号值小于SIGRTMIN的信号。信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。

signal() 和 sigaction() 函数的区别

  1. 他们都不能将 SIGRTMIN以前的信号变为可靠信号(都不支持排队,仍然有可能丢失,依然是不可靠信号)。
  2. SIGRTMIN 之后的信号都是可靠信号,都支持排队。
  3. signal() 是通过 sigaction() 来实现的。
  4. 经过 sigaction() 安装的信号 能传递信息给信号处理函数(对所有信号都成立)
  5. 经过 signal() 安装的信号不能像 信号处理函数传递信息。
  6. 对信号发送函数来说是一样的。

是不是他们都必须配套使用呢?

实时信号 与 非实时信号

早期Unix系统只定义了32种信号,Ret hat7.2支持64种信号,编号0-63(SIGRTMIN=31,SIGRTMAX=63),将来可能进一步增加,这需要得到内核的支持。前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。实时信号是POSIX标准的一部分,可用于应用进程。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。

所以从概念上来说,二者是等价的。

三、进程对信号的响应

进程可以通过三种方式来响应一个信号: (1) 忽略信号,即对信号不做任何处理。 其中有两个信号不能被忽略: SIGKILL 和 SIGSTOP。(2) 捕捉信号。 定义信号处理函数,当信号发生的时候,执行相应的信号处理函数。 (3) 执行缺省操作。 Linux对每个信号都规定了默认操作。 注意, 进程对实时信号的默认反应 是 终止进程。

Linux 究竟采用上述哪一种方式来响应信号,取决于给相应的 API 函数的参数。


四、信号的发送

发送信号的函数有: kill()、raise()、sigqueue()、alarm()、setitimer()、以及 abort()。

kill()

#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int signo);

参数pid的值 信号的接收进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程

其中 Signo 是信号的值,当为0的时候是空信号,实际上是不发送任何的信号,但是照常进行错误检查,因此,可以用于检查目标进程是否存在,以及当前进程是否具有向目标进程发送信号的权限。(root权限的进程可以向任何进程发送信号,非root 权限的进程只能向属于 同一个 session 或者 同一个用户的进程发送信号。)

kill() 最经常用于向 pid > 0的进程发送洗好,调用成功返回0,否则返回-1。 对于 pid < 0的情况,各个版本的说法不一样,其实很简单,参阅内核源代码 kernel/signal.c 即可。上述代码就是参考 red hat 7.2

raise()

#include <signal.h>
int raise(int signo)

向进程本身发送信号,参数是为即将发送的信号值。调用成功返回0,调用失败返回-1。

sigqueue()

#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)

调用成功返回0,调用失败返回-1.

sigqueue() 是比较新的发送信号 的系统调用,主要是针对实时信号提出来的(当然也支持前32中),支持信号带有参数,与函数sigaction()配合使用

sigqueue() 第一个参数指向接受信号的进程ID。第二个参数确定即将发送的信号,第三个参数是一个联合数据结构 union sigval,指定了信号传递的参数,即通常所说的4字节值。

typedef union sigval{
    int sival_int;
    void *sigval_ptr;
} sigval_t;

sigqueue()kill() 系统调用多了更加的附加信息,但是 sigqueue() 只能向一个进程发送信号,而不能向一个进程组发送信号。 如果 signo == 0,将会执行错误检查,但是实际上不发送任何信号,0值可以用于检查 pid 的有效性,以及当前进程是否有权限向 目标进程发送信号。

在调用 sigqueue()的时候,sigval_t指定的信息会拷贝到 3参数的信号处理函数(3参数的信号处理函数由 sigaction安装,并且设定了sa_sigaction指针)siginfo_t的结构中,这样信号处理函数就可以处理这些信息了。 由于sigqueue系统调用支持发送带参数信号,所以要比 kill() 系统调用的功能强大很多。

alarm() 系统调用

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

专门为信号 SIGALARM信号而设置。 在指定的时间 seconds 秒后,向进程本身发送SIGALRM信号,所有有称为闹钟信号。进程调用 alarm()后,任何以前设置的 alarm() 系统调用都将无效,如果参数 seconds 为0,那么进程内将不包含任何的闹钟时间。 (相当于一个进程本身只能有一个 alarm 了)

setitimer() 系统调用

#incldue <sys/time.h>
int setitimer(int which, const struct itimerval* value, struct itmerval* ovalue);

setitimer() 功能和 alarm() 类似,但是要更强大: 定时器功能。

  1. 首先 setitimer() 支持三种类型定时器,第一个参数指定定时器类型。
  2. 第二个参数是结构体 struct itimerval的一个实例。
  3. 第三个参数可不用 为 NULL。

定时器类型如下:

 - ITIMER_REAL  也就是墙上时钟。 在经过指定的时间之后,内核将发送信号 `SIGALRM` 给进程。
 - ITIMER_VIRTUAL 程序用户态执行时间。 经过指定的时间之后,内核将发送`SIGALRM`给进程。
 - ITIMER_PROF  程序用户态+内核态执行总时间。 在经过指定的时间之后,内核将发送 信号 `SIGALRM` 给进程。

setitimer() 执行成功之后返回0,执行失败之后返回-1.

abort() 系统调用

#include <stdlib.h>
void abort(void);

向进程发送 SIGABORT信号,默认情况下进程会退出,当然可以定义自己的信号处理函数。即使 SIGABORT被进程设置为 阻塞信号,调用 abort()之后,SIGABORT依然可以被进程接受。函数没有返回值。


五、信号的安装

如果进程要处理某一信号,那么就要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。

linux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()在可靠信号系统调用的基础上实现, 是库函数。它只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。

## signal() 库函数

#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int)
  1. 第一参数是指定信号的值。
  2. 第二个参数指定信号处理函数: void (*handler)(int)。 可以写成 SIG_IGN忽略该信号,也可以写成 SIG_DFL 系统默认处理。
  3. 函数返回值:返回上一个信号处理函数。

如果 signal() 调用成功,返回最后一次为安装该信号 signum而调用 signal() 时候的信号处理函数 handler 的值。 失败则返回 SIG_ERR

sigaction() 系统调用

#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact)

sigaction 函数用于改变进程接收到特定信号后的行为。
1. 第一个参数是 信号的值,可以是除 SIGKILL 以及 SIGSTOP以外的任何一个信号。如果是这两个信号,那么将导致安装错误。
2. 第二个参数是指向 结构体 const struct sigaction 的一个指针,在这个结构体中,指定了对特定信号的处理,可以为空,那么进程会按照缺省方式对该信号进行处理。
3. 第三个参数 oldact 指向的对象用来保存原来对信号的处理,可以指定 oldcat 为NULL。

如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性。

strut sigaction {
    union {
        __sighandler_t _sa_handler;
        void (*_sa_sigaction)(int, struct siginfo*, void *);
    }_u;
    sigset_t sa_mask;
    unsigned long sa_flags;
    void (*sa_restorer)(void);  //sa_restorer 已经过时, POSIX不再支持
}
  1. 首先 _sa_handler*_sa_sigaction 指定 信号处理函数。 二者是一个union结构体,所以只能使用其中之一。 除了可以是用户自定义的信号处理函数,还可以是 SIG_DFL(缺省处理方式) 和 SIG_IGN(忽略信号)。

  2. _sa_handler 指定的处理函数是 void (*sa_handler)(int) 形式,只能够一个函数参数 信号值,所以不能传递除了信号值之外的任何信息。

  3. _sa_sigaction 指定的处理函数是 void (*sa_sigaction)(int, siginfo_t *, void) 此种类型: (1) 第一个参数是信号值。(2)第二个参数是指向 结构体 siginfot_t的指针,包含有信号携带的数据信息。 (3)第三个参数不使用,因为POSIX 没有使用该参数的标准。

  4. 结构体siginfo_t 如下:

siginfo_t {
                  int      si_signo;  /* 信号值,对所有信号有意义*/
                  int      si_errno;  /* errno值,对所有信号有意义*/
                  int      si_code;   /* 信号产生的原因,对所有信号有意义*/
        union{          /* 联合数据结构,不同成员适应不同信号 */  
          //确保分配足够大的存储空间
          int _pad[SI_PAD_SIZE];
          //对SIGKILL有意义的结构
          struct{
              ...
              }...
            ... ...
            ... ...          
          //对SIGILL, SIGFPE, SIGSEGV, SIGBUS有意义的结构
              struct{
              ...
              }...
            ... ...
            }
      }
}

为了便于阅读,在说明问题的时候,经常把该结构表示成下面的形式:

siginfo_t {
int      si_signo;  /* 信号值,对所有信号有意义*/
int      si_errno;  /* errno值,对所有信号有意义*/
int      si_code;   /* 信号产生的原因,对所有信号有意义*/
pid_t    si_pid;    /* 发送信号的进程ID,对kill(2),实时信号以及SIGCHLD有意义 */
uid_t    si_uid;    /* 发送信号进程的真实用户ID,对kill(2),实时信号以及SIGCHLD有意义 */
int      si_status; /* 退出状态,对SIGCHLD有意义*/
clock_t  si_utime;  /* 用户消耗的时间,对SIGCHLD有意义 */
clock_t  si_stime;  /* 内核消耗的时间,对SIGCHLD有意义 */
sigval_t si_value;  /* 信号值,对所有实时有意义,是一个联合数据结构,
                          /*可以为一个整数(由si_int标示,也可以为一个指针,由si_ptr标示)*/

void *   si_addr;   /* 触发fault的内存地址,对SIGILL,SIGFPE,SIGSEGV,SIGBUS 信号有意义*/
int      si_band;   /* 对SIGPOLL信号有意义 */
int      si_fd;     /* 对SIGPOLL信号有意义 */
}

siginfo_t结构体中的 联合数据成员,应该确保该结构适应所有的信号,比如对于实时信号来说,则实际采用下面的结构:

typedef struct {
        int si_signo;
        int si_errno;           
        int si_code;            
        union sigval si_value;  
        } siginfo_t;

其中第四个域union sigval si_value同样为一个联合结构体:

union sigval {
        int sival_int;      
        void *sival_ptr;    
        }

采用联合数据结构,说明siginfo_t结构中的si_value要么持有一个4字节的整数值,要么持有一个指针,这就构成了与信号相关的数据。在信号的处理函数中,包含这样的信号相关数据指针,但没有规定具体如何对这些数据进行操作,操作方法应该由程序开发人员根据具体任务事先约定。

前面在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数就是sigval联合数据结构,当调用sigqueue时,该数据结构中的数据就将拷贝到信号处理函数的第二个参数中。这样,在发送信号同时,就可以让信号传递一些附加信息。信号可以传递信息对程序开发是非常有意义的。

这里写图片描述

  1. sa_mask 指定在信号处理程序执行过程中, 那些信号应当被阻塞。 缺省情况下,当前信号被阻塞,防止信号的嵌套发送, 若 sa_flag 指定 SA_NODEFER 或者 SA_NOMASK 标志位,可可以不让本信号在信号处理程序执行过程中被阻塞。 信号被阻塞由下文可见。

注:请注意sa_mask指定的信号阻塞的前提条件,是在由sigaction()安装信号的处理函数执行过程中由sa_mask指定的信号才被阻塞

  1. sa_flagsa_flags中包含了许多标志位,包括刚刚提到的SA_NODEFERSA_NOMASK标志位。另一个比较重要的标志位是SA_SIGINFO,当设定了该标志位时,表示信号附带的参数可以被传递到信号处理函数中,因此,应该为sigaction结构中的sa_sigaction指定处理函数,而不应该为sa_handler指定信号处理函数,否则,设置该标志变得毫无意义。即使为sa_sigaction指定了信号处理函数,如果不设置SA_SIGINFO,信号处理函数同样不能得到信号传递过来的数据,在信号处理函数中对这些信息的访问都将导致段错误(Segmentation fault)。

注:很多文献在阐述该标志位时都认为,如果设置了该标志位,就必须定义三参数信号处理函数。实际不是这样的,验证方法很简单:自己实现一个单一参数信号处理函数,并在程序中设置该标志位,可以察看程序的运行结果。实际上,可以把该标志位看成信号是否传递参数的开关,如果设置该位,则传递参数;否则,不传递参数(这种看法很好)

信号集(sigset_t) 以及 信号集操作函数

信号集被定义为一种数据类型:

typedef struct {
            unsigned long sig[_NSIG_WORDS];   //数据类型
            } sigset_t

信号集用来描述信号的集合,linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用。下面是为信号集操作定义的相关函数:

#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(const sigset_t *set, int signum);

/*对单个集合添加删除元素的操作*/
//初始化由set指定的信号集,信号集里面的所有信号被清空
sigemptyset(sigset_t *set);
//调用该函数后,set指向的信号集中将包含linux支持的64种信号
sigfillset(sigset_t *set);
//在set指向的信号集中加入signum信号
sigaddset(sigset_t *set, int signum);
//在set指向的信号集中删除signum信号
sigdelset(sigset_t *set, int signum);
//判定信号signum是否在set指向的信号集中
sigismember(const sigset_t *set, int signum);

七、信号阻塞与信号未决:

每个进程都有一个用来描述哪些信号递送到进程时将被阻塞的信号集,该信号集中的所有信号在递送到进程后都将被阻塞。

注意:不能递送,是指不能被检测出来,但是却依旧存在。如果将此信号从信号阻塞集中删除,那么该信号还是可以递送给该进程。

下面是与信号阻塞相关的几个函数:

#include <signal.h>
//用来检测或者更改信号屏蔽字
int  sigprocmask(int  how,  const  sigset_t *set, sigset_t *oldset));
//获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。
int sigpending(sigset_t *set));
int sigsuspend(const sigset_t *mask));

其中:
sigprocmask()函数能够根据参数how来实现对信号集的操作,操作主要有三种:

参数how 进程当前信号集
SIG_BLOCK 在进程当前阻塞信号集中**添加**set指向信号集中的信号
SIG_UNBLOCK 如果进程阻塞信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
SIG_SETMASK 更新进程阻塞信号集为set指向的信号集(赋值)

sigpending(sigset_t *set))获得当前已递送到进程,却被阻塞的所有信号,在set指向的信号集中返回结果。

sigsuspend(const sigset_t *mask))用于在接收到某个信号之前, 临时用mask替换进程的信号掩码, 并暂停进程执行,直到收到信号为止。sigsuspend 返回后将恢复调用之前的信号掩码。信号处理函数完成后,进程将继续执行。该系统调用始终返回-1,并将errno设置为EINTR。

到此为止,总算是稍微理解了信号机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值