.信号的概念

1.1 信号的机制

信号是软件层面上实现的中断,早期常被称为“软中断”

由于信号是通过软件方法实现的,其实现手段导致信号有很强的延时性,但是对用户而言,这个延时时间非常短,不易察觉。

每个进程收到的所有信号都是由内核复杂发送和处理


1.2 信号的状态

1.递达:递送并且到达进程

2.未决:产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。

 

1.3 信号的处理方式

1.执行默认动作

2.忽略(丢弃)

3.捕捉(调用用户处理函数)

PSLinux内核的进程控制块PCB是一个结构体,task_struct, 除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集。

 

1.4 阻塞信号集(信号屏蔽字):将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,再收到该信号,该信号的处理将推后(解除屏蔽后)

 

1.5 未决信号集

1.信号产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应位翻转回0。这一时刻往往非常短暂。

2.信号产生后由于某些原因(主要是阻塞)不能抵达。这类信号的集合称为未决信号集。在屏蔽解除前,信号一直处于未决状态。

 

1.6 信号的4要素

1.编号

2.名称

3.事件

4.默认处理动作:

       Term:终止进程

       lgn    忽略信号(默认即时对该种信息忽略操作)

       Core:终止进程,生成Core文件。(查验进程死亡原因,用于gdb调试)

       Stop:    停止(暂停)进程

       Cont:继续运行进程

PS: 查看信号的相关信息可以通过 man 7 signal命令来查阅man page文档

.产生信号的五种方式:

2.1 终端按键产生信号

Ctrl+c ------ 2) SIGINT(终止/中断) “INT” == interrupt

Ctrl+z ------ 20) SIGTSTP(暂停/停止) “T” == terminal 终端

Ctrl+\-------3) SIGQUIT(退出)

 

2.2 系统调用产生

1.kill:给指定进程发送指定信号(不一定杀死)

int kill(pid_t pid, int sig); 成功:0;失败:-1ID非法,信号非法,普通用户杀init进程等权级问题),设置errno

       sig:不推荐直接使用数字,应使用宏名,因为不同操作系统信号编号可能不同,但名称一致。

       pid > 0:发送信号给指定进程。

       pid == 0:发送信号给与调用kill函数进程属于同一进程组的所有进程。

       pid < 0:取|pid|发给对应的进程组。

       pid == -1:发送给进程有权限发送的linux系统中所有进程

       PS

       进程组:每个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组长,默认进程组ID与进程组长ID相同。

       权限保护super用户(root)可以发送信号给任意用户,普通用户是不能向系统用户发送信号的。kill -9 (root 用户的pid)是不可以的。同样,普通用户也不能向其他普通用户发送信号,终止其进程。只能向自己创建的进程发送信号。普通用户基本规则是:发送实际或有效用户ID==接收者实际或有效用户ID

 

2.raise:给当前进程发送指定信号(自己给自己发)

raise(signo) == kill(getpid(), signo);

int raise(int sig); 成功:0,失败:非0

 

3.abort:给自己发送异常终止信号 -------6SIGABRT信号,终止并产生core文件

void abort(void); 改函数无返回

2.3 软件条件产生

1.alarm函数:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14SIGALRM信号。进程收到该信号,默认动作终止。

PS:每个进程有且只有唯一一个定时器,定时与进程状态无关(自然定时法),就绪,运行,挂起(阻塞,暂停)、终止、僵尸无论进程处于何种状态,alarm都计时。

unsigned int alarm(unsigned int seconds); 返回0或剩余的秒数,无失败。

常用:取消定时器alarm(0)或返回久闹钟余下秒数。

2.setitimer函数:设置定时器(闹钟)。可代替alarm函数。精确微秒us,可以实现周期定时。

int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);

参数:which:指定定时方式

1.自然定时:ITIMER_REAL ----14)SIGLARM ----计算自然时间

       2.虚拟空间计时(用户空间):ITIMER_VIRTUAL-----26)SIGVTALRM ---只计算进程占用CPU的时间

       3.运行时计时(用户+内核):ITIMER_PROF---27SIGPROF ---计算占用CPU以及执行系统调用的时间

 

struct itimerval里面的参数

              it_interval:用来设定两次定时任务之间间隔的时间

              it_value:定时的时长

 

DEMO利用setitimer周期打印信息

#include <iostream>

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/time.h>

#include <signal.h>

 

typedef void(*sighandler_t) (int);

 

using namespace std;

 

void do_something(int signalno){

       cout << "catch a signal" << endl;

       cout << "signal number: " << signalno << endl;

}

 

int main(){

 

       sighandler_t handler = signal(SIGALRM, do_something);

 

       if(handler == SIG_ERR){

              perror("signal error");

              exit(1);

       }

 

       struct itimerval it;

 

       it.it_interval.tv_sec = 2;

       it.it_interval.tv_usec = 0;

       it.it_value.tv_sec = 2;

       it.it_value.tv_usec = 0;

 

       setitimer(ITIMER_REAL, &it, NULL);

 

       int second = -1;

       while(true){

              cout << ++second % 2 + 1 << " second" << endl;  

              sleep(1);

       }

 

       return 0;

}

 

 

 

2.4 硬件异常产生信号

非法访问内存(段错误)--------11) SIGSEGV(段错误)

0错误---------8) SIGFPE(浮点数除外)”F” == float

内存对齐出错(总线错误)--------7)SIGBUS


2.5 命令产生

kill, killall

 

.信号集操作函数

内核通过读取未决信号集来判断信号是否应被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。已达到屏蔽指定信号的目的。


3.1 信号集设定

sigset_t set;     //typedef unsigned long sigset_t

int sigemptyset(sigset_t *set);  //将某个信号集清成功:0; 失败:-1

int sigfillset(sigset_t *set);//将某个信号集置1成功:0; 失败:-1

int sigaddset(sigset_t *set, int signum);//将某个信号加入信号集 成功:0; 失败:-1

int sigdelset(sigset_t *set, int signum);//将某个信号清出信号集 成功:0; 失败:-1

int sigismember(const sigset_t *set, int signum);//判断某个信号是否在信号集中 返回值:在集合:1; 不在:0;出错:-1

PSsigset_t 类型的本质是位图。但不应该直接使用位操作,而应该使用上述函数,保证跨系统操作有效。

 

3.2 sigprocmask函数

用来屏蔽信号解除屏蔽也使用该函数。其本质,读取或修改进程的信号屏蔽字(PCB中)

严格注意:屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略表示将信号丢弃处理。

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);成功:0,失败:-1,设置errno

参数:

set:传入参数,是一个位图,set中哪个位置置1,就表示当前进程屏蔽哪个信号。

oldset:传出参数,保存旧的信号屏蔽集。

how参数取值:假设当前的信号屏蔽字为mask

1.SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号。相当于mask = mask | ~set

2.SIG_UNBLOCK:当how 设置为此,set表示需要解除屏蔽的信号。相当于mask = mask & ~set

3.SIG_SETMASK:当how 设置为此,set表示用于替代原始屏蔽集的新屏蔽集。相当于mask = set

PS:调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。


3.3 sigpending函数

读取当前进程的未决信号集

int sigpending(sigset_t *set);

set 为传出参数。

返回值:成功:0;失败:-1,设置errno


.信号捕捉

4.1 signal函数

使用signal注册一个信号捕捉函数:

      typedef void(*sighandler_t) (int);

       sighandler_t signal(int signal_no, sighandler_t handler);

 

该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。因此应该尽量避免使用它,取而代之使用sigaction函数


4.2 sigaction函数

修改信号处理动作(通常在Linux用其来注册一个信号的捕捉函数)

#include <signal.h>

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

参数:

       signum:信号编号

       act:传入参数,新的处理方式

       oldact:传出参数,旧的处理方式。

       struct sigaction结构体

       struct sigaction {

               void     (*sa_handler)(int);

               void     (*sa_sigaction)(int, siginfo_t *, void *); //较少使用

               sigset_t   sa_mask;

               int        sa_flags;

               void     (*sa_restorer)(void); // 过时,弃用

           };

       其中

       1.sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可以赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作。

       2.sa_mask:调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性的设置。

      

       3.sa_flags0,表使用默认属性(回调函数执行期间自动屏蔽本信号)

                SA_SIGINFO:选用sa_sigaction来指定捕捉函数

                SA_INTERRURT:系统调用被信号中断后,不重启

                SA_RESTART:自动重启

                SA_DEFER:不自动屏蔽本信号



信号捕捉特性

1.进程正常运行时,默认PCB中有一个信号屏蔽字,它决定了进程自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到该信号后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由这个默认的信号屏蔽字来指定,而是用sa_mask来指定。调用完信号处理函数,再恢复使用默认信号屏蔽字。

2.XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。

3.阻塞的常规信号不支持排队,产生多次只记录一次(后32个实时信号支持排队)

 

4.3 Linux内核实现信号捕捉过程

内核实现信号捕获原理.png

DEMO

捕捉Ctrl + c信号和周期性的SIGALRM信号

#include <iostream>

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/time.h>

#include <signal.h>

#include <string>

 

using namespace std;

 

void do_something(int signalno){

       cout << "catch a signal" << endl;

       cout << "signal number: " << signalno << endl;

       cout << "signal name: ";

       cout << (signalno == SIGINT ? "SIGINT" : "SIGALRM");      

       cout << endl;

             

       int cal = 0;

       while(cal++ < 2){

              cout << "Dealing with the signal of ";

              cout << (signalno == SIGINT ? "SIGINT" : "SIGALRM");

              cout << endl;

              sleep(1);

       }

 

}

 

int main(){

 

       sigset_t set, time_set;

       sigemptyset(&set);

       sigemptyset(&time_set);

 

       //SIGALRM加入屏蔽集,在Ctrl + c信号处理期间将忽略SIGALRM信号

       sigaddset(&set, SIGALRM);

       //SIGINT加入屏蔽集, SIGALRM信号处理期间将忽略终端Ctrl+c信号

       sigaddset(&time_set, SIGINT);

 

       struct sigaction act, time_act;

       act.sa_handler = do_something;

       act.sa_mask = set;

       act.sa_flags = 0;

 

       time_act.sa_handler = do_something;

       time_act.sa_mask = time_set;

       time_act.sa_flags = 0;

 

       //注册Ctrl + c信号捕捉函数

       sigaction(SIGINT, &act, NULL);

       //注册SIGALRM信号捕捉函数

       sigaction(SIGALRM, &time_act, NULL);

 

       struct itimerval it;

 

       it.it_interval.tv_sec = 2;

       it.it_interval.tv_usec = 0;

       it.it_value.tv_sec = 2;

       it.it_value.tv_usec = 0;

 

       setitimer(ITIMER_REAL, &it, NULL);

 

       int second = -1;

       while(true);

 

       return 0;

}