Linux----信号
Posted 4nc414g0n
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux----信号相关的知识,希望对你有一定的参考价值。
信号
1)引入
信号是进程之间事件异步通知的一种方式,属于软中断
kill -l查看所有信号
列表中,编号为1 ~ 31的信号为传统UNIX支持的信号,是不可靠信号(非实时的)
,编号为34 ~ 64的信号是后来扩充的,称做可靠信号(实时信号)
,实时信号底层也有一个实时信号队列来维护
实时信号请参考
:Unix/Linux编程:实时信号
运行一个死循环程序
执行命令kil -2 [pid]或kill -SIGINT [pid]等价于crtl+c
解释:
用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程前台进程因为收到信号,进而引起进程退出
信号的产生方式:
- kill命令
- 硬件产生
- 程序异常(core dump)
–进程角度程序崩溃
–OS角度进程收到信号
–… …
信号的识别:
- 进程收到信号,并不是马上处理,而是在合适的时候(进程在收到信号的时候可能在处理更重要的事情)
信号的处理:
- 默认(部分是终止进程,部分是特定的功能)
- 忽略信号
- 自定义(捕捉信号)
信号的本质:
- 保存在进程PCB,进程控制块的task_struct
- 通过一个位图unsigned int signals,比特位的内容(0或1)表示是否有信号,比特位的位置表示谁发送的信号
- 发送信号的本质就是写task_struct的位图,OS通过修改对应进程的信号位图发送信号
2)产生信号
①通过终端按键产生信号
CoreDump(核心转储)
介绍:
当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部 保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件
命令
ulimit -a
显示当前的各种用户进程限制
可以看到core file size是unlimited(系统默认打开了core dump)
如果不生成core文件:
检查core产生路径是否正确cat /proc/sys/kernel/core_pattern
,如果路径不在当前目录下,则设置:echo "./core-%e-%p-%s" > /proc/sys/kernel/core_pattern
测试:
int a=10; int b=0; a/=b
生成一个core文件
gdb调试:core-file [core文件]
② 调用系统函数向进程发信号
#include <signal.h>
int raise(int sig);
功能
:raise函数可以给当前进程发送指定的信号(自己给自己发信号)
代码如下:
void handler(int signo) cout<<"signo is "<<signo<<endl; exit(1); for(int i=0;i<=31;i++) signal(i, handler); sleep(3); raise(2);
收到2号信号
#include <stdlib.h>
void abort(void);
功能
:就像exit函数一样,abort函数总是会成功的,所以没有返回值
6号信号SIGABRT
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
功能
: kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号
实现自己的kill命令(需要用到命令行参数)
int main(int argc, char *argv[]) if(argc!=3) cout<<"wrong usage"<<endl; kill(atoi(argv[2]), atoi(argv[1])); return 0;
③ 由软件条件产生信号
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
功能:
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程
下面是一秒内数数的次数代码,一秒后被SIGALRM信号终止
int count=0; alarm(1) for(;1;count++) printf("%d ",count);
④ 硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号:
- 当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程
- 当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程
- … …
3)阻塞信号
① 信号的保存
引入(了解其他):
- 实际执行信号的处理动作称为信号递达(Delivery)
- 信号从产生到递达之间的状态,称为信号未决(Pending)
- 进程可以选择阻塞 (Block)某个信号
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
- 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
在内核中的表现形式:
pending位图
:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否收到信号。OS发送信号本质是修改task_ struct pending位 图的内容block位图
:比特位的位置代表信号的编号,比特位的内容(0 or 1) 代表是否阻塞该信号handler数组
:用信号的编号,作为数组的索引,找到该信号对应的信号处理方式,然后指向对应的方法(递达)(信号自定义捕捉方法是用户提供的
)
注意
:内核首先判断信号屏蔽状态字是否阻塞,如果该信号被设为为了阻塞的,那么信号未决状态字(pending)相应位制成1;若该信号阻塞解除,信号未决状态字(pending)相应位制成0;表示信号此时可以抵达,也就是可以接收该信号
② sigset_t
信号集用来描述信号的集合,每个信号占用一位(64位),每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的,因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略
注意:不同平台,不同版本操作系统,不同位数的操作系统 在内核中的sigset_t实现是不同的,见下图:
有的可能通过一个结构体来存储,有的会用一个unsigned long来存储
③ 信号集操作函数
1.设置即判断信号集函数
在main函数内定义一个
sigset_t set
,是在栈上开辟空间,这里的栈是用户栈
,而我们要设置的是OS
的进程属性,同时,底层sigset_t实现可能是不同的,所以系统调用接口不可或缺
#include <signal.h>
int sigemptyset(sigset_t *set);
功能
:初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号(全置0)
返回值
:成功为0,出错-1
set
:信号集
#include <signal.h>
int sigfillset(sigset_t *set);
功能
:初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号(全置1)
返回值
:成功为0,出错-1
set
:信号集
#include <signal.h>
int sigaddset (sigset_t *set, int signo);
功能
:在该信号集中添加某种有效信号(指定位置设置为1)
返回值
:成功为0,出错-1
set
:信号集
signo
:信号
#include <signal.h>
int sigdelset(sigset_t *set, int signo);
功能
:在该信号集中删除某种有效信号(指定位置设置为0)
返回值
:成功为0,出错-1
set
:信号集
signo
:信号
#include <signal.h>
int sigismember(const sigset_t *set, int signo);
功能
:sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种 信号,若包含则返回1,不包含则返回0,出错返回-1
set
:信号集
signo
:信号
2.sigprocmask和sigpending
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
功能
:读取或更改进程的信号屏蔽字(阻塞信号集)
返回值
:成功为0,出错-1
注意:
- 如果oldset是非空指针,则读取进程的当前信号屏蔽字通过oldset参数传出
- 如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改
- 如果oldset和set都是非空指针,则先将原来的信号 屏蔽字备份到oldset里,然后根据set和how参数更改信号屏蔽字
set
:进程当前的信号屏蔽字 (阻塞信号集)
oldset
:输出型参数,备份set
how的选项:
SIG_BLOCK
:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|setSIG_UNBLOCK
:set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~setSIG_SETMASK
:设置当前信号屏蔽字为set所指向的值,相当于mask=set
#include <signal.h>
int sigpending(sigset_t *set);
功能
:读取当前进程的未决(pending)信号集,通过set参数传出
返回值
:调用成功则返回0,出错则返回-1
set:输出型参数
④ 综合代码测试
实现打印阻塞信号集,并在发送2号信号后打印阻塞信号集,再捕捉信号观察恢复的阻塞信号集
- 设置进程的pending位图为阻塞2号信号
- 尝试发送2号信号,10秒后回复原来的pending,打印
void handler(int signo) cout<<"signo is "<<signo<<endl; //exit(1); void showpending(sigset_t *pending) for(int i=1;i<32;i++) if(sigismember(pending, i))//检查此pending内是否有i号信号 cout<<1; else cout<<0; cout<<endl; int main(int argc, char *argv[]) signal(2,handler);//捕捉2号信号后自定义处理 sigset_t in,out; //0初始化 sigemptyset(&in); sigemptyset(&out); sigaddset(&in, 2);//用户栈设置 sigprocmask(SIG_SETMASK, &in, &out);//设置OS的pending位图 int count=0; sigset_t pending;//用于获取未决状态的信号集 while(1) sigpending(&pending);//获取未决状态的信号集(信号产生和递达间的状态) showpending(&pending);//打印pending sleep(1); count++; if(count==10) sigprocmask(SIG_SETMASK, &out, &in);//相当于回复原来的状态 cout<<"now sigset: "; showpending(&in);//打印 cout<<"recover: "; showpending(&out);//打印 return 0;
分析:
开始设置阻塞信号集为0100…000, 打印的是pending信号集
当接受到ctrl+c也就是2号信号的时候,pending信号集会查看block信号集发现有阻塞,然后pendinig信号集被修改为0100…000
10秒后,回复pending信号集000…000
4)信号捕捉
① 内核信号捕捉
注意:
- 每个用户进程都有自己的用户级页表,但是OS只有一份,所以我们只需要.维护一份内核级页表
- CPU中会存在-一个权限相关的寄存器数据标识所处的状态,判断你使用的是哪个种类的页表
- 用户态和核心态的权限级别不同,决定看到的资源是不一样的
- 信号捕捉的时候,就必须由
内核态切换为用户态
,操作系统不信任任何用户,当在内核态执行自定义捕捉方式的时候,可能会不安全,所以必须切换到用户态再执行
内核信号捕捉
:如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号,信号处理函数的代码在用户空间
处理过程:
- 用户程序注册了SIGQUIT信号的处理函数sighandler,当前正在执行main函数,这时发生中断或异常切换到内核态
- 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达
- 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程
- sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态
- 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行
② signal
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
功能
:通过handler修改信号的处理方式
signum:信号(比如2:SIGINT)
注意
:sighandler_t是一个函数指针类型,所以handler是一个函数指针,
在signum-generic.h中有一个SIG_IGN
宏定义,可供handler参数使用,表示忽略信号,定义如下(将1强转为sighandler_t类型):
代码测试
:void handler(int signo) cout<<"signo is "<<signo<<endl; int main() signal(SIGINT, handler);//收到2号信号就打印signo is [signo] pid_t pid=getpid(); while(1) cout<<"process"<<pid<< "proceeding"<<endl; return 0;
使用其他信号终止进程,比如kill -3
注意
:可以对大部分信号进行捕捉或忽略,但是有少部分信号不能自定义(比如9号信号)
更改signal函数的signum参数为SIGKILL,直接kill进程
11号 segment fault 自行测试
③ sigaction
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
功能
: sigaction函数可以读取和修改与指定信号相关联的处理动作
- 若act指针非空,则根据act修改该信号的处理动作
- 若oldact指针非空,则通过oact传出该信号原来的处理动作,act和oldact指向sigaction结构体
返回值
:调用成功则返回0,出错则返回- 1
sa_mask
:一个调用信号捕捉函数之前要加到进程信号屏蔽字中的信号集
(如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字)
sa_flag
:信号处理选项,一般默认设为0
参考:sigaction函数sa_flags各标志影响的实例讲解
代码如下:
void handler(int signo) while(1) cout<<"signo is "<<signo<<endl; sleep(1); //exit(1); int main(int argc, char *argv[]) struct sigaction act, oldact; act.sa_handler=handler; act.sa_flags=0; sigemptyset(&act.sa_mask); sigaddset(&act.sa_mask,3); sigaddset(&act.sa_mask,4); sigaction(SIGINT,&act,&oldact); while(1) cout<<"progress"<<endl; sleep(1); return 0;
2,3,4号信号均不可以将其终止
5)可重入函数
这里是引用
6)从信号角度重新理解关键字volatile
7)SIGCHILD信号
以上是关于Linux----信号的主要内容,如果未能解决你的问题,请参考以下文章