linux 信号机制
文章目录
root@ubuntu:# 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. 实时信号非实时信号
如上,kill列举出所有信号。实时信号与非实时信号又叫做可靠信号与不可靠信号。SIGRTMIN 及其以后的是实时信号,之前的是非实时信号。区别是实时信号支持重复排队,但是非实时信号不支持。非实时信号在排队时候会默认只出现一次,意思就是即使多次发送也终将只收到一个。在队列的取出顺序上也有区别,即最先去除的信号一定是实时信号。
PS:
- kill/killall 默认发送SIGTERM 信号。
- linux下 SIGKILL不能被阻塞、或忽略。
- 所有未定义处理函数的信号,默认退出进程。
- 信号被设置block后仍存在于队列中只是不被处理,如果放开屏蔽将会被处理。
- 信号可以中断sleep调用引起睡眠的进程。
2. 信号状态:
信号的”未决“是一种状态,指的是从信号的产生到信号被处理前的这一段时间;信号的”阻塞“是一个开关动作,指的是阻止信号被处理,但不是阻止信号产生。
例如在sleep前用sigprocmask阻塞了退出信号,然后sleep,然后在sleep的过程中产生一个退出信号,但是此时退出信号被阻塞过,(中文的”阻塞”在这里容易被误解为一种状态,实际上是一种类似于开关的动作,所以说“被阻塞过”,而不是“被阻塞”)所以处于“未决”状态,在 sleep后又用sigprocmask关掉退出信号的阻塞开关,因为之前产生的退出信号一直处于未决状态,当关上阻塞开关后,马上退出“未决”状态,得到处理,这一切发生在sigprocmask返回之前。
3. 信号生命周期:
对于一个完整的信号生命周期(从信号发送到相应的处理函数执行完毕)来说,可以分为三个重要的阶段,这三个阶段由四个重要事件来刻画:1.信号诞生;2. 信号在进程中注册完毕;3.信号在进程中的注销完毕;4.信号处理函数执行完毕。相邻两个事件的时间间隔构成信号生命周期的一个阶段。
下面阐述四个事件的实际意义:
- 信号"诞生"。信号的诞生指的是触发信号的事件发生(如检测到硬件异常、定时器超时以及调用信号发送函数kill()或sigqueue()等)。
- 信号在目标进程中"注册";
进程的task_struct结构中有关于本进程中未决信号的数据成员:
struct sigpending pending;
struct sigpending
{
struct sigqueue *head, **tail;
sigset_t signal;
};
信号在进程中注册指的就是信号值加入到进程的未决信号集中(sigpending结构的第二个成员sigset_t signal),并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。
信号在进程中的注销。在目标进程执行过程中,会检测是否有信号等待处理(每次从系统空间返回到用户空间时都做这样的检查)。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。是否将信号从进程未决信号集中删除对于实时与非实时信号是不同的。对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用gqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则应该把信号在进程的未决信号集中删除(信号注销完毕)。否则,不在进程的未决信号集中删除该信号(信号注销完毕)。进程在执行信号相应处理函数之前,首先要把信号在进程中注销。
信号生命终止。进程注销信号后,立即执行相应的信号处理函数,执行完毕后,信号的本次发送对进程的影响彻底结束。
4. 信号的执行和注销
内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。当所有未被屏蔽的信号都处理完毕后,即可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。
处理信号有三种类型:进程接收到信号后退出;进程忽略该信号;进程收到信号后执行用户设定用系统调用signal的函数。当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。而且执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行(如果用户定义的函数在内核态下运行的话,用户就可以获得任何权限)。
eg:
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void myHandler(int num)
{
int ret = 0;
if (SIGUSR1 == num)
{
sigset_t set;
ret = sigemptyset(&set);
assert(!(-1 == ret));
ret = sigaddset(&set, SIGINT);
assert(!(-1 == ret));
ret = sigaddset(&set, SIGRTMIN);
assert(!(-1 == ret));
ret = sigprocmask(SIG_UNBLOCK, &set, NULL);
assert(!(-1 == ret));
printf("解除阻塞 recv sig num: %d\n", num);
}
else if (num == SIGINT || num == SIGRTMIN)
{
printf("recv sig num: %d\n", num);
}
else
{
printf(" 其他信号recv sig num: %d\n", num);
}
}
int main(void)
{
pid_t pid;
int ret = 0;
// 设置回调函数
struct sigaction act;
act.sa_handler = myHandler;
act.sa_flags = SA_SIGINFO;
// 注册非实时信号的处理函数
ret = sigaction(SIGINT, &act, NULL);
assert(!(-1 == ret));
// 注册实时信号的处理函数
ret = sigaction(SIGRTMIN, &act, NULL);
assert(!(-1 == ret));
// 注册用户自定义信号
ret = sigaction(SIGUSR1, &act, NULL);
assert(!(-1 == ret));
// 把 SIGINT SIGRTMIN 军添加到阻塞状态字中
sigset_t set;
ret = sigemptyset(&set);
assert(!(-1 == ret));
ret = sigaddset(&set, SIGINT);
assert(!(-1 == ret));
ret = sigaddset(&set, SIGRTMIN);
assert(!(-1 == ret));
ret = sigprocmask(SIG_BLOCK, &set, NULL);
assert(!(-1 == ret));
pid = fork();
assert(!(-1 == ret));
if (0 == pid)
{
union sigval value;
value.sival_int = 10;
int i = 0;
// 发三次不稳定信号
for (i = 0; i < 3; i++)
{
ret = sigqueue(getppid(), SIGINT, value);
assert(!(-1 == ret));
printf("发送不可靠信号 ok\n");
}
// 发三次稳定信号
value.sival_int = 20;
for (i = 0; i < 3; i++)
{
ret = sigqueue(getppid(), SIGRTMIN, value);
assert(!(-1 == ret));
printf("发送可靠信号ok\n");
}
// 向父进程发送 SIGUSR1 解除阻塞
ret = kill(getppid(), SIGUSR1);
assert(!(-1 == ret));
}
while (1)
{
sleep(1);
}
return 0;
}
信号掩码和信号处理函数的继承
信号处理函数的继承
- 信号处理函数是进程属性,所以进程里的每个线程的信号处理函数是相同的。
- 通过fork创建的子进程会继承父进程的信号处理函数。
- execve 后设置为处理的信号处理函数会被重置为默认函数,设置为忽略的信号保持不变。意思是如果父进程里信号设置处理为SIG_IGN,那么等到子进程被exec了,这个信号的处理还是被忽略,不会重置为默认函数。
eg:
// test.c --> test
#include <stdlib.h>
typedef void (*sighandler_t)(int);
static sighandler_t old_int_handler;
static sighandler_t old_handlers[SIGSYS + 1];
void sig_handler(int signo)
{
printf("receive signo %d\n",signo);
old_handlers[signo](signo);
}
int main(int argc, char **argv)
{
old_handlers[SIGINT] = signal(SIGINT, SIG_IGN);
old_handlers[SIGTERM] = signal(SIGTERM, sig_handler);
int ret;
ret = fork();
if (ret == 0) {
//child
// 这里execlp将运行 test2 作为子进程。
execlp("/tmp/test2", "/tmp/test2",(char*)NULL);
}else if (ret > 0) {
//parent
while(1) {
sleep(1);
}
}else{
perror("");
abort();
}
}
================================================
test2.c --> test2
#include <stdio.h>
int main(int argc, char **argv)
{
while(1) {
sleep(1);
}
return 0;
}
结论:test换成test2后,SIGINT的处理方式还是忽略,SIGTERM被重置为默认的方式。
信号掩码的继承
信号掩码有以下规则:
- 每个线程可以有自己信号掩码。
- fork出来的子进程会继承父进程的信号掩码,exec后信号掩码保持不变。如果父进程是多线程,那么子进程只继承主线程的掩码。
- 针对进程发送的信号,会被任意的没有屏蔽该信号的线程接收,注意只有一个线程会随机收到。linux下如果都可以所有线程都可以接收信号,那么信号将默认发送到主线程,posix系统是随机发送。
- fork之后子进程里pending的信号集初始化为空,exec会保持pending信号集。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
typedef void (*sighandler_t)(int);
static void *thread1(void *arg)
{
sigset_t set;
printf("in thread1\n");
sigemptyset(&set);
sigaddset(&set, SIGTERM);
pthread_sigmask(SIG_BLOCK, &set, NULL);
while(1) {
sleep(1);
}
}
static void sigset_print(sigset_t *set)
{
int i;
for (i = 1; i <= SIGSYS; i++) {
if (sigismember(set, i)) {
printf("signal %d is in set\n",i);
}
}
}
int main(int argc, char **argv)
{
int ret;
sigset_t set;
pthread_t pid;
pthread_create(&pid, NULL, thread1, NULL);
sleep(1);
sigemptyset(&set);
sigaddset(&set, SIGINT);
pthread_sigmask(SIG_BLOCK, &set, NULL);
ret = fork();
if (ret == 0) {
//child
pthread_sigmask(SIG_BLOCK, NULL, &set);
sigset_print(&set);
while(1) {
sleep(1);
}
}else if (ret > 0) {
//parent
while(1) {
sleep(1);
}
}else{
perror("");
abort();
}
}
结论:只有在主线程里设置的掩码才被子进程继承了。这里面的原因在于linux里的fork只是复制了调用fork()的那个线程,因此在子进程里只有父进程的主线程被拷贝了,当然信号掩码就是父进程的主线程的信号掩码的复制了。再次验证证明,如果是在thread1里调用fork,那么子进程的信号掩码就会是thread1的拷贝了。
sigwait 与多线程
sigwait函数:
- sigwait等一个或者多个指定信号发生。
它所做的工作只有两个:第一,监听被阻塞的信号;第二,如果所监听的信号产生了,则将其从未决队列中移出来。sigwait并不改变信号掩码的阻塞与非阻塞状态。
- 在POSIX标准中,当进程收到信号时,如果是多线程的情况,我们是无法确定是哪一个线程处理这个信号。而sigwait是从进程中pending的信号中,取走指定的信号。这样的话,如果要确保sigwait这个线程收到该信号,那么所有线程含主线程以及这个sigwait线程则必须block住这个信号,因为如果自己不阻塞就没有未决状态(阻塞状态)信号,别的所有线程不阻塞就有可能当信号过来时,被其他的线程处理掉。
PS:
在多线程代码中,总是使用sigwait或者sigwaitinfo或者sigtimedwait等函数来处理信号。而不是signal或者sigaction等函数。因为在一个线程中调用signal或者sigaction等函数会改变所以线程中的信号处理函数,而不是仅仅改变调用signal/sigaction的那个线程的信号处理函数。
apis
信号发生函数
- kill(pid_t pid, int signum);
- int sigqueue(pid_t pid, int sig, const union sigval value);
- pthread_kill(pthread_t tid, int signum);
- raise(int signum); // 发送信号到自己
- void alarm(void);
- void abort(void);
- int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
PS:
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
信号处理函数
- signal(int signum, void (*handler)(int signum))
- sigaction(int signum, struct sigaction* newact, sigaction* oldact)
sigaction act;
act.sa_handler = handler;
act.sa_flags = SA_SIGINFO;
// 注册信号的处理函数
sigaction(SIGINT, act, NULL);
信号掩码函数
- sigprocmask(int how, struct sigaction* set,struct sigaction* oldset)
- pthread_sigmask(int how, struct sigaction* set,struct sigaction* oldset)
sigprocmask用于设置进程的信号掩码,pthread_sigmask用于设置线程的信号掩码,二者参数相同。第一个参数有
SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK
。
信号集合变量
sigset_t set
sigemptyset(&set) //清空阻塞信号集合变量
sigfillset(&set) //添加所有的信号到阻塞集合变量里
sigaddset(&set,SIGINT) //添加单一信号到阻塞信号集合变量
sigdelset(&set,SIGINT) //从阻塞信号集合变量中删除单一信号
sigismember(&set,int signum) //测试信号signum是否包含在信号集合set中,如果包含返回1,不包含返回0,出错返回-1。错误代码也只有一个EINVAL,表示signum不是有效的信号代码。
信号屏蔽函数
- int sigpending(sigset_t *set); // 返回阻塞的信号集
- int sigsuspend(const sigset_t *mask);
sigsuspend表示临时将信号屏蔽字设为mask,并挂起进程直到有信号产生(非屏蔽信号才能唤醒或终止进程),如果信号处理函数返回,那么siguspend将恢复之前的信号屏蔽字(temporarily)
假设sisuspend阻塞进程时产生了信号A,且A不是mask内的屏蔽信号,那么A的信号处理函数有两种情形,一:直接终止进程,此时进程都不存在了,那么sigsuspend当然无须返回了(不存在进程了sigsuspend也不存在了,函数栈嘛); 二:如果信号A的处理函数返回,那么信号屏蔽字恢复到sigsuspend之前的(sigsuspend调用时将信号屏蔽字设为mask,所以要恢复到sigsuspend调用之前的),然后sigsuspend返回-1并将error置为EINTR.
linux 信号机制的更多相关文章
- 利用linux信号机制调试段错误(Segment fault)
在实际开发过程中,大家可能会遇到段错误的问题,虽然是个老问题,但是其带来的隐患是极大的,只要出现一次,程序立即崩溃中止.如果程序运行在PC中,segment fault的调试相对比较方便,因为可以通过 ...
- Linux信号机制
Linux信号(signal) 机制分析 [摘要]本文分析了Linux内核对于信号的实现机制和应用层的相关处理.首先介绍了软中断信号的本质及信号的两种不同分类方法尤其是不可靠信号的原理.接着分析了内核 ...
- 利用linux信号机制调试段错误(Segment fault)【转】
转自:http://blog.csdn.net/ab198604/article/details/6164517 版权声明:本文为博主原创文章,未经博主允许不得转载. 在实际开发过程中,大家可能会遇到 ...
- linux信号机制与python信号量
1.信号本质 软中断信号(signal,又简称为信号)用来通知进程发生了异步事件.在软件层次上是对中断机制的一种模拟,在原理上,一个进程收到一个信号与处理器收到一个中断请求可以说是一样的.信号是进程间 ...
- xenomai内核解析之信号signal(一)---Linux信号机制
版权声明:本文为本文为博主原创文章,转载请注明出处.如有错误,欢迎指正.博客地址:https://www.cnblogs.com/wsg1100/ 目录 1. Linux信号 1.1注册信号处理函数 ...
- linux信号机制 - 用户堆栈和内核堆栈的变化【转】
转自:http://itindex.net/detail/16418-linux-%E4%BF%A1%E5%8F%B7-%E5%A0%86%E6%A0%88 此文只简单分析发送信号给用户程序后,用户堆 ...
- Linux信号机制代码示例
1 基本功能: 本Blog创建了两个进程(父子进程): 父进程: 执行文本复制操作,当收到 SIGUSR1信号后,打印出现在文件复制的进度: 子进程: 每个固定时间段向父进程发送一个 SIGUSR1 ...
- Linux信号(signal) 机制分析
Linux信号(signal) 机制分析 [摘要]本文分析了Linux内核对于信号的实现机制和应用层的相关处理.首先介绍了软中断信号的本质及信号的两种不同分类方法尤其是不可靠信号的原理.接着分析了内核 ...
- linux中的信号机制
概述 Linux信号机制是在应用软件层次上对中断机制的一种模拟,信号提供了一种处理异步事件的方法,例如,终端用户输入中断键(ctrl+c),则会通过信号机制停止一个程序[1]. 这其实就是向那个程序( ...
随机推荐
- CSS动画实例:旋转的圆角正方形
在页面中放置一个类名为container的层作为效果呈现容器,在该层中再定义十个名为shape的层层嵌套的子层,HTML代码描述如下: <div class="container&qu ...
- 聊聊MySQL常用的4种主从复制架构
目录 一主多从复制架构 多级复制架构 双主(Dual Master)复制架构 多源(Multi-Source)复制架构 如何优化主从延迟问题? 复制的4中常见架构有一主多从复制架构.多级复制架构.双主 ...
- 《Java从入门到失业》第一章:计算机基础知识(一):二进制和十六进制
0 前言 最近7年来的高强度工作和不规律的饮食作息,压得我有些喘不过气,身体也陆续报警.2018年下半年的一场病,让我意识到了这个问题的严重性,于是开始强制自己有规律饮食和作息,并辅以健身锻炼,不到2 ...
- golang fmt包
fmt fmt包实现了类似C语言printf和scanf的格式化I/O.主要分为向外输出内容和获取输入内容两大部分. 向外输出 标准库fmt提供了以下几种输出相关函数. Print Print系列函数 ...
- kolla快速集成openstack-ocata和opencontrail-4.0.1.0单节点
参考链接: kolla快速集成openstack-ocata和opencontrail-4.0.1.0单节点 https://github.com/Juniper/contrail-docker/wi ...
- golang grpc demo
1.grpm 安装: git clone https://github.com/grpc/grpc-go.git $GOPATH/src/google.golang.org/grpc 2.proto, ...
- Hop: Heterogeneity-aware Decentralized Training
郑重声明:原文参见标题,如有侵权,请联系作者,将会撤销发布! 以下是对本文关键部分的摘抄翻译,详情请参见原文. ASPLOS 2019 Abstract 最近的研究表明,在机器学习的背景下,去中心化算 ...
- pandas外部数据的读取构造数据框-文本文件读取(一种utf-8中文编码乱码处理经验)
上面一篇文章有记录pandas构造数据框的方式有二维数组,字典,嵌套的列表和元组等,本篇用于介绍通过外部数据读取的方式来构造数据框. python读取外部数据集的时候,这些数据集可能包含在文本文件(c ...
- Shell编程—结构化命令(2)
1for命令 for命令的基本格式: for var in list do commands done 在list参数中,你需要提供迭代中要用到的一系列值. 1.1读取列表中的值 例子: $ vim ...
- PHP学习中的一些总结(持续更新)
文件上传部分 在前台的<form>表单中 hidden隐藏域的MAX_FILE_SIZE可以起到实质性的控制作用,即在文件上传之前就可以判断文件的大小,格式为: <form acti ...