信号

信号的概念

  • 摔杯为号

  • 一只穿云箭,千军万马来相见

从这里可以看出信号几个特性,简单,迅速,明显,有条件触发的,多种的

信号是信息的载体,linux 古老的经典的通信方式,主要的通信手段

早期的信号不是很可靠,POSIX.1对可靠信号例程进行了标准化

信号基于linux内核进程间通信,内核发送内核处理

时钟中断基于硬件

信号早期相当于软中断

软中断与硬件中断相比实际上并不是很可靠

信号的4要素

  • 信号名
  • 信号编号
  • 信号默认处理动作
    • term:终止进程
    • ign:忽略,子进程死亡,默认会向父进程发送这样的信号,让父进程保持运行,比如SIGHLD
    • Core:终止进程,GDB等调试的工具中比较常见
    • Stop:停止(暂停)进程
    • Cont:继续
  • 信号对应事件

man 7 signal查看帮助文档

信号的产生5种方式

注意:这里讨论的是ARM/X86的系统内核下的信号编号

  • 按键产生
    • Ctrl+c 产生SIG INT(2)终止/中断(interrupt)
    • Ctrl+z 产生SIG T STP(20)终端暂停
    • Ctrl+\ 产生SIGQUIT(3)退出
  • 系统调用
    • kill,raise,
  • 软件条件
    • 定时器alarm
  • 硬件异常
    • 内存非法访问 SIGSEGV(11)段错误
    • 除零 SIGFPE(8) 浮点数例外
    • 总线 SIGBUS(7)总线错误
  • 命令产生
    • kill命令 默认信号是11(终止进程)

信号状态

递达态

信号产生后,内核会立刻递送,并到达,速度相当于光速,可以认为是瞬时的;

阻塞态(屏蔽)(未决)

信号产生后不可递达目的地接收时的状态

记录着信号状态的是位于进程控制块PCB中的

  • 阻塞信号集(信号屏蔽字)
  • 未决信号集
    • 没有进行处理的信号集合

在产生信号的前提下,阻塞信号集影响未决信号集

信号的3种处理方式

  • 执行信号的默认动作
  • 草,走,忽略(丢弃)
  • 捕捉(调用户的处理函数)

异常处理

当出现信号都被屏蔽,捕捉或者忽略,唯一不会被阻塞的忽略的捕捉的两个信号

  • SIGKILL(9)
    • 杀死进程
  • SIGSTOP(19)
    • 停止进程

常见信号产生函数

kill

注意kill函数其实并不是真的杀死进程,需要根据你选择发送的信号决定

很简单看 man 文档就能知道了,这里直接上示例

1
int ret = kill(pid, SIGKILL);

pid有四个特殊值:

  • 0 同一个进程组的所有进程
  • >0指定进程
  • \<0 指定进程组
  • -1表示可发送的(权限)所有进程

raise

作用给当前进程发送一个信号(给自己发)

1
int ret = raise(SIGSTOP);

abort

作用就是自己给自己发一个异常信号

1
abort();

软件条件产生信号

alarm

设置一个定时器,设定内核指定时间发送一个SIGALRM(14)信号

每个进程只有一个定时器

自然定时法,与进程的状态无关

返回值比较有意思,返回的是上一次调用距离当前调用的时间差

测试我的计算机一秒能数多少个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <unistd.h>
#include<stdio.h>

int main (){
//计算我的计算机一秒能执行多少次++的操作
long count = 0;
alarm(1);
while (1)
{
count++;
printf("%ld \n", count);
}
return 0;
}

发现我的一秒只能执行140615次打印和++操作,确实挺拉的

如果将打印操作重定向out中,可以

1
cat out | awk 'END {print}'

发现执行10590892了次++

可以发现IO极大的拖慢执行效率

实际执行时间 = 系统时间 + 用户时间 +等待时间 (等硬件资源,系统资源)

可以用 time 看出具体的系统时间和用户时间

setitimer函数

功能比timer更好更全,所以普遍使用该函数作为定时器

  • 可指定的三种计时方式
    • TIMER_REAL自然定时 发送的信号SIGALRM
    • TIMER_VIRTUAL虚拟空间计时(用户空间)发送的信号 SIGVTALRM
    • ITIMER_PROF运行时计时(用户+内核)发送的信号SIGPROF
    • 注意三种定时器信号意味着一个进程中可以同时设置三种不同类型的定时器
  • 可以指定精度微秒,可循环的定时器

通过代码进行分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>

unsigned int my_alarm(unsigned int sec){
//定义存储定时器的数据的结构体变量
struct itimerval it, oldit;
int ret;
it.it_value.tv_sec = sec;//秒
it.it_value.tv_usec = 0;//微秒
//下一次的定时定时器的时间
//设置为0说明没有下一次定时器,只有一个
it.it_interval.tv_sec = 0;
it.it_interval.tv_usec = 0;
//第一个参数是选择计时类型,第二个是传入定时器的数据,第三个是传出参数,接收的是上一个定时器的间隔时间差
//有了计时类型就同时设定三种不同类型的定时器
ret = setitimer(ITIMER_REAL, &it, &oldit);
if(ret == -1){
perror("setitimer");
exit(1);
}
return oldit.it_value.tv_sec;
}

int main(){

int i = 0;
my_alarm(1);
for(; ; i++){
printf("%d\n");
}
return 0;

}

如果要实现一个循环的定时器(周期定时)代码实现如下

具体:实现一个循环1秒发送SIGALRM信号的定时器,和一个0.5s的发送SIGTVALRM信号的定时器。信号调用用户自定义的行为:显示抓取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <sys/time.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void sigroutine(int signo){
switch (signo)
{
case SIGALRM:
printf("Catch a signal - SIGALRM\n");
break;
case SIGVTALRM:
printf("Catch a signal - SIGTVALRM\n");
default:
break;
}
return ;
}

void my_alarm(unsigned int sec, unsigned int usec, int timetype ){
struct itimerval it;
int ret;
it.it_value.tv_sec = sec;
it.it_value.tv_usec = usec;

it.it_interval.tv_sec = sec;
it.it_interval.tv_usec = usec;
int type = timetype;
ret = setitimer(type, &it, NULL);
if(ret == -1){
perror("setitimer");
exit(1);
}
return;

}

int main(){
signal(SIGALRM, sigroutine);
signal(SIGVTALRM, sigroutine);
my_alarm(1, 0, ITIMER_REAL);
my_alarm(0, 500000, ITIMER_VIRTUAL);
for( ;; );
}

c++的写法可以使用lambda函数写捕获能精简自定义响应函数部分

信号集操作函数

对未决信号集和阻塞信号集的操作

信号集的设定

本质上是一个集合,一个无序不重复的数组,类似C++的set容器,在c库types.h中定义了一个sigset_t,用来存放信号集合

提供了5个函数对该sigset_t进行操作

  • sigemptyset(sigset_t* set) ; 将信号集清0
  • sigfillset(sigset_t* set ); 将信号集设置为1
  • sigaddset(sigset_t* set, int signum);将某一个信号加入集合当中,信号编号对应值设置为1
  • sigdelset(sigset_t* set, int signum);将某一个信号删除。将编号对应值设置为0
  • sigismember(sigset_t* set, int signum):取信号在该信号集的对应的值

实际上未决信号集屏蔽信号集都在pcb中,用户函数是无法直接对其进行位操作,正常也不会

sigpromask()

我们需要使用一个函数对将我们自定义的集合设置到屏蔽信号集中

1
int ret = sigprocmask(int how, const sigset_t* set,  sigset_t* oldset)

需要注意的是,屏蔽实际上只是暂缓信号,直到解除屏蔽

  • SIG_BLOCK 表示需要屏蔽的信号 mask = mask | set
  • SIG_UNBLOCK 表示需要解除屏蔽的信号, mask = mask& ~set
  • SIG_SETMASK 表示set用于替代原始屏蔽集的新屏蔽集 mask = set

sigpending()

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

1
int sigpending(sigset_t* set);

程序示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void printPending(sigset_t* pend){
for (int i = 1; i < 33; i++)
{
if(sigismember(pend,i)){
putchar('1');
}else{
putchar('0');
}
}
printf("\n");

}


int main (){
sigset_t myset, oldset, pend;

sigemptyset(&myset);
sigaddset(&myset, SIGQUIT);
sigaddset(&myset, SIGTSTP);
sigaddset(&myset, SIGINT);

sigprocmask(SIG_BLOCK, &myset, &oldset);

while(1){
sigpending(&pend);
printPending(&pend);
sleep(1);
}
return 0;
}

信号捕捉

signal

注册一个内核信号捕捉的行为

类似qt信号槽,前端插件开发中的hooks钩子

代码示例

1
2
3
4
5
__sighandler_t  handler= signal(SIG, handler);
if(handler == SIG_ERR){
perror("signal error");
exit
}

注意返回的值是一个函数指针,指向的其实就是handler这个函数

由此可见这是一个典型的callback函数

signal会打破进程的睡眠状态无论sleep(N) N值有多大,只要接受到相应信号将会打破主函数当前的sleep状态,执行下一条语句

1
2
3
4
5
6
7
signal(SIGALRM, signal_func);
signal(SIGINT, signal_func);
alarm(1);
while (1) {
sleep(300);//会直接打断,继续下一条语句
printf("主函数打印 %d\n", getpid());
}

sigaction

作用比signal更强。

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

先来看看这个结构体又是什么构造,看看man文档

  • sa_sigaction

    • 扩展信号响应函数指针
  • sigset_t sa_mask

    • 使用sa_mask 进行信号的临时屏蔽阻塞
      • 一般用于捕捉函数需要运行中收到同一个或者其他信号时,保证捕捉响应函数处理结束
      • 仅在捕捉响应函数执行期间生效
  • int sa_flag

    • 标志位
    • 0 采用默认的属性,在信号捕捉函数执行期间自动屏蔽本信号
    • SA_NOCLDSTOP 子进程暂停时不提醒
    • SA_NOCLDWAIT 子进程死亡不回收
    • SA_NODEFER 不屏蔽信号响应函数中的信号
    • SA_ONSTACK 信号响应函数在替补栈中分配内存
    • SA_RESETHAND 响应函数执行一遍重置信号响应策略(只执行一次)
    • SA_RESTART 自动重启被该信号中断的某些系统调用
    • SA_SIGINFO 使用扩展信号响应函数而不是标准响应函数
  • viod* (sa_handler)(int);

    • 阔号代表两个可用类型
    • 实际上就是捕捉函数的函数指针

多说无益代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>

void docatch(int signo){
printf("%d signal is caught\n", signo);
//sleep(10);
}

int main (){
struct sigaction act;
act.sa_handler = docatch;
sigemptyset(&act.sa_mask);
// sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;

int ret = sigaction(SIGINT, &act, NULL);
if(ret < 0){
perror("sigaction error");
exit(1);
}

for( ;; );
return 0;
}

需要注意的特性

  • PCB中的信号屏蔽集mask和函数提供的sa_mask,在响应函数运行期间sa_mask最优

  • 当出现多个同一个信号被屏蔽阻塞,解除屏蔽后,只会处理一次信号,不户籍进行排队

内核捕捉信号过程

  1. 执行主控制流程的某条指令时因为中断或者异常系统调用进入内核
  2. 内核处理完异常准备回用户模式之前,先处理当前进程中可以递送的信号
  3. 如果信号的处理动作 是自定义的信号处理函数,则回到用户模式执行信号处理函数(而不是回到主控制流程)
  4. 信号处理函数返回时执行特殊的系统调用sigreturn 再次进入内核
  5. 在内核中,在返回用户模式,从主控制流程中上次被中断的地方继续向下执行

图,改日再画