常见信号及其产生条件
(1). SIGABRT
abort
产生
(2). SIGALRM
alarm
设置的时间到达
(3). SIGCHLD
子进程状态改变(终止/停止).
这里的停止可以理解为暂停的意思.暂停即为不继续执行,但仍有恢复执行可能(通过继续).
这里的终止可理解为停止的意思.停止了则意味着执行结束(正常或异常).没有恢复执行可能(重新启动不算恢复).
(4). SIGCONT
子进程由暂停变为继续
(5). SIGHUP
控制终端断开时,发此信号给终端所关联的会话首进程(可能是后台进程,也可能为前台进程).
正常下,终端产生的信号,总是传递给关联到此终端的前台进程.
会话首进程终止时,也产生SIGHUP
,但将其送给会话的前台进程组内所有进程.
守护进程不关联终端,也不是前台进程,绝对不可能收到此消息.
但可人为向守护进程发此消息,让其做自定义工作.
(6). SIGINT
终端中断
(7). SIGIO
异步I/O
(8). SIGKILL
用于杀死进程,其响应方式不可更改
(9). SIGPIPE
管道没有进程以读方式打开时,对其执行写产生.SOCK_STREAM
套接字来自对端的RST
已经收到,后续本地对起执行write
时,产生.
(10). SIGQUIT
终端退出(终端上按退出键).
(11). SIGSEGV
无效内存引用.
(12). SIGSTOP
停止一个进程,这里的停止可以理解为暂停的意思.暂停即为不继续执行,但仍有恢复执行可能(通过继续).
(13). SIGSYS
无效系统调用
(14). SIGTERM
让进程有机会在被终止前做好清理工作,系统关机时,init
进程通常给所有进程发SIGTERM
信号(默认是终止进程),隔一段固定时间后,给所有仍在运行的进程发SIGKILL
.
(15). SIGTSTP
终端停止符(终端上按挂起键).
(16). SIGTTIN
后台进程读其控制终端时
(17). SIGTTOU
后台进程写控制终端时
(18). SIGURG
套接字的紧急情况
(19). SIGUSR1
用户自定义
(20). SIGUSR2
用户自定义
exec
exec
运行的进程中,exec
执行前进程的信号处理设置失效.
fork
产生新进程,新进程继承父进程对信号的处理设置.
异步信号安全/可重入函数
进程在执行指令序列中,每个CPU
指令执行完毕时,都有可能被信号中断,转而执行信号处理函数的指令序列.
在信号处理函数指令序列执行中,每个CPU
指令执行完毕时,也可能再次产生新的信号,转而执行对应的信号处理函数的指令序列.
一个信号处理函数执行完毕,会返回被其打断的上一层次指令序列打断处/打断处下一指令接着执行.
如果程序正在执行malloc
的指令序列,这时被信号打断,执行信号的处理,信号处理中也执行了malloc
我们直到malloc
是基于一个进程全局资源来维护的.在上一个malloc
执行到一半时,又执行一个malloc
,会对全局资源的维护造成混乱和错误.所以,称这样的函数malloc
即为不可重入的.对这样的函数一般不再信号处理函数中进行调用,若调用则可能存在上述调用时序下让程序发生混乱的可能.
但也有一些函数,比如执行一些基于函数内本地变量的计算返回结果等,这些函数即使正常执行被打断,在插入的处理函数中再次执行,因为函数并不操作全局数据,每次函数操作的都是栈数据,所以,这样的调用不会有让程序发生混乱和错误的风险.称这样的函数为可重入的/异步线程安全.
一般,在信号处理函数中若要调用函数,我们应调用可重入的函数.来保证我们的信号处理函数在任何执行序列下均不会有让程序混乱,出错的可能.另外一个值得注意的是,一般系统调用出错均设置出错码,而出错码又是一个全局变量.针对此特殊情况,采取的策略一般是:信号处理函数开始时,用本地变量记录此时的出错码.信号处理函数返回时,恢复进入时的出错码.
以下整理了一些可重入的系统调用,可以安全的在信号处理函数中调用这些函数:
信号
信号产生时,内核记录信号类型,目标进程.
若目标进程未阻塞此信号,则设置目标进程的信号标记.目标进程将下接下来中断返回时执行相应的信号处理.
若目标进程阻塞了此信号,则设置目标进程的信号标记,但在解除对其阻塞前,目标进程不会在中断返回时执行相应的信号处理.
对非实时信号,若内核在已经记录了某(信号,进程)信息下,又收到重叠的(信号,进程)消息,则仍然只对此(信号,进程)维持一个记录(不排队).
kill,raise
// pid:
// > 0 进程ID为pid的进程
// = 0 和调用进程同组的所有进程
// -1 所有进程
// < 0 属于进程组|pid|的所有进程
// 不发给系统进程集中进程,需对目标进程有发送权限
// 调用进程是否可向目标进程发送成功,需要权限检查通过
int kill(pid_t pid, int signo);
alarm,pause
// 指定时间后给进程发SIGALRM
unsigned int alarm(unsigned int seconds);
int pause();
信号集
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
// 若真,返回1。若假,返回0。
int sigismember(const sigset_t *set, int signo);
sigprocmask
// SIG_BLOCK 增加集合位
// SIG_UNBLOCK 减少集合位
// SIG_SETMASK 设置集合位
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);
sigpending
// set中获取进程此时所有阻塞信号构成的信号集合
int sigpending(sigset_t *set);
sigaction
int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);
struct sigaction
{
// fun addr/SIG_IGN/SIG_DFL
void (*sa_handler)(int);
// signals to block
sigset_t sa_mask;
// 设置了SA_SIGINFO标志时的处理函数
void (*sa_sigaction)(int, siginfo_t*, void*);
int sa_flags;
};
可靠信号语义:
(1). 信号中断产生,让进程执行信号处理程序期间.此中断信号自动被屏蔽.
(2). sigaction
中,信号处理程序为sa_handler
.信号处理期间sa_mask
与中断信号构成的集合被屏蔽.信号处理结束,恢复信号屏蔽为中断前状态.
(3). 信号处理一旦用sigaction
进行设置,此后一直有效.
(4). 关于信号是否会中断系统调用,即使设置了SA_RESTART
有些系统也仍然会中断.可统一按会中断系统调用处理.
typedef void(*Sigfunc)(int);
Sigfunc signal(int signo, Sigfunc func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
#ifdef SA_INTERRUPT
ct.sa_flags |= SA_INTERRUPT;
#endif
if(sigaction(signo, &act, &oact) < 0)
{
return SIG_ERR;
}
return (oact.sa_handler);
}
sigsetjmp,siglongjmp
// 参数2非0时,首次调sigsetjmp会存储进程信号屏蔽字到env,供后续siglongjmp恢复
int sigsetjmp(sigjmp_buf env, int savemask);
int siglongjmp(sigjmp_buf env, int val);
引入背景:
setjmp,longjmp
也用来实现非局部转移.但longjmp
在信号处理中实现非局部转移时,有的实现在转移后,恢复对信号处理关联信号的阻塞为处理前,有的实现不处理.sigsetjmp, siglongjmp
允许显式指定在env
中记录进程的信号屏蔽字,用于siglongjmp
时恢复
sig_atomic_t
是ISO C
定义的变量类型,特点是写此类型变量不会被中断.volatile
用于高速编译器变量在内存中,每次获取变量都要从内存读其值,而不要将其优化到寄存器.两者一般联合起来使用.
sigsuspend
int sigsuspend(const sigset_t *sigmask);
以原子方式[执行不会被信号中断]执行
(1). 设置进程的信号屏蔽字为sigmask
(2). 进程挂起
进程继续条件:
捕捉到一个信号,且从该信号的信号处理返回,则sigsuspend
返回-1
,进程继续
引入背景:
若以单独语句实现
(1). 设置进程信号屏蔽字为sigmask
(2). 进程挂起
则存在以下可能:在(1)
执行前进程阻塞了信号X
,且(1)
执行时,针对此进程的信号X
已经产生,但由于进程阻塞了它,故未发给进程.
执行(1)
后,如sigmask
中解除了对X
的阻塞.则在执行2
前,信号X
被进程接收,执行其处理.
执行(2)
时,进程阻塞等待被信号中断.但此前处理的X
,此时,不会再引起(2)
中阻塞的中断.
如用sigsuspend.(1),(2)
以原子方式执行.可以保证不会产生上述现象.
信号的应用:用于实现父子进程间的同步
利用sigsuspend
实现阻塞.利用kill
实现解除阻塞.
另一种父子同步是,父亲用wait
阻塞.子进程终止通过发SIGCHLD
给父亲,解除阻塞.(父进程需忽略SIGCHLD
信号处理,如不忽略,此信号默认处理将终止父进程.不用显式处理是因为父进程需用wait
阻塞等待同步.而不是异步信号来时,执行一次wait
).
终端控制信号会发给前台进程组所有进程,如只想让特定信号发给指定进程,则需对前台进程组进行相关信号的阻塞等额外处理.
abort
void abort(void);
发SIGABRT
给调用进程
作业控制信号
(1). SIGCHLD
子进程停止/终止时,给父进程发.
(2). SIGCONT
发给停止的子进程,让其继续.
(3). SIGSTOP
不可被捕捉或忽略
(4). SIGTSTP
(5). SIGTTIN
后台进程组成员读控制终端
(6). SIGTTOU
后台进程组成员写控制终端
strsignal
char* strsignal(int signo);
信号处理范例
#include "apue.h"
static void sig_alrm(int signo)
{
}
// 通过alarm实现sleep--alarm + 处理SIGALRM
unsigned int sleep(unsigned int seconds)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
// 设置好对SIGALRM处理方式
newact.sa_handler = sig_alrm;
sigemptyset(&newact.sa_mask);
newact.sa_flags = 0;
sigaction(SIGALRM, &newact, &oldact);
// 为何要提前阻塞SIGALRM
// 这样可以避免在alarm后,进入阻塞等待SIGALRM打断这一时间段内收到SIGALRM
// 造成后续阻塞无法被此SIGALRM所打断
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
sigprocmask(SIG_BLOCK, &newmask, &oldmask);
alarm(seconds);
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
// 以原子方式[整个执行序列不会被信号打断]
// 设置可接收SIGALRM+进入阻塞等待
sigsuspend(&suspmask);
// 剩余时间获取
unslept = alarm(0);
// 恢复对SIGALRM处理,恢复信号阻塞
sigaction(SIGALRM, &oldact, NULL);
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return(unslept);
}