SIG
开头。如SIGABRT
是终止信号,进程调用abort
函数产生这种信号。void abort(void);
abort()
首先解除了对SIGABRT(6)
信号的阻止和忽略,然后为调用进程发送该信号(就像调用了 raise(SIGABRT)
一样)。 导致进程的非正常终止,除非SIGABRT
信号被捕获,并且信号处理函数没有返回(调用exit
、_exit
、 _Exit
、longjmp
或siglongjmp
使信号处理函数没有返回)。abort()
函数导致进程终止,则关闭和刷新所有打开的文件流。delete
或Ctrl+C
键,通常产生中断信号SIGINT
。0
、无效的内存引用等。因为这些条件通常由硬件检测到,并且通知内核。然后内核为该进程产生适当信号。kill(2)
函数将任意信号发送给另一个进程或进程组:接收信号进程和发送信号进程的所有者必须相同,或发送方必须是超级用户root
kill(1)
命令将信号(默认是SIGTERM
)发送给进程。此命令只是kill
函数的接口,可用此命令终止一个失控的后台程序。SIGURG
(在网络连接上传来带外的数据)、SIGPIPE
(在管道的读进程已终止后,一个进程写此管道)、SIGALRM
(进程设置的定时器已经超时)等忽略此信号
SIGKILL
和SIGSTOP
绝对不能忽略以外,大多数信号都可使用这种方式处理。SIGKILL
和SIGSTOP
不能被忽略的原因是,它们向内核和超级用户提供了使进程终止或停止的可靠方法(可以理解为终止、停止进程的终极方法)。捕捉该信号
SIGKILL
和SIGSTOP
不能被捕捉终止+core
表示在进程当前工作目录的core
文件中复制了该进程内存映像(默认文件名为core
,这种行为即coredump
),大多数Unix系统调试程序(如gdb
)都使用core
文件检查进程终止时的状态。硬件故障
对应于具体定义的硬件故障,需要通过对应操作系统手册查看这些信号对应于哪些错误Core Dump注解
当程序运行的过程中异常终止或崩溃,操作系统会将程序当时的内存状态记录下来,保存在一个文件中,这种行为就叫做Core Dump
(中文有的翻译成“核心转储”)。我们可以认为 core dump
是“内存快照”,但实际上,除了内存信息之外,还有些关键的程序运行状态也会同时 dump
下来,例如寄存器信息(包括程序指针、栈指针等)、内存管理信息、其他处理器和操作系统状态和信息。core dump
对于编程人员诊断和调试程序是非常有帮助的,因为对于有些程序错误是很难重现的,例如指针异常,而 core dump
文件可以再现程序出错时的情景。
如果没有进行core dump
的相关设置,默认是不开启的。可以通过ulimit -c
查看是否开启。如果输出为0
,则没有开启,需要执行ulimit -c unlimited
开启core dump
功能。
不产生core
文件的条件:
ulimit -c
查看core
文件有没有限制大小。如果是0
则说明禁止了core
文件产生id
(即调用setuid
),但当前用户并非该程序文件的所有者id
(即调用setgid
),但当前用户并非该程序文件的组所有者core
文件产生目录的写权限core
文件太大,磁盘空间不足SIGABRT:
abort
函数产生此信号,进程异常终止。SIGIOT:
SIGIOT
与SIGABRT
有相同值#define SIGIOT SIGABRT
SIGALRM:
alarm
、setitimer
函数设置的定时器、间隔时间超时产生此信号。SIGBUS:
SIGCHLD:
SIGCHLD
信号发送给父进程。系统默认会忽略此信号。如果父进程希望得知子进程的这种状态改变,那么通常在信号捕捉函数中调用wait
等函数获取子进程pid
及终止状态。SIGCONT:
SIGEMT:
SIGFPE:
SIGHUP:挂断信号。
session
首进程退出时,该信号被发送到该session
中的前台进程组和后台进程组中的每一个进程stopped
)状态的每一个进程都会收到挂断(SIGHUP
)信号,接着又收到继续(SIGCONT
)信号。SIGHUP
信号的默认处理是终止收到该信号的进程。所以若程序中没有捕捉该信号,当收到该信号时,进程就会退出。SIGILL:
SIGINT:中断信号
delete
或Ctrl+C
),产生此信号发送至前台进程组中的每一个进程。当一个进程失控或者在终端产生大量不需要输出时,常通过此命令终止它。SIGKILL:
SIGPIPE:
SOCK_STREAM
的socket
已不再连接时,进程写入该套接字也产生此信号。SIGPOLL:
SIGIO:
SIGIO
与SIGPOLL
有相同的值,默认行为是终止该进程。#define SIGIO SIGPOLL
SIGPROF:
setitimer
函数设置的梗概统计间隔定时器超时产生此信号。在未来可能将此信号移除SIGPWR:
SIGPWR
信号。通常是接到蓄电池电压过低信息的进程将SIGPWR
发送给init
进程,然后由init
处理停机操作。SIGPWR
的默认动作是终止相关进程。SIGQUIT:退出信号
Ctrl+\
)产生此信号,并发送给前台进程组的所有进程。此信号不仅终止前台进程组,还产生一个core
文件。SIGSEGV:
SIGSTOP:
SIGKILL
,该信号不能被阻塞、忽略和捕获SIGSYS:
SIGTERM:
SIGKILL
,SIGTERM
可以被捕获或忽略,因此允许让程序有机会在退出之前做好清理工作,从而优雅地终止。SIGTRAP:
SIGTSTP:
Ctrl+Z
)时,将该信号发送到前台进程组中的所有进程加粗样式。 SIGTSTP
与SIGSTOP
都是使进程暂停(都使用SIGCONT
让进程重新激活)。唯一的区别是SIGSTOP
不可以捕获和忽略,而SIGTSTP
可以。SIGTTIN:
read
控制终端操作返回出错,不产生此信号,errno
设置为EIO
。SIGTTOU:
errno
设置为EIO
。tcsetattr
、tcsendbreak
、tcdrain
、tcflush
、tcflow
以及tcsetpgrp
也能产生SIGTTOU
信号。SIGURG:
SIGUSR1:
SIGUSR2:
SIGVTALRM:
setitimer
函数设置的虚拟间隔时间超时产生此信号SIGWINCH:
ioctl
得到或设置窗口大小。如果用ioctl
设置窗口大小命令更改了窗口大小,则内核将该信号发送至前台进程组。SIGXCPU:
SIGXFSZ:
设置调用进程收到指定信号时的动作(忽略、使用系统默认动作或捕获)
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum
参数:信号名,如SIGABRT
handler
参数
SIG_IGN
:忽略此信号(不能用于SIGKILL
和SIGSTOP
)#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
SIG_DFL
:系统默认动作#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
SIGKILL
和SIGSTOP
):在信号发生时,调用该函数(该函数有一个int
形参,即为该信号编号)。称这个操作是捕捉该信号,称此函数为信号处理程序或信号捕捉函数。SIG_ERR
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
signal
函数的一些特点
exec
一个程序后,通常所有信号的处理都是忽略或者使用系统默认操作。如果调用exec
前对某个信号忽略,则exec
后仍为忽略;但是如果调用exec
前对某个信号捕获,则exec
后对该信号更改为使用默认操作(因为信号捕捉函数的地址在exec
的新程序中毫无意义)。fork
时,子进程继承父进程的信号处理方式,因为子进程复刻父进程内存映像,因此信号捕捉函数地址在子进程中有效signal
函数的一个缺陷:
signal
返回值知道之前对于指定信号的处理方式)。因此可以使用sigaction
函数确定一个信号的处理方式,而无需改变它实例:给出一个简单的信号处理程序,它捕获了两个用户定义的信号并打印信号编号。下面提及的pause
函数使调用进程在接到一信号前挂起。
#include "apue.h"
static void sig_usr(int); /* one handler for both signals */
int
main(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)//handler参数sig_usr指定了信号处理程序的地址
err_sys("can't catch SIGUSR1");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)//handler参数sig_usr指定了信号处理程序的地址
err_sys("can't catch SIGUSR2");
for ( ; ; )
pause();
}
static void
sig_usr(int signo) /* argument is signal number */
{
if (signo == SIGUSR1)
printf("received SIGUSR1\n");
else if (signo == SIGUSR2)
printf("received SIGUSR2\n");
else
err_dump("received signal %d\n", signo);
}
命令行输出
lh@LH_LINUX:~/桌面/apue.3e/signals$ ./sigusr & (在后台启动进程)
[1] 3381 (作业控制shell打印作业编号和进程ID)
lh@LH_LINUX:~/桌面/apue.3e/signals$ kill -USR1 3381 (向进程发送SIGUSR1)
received SIGUSR1
lh@LH_LINUX:~/桌面/apue.3e/signals$ kill -USR2 3381(向进程发送SIGUSR2)
received SIGUSR2
lh@LH_LINUX:~/桌面/apue.3e/signals$ kill 3381 (向进程发送SIGTERM)
[1]+ 已终止 ./sigusr
kill
命令将信号发送给它。注意:kill
并不指代杀死,只是将一个信号发送给一个进程或进程组。该信号是否终止取决于该信号的类型,以及进程是否安排了捕捉该信号。SIGTERM
信号,而对该信号的系统默认动作是终止,所以当向该进程发送SIGTERM
信号后,该进程就终止。signal
函数的一些特点
注意,exec
一个程序后,通常所有信号的处理都是忽略或者使用系统默认操作。如果调用exec
前对某个信号忽略,则exec
后仍为忽略;但是如果调用exec
前对某个信号捕获,则exec
后对该信号更改为使用默认操作(因为信号捕捉函数的地址在exec
的新程序中毫无意义)。
shell
,当在后台执行一个进程,例如:cc main.c &
shell
自动将后台程序对中断和退出信号的处理方式设置为忽略。于是,当按下中断字符时就不会影响到后台进程。如果没有做这样的处理,那么当按下中断字符时,它不但终止前台进程,也终止所有后台进程。当fork
时,子进程继承父进程的信号处理方式,因为子进程复刻父进程内存映像,因此信号捕捉函数地址在子进程中有效
signal函数的一个缺陷:
SIGINT
和SIGQUIT
为例,很多捕捉这两个信号的交互程序有下列形式的代码:void sig_int(int),sig_quit(int)
if(signal(SIGINT,SIG_IGN) != SIG_IGN)
signal(SIGINT,sig_int);
if(signal(SIGQUIT,SIG_IGN) != SIG_IGN)
signal(SIGINT,sig_quit);
这样处理后,仅当SIGINT
和SIGQUIT
当前未被忽略时,进程才会捕捉它们。signal
返回值知道之前对于指定信号的处理方式)。因此可以使用sigaction
函数确定一个信号的处理方式,而无需改变它。pause
函数(它使进程挂起直到收到一个信号)和wait函数errno
设置为EINTR
(系统调用被中断)。可以理解为一个信号发生了,进程捕捉到它,这意味着已经发生了某种事情,所以是个好机会应当唤醒阻塞的系统调用。read
/write
部分数据量的相应系统调用,此时被信号中断,有两种方式(例如read
系统调用已经接收并传送数据至应用程序缓冲区,但尚未接收到应用程序请求的全部数据):
errno
设置为EINTR
ioctl
、read
、readv
、write
、writev
、wait
和waitpid
sleep
这样的函数:调用sleep
函数的线程休眠seconds
秒。如果中间有一个未被忽略的信号到达则终止休眠。malloc
,在其堆中分配另外的存储空间,而此时由于捕捉到信号而插入执行该信号处理程序,其中又调用malloc
会发生什么?
malloc
通常为它所分配的存储区维护一个链表,而插入执行信号处理程序时进程可能正在更改此链表。getpwnam
这种将其结果存放在静态存储单元中的函数,其间插入执行信号处理程序,它又调用这样的函数(如信号处理函数内部又调用getpwnam
)会发生什么?
在信号处理程序中保证调用安全的函数,这些函数是可重入的并被称为是异步信号安全的(即为在函数A执行期间中断执行信号处理程序,在信号处理程序中可能再次调用函数A,但是不会造成问题)。这种函数除了可重入外(信号处理程序中再次调用被信号中断的函数),在执行信号处理操作期间会阻塞任何会引起不一致的信号发送。
没有列入上表的函数大多是不可重入的,不可重入函数通常有以下特点:
malloc
或free
I/O
函数。标准I/O
库的很多实现都不以可重入方式而是使用全局数据结构并且要注意,对于errno
,因为信号处理程序可能会修改errno
原先值,因此应当在调用前保存errno
,在调用后恢复errno
。
如果应用程序要做更新全局数据结构这样的事情,而同时要捕捉某些信号,而这些信号的处理程序又会引起执行siglongjmp
,则在更新这种数据结构时要阻塞此类信号。(因为可能导致这些全局数据结构是部分更新的)
综上所述,在信号处理函数中调用一个非可重入函数,其结果不可预知。因此在信号处理函数中不能调用非可重入函数。
SIGCLD
等同于SIGCHLD
。在Linux平台的源码中有如下定义。#define SIGCLD SIGCHLD
generation
0
)、软件条件(如alarm
定时器超时)、终端产生的信号或调用kill
函数。delivery
pending
每个进程都有信号屏蔽字(signal mask),它规定当前要阻塞递送到该进程的信号集。对于每一种可能的信号,该屏蔽字中都有一位与之对应。如果该位已设置,则它对应的信号是被阻塞的。
注意,在信号处理函数被调用时,操作系统建立的新信号屏蔽字包含正被递送的信号(即触发本次捕获的信号),信号处理函数返回时再恢复信号屏蔽字。因此保证在处理一个给定信号时,如果该信号再次发生,那么它将被阻塞到前一个信号的处理结束为止。
kill
函数将信号发送给进程或进程组,raise
函数向进程自身发送信号。调用raise(sig)
等价于调用kill(getpid(),sig)
int kill(pid_t pid, int sig);
int raise(int sig);
kill
的pid
参数
pid > 0
:将信号发送给指定进程pid == 0
: 将信号发送给与发送进程属于同一进程组的所有进程,并且发送进程具有权限向这些进程发送信号pid < 0
: 将信号发送给进程组ID
等于pid
绝对值的所有进程,并且发送进程具有权限向这些进程发送信号pid == -1
: 将信号发送给有权限向它们发送的所有进程。除了进程1
(init)sig==0
,则说明是空信号,kill
仍然执行正常的错误检查但是不发送信号。常被用来确定一个特定进程是否存在。如果向一个不存在的进程发送空信号,kill
函数返回-1
。ID
或有效用户ID
等于接受者的实际用户ID
或有效用户ID
。SIGCONT
信号,则进程可以将它发送给属于同一会话的任一进程kill
为调用进程产生信号,并且如果该信号是不被阻塞的,那么在kill
函数返回之前,该信号或者其他某个未决、未阻塞信号就被递送给了该进程。alarm
函数设置一个定时器(秒数),将来某个时刻定时器超时产生SIGALRM
信号。如果忽略或不捕捉该信号,默认动作是终止该进程 unsigned int alarm(unsigned int seconds);
alarm
会覆盖之前的alarm
。即如果在调用alarm
时上一次为该进程注册的alarm
还没有超时,则该闹钟时间的余留值用作本次调用的返回值,并且以前注册的闹钟时间被新值替代。SIGALRM
,必须在alarm
调用前安装信号捕获程序。pause
函数是一个慢速系统调用,使调用进程挂起直到捕捉到一个信号int pause(void);
pause
才返回。此时pause
返回-1
,errno
设置为EINTR
longjmp
函数一定要小心,因为如果该信号中断了其他信号处理函数,那么longjmp
将会提早终止这些信号处理函数。SIGALRM
中断了某个其他信号处理程序,调用longjmp
是否会提早终止该信号处理程序。#include "apue.h"
unsigned int sleep2(unsigned int);
static void sig_int(int);
int
main(void)
{
unsigned int unslept;
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
unslept = sleep2(5);
printf("sleep2 returned: %u\n", unslept);
exit(0);
}
static void
sig_int(int signo)
{
int i, j;
volatile int k;
/*
* Tune these loops to run for more than 5 seconds
* on whatever system this test program is run.
*/
printf("\nsig_int starting\n");
for (i = 0; i < 300000; i++)
for (j = 0; j < 4000; j++)
k += i * j;
printf("sig_int finished\n");
}
下面是sleep2
函数的实现#include
#include
#include
static jmp_buf env_alrm;
static void
sig_alrm(int signo)
{
longjmp(env_alrm, 1);
}
unsigned int
sleep2(unsigned int seconds)
{
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
return(seconds);
/*
如果不使用setjmp(),则alarm()和pause()之间有一个竞争条件。
在一个繁忙的系统中,可能alarm()在调用pause()之前超时,并调用了信号处理程序。
如果发生了这种情况,则在调用pause()后,如果没有捕捉到其他信号,调用者将永远被挂起。
使用setjmp()可以解决这个问题,即使pause()从未执行,在发生SIGALRM时,sleep2函数也返回
*/
if (setjmp(env_alrm) == 0) {
alarm(seconds); /* start the timer */
pause(); /* next caught signal wakes us up */
}
return(alarm(0)); /* turn off timer, return unslept time */
}
命令行输出lh@LH_LINUX:~/桌面/apue.3e/signals$ ./tsleep2
^C (键入中断字符)
sig_int starting
sig_int finished
sleep2 returned: 2
sig_int()
提前终止,因为他的运行时间>5s,所以语句printf("sig_int finished\n");
并未执行就退出了。sigset_t
以包含一个信号集,该数据类型能够表示多个信号的集合,该数据类型会被sigprocmask
等函数使用。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);
sigemptyset
:
set
指向的信号集,清除其中所有信号sigfillset
:
set
指向的信号集,使其置位所有信号sigemptyset
或者sigfillset
sigaddset
:
set
指向的信号集中添加指定信号signum
sigdelset
:
set
指向的信号集中删除指定信号signum
sigismember
:
set
信号集中是否有信号signum
#include
#include
#define sigemptyset(ptr) (*(ptr) = 0)
/*sigfillset返回值必须为0,使用C语言的逗号算符,它将逗号算符后的值作为表达式的值返回。*/
#define sigfillset(ptr) (*(ptr) = ~(sigset_t )0,0)
/*
* usually defines NSIG to include signal number 0.
*/
#define SIGBAD(signo) ((signo) <= 0 || (signo) >= NSIG)
int
sigaddset(sigset_t *set, int signo)
{
if (SIGBAD(signo)) {
errno = EINVAL;
return(-1);
}
*set |= 1 << (signo - 1); /* turn bit on */
return(0);
}
int
sigdelset(sigset_t *set, int signo)
{
if (SIGBAD(signo)) {
errno = EINVAL;
return(-1);
}
*set &= ~(1 << (signo - 1)); /* turn bit off */
return(0);
}
int
sigismember(const sigset_t *set, int signo)
{
if (SIGBAD(signo)) {
errno = EINVAL;
return(-1);
}
return((*set & (1 << (signo - 1))) != 0);
}
sigprocmask
函数检测、更改进程的信号屏蔽字int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
若oldset
非空,那么进程之前的信号屏蔽字通过oldset
返回。
若set
非空,则how
指示如何根据set
修改当前信号屏蔽字
how
参数:
SIG_BLOCK
:阻塞信号,即之前的信号集和set
做按位或操作,即并集。set
包含了希望被阻塞的信号SIG_UNBLOCK
:解除信号阻塞,即和set
的补集求交集。set
包含了希望被解除阻塞的信号SIG_SETMASK
:赋值信号屏蔽字在调用该函数后如果有任何未决的、不再阻塞的信号,则在函数返回之前,至少将其中之一递送给该进程。
sigpending
函数返回一个信号集,以指示当前处于未决状态的信号(即已经产生但是由于被阻塞而不能递送的信号)int sigpending(sigset_t *set);
#include "apue.h"
static void sig_quit(int);
int
main(void)
{
sigset_t newmask, oldmask, pendmask;
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
err_sys("can't catch SIGQUIT");
/*
* Block SIGQUIT and save current signal mask.
*/
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
sleep(5); /* SIGQUIT here will remain pending */
if (sigpending(&pendmask) < 0)
err_sys("sigpending error");
if (sigismember(&pendmask, SIGQUIT))
printf("\nSIGQUIT pending\n");
/*
* Restore signal mask which unblocks SIGQUIT.
*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
printf("SIGQUIT unblocked\n");
sleep(5); /* SIGQUIT here will terminate with core file */
exit(0);
}
static void
sig_quit(int signo)
{
printf("caught SIGQUIT\n");
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
err_sys("can't reset SIGQUIT");
}
命令行输出:lh@LH_LINUX:~/桌面/apue.3e/signals$ ./critical
^\ (终端输入退出字符Ctrl+\,产生信号一次)
SIGQUIT pending (从sleep返回后)
caught SIGQUIT (在信号处理程序中)
SIGQUIT unblocked (从sigprocmask返回后)
^\Quit(coredump) (再次产生信号)
lh@LH_LINUX:~/桌面/apue.3e/signals$ ./critical
^\^\^\^\^\^\^\^\ (终端多次输入退出字符Ctrl+\,产生信号8次)
SIGQUIT pending
caught SIGQUIT (只产生信号一次)
SIGQUIT unblocked
^\Quit(coredump) (再次产生信号)
SIGQUIT
信号,保存了当前信号屏蔽字(以便以后恢复),然后休眠5秒。在此期间所产生的退出信号SIGQUIT
都被阻塞,不递送至该进程,直到该信号不再被阻塞。SIGQUIT pending
),然后通过SIG_SETMASK
方法将SIGQUIT
设置为不再阻塞。sigprocmask
返回之前,它被递送到调用进程。即:先打印caught SIGQUIT
,再打印SIGQUIT unblocked
shell
发现子进程异常终止时输出QUIT(coredump)
信息。注意,第二次运行该程序时,在进程休眠期间使SIGQUIT
信号产生了10次,但是解除了对信号的阻塞后,只向进程传送一次SIGQUIT
。从中可以看出在此系统上没有将信号进行排队。检查、修改指定信号的处理动作。此函数用于取代UNIX早期版本使用的signal
函数。
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
若act
指针非空,则表示要修改其动作。如果oldact
非空,则通过该参数返回指定信号的上一个动作。
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_handler
:信号捕捉函数地址,可以是 SIG_DFL
表示默认动作,SIG_IGN
表示忽略此信号sa_mask
:
sa_flags
:
SA_INTERRUPT
:由此信号中断的系统调用不自动重启动SA_RESTART
:由此信号中断的系统调用自动重启动SA_NOCLDSTOP
:如果 signo
是 SIGCHLD
,则在子进程停止时不产生此信号;在子进程终止时仍产生此信号SA_NOCLDWAIT
:如果 signo
是 SIGCHLD
,则调用进程的子进程终止时不会变成僵尸进程(不会发出SIGCHLD
信号)SA_NODEFER
:当捕捉到此信号时,在执行其信号捕捉函数时,系统不自动阻塞此信号(除非sa_mask
中包含此信号)。SA_RESETHAND
:在进入信号处理程序时,将该信号处理方式设置为SIG_DFL
,并清除SA_SIGINFO
标志SA_SIGINFO
:此选项对信号处理程序提供了附加信息,即使用sa_sigaction
信号处理程序而不是sa_handler
sa_sigaction
:
SA_SIGINFO
标志时,使用该信号处理程序。由于sa_handler
和sa_sigaction
的实现可能共用同一存储区,因此这两个字段只能有一个。void handler(int signo);
SA_SIGINFO
标志,则信号处理函数是以下形式void handler(int signo, siginfo_t *info, void *context);
siginfo_t
结构体包含了信号产生原因的有关信息,这样在信号处理函数中我们就可以通过第二个参数知道更多具体与该信号相关的信息。其应该至少包含以下字段 siginfo_t {
int si_signo; /* Signal number */
int si_errno; /* An errno value */
int si_code; /* 发出信号具体原因 */
pid_t si_pid; /* Sending process ID */
uid_t si_uid; /* Real user ID of sending process */
int si_status; /* Exit value or signal */
sigval_t si_value; /* Signal value */
void *si_addr; /* Memory location which caused fault */
...
}
si_code
即为信号发生具体原因:context
可以被强制类型转换为ucontext_t
类型,该结构体标识信号传递时进程上下文,该ucontext_t
结构至少包含以下字段typedef struct ucontext_t
{
struct ucontext_t *uc_link;
stack_t uc_stack;
mcontext_t uc_mcontext;
sigset_t uc_sigmask;
...
} ucontext_t;
uc_link
:为当前context
执行结束之后要执行的下一个context
,若uc_link
为空,执行完当前context
之后退出程序。uc_sigmask
: 执行当前上下文过程中需要阻塞的信号列表,即信号屏蔽字uc_stack
: 为当前context
运行的栈信息。uc_mcontext
: 保存具体的程序执行上下文,如PC
值,堆栈指针以及寄存器值等信息。它的实现依赖于底层,是平台硬件相关的。此实现不透明实例:用sigaction
实现signal
函数(这是POSIX所希望的,有些子系统支持老的不可靠信号语义signal
函数,其目的是实现二进制向后兼容)
#include "apue.h"
/*Reliable version of signal(),using POSIX sigaction().*/
sigfunc*
signal(int signo,sigfunc* func)
{
struct sigaction act,oact;
act.sa_handler = func; //设置信号捕捉函数地址
sigemptyset(&act.sa_mask); //初始化act结构体的sa_mask成员
act.sa_flags = 0;
/*不希望重启动由SIGALRM信号中断的系统调用。原因:希望对I/O操作可以设置时间限制*/
if(signo == SIGALRM){
#define SA_INTERRUPT /*提高可移植性,Linux定义了这个标志*/
act.flags |= SA_INTERRUPT;//由此信号中断的系统调用不自动重启动
#endif
}else{
act.flags |= SA_RESTART; //除了SIGALRM信号,被这些信号中断的系统调用都能自动重启动。
}
/*act指针非空,表示要修改其动作。oldact非空,通过该参数返回指定信号的上一个动作。*/
if(sigaction(signo,&act,&oact)<0)
return(SIG_ERR);
return(oact.sa_handler)//返回之前的信号处理程序的地址
}
sigaction
设置的,那么其默认方式是不重新启动系统调用。除非说明了SA_RESTART
标志,否则sigaction
函数不再重启被中断的系统调用。系统在进入信号处理程序时,会将该信号自动加入到信号屏蔽字中,这阻止了后来产生的这种信号中断该信号处理程序,然后再信号处理程序返回时恢复信号屏蔽字。
但是如果在信号处理函数中使用longjmp
非局部转移到setjmp
处,会导致信号屏蔽字无法恢复。解决方案是调用sigsetjmp
和siglongjmp
而不是使用setjmp
和longjmp
。
int sigsetjmp(sigjmp_buf env, int savesigs);
void siglongjmp(sigjmp_buf env, int val);
setjmp
和longjmp
的唯一区别是sigsetjmp
增加了参数savesigs
。如果该参数非0
,则在env
参数中保存进程的当前信号屏蔽字,此时在信号处理函数中调用siglongjmp
进行非局部跳转到sigsetjmp
,会导致恢复保存的信号屏蔽字。实例:演示在信号处理程序被调用时,系统设置的信号屏蔽字如何自动地包括刚被捕捉到的信号。也展示了使用sigsetjmp
和siglongjmp
函数的方法
#include "apue.h"
#include
#include
static void sig_usr1(int);
static void sig_alrm(int);
static sigjmp_buf jmpbuf;
/*注意这里使用到了原子变量*/
static volatile sig_atomic_t canjump;
int
main(void)
{
if (signal(SIGUSR1, sig_usr1) == SIG_ERR)
err_sys("signal(SIGUSR1) error");
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
err_sys("signal(SIGALRM) error");
/*注意:该函数打印了屏蔽字*/
pr_mask("starting main: "); /* {Prog prmask} */
if (sigsetjmp(jmpbuf, 1)) {
pr_mask("ending main: ");
exit(0);
}
/*防止在jmpbuf(跳转缓冲)尚未由sigsetjmp初始化时调用信号处理程序。*/
canjump = 1; /* now sigsetjmp() is OK */
for ( ; ; )
pause();
}
static void
sig_usr1(int signo)
{
time_t starttime;
if (canjump == 0)
return; /* unexpected signal, ignore */
pr_mask("starting sig_usr1: ");
alarm(3); /* SIGALRM in 3 seconds */
starttime = time(NULL);
for ( ; ; ) /* busy wait for 5 seconds */
if (time(NULL) > starttime + 5)
break;
pr_mask("finishing sig_usr1: ");
canjump = 0;
siglongjmp(jmpbuf, 1); /* jump back to main, don't return */
}
static void
sig_alrm(int signo)
{
pr_mask("in sig_alrm: ");
}
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/signals$ ./mask & (在后台启动进程)
[1] 9971
lh@LH_LINUX:~/桌面/apue.3e/signals$ starting main: (作业控制shell打印其进程ID)
^C
lh@LH_LINUX:~/桌面/apue.3e/signals$ kill -USR1 9971 (向该进程发送SIGUSR1)
starting sig_usr1: SIGUSR1
lh@LH_LINUX:~/桌面/apue.3e/signals$ in sig_alrm: SIGUSR1 SIGALRM
finishing sig_usr1: SIGUSR1
ending main:
(输入回车)
[1]+ 已完成 ./mask
程序中使用到了sig_atomic_t
类型
signal
)的时候,有时对于一些变量的访问希望不会被中断,无论是硬件中断还是软件中断,这就要求访问或改变这些变量需要在计算机的一条指令内完成。通常情况下,int
类型的变量通常是原子访问的,也可以认为 sig_atomic_t
就是int
类型的数据,因为对这些变量要求一条指令完成,所以sig_atomic_t
不可能是结构体,只会是数字类型。sig_atomic_t
类型总是用volatile
修饰,因为该变量总是由两个不同的控制线程-main
函数和异步执行的信号处理程序访问,因此必须保证每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据,如果没有volatile
关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,导致将出现不一致的现象。volatile sig_atomic_t flag;
main()
),中间部分(sig_usr1
)和右面部分(sig_alrm
)SIGUSR1
SIGUSR1|SIGALRM
从命令行输出可以看到:当调用一个信号处理程序时,被捕捉到的信号加到进程的当前信号屏蔽字中。当从信号处理程序返回时,恢复原来的屏蔽字。另外,siglongjmp
恢复了由sigsetjmp
所保存的信号屏蔽字。如果使用longjmp
和setjmp
则不会恢复。
考虑sigprocmask
函数中提出的一点:如果在调用该函数后如果有任何未决的、不再阻塞的信号,则在函数返回之前,至少将其中之一递送给该进程。
那么如果针对下面代码则会出现问题:
sigprocmask(SIG_SETMASK,&oldmask,NULL);
pause();
sigprocmask
解除了某个信号阻塞,而在此期间的确该信号被阻塞了,由上所述,那么就好像该信号发生在sigprocmask
和pause
函数之间。(或者在sigprocmask
和pause
函数之间的确有某个未阻塞的信号被递送了),那么将会导致pause
函数一直阻塞下去,即sigprocmask
和pause
函数之间的这个时间窗口中的信号丢失了。针对此问题,需要在一个原子操作中解除信号阻塞并使进程休眠。因此可以使用sigsuspend
函数,该函数在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。
int sigsuspend(const sigset_t *mask);
mask
,在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号并且从该信号处理程序返回,则sigsuspend
返回,并且恢复信号屏蔽字为sigsuspend
之前的值(并且返回-1
,errno
设为EINTR
)。实例:保护代码临界区,使其不被特定信号中断的正确方法。
#include "apue.h"
static void sig_int(int);
int
main(void)
{
sigset_t newmask, oldmask, waitmask;
/*起初无信号被屏蔽,预期输出:program start:*/
pr_mask("program start: ");
/*注册sig_int信号处理函数*/
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
/*waitmask信号集中包含SIGUSR1信号*/
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1);
/*newmask信号集中包含SIGINT信号*/
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/*
* Block SIGINT and save current signal mask.
*/
/*阻塞newmask信号集,即阻塞SIGINT,并将老的信号屏蔽字保存在oldmask中*/
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/*
* Critical region of code.
*/
/*打印临界区的信号屏蔽字,预期输出:in critical region:SIGINT*/
pr_mask("in critical region: ");
/*
* Pause, allowing all signals except SIGUSR1.
*/
/*将进程信号屏蔽字设置为SIGUSR1,并挂起进程*/
if (sigsuspend(&waitmask) != -1)
err_sys("sigsuspend error");
/*通过按下Ctrl+c按键,将进入sig_int信号处理函数,预期输出:
in sig_int: SIGINT SIGUSR1 (进入信号处理程序自动阻塞当前信号)
*/
/*从sigsuspend返回之后,进程的信号屏蔽字设置为sigsuspend之前的值。预期输出after return from sigsuspend: SIGINT
*/
pr_mask("after return from sigsuspend: ");
/*
* Reset signal mask which unblocks SIGINT.
*/
/*将信号屏蔽字设置为最开始的状态*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/*
* And continue processing ...
*/
/*预期输出:program exit:*/
pr_mask("program exit: ");
exit(0);
}
static void
sig_int(int signo)
{
pr_mask("\nin sig_int: ");
}
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/signals$ ./suspend1
program start:
in critical region: SIGINT
^C (键入中断字符)
in sig_int: SIGINT SIGUSR1
after return from sigsuspend: SIGINT
program exit:
实例:等待信号处理程序设置一个全局变量,该程序用于捕捉中断信号和退出信号,希望仅当捕捉到退出信号时,才唤醒主例程。
#include "apue.h"
volatile sig_atomic_t quitflag; /* set nonzero by signal handler */
static void
sig_int(int signo) /* one signal handler for SIGINT and SIGQUIT */
{
if (signo == SIGINT)
printf("\ninterrupt\n");
else if (signo == SIGQUIT)
quitflag = 1; /* set flag for main loop */
}
int
main(void)
{
sigset_t newmask, oldmask, zeromask;
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
if (signal(SIGQUIT, sig_int) == SIG_ERR)
err_sys("signal(SIGQUIT) error");
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
/*
* Block SIGQUIT and save current signal mask.
*/
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
while (quitflag == 0)
sigsuspend(&zeromask);
/*
* SIGQUIT has been caught and is now blocked; do whatever.
*/
quitflag = 0;
/*
* Reset signal mask which unblocks SIGQUIT.
*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
exit(0);
}
命令行输出:
lh@LH_LINUX:~/桌面/apue.3e/signals$ ./suspend2
^C
interrupt
^C
interrupt
^C
interrupt
^C
interrupt
^\lh@LH_LINUX:~/桌面/apue.3e/signals$
void abort(void);
SIGABRT
信号发送给调用进程raise(SIGABRT)
,不应忽略此信号。
abort
仍不会返回到其调用者。如果捕捉到此信号,那么信号处理程序不能返回的唯一方法是调用exit
、_exit
、_Exit
、longjmp
或siglongjmp
。即abort
导致进程的非正常终止,除非SIGABRT
信号被捕获,并且信号处理函数没有返回(使用了longjmp
等函数使信号处理函数没有返回)。abort
不理会进程对此信号的阻塞或忽略。abort
调用信号处理程序的意图:abort
终止该进程。abort
要终止进程,则它对所有打开标准I/O
流的效果应当与进程终止前对每个流调用fclose
相同。abort
函数的实现#include
#include
#include
#include
void
abort(void) /* POSIX-style abort() function */
{
sigset_t mask;
struct sigaction action;
/* Caller can't ignore SIGABRT, if so reset to default */
sigaction(SIGABRT, NULL, &action);
/*查看是否执行默认动作,若是则冲洗所有的标准I/O实现,否则让他定义成默认动作*/
if (action.sa_handler == SIG_IGN) {
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL);
}
if (action.sa_handler == SIG_DFL)
fflush(NULL); /* flush all open stdio streams */
/* Caller can't block SIGABRT; make sure it's unblocked */
sigfillset(&mask);
sigdelset(&mask, SIGABRT); /* mask has only SIGABRT turned off */
sigprocmask(SIG_SETMASK, &mask, NULL);
kill(getpid(), SIGABRT); /* send the signal */
/* If we're here, process caught SIGABRT and returned */
fflush(NULL); /* flush all open stdio streams */
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL); /* reset to default */
sigprocmask(SIG_SETMASK, &mask, NULL); /* just in case ... */
kill(getpid(), SIGABRT); /* and one more time */
exit(1); /* this should never be executed ... */
}
system
函数阻塞SIGCHLD
:system
函数时,应当阻塞对父进程递送SIGCHLD
信号。否则,当system
创建的子进程结束时,system
的调用者可能错误的认为它自己的一个子进程结束了,然后在SIGCHLD
信号处理程序中通过wait
函数获取子进程终止状态。由于该子进程终止状态已被获取过了,因此就阻止了system
函数获取子进程的终止状态并将其作为返回值。system
函数忽略SIGINT
和SIGQUIT
:Ctrl+C
会将SIGINT
发送给前台进程组、在终端键入Ctrl+\
会将SIGQUIT
发送给前台进程组。但是在system
期间这两个信号应该只发送给正在运行的程序:即system
函数中创建的子进程。因为由system
执行的命令可能是交互式命令(如ed
编辑器),以及system
函数的调用者在system
执行期间放弃了控制,等待该命令程序执行结束,所以system
调用者就不应该接收这两个终端产生的信号。这也是为何规定system
的调用者在等待命令完成时应当忽略这两个信号。sleep
函数使调用进程被挂起直到满足以下条件:
该函数返回未休眠剩余的秒数
unsigned int sleep(unsigned int seconds);
注意,sleep
可以由alarm
函数实现,但是可能造成sleep
和alarm
函数互相影响。比如先alarm(10)
,然后再sleep(3)
,那么对SIGALRM
信号的产生情况造成影响。
因此Linux
使用nanosleep
实现sleep
。因为nanosleep
不涉及产生任何信号,即与闹钟定时器相互独立,所以该实现的sleep
函数不会与其他时间相关函数如alarm
产生交互影响。
int nanosleep(const struct timespec *req, struct timespec *rem);
struct timespec {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds [0 .. 999999999] */
};
sleep
类似,但是提供纳秒级别精度。req
参数指定进程休眠时间,rem
函数返回未休眠的剩余时间。nanosleep
函数并不涉及产生任何信号,所以不用担心与其他函数的交互。int clock_nanosleep(clockid_t clock_id, int flags,nconst struct timespec *request, struct timespec *remain);
clock_id
:延迟时间基于的时钟
CLOCK_REALTIME
:系统实时时间,即从1970年开始的时间CLOCK_MONOTONIC
:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响CLOCK_PROCESS_CPUTIME_ID
:本进程到当前代码的CPU时间CLOCK_THREAD_CPUTIME_ID
:本线程到当前代码的CPU时间flags
:控制时间是绝对还是相对的
0
:相对的,即希望休眠的时间长度TIMER_ABSTIME
:绝对的,即希望休眠到时钟到达某个特定的时间request
和remain
:和nanosleep
一致。使用绝对时间时,remain
参数无用clock_nanosleep(CLOCK_REALTIME,0,req,rem)
相当于nanosleep(req,rem)
sigaction
函数安装信号处理程序时指定SA_SIGINFO
标志。如果没有此标志,信号会阻塞延迟,但是是否进入队列取决于具体实现sigaction
结构的sa_sigaction
成员中(不是sa_handler
)提供信号处理程序。实现可能允许用户使用sa_handler
字段,但是不能获取sigqueue
函数发出的额外信息sigqueue
发出信号int sigqueue(pid_t pid, int sig, const union sigval value);
SIGCHLD
:子进程停止或终止SIGCONT
:如果进程已停止,则使其继续运行SIGSTOP
:停止信号(不能被捕捉或忽略)SIGTSTP
:交互式停止信号SIGTTIN
:后台进程组成员读控制终端SIGTTOU
:后台进程组成员写控制终端SIGTSTP
、SIGSTOP
、SIGTTIN
、SIGTTOU
),对进程的任一未决SIGCONT
信号丢弃SIGCONT
信号时,对同一进程的任一未决停止信号被丢弃。sys_siglist
获取信号编号和信号名间的映射extern const char *const sys_siglist[_NSIG];//其中数组下标即为信号编号
psignal
函数打印与信号编号对应的字符串void psignal(int sig, const char *msg);
perror
,通常是在stderr
打印出msg
参数,后面跟一个冒号一个空格,然后打印出该信号的说明。sigaction
信号处理程序中有siginfo_t
结构,也可以通过siginfo_t
结构,使用psiginfo
函数打印出信号编号更多的信息void psiginfo(const siginfo_t *pinfo, const char *msg);
char *strsignal(int sig);
int main(int argc, char* argv[]) {
cout << "sys_list数组 : " << sys_siglist[SIGCHLD] << endl;
psignal(SIGCHLD,"psignal函数 ");
cout << "strsignal函数 : " << strsignal(SIGCHLD) << endl;
}
/*打印:
sys_list数组 : Child exited
psignal函数 : Child exited
strsignal函数 : Child exited
*/