信号及其使用
参考博客
Linux信号的产生和处理
信号及其使用
信号的产生
信号由内核产生,信号的生成与事件的发生相关,事件的发生源有3类:
1、用户
用户在终端上按下某些按键时会产生信号,如**Ctrl+C
产生SIGINT
信号,Ctrl+\
** 产生**SIGQUIT
信号,Ctrl+Z
**产生SIGTSTP信号等,终端驱动程序通知内核产生信号,发送至相应进程
2、内核
硬件异常产生的信号(如除数为0、无效的存储访问等),通常会由硬件(如CPU)检测到,将其通知给Linux操作系统内核,内核产生相应信号发送给该事件发生时的进程
3、进程
- 在终端调用**
kill
**命令可向进程发送任意信号 - 进程调用**
kill
或sigqueue
**函数可以发送信号 - 检测到满足条件时发出信号,如**
alarm
或settimer
设置的定时器超时后产生SIGALRM
**信号
信号的种类
使用 kill -l 命令可以查看系统定义的信号列表:
prejudice@prejudice-VirtualBox:~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2
63) SIGRTMAX-1 64) SIGRTMAX
-
1~31为系统信号,有固定含义
-
34~64为实时信号,可由用户定义,所有实时信号的默认动作是终止进程
-
详细介绍
1) SIGHUP 本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。 登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录,wget也能继续下载。 此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。 2) SIGINT 程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。 3) SIGQUIT 和SIGINT类似, 但由QUIT字符(通常是Ctrl-\)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。 4) SIGILL 执行了非法指令. 通常是因为可执行文件本身出现错误, 或者试图执行数据段. 堆栈溢出时也有可能产生这个信号。 5) SIGTRAP 由断点指令或其它trap指令产生. 由debugger使用。 6) SIGABRT 调用abort函数生成的信号。 7) SIGBUS 非法地址, 包括内存地址对齐(alignment)出错。比如访问一个四个字长的整数, 但其地址不是4的倍数。它与SIGSEGV的区别在于后者是由于对合法存储地址的非法访问触发的(如访问不属于自己存储空间或只读存储空间)。 8) SIGFPE 在发生致命的算术运算错误时发出. 不仅包括浮点运算错误, 还包括溢出及除数为0等其它所有的算术的错误。 9) SIGKILL 用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。 10) SIGUSR1 留给用户使用 11) SIGSEGV 试图访问未分配给自己的内存, 或试图往没有写权限的内存地址写数据. 12) SIGUSR2 留给用户使用 13) SIGPIPE 管道破裂。这个信号通常在进程间通信产生,比如采用FIFO(管道)通信的两个进程,读管道没打开或者意外终止就往管道写,写进程会收到SIGPIPE信号。此外用Socket通信的两个进程,写进程在写Socket的时候,读进程已经终止。 14) SIGALRM 时钟定时信号, 计算的是实际的时间或时钟时间. alarm函数使用该信号. 15) SIGTERM 程序结束(terminate)信号, 与SIGKILL不同的是该信号可以被阻塞和处理。通常用来要求程序自己正常退出,shell命令kill缺省产生这个信号。如果进程终止不了,我们才会尝试SIGKILL。 17) SIGCHLD 子进程结束时, 父进程会收到这个信号。 如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸进程。这种情况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程来接管)。 18) SIGCONT 让一个停止(stopped)的进程继续执行. 本信号不能被阻塞. 可以用一个handler来让程序在由stopped状态变为继续执行时完成特定的工作. 例如, 重新显示提示符 19) SIGSTOP 停止(stopped)进程的执行. 注意它和terminate以及interrupt的区别:该进程还未结束, 只是暂停执行. 本信号不能被阻塞, 处理或忽略. 20) SIGTSTP 停止进程的运行, 但该信号可以被处理和忽略. 用户键入SUSP字符时(通常是Ctrl-Z)发出这个信号 21) SIGTTIN 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号. 缺省时这些进程会停止执行. 22) SIGTTOU 类似于SIGTTIN, 但在写终端(或修改终端模式)时收到. 23) SIGURG 有”紧急”数据或out-of-band数据到达socket时产生. 24) SIGXCPU 超过CPU时间资源限制. 这个限制可以由getrlimit/setrlimit来读取/改变。 25) SIGXFSZ 当进程企图扩大文件以至于超过文件大小资源限制。 26) SIGVTALRM 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间. 27) SIGPROF 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间. 28) SIGWINCH 窗口大小改变时发出. 29) SIGIO 文件描述符准备就绪, 可以开始进行输入/输出操作. 30) SIGPWR Power failure 31) SIGSYS 非法的系统调用。 在以上列出的信号中,程序不可捕获、阻塞或忽略的信号有:SIGKILL,SIGSTOP 不能恢复至默认动作的信号有:SIGILL,SIGTRAP 默认会导致进程流产的信号有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ 默认会导致进程退出的信号有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM 默认会导致进程停止的信号有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU 默认进程忽略的信号有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH
对信号的响应
- 捕获信号
为要捕获的信号指定信号处理函数,信号发生时自动调用该函数,在函数内部实现该信号处理 - 忽略信号
多数信号可使用该方式,但**SIGKILL
和SIGSTOP
信号不可忽略,同时也不可被捕获和阻塞
**
忽略某些硬件异常产生的信号(除0)会产生不可预测行为 - 按系统默认方式处理
大部分信号默认操作是终止进程,所有实时信号的默认动作是终止进程
信号处理
内核在接收到信号后,未必会马上对信号进行处理,而是选择在适当的时机,例如在发生中断、发生异常或系统调用返回时,以及在将控制权切换至进程之际,处理所接收的信号
但对于用户进程,进程在接收到信号后,会暂停代码的执行,并保存当前的运行环境,转而执行信号处理程序,待信号处理结束后,才恢复中断点的运行环境,按正常流程继续执行
信号的捕获
signal()函数
#include <signal.h>
/* Set the handler for the signal SIG to HANDLER, returning the old
handler, or SIG_ERR on error.
By default `signal' has the BSD semantic. */
extern __sighandler_t signal (int __sig, __sighandler_t __handler)
__THROW;
/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);
- **
signal
根据sig
信号编号设置信号处理函数,信号到达时就会执行handler
**指定的函数 - 若**
handler
不是函数指针,则必须为SIG_IGN
(忽略该信号)或SIG_DFL
**(执行默认操作) signal()
函数执行成功返回以前的信号处理函数指针,错误返回SIG_ERR
(-1)
示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void func(int sig)
{
cout << endl << sig << " signal is captured!" << endl;
signal(SIGINT, SIG_DFL);
}
int main()
{
signal(SIGINT, func);
int i = 0;
while (true)
{
cout << "第" << i + 1 << "只猪;" << endl;
++i;
sleep(1);
}
return 0;
}
打印输出:
prejudice@prejudice-VirtualBox:~/Cplus_learning/bin$ ./signal
第1只猪;
第2只猪;
第3只猪;
第4只猪;
^C
2 signal is captured!
第5只猪;
第6只猪;
第7只猪;
第8只猪;
^C
疑问:
- **
sig
**参数如何产生 - func()函数不会**
阻塞
**main()函数的执行吗
信号的屏蔽
信号屏蔽就是临时阻塞信号被发送至某个进程,它包含一个被阻塞的信号集。通过操作信号集,可增加和删除需要阻塞的信号。信号屏蔽与信号忽略不同,当进程屏蔽某个信号时,内核将不发送该信号至屏蔽它的进程,直至该信号的屏蔽被解除;而对于信号忽略,内核将被忽略的信号发送至进程,只是进程对被忽略的信号不进行处理
POSIX.1 标准定义了数据类型**sigest_t
可以存放一个信号集(由多个信号构成的集合),并且定义了5个处理信号集的函数。在Linux中,包含signal.h头文件即可引用sigest_t
**和这5个信号集处理函数
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
#include <signal.h>
/* Clear all signals from SET. */
extern int sigemptyset (sigset_t *__set) __THROW __nonnull ((1));
/* Set all signals in SET. */
extern int sigfillset (sigset_t *__set) __THROW __nonnull ((1));
/* Add SIGNO to SET. */
extern int sigaddset (sigset_t *__set, int __signo) __THROW __nonnull ((1));
/* Remove SIGNO from SET. */
extern int sigdelset (sigset_t *__set, int __signo) __THROW __nonnull ((1));
/* Return 1 if SIGNO is in SET, 0 if not. */
extern int sigismember (const sigset_t *__set, int __signo)
__THROW __nonnull ((1));
- **
sigemptyset
**信号集__set排除所有信号 - **
sigfillset
**信号集__set包括所有信号
sigaction()函数
linux中sigaction函数详解
/* Get and/or set the action for signal SIG. */
extern int sigaction (int __sig, const struct sigaction *__restrict __act,
struct sigaction *__restrict __oact) __THROW;
- sig是准备捕获或忽略的信号
- act是将要设置得到信号处理动作
- oldact是取回原先的信号处理动作
- 成功返回0,失败返回1,并设置errno变量
struct sigaction
{
void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);
sigset_t sa_mask;
int sa_flags;
void (*sa_restorer)(void);
}
如果act指针非空,则表示要修改sig信号的处理动作。如果 oldact指针非空,则系统在其中返回该信号的原先动作
- **
sa_handler
**此参数和signal()的参数handler相同,代表新的信号处理函数 sa_mask
用来设置在处理该信号时暂时将sa_mask 指定的信号集搁置
当要更改信号动作时,如果act参数的sa_handler 指向一个信号捕获函数(不是常数SIG_IGN或SIG_DFL),则sa_mask字段说明了一个信号集。在调用信号捕获函数之前,该信号集被加入进程的信号屏蔽字中。仅当从信号捕获函数(也称“信号处理程序”)中返回时才将进程的信号屏蔽字恢复为原先值。这样,在调用信号处理程序时就能阻塞某些信号。在信号处理程序被调用时,系统建立的新信号屏蔽字会自动包括正被递送的信号。因此保证了在处理一个给定的信号时,如果这种信号再次发生,那么它会被阻塞到对前一个信号的处理结束为止。系统在同一种信号产生多次的情况下,通常并不将它们排队,所以如果在某种信号被阻塞时,它产生了5次,那么对这种信号解除阻塞后,其信号处理函数通常只会被调用一次
sigaction结构的**sa_flags
**字段用于设置对信号进行处理的可选项,常用选项及其含义如下:
- SA_NOCLDSTOP:子进程停止时不产生SIGCHLD信号
- SA_RESETHAND:在信号处理函数入口处将把此信号的处理方式重置为SIG_DFL
- SA_RESTART:重启可中断的函数而不是给出EINTR错误
- SA_NODEFER:捕获到信号时不将它添加到信号屏蔽字当中,即不自动阻塞当前捕获到的信号
推荐使用SA_RESTART的理由是,程序中使用的许多系统调用都是可中断的,当接到一个信号时,它们将返回一个错误并设置errno,以此表明函数是因为一个信号而返回的。在设置了SA_RESTART的情况下,信号处理函数执行完后被中断的系统调用将被重启
示例:
#include <unistd.h>
#include <signal.h>
#include <iostream>
using namespace std;
void show_handler(int sig)
{
cout << endl << sig << " signal is captured!" << endl;
for (int i = 0; i < 5; i++)
{
cout << "第" << i+1 << "只猪;" <<endl;
sleep(1);
}
}
int main(void)
{
int i = 0;
struct sigaction act;
sigemptyset(&act.sa_mask);
act.sa_handler = show_handler; //设置中断函数
sigaddset(&act.sa_mask, SIGQUIT); //屏蔽“ctrl+\",中断后处理
act.sa_flags = SA_RESTART; //中断后重启main函数
// act.sa_flags = 0;
sigaction(SIGINT, &act, 0);
while (true)
{
cout << i+1 << "只羊;" << endl;
sleep(1);
i++;
}
}
运行输出:
prejudice@prejudice-VirtualBox:~/Cplus_learning/bin$ ./sigaction
1只羊;
2只羊;
3只羊;
^C //捕获中断信号,执行中断函数
2 signal is captured!
第1只猪;
第2只猪;
第3只猪;
^C第4只猪;
^C第5只猪; //多次捕获相当于一次
2 signal is captured!
第1只猪;
第2只猪;
第3只猪;
第4只猪;
第5只猪;
4只羊; //sa_flags设置为SA_RESTART中断函数执行完后继续执行主函数
5只羊;
6只羊;
^C
2 signal is captured!
第1只猪;
第2只猪;
第3只猪;
^\第4只猪; //"ctrl+\"被阻塞,中断函数结束后执行该信号动作
^C第5只猪;
2 signal is captured!
第1只猪;
第2只猪;
第3只猪;
第4只猪;
第5只猪;
退出 (核心已转储) //执行"ctrl+\"
💡 signal()函数因为不同版本的Linux都不同,且性能也不如sigaction()函数,因此尽量不用
The behavior of signal() varies across UNIX versions, and has also varied historically
across different versions of Linux. Avoid its use: use sigaction(2) instead.
信号的发送
信号发送的关键是让系统知道该向哪个进程发送信号,以及发送什么信号。只要明确了这两点,就可以很方便地发送信号了
进程除了从用户和内核接收信号外,还可以接收其他进程发送的信号
Linux内核提供的发送信号的应用编程接口主要有**kill
、raise
、sigqueue
、alarm
、settimer
和abort
**等
kill()函数
Linux 下的KILL函数的用法
#include <sys/types.h>
#include <signal.h>
/* Send signal SIG to process number PID. If PID is zero,
send SIG to all processes in the current process's process group.
If PID is < -1, send SIG to all processes in process group - PID. */
#ifdef __USE_POSIX
extern int kill (__pid_t __pid, int __sig) __THROW;
#endif /* Use POSIX. */
成功则返回0, 出错则返回-1
_**pid参数**
有以下4种情况:
- pid > 0: 将该信号发送给进程ID为pid的进程
- pid == 0: 将该信号发送给与发送进程属于同一进程组的所有进程(不包括内核进程和init进程). 此时, 发送进程必须具有向这些进程发送信号的权限
- pid < 0: 将该信号发给其进程组ID等于pid绝对值的所有进程(不包括内核进程和init进程). 此时, 发送进程必须具有向这些进程发送信号的权限
- pid == -1: 将该信号发送给发送进程有权限向它们发送信号的系统上的所有进程.(不包括内核进程和init进程)
_sig参数
POSIX.1将编号为0的信号定义为空信号. 如果_sig参数是0, 则kill仍执行正常的错误检查, 但不发送信号. 这被用来确定一个进程是否存在
示例:
#include <sys/wait.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
int main(void)
{
pid_t childpid;
int status;
int retval;
childpid = fork();
if (-1 == childpid)
{
perror("fork()");
exit(EXIT_FAILURE);
}
else if (0 == childpid)
{
puts("In child process");
sleep(100); //让子进程睡眠,看看父进程的行为
exit(EXIT_SUCCESS);
}
else
{
if (0 == (waitpid(childpid, &status, WNOHANG)))
{
retval = kill(childpid, SIGKILL);
if (retval)
{
puts("kill failed.");
perror("kill");
waitpid(childpid, &status, 0);
}
else
{
printf("%d killed\n", childpid);
}
}
}
exit(EXIT_SUCCESS);
}
运行输出:
prejudice@prejudice-VirtualBox:~/Cplus_learning/bin$ ./kill
7446 killed
在确信fork调用成功后,子进程睡眠100秒,然后退出
同时父进程在子进程上调用waitpid函数,但使用了WNOHANG选项,所以调用waitpid后立即返回
父进程接着杀死子进程,如果kill执行失败,返回-1,否则返回0
如果kill执行失败,父进程第二次调用waitpid,保证他在子进程退出后再停止执行,否则父进程显示一条成功消息后退出
raise()函数
#include <sys/types.h>
#include <signal.h>
/* Raise signal SIG, i.e., send SIG to yourself. */
extern int raise (int __sig) __THROW;
成功则返回0, 出错则返回-1
raise函数是通过kill实现
raise(sig)
等价于
kill(getpid(),sig)
- kill和raise是用来发送信号的
- kill把信号发送给进程或进程组
- raise把信号发送给(进程)
自身
示例:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void signal_catchfunc(int);
int main()
{
int ret;
signal(SIGINT, signal_catchfunc);
printf("开始生成一个信号\n");
ret = raise(SIGINT);
if (ret != 0)
{
printf("错误,不能生成SIGINT信号\n");
exit(0);
}
printf("退出....\n");
return 0;
}
void signal_catchfunc(int signal)
{
printf("捕获信号:%d\n", signal);
}
运行输出:
prejudice@prejudice-VirtualBox:~/Cplus_learning/bin$ ./raise
开始生成一个信号
捕获信号:2
退出....