linux下的信号

一、信号的概念
说道信号大多数人可能会想到在公路上的红绿灯,在我们linux也有着信号的概念。那么linux下的信号到底是怎么回事呢?我们还是今天就看看linux下的信号。
信号其实是一种软件中断,它为程序提供了处理异步事件的方法。异步事件就是事件可能会在任何时间内发生,很多重要的而程序都有对信号的处理,在linux下我们是通过命令 kill -l来查看系统中所有的信号列表和它们的信号编号。
这里写图片描述
我们可以看到31~34之间是没有信号的,我们把1~31号信号称为普通信号,把34~64号信号称为实时信号。所有的信号都包含在signal.h中,且都定义成正整数常量,也就是它们的信号编号。
二、信号的产生形式
linux下的信号一般有四种 产生方式:
1、来自键盘的信号
用户在终端按下某些按键时,终端驱动程序会发送信号给前台的进程(使前台进程终止的信号)。ctrl+c发送SIGINT信号、ctrl+\发送SIGQUIT信号、ctrl+z发送SIGTSTP信号。SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,SIGTSTP信号使前台进程停止。

2、硬件异常产生信号
除数为0、无效的内存引用对应的SIGSEGV信号,硬件异常产生信号不同,一旦硬件异常产生,那么它会一直存在,直到程序被终止位置,所以处理硬件异常信号一般都采用终止程序的方法。
3、通过系统调用向进程发送信号
进程可以通过在shell下运行kill指令来对某个进程发送信号,kill指令是kill系统调用的一个接口。

int kill(pid_t id,int signo);  //向进程id发送signo信号  
int raise(int signo);   //自己给自己发送signo信号  

返回值:成功返回的是0,失败返回的是-1。
4、由软件条件产生信号

#include <unisted.h>   
unsigned int alarm(unsigned int seconds);  //告诉操作系统seconds时间后给自己发送一个SIGALRM信号

返回值:返回0或者以前设定的闹钟还余下的秒数
三、信号的处理方式
产生了信号,当然也要对信号进行处理。下面来看一下linux中信号的处理方式:
1、信号忽略
忽略此信号,大多数信号都可以采用这种方式进行处理,除了9( SIGKILL )和19(SIGSTOP )信号。因为这两种信号都直接向内核提供了进程终止和停止的可靠办法。SIGKILL还有硬件异常信号我们最好不要忽略,因为硬件异常一旦产生如果不进行处理就会一直存在。
2、信号捕捉
进程要通知内核在某种信号产生时,需要调用一个用户函数,在用户函数中,用户可以自己定义信号处理的方式,在Linux下我们不能捕捉SIGKILL信号和SIGSTOP信号。 用户自定义动作
3、执行默认动作
一般情况下是终止该进程

四、信号中的处理函数
1、signal函数(指定信号的处理方式)

#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

signum:
参数为信号名或者信号的编号。
handler:
为指向返回值为void,参数为int的函数指针指针,或者是SIG_IGN或SIG_DFL的宏定义。
在signal.h的头文件中,上面的宏定义被定义为以下:

#define SIG_ERR (void(*)()) -1
#define SIG_DFL (void(*)()) 0
#define SIG_IGN (void(*)()) 1

SIG_DFL指定信号处理的方式为默认方式,SIG_IGN指定信号处理的方式为忽略。

2、alarm函数(闹钟函数)

unsigned int alarm(unsigned int seconds);

alarm函数相当于一个闹钟,它可以为进程注册闹钟时间( 例如使用alarm(5)可以为进程注册5秒钟的闹钟时间,5秒后会产生SIGALRM信号 )。如果在调用alarm函数时,之前已经为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的值返回。以前注册的闹钟时间则被新值所取代;
使用alarm(0)可以取消以前所注册的闹钟,并返回之前注册的闹钟的剩余时间。


3、pause函数

int pause(void);

pause函数是检测进程有没有从信号处理函数中返回,只有当执行了一个信号处理程序并从其返回时,pause才返回,否则pause将一直挂起调用进程。
当进程执行了信号处理程序时,pause返回-1,并将errno设置为EINTR。

五、Core Dump
Core Dump:称为核心转储,即当一个进程异常退出之前,将进程的用户空间内存数据全部以文件方式保存到磁盘上,文件名则称作是core,方便gdb的调试。
Post-mortem Debug(事后调试):进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因。
一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中),如下:
这里写图片描述
默认下系统分配core文件大小为0,是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全,并且若一个较大程序每次运行都要产生一个core文件,很可能将硬盘空间占用完。
我们可以用一下命令进行更改:
ulimit -c 1024 //设置core文件大小上限为1024字节
这里写图片描述
现在我们写一个代码(除0出错的代码),看一下core文件
这里写图片描述
代码如下:有一个code dumped
这里写图片描述
我们可以看到有一个core类型的文件,文件还是挺大的。
这里写图片描述
我们可以使用core文件在GDB中调试,来找错误。
直接在gdb中打开那个core文件,core-file core.2306
就会直接告诉你出错的地方。
六、信号阻塞和信号的处理机制
我们先看一下信号的状态:
信号的状态:
信号递达(Delivery):
我们将实际执行信号的处理动作称为信号递达。
信号未决(Pending):
我们将信号从产生到递达之间的状态称为信号未决。
信号阻塞(Block):
进程可以选择阻塞某个信号,也可以理解为屏蔽某个信号。
注意:
阻塞和忽略是不同的,阻塞是进程没有收到该信号,而忽略是进程收到信号后的一种处理方式。
信号未决和信号阻塞的状态都被组织在两张位图中的,即位置表示信号编号,如果在pending表中该位置为1则表示被pending为0表示尚未被pending,如果在block表中则表示是否被block。
每个进程中操作系统都会为其分配一整套的block表,pending表以及handler表
这里写图片描述
进程将收到的信号存放在PCB中,PCB中有三个与上面三个状态相对应的位图表。(用于表示信号的状态,状态只有是与否两个概念,我们中0,1来表示方便且节省空间,所以用位图。)
每个信号都在阻塞表(block)和未决表(pending)中有一个0或1的状态。还有一个handler表,类似于一个函数指针数组,每个指针都指向指定信号的处理方式。
linux下的信号处理机制 如图:
这里写图片描述
内核是如何产生信号的?内核又是在什么时候产生信号的呢?
信号发送给进程之后,进程将收到的信号存放在pcb的某些字段中,信号是在由内核态切换到用户态的时候来处理信号的。
内核可因为执行当前主控流程的某条指令是因为中断,异常或者系统调用进入内核态。
内核处理完异常准备返回用户态之前,会检查当前进程的PCB中是否有递达的信号。若有,根据处理方式又可分为三种情况 :
1、忽略,将pending表中的信号位置由1置0,返回用户态
2、执行默认动作,默认动作一般为终止程序。此时不返回,执行终止进程流程。
3、执行自定义动作(信号捕捉)。

信号的处理动作是用户自定义的动作,内核会先切换到用户态执行信号处理函数,信号处理函数返回时执行系统调用sigreturn再次返回内核态。然后再从内核态返回用户态从上次异常或中断的地方继续执行。此种方式最为繁琐,共有四次内核用户之间的切换。
这里写图片描述

七、信号集
我们知道信号在pending表中只是一个bit为标志(0或1)我我们以相同的31个信号的Block和Pending标志,而这就是信号集(sigset_t)
信号集的操作函数:

#include <signal.h>  
int sigemptyset(sigset_t *set);   //初始化set所指向的信号集,使set包含所有信号  
int sigfillset(sigset_t *set);        //初始化set所指向的信号集,使其中所有信号的对  
表示该信号集的有效信号包括系统支持的所有信号  
int sigaddset(sigset_t *set, int signum);   //将一个信号添加到已经存在的信号集中  
int sigdelset(sigset_t *set, int signum);   //从已有信号集中删除一个信号  
int sigismember(const sigset_t *set, int signum);  //测试信号集是否包含signum信号 

1、函数sigprocmask

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

该函数用来规定当前阻塞而不能递达给进程的信号集。调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
参数:
how参数指定了这个函数工作的方式,有三种情况:
1、SIG_BLOCK:
当前的信号屏蔽字与参数set指针指向的信号屏蔽字组成并集,构成新的信号屏蔽字。set包含了希望阻塞的新信号。
2、SIG_UNBLOCK:
当前的信号屏蔽字与参数set指针指向的信号屏蔽字补集的交集,构成新的信号屏蔽字。set包含了希望希望解除阻塞的信号。
3、SIG_SETMASK:
将当前进程的信号屏蔽字设置成set所指向的值

set指针:指向一个合适的信号屏蔽字
oldset指针:当我们修改了当前的信号屏蔽字之后,需要保存之前的信号屏蔽字,以便回复之前的工作状态。

2、函数sigpending

int sigpending(sigset_t *set);

返回当前信号的pengding信号集。通过参数指针set返回。

3、函数sigaction

nt sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

函数的功能是检查或修改指定信号的处理动作。
参数:
signum为指定信号,act为一个结构体指针,注意这个结构体的名字与本函数的名字相同,但他们是两个概念,不要混淆。 oldact。
若act非空,表示要修改指定信号的处理动作;当oldact指针为空的时候,由oldact返回上一个信号的动作

4、函数pause
pause函数功能是使当前进程挂起,直到有信号递达。
如果信号的处理动作是默认终止,则程序终止,pause没有机会返回。
如果是忽略,则进程继续处于挂起状态。
如果是执行用户自定信号处理函数,则pause返回-1。error设置为EINTR,所以pause只有出错返回。EINTR表示信号被中断。

八、代码展示

#include <stdio.h>  
#include <signal.h>  
#include <unistd.h>  
#include <sys/types.h>  

void myhander(int sig)  
{  
    printf("pid## %d receiving sig## %d\n",getpid(),sig);  
}     
void PrintPending(sigset_t set)  
{  
    int i=1;  
    for(;i<32;++i)  
    {  
        if(sigismember(&set,i))  
        {  
            printf("1 ");  
        }  
        else  
        {  
            printf("0 ");  
        }  
    }  
    printf("\n");  
}  
int main()  
{   
    sigset_t s;  
    sigemptyset(&s);   
    sigaddset(&s,2);  

    sigset_t oldset;   
    sigprocmask(SIG_SETMASK,&s,&oldset);

    signal(2,myhander);
    int count=0;  
    while(1)  
    {  
        sigset_t s1;  
        sigprocmask(0,NULL,&s1);  
        printf("block list:");  
        PrintPending(s1);  

        sigset_t p;  
        sigpending(&p);
        printf("pend  list:");  
        PrintPending(p);

        if(count==10)  
        {  
            sigprocmask(SIG_SETMASK,&oldset,&s);   
        }  
        sleep(1);  

        ++count;  
    }  
    return 0;  
}  

(1)在前10秒count<=10时,2号信号被阻塞,此时block表与pending表依次为:
这里写图片描述
在前10秒因为2号信号被阻塞,若这时键盘发送Ctrl-C,2号信号不会递达(不执行自定义捕捉函数),其会先保持在未决状态,此时block表与pending表依次为:
这里写图片描述
pending表的2号位置为1
这里写图片描述
我们继续发送,效果还是一样的 2号还是1
这里写图片描述
10s发送之后,阻塞取消,信号发送成功。此时我们再发2号信号结果还是0。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值