信号及信号来源

信号本质

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

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

信号来源

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

信号的种类

类型 内容 说明
不可靠信号 信号值小于SIGRTMIN的,这些信号是从早期的Unix继承来的 问题:
1.每次处理完信号后,就对信号的响应设为默认值,但是该问题Linux对其改进,每次调用完信号处理函数后,不必重新安装
2.信号可能会丢失(不支持排队)
可靠信号 信号值在SIGRTMIN和SIGRTMAX之间的信号 1.可靠信号的发送和注册函数变为sigqueue和sigaction
2.可排队
实时信号和非实时信号 非实时信号都不支持排队,都是不可靠信号
实时信号都支持排队,都是可靠信号

可使用kill -l查看全部信号

进程对信号的响应

名称 含义
忽略信号 即对信号不做任何处理,其中SIGKILL和SIGSTOP不可忽略
捕捉信号 信定义信号处理函数,当信号发生时执行相应的处理函数
缺省操作 执行信号默认的缺省操作,其中实时信号的缺省操作是进程终止

信号集及相关函数

相关概念介绍

名称 含义
未决状态 在信号产生(generation)和递送(delivery)之间(可能相当长)的时间间隔内,该信号处于未决(pending)状态,这种信号称为挂起(suspending)的信号
未决(未处理的)信号队列 内核为每个进程维护一个未决(未处理的)信号队列,信号产生时无论是否被阻塞,首先放入未决队列里。当时间片调度到当前进程时,内核检查未决队列中是否存在信号。若有信号且未被阻塞,则执行相应的操作并从队列中删除该信号;否则仍保留该信号
信号屏蔽字(信号阻塞集) 每个进程都有一个信号屏蔽字(signal mask),规定当前要阻塞递送到该进程的信号集。对于每个可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则该信号当前被阻塞
所谓阻塞并不是禁止传送信号, 而是暂缓信号的传送。若将被阻塞的信号从信号阻塞集中删除,且对应的信号在被阻塞时发生了,进程将会收到相应的信号

信号集

信号集用来描述信号的集合,linux所支持的所有信号可以全部或部分的出现在信号集中,主要与信号阻塞相关函数配合使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
结构定义:
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t

//初始化由set指定的信号集,信号集里面的所有信号被清空
int sigemptyset(sigset_t *set);

//调用该函数后,set指向的信号集中将包含linux支持的64种信号
int sigfillset(sigset_t *set);

//判定信号signum是否在set指向的信号集中
int sigismember(const sigset_t *set, int signum);

//在set指向的信号集中加入signum信号
int sigaddset(sigset_t *set, int signum);

//在set指向的信号集中加入signum信号
int sigdelset(sigset_t *set, int signum);

//以上函数成功返回0,失败返回-1

信号阻塞

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//根据how参数来对信号集进行对应的操作:
//SIG_BLOCK:在进程当前屏蔽信号集中添加set指向信号集中的信号
//SIG_UNBLOCK:如果进程屏蔽信号集中包含set指向信号集中的信号,则解除对该信号的阻塞
//SIG_SETMASK:更新进程屏蔽信号集为set指向的信号集

//若oldset不为NULL,则将旧的信号阻塞集通过该参数返回
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

//获得当前进程的未决信号队列
int sigpending(sigset_t *set);

//挂起线程且暂时使用mask代替当前信号阻塞集,直到信号到达
//1.若信号的处理是终止进程,则该函数不返回
//2.若注册了信号处理函数,则待信号处理函数执行完毕后,该函数再返回
int sigsuspend(const sigset_t *mask);

//以上函数成功返回0,失败返回错误码

信号发送函数

kill

1
2
int kill(pid_t pid, int sig);
功能:给任意进程发送任意信号
名称 说明 备注
参数 pid (1)若pid > 0,则发送信号给pid的进程
(2)若pid = 0,则发送信后给本进程组的所有进程
(3)若pid = -1,则发送信号到调用进程有权发送信号的每个进程,init进程除外
(4)若pid < -1,则发送信号给进程组ID为-pid中的每个进程
若sig=0,则不发送任何信号,但是参数检测仍然进行,这可以用来检查pid参数是否正确(进程是否存在或允许发送信号)
sig 待发送信号
返回值 int 成功返回0,失败返回-1,错误值在errno中

sigqueue

1
2
int sigqueue(pid_t pid, int sig, const union sigval value);
功能:给任意进程发送任意信号,并且可以传递数据
名称 说明 备注
参数 pid 则发送信号给pid的进程 1.该函数是比较新的发送信号系统调用,主要针对实时信号提出的,也支持前32中,常常配合sigaction一起使用
2.该函数和kill类似,但只能发送给一个进程,不能发送给进程组,当sig=0时的行为和kill一致
3.该函数可在发送信号时传递数据给3参数信号处理函数,通过value参数进行,定义如下:
union sigval {
     int sival_int;
     void *sival_ptr;
};
sig 待发送信号
value 随信号一起传递的数据
返回值 int 成功返回0,失败返回-1,错误值在errno中

raise

1
2
int raise(int sig);
功能:给本进程或线程发送任意信号
名称 说明 备注
参数 sig 待发送信号 1.在单线程程序中等价于kill(getpid(), sig);
2.在多线程程序中等价于pthread_kill(pthread_self(), sig);
3.该函数会在信号处理函数执行完成后返回
返回值 int 成功返回0,失败返回非0

alarm

1
2
unsigned int alarm(unsigned int seconds);
功能:在seconds秒后给本进程发送SIGALRM信号
名称 说明 备注
参数 seconds 时间 1.alarm默认处理是终止进程
2.若seconds=0,则任何未决的alarm都会被取消
3.注意:这个函数是无阻塞的
返回值 unsigned int 如果以前没有设置过alarm或者已经超时,那么返回0
如果以前设置过alarm,那就返回剩余的时间,并且重新设定定时器

abort

1
2
void abort(void);
功能:给本进程发送SIGABRT信号
名称 说明 备注
参数 void 1.该函数先解除对SIGABRT信号的屏蔽
2.不论该信号被屏蔽或是注册了信号处理函数,它总会终止进程,该函数通过回复SIGABRT的默认配置然后再次发出该信号来完成此操作。(除非你未从信号处理函数返回(see longjump)
返回值 void

信号处理函数

signal

1
2
3
typdef void (*sighandler_t )( int );
sighandler_t signal ( int signum, sighandler_t handler);
功能:注册简单的信号处理函数
名称 说明 备注
参数 signum 待注册的信号 注意:SIGKILL和SIGSTOP不可注册处理函数
handler 注册的信号处理函数,可填写:
SIG_IGN:忽略该信号
SIG_DFL:系统默认方式处理该信号
返回值 sighandler_t 成功返回上一次安装的信号处理函数(若是第一次注册返回NULL),失败返回SIG_ERR,错误值存在errno中

sigaction

1
2
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能:注册信号处理函数,该函数可具备3个参数且可接受附加参数
名称 说明 备注
参数 signum 待注册的信号 act指定了相关的处理函数,定义如下:
struct sigaction {
     void (*sa_handler)(int);
     void (*sa_sigaction)(int, siginfo_t *, void *);
     sigset_t sa_mask;
     int sa_flags;
     void (*sa_restorer)(void);
};
该结构体中sa_restorer已经过时不用了,
(1)sa_handler还是以前的那种信号处理函数,参数只有信号值
(2)sa_sigaction是3参数信号处理函数,下面介绍
(3)sa_mask指定在信号处理函数中,屏蔽那些信号,缺省情况下是当前信号,防止信号的嵌套
(4)sa_flags,该参数中指定了一些标志位:
SA_NODEFER/SA_NOMASK:指定在信号处理函数中不将本信号加入阻塞集,后者是过时的同义参数
SA_SIGINFO:表示信号附带的参数可以被传递到信号处理函数,使用3参数处理函数中必须将该位置位
其余此处暂不介绍
注意:信号处理函数只能注册其中一个
act 注册的信号处理函数
oldact 返回旧的处理函数,可填写NULL
返回值 int 成功返回0,失败返回-1,错误值在errno中

3参数信号处理函数sa_sigaction

1
2
3
4
5
6
7
8
9
10
11
void (*sa_sigaction)(int, siginfo_t *, void *);

//参数:sig,info,ucontext
//sig是信号值,第三个参数未使用,info参数结构体定义如下:
typedef struct {
int si_signo;
int si_errno;
int si_code;
union sigval si_value;
} siginfo_t;
//这个结构体中的第四个参数,就是sigqueue给信号传递的那个结构体了

可重入函数

为了增强程序的稳定性,在信号处理函数中应使用可重入函数。因为进程在收到信号后,就将跳转到信号处理函数去接着执行。如果信号处理函数中使用了不可重入函数,那么信号处理函数可能会修改原来进程中不应该被修改的数据,这样进程从信号处理函数中返回接着执行时,可能会出现不可预料的后果。不可再入函数在信号处理函数中被视为不安全函数。

所谓可重入函数是指一个可以被多个任务调用的过程,任务在调用时不必担心数据是否会出错

如何编写可重入函数:

  1. 不使用(返回)静态的数据、全局变量(除非用信号量互斥)。
  2. 不调用动态内存分配、释放的函数。
  3. 不调用任何不可重入的函数(如标准I/O函数)

即使信号处理函数使用的都是可重入函数(常见的可重入函数),也要注意进入处理函数时,首先要保存errno的值,结束时,再恢复原值。因为,信号处理过程中,errno值随时可能被改变。