【Linux】第七章-进程信号

第七章-进程信号

信号基本认识

  生活中处处充满信号,信号的存在就是是为了传递和表达信息,通知事件的发生。并且信号是有生命周期的,信号在产生后到处理完毕之前才是信号的有效期。

  在对于系统来说,当软件中断,便会产生信号,通知发生了某件事情,为了说明不同的事件,所以系统中的信号有不同的种类。

信号的种类

  在Linux下我们可以使用kill -l产看信号的种类,会发现一共有62种不同的信号。其中1-31号信号每个都有各自对应的事件,它们继承于Unix,被称为非可靠信号/非实时信号;而34-64号信号是后续添加的信号,并无对应的事件,成为可靠信号/实时信号。所谓可靠信号与非可靠信号的区别在于非可靠信号是可能会丢失的,有可能产生多次信号才只会处理一次,而可靠信号产生多少次信号就必定会执行多少次。实时信号与非实时信号的区别在于实时信号在信号产生后便会立刻处理,而非实时信号则可能会延时处理。

信号的生命周期

  操作系统产生信号,注册在进程中,进程就收到了信号,但并不会立刻处理,在处理前会先注销信号,之后才会处理信号。

    产生->信号在进程中注册->信号在进程中注销->处理信号

信号的产生

  信号的产生分为两个途径:硬件产生软件产生

硬件产生

  我们在终止我们的进程时往往会使用快捷键ctrl + c,其实这个快捷键原理就是操作系统向进程发送了SIGINT信号造成进程结束,这就是硬件产生的信号。由硬件产生的信号还有很多例如ctrl + |:终止信号ctrl + z 停止信号等等。

软件产生

  我们在强杀进程时会使用kill -9 pid,这里kill就是一个软件,会向指定进程发送指定信号,在使用强杀时则是发送了9号信号,将进程强制杀死。

  同时我们还可以在进程中向进程发送信号,C语言中将信号已经提前定义成了宏为了方便我们使用一些函数来发送信号。

kill()

  int kill(pid_t pid, int sig);函数来向指定进程发送指定信号。

raise

  使用int raise(int sig);向调用进程发送指定信号。

abort

  使用abort()向调用进程发送SIGABRT信号。

alarm

  使用unsigned int alarm(unsigned int seconds);seconds秒后向调用进程发送信号。

核心转储

  核心转储文件(core dump)是在事件发生后记录事件发生的过程和原因的文件,方便我们事后调试,由于核心转储文件占用空间,默认是关闭的。我们可以用ulimit -a查看core dump是否开启,使用ulimit -c 大小设置核心转储文件大小,开启核心转储。在发生段错误后我们即可使用gdb core.pid来查看核心转储文件的信息。

  不过,我们直到在发生内存错误的时候Linux会报错说产生段错误并且退出进程,产生核心转储文件,但其实进程退出并非是由进程自己退出的,而是收到了操作系统发出的11号信号SIGSEGV,一旦进程收到这个信号便会终止并且报错说产生段错误随后生成核心转储文件,因此进程的退出和终止其实是由这个信号导致的。当发生内存错误时系统会自动调用kill()函数发送SIGSEGV信号到该进程,终止进程。

信号的注册

  给一个进程注册信号是在进程的pcb中进行了一个标记,这个标记由一个结构体sigset_t利用位图构成信号集进行标记,并且存入pcb中的pending信号集中,用来对应62种信号是否已经注册到进程中。同时还有另外一个sigqueque队列,用来按照注册顺序逐一处理信号。信号传入进程将pending上对应位置1表示此类信号存在,随后将信号存入sigqueque队列。但非可靠信号和可靠信号的注册还有区别。

非可靠信号的注册

  当信号传给进程时,进程会判断pending集合位图相应位是否为1;若为0则将其置1,并将其组织到sigqueque队列中;若为1则什么都不做(信号丢失)。

可靠信号的注册

  信号传递给进程时,进程不管位图上是否为1,都会将位图置1,并且再添加到sigqueque队列中,如果有多次传入则拥有多个结点(信号不会丢失)。

信号的注销

非可靠信号的注销

  因为非可靠信号的信号结点只有一个,因此删除结点,位图直接置0;

可靠信号的注销

  因为可靠信号的信号结点可能有多个,若还有相同结点,则位图依然置1,否则置0。

信号的处理

  信号处理有三大方式:默认,忽略,自定义。

信号捕捉初识

signal()

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
46
47
48
49
50
51
52
#include <stdio.h>                           
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
/**
*
* typedef void (*sighandler_t)(int);
* sighandler_t signal(int signum, sighandler_t handler);
* signum: 信号编号
* handler: 信号处理方式,函数指针
* SIG_DFL 默认处理方式
* SIG_IGN 忽略处理方式
* 修改信号的处理方式
*/
void sigcb(int signo)
{
printf("signal:%d\n", signo);
}
int main()
{
//signal(SIGINT, SIG_DFL);//执行默认的SIGINT功能
//signal(SIGINT, SIG_IGN);//忽略SIGINT信号的处理
signal(SIGINT, sigcb);//按自定义方式处理
while(1)
{
printf("----------\n");
sleep(1);
}
}



[misaki@localhost 第七章-进程信号]$ ./signal
----------
----------
^Csignal:2
----------
----------
----------
----------
^Csignal:2
----------
^Csignal:2
----------
^Csignal:2
----------
^Csignal:2
----------
----------
----------
----------
^\退出(吐核)

  可以看到我们再向进程发送SIGINT信号时,执行的已经是我们自定义的功能了。

  信号默认的处理方式和忽略处理方式都是在内核中定义好的,那么自定义处理方式又是怎么捕捉的呢?

  当进程因为系统调用、中断或者异常进入内核态,当完成内核功能,准备返回用户态主控流程时检测是否有信号待处理,如果有调用do_signal()函数处理信号,如果信号处理方式是自定义的则返回用户态执行自定义处理函数,再通过sigreturn()返回内核态,再次检查是否有信号待处理,若无则调用sys_sigreturn返回用户态从主控流程上次被中断的地方继续执行。

sigaction()

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
#include <stdio.h>                                                           
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>
struct sigaction act, oldact;
void sigcb(int signo)
{
printf("signal:%d\n", signo);
//将原本的处理方式还原
sigaction(SIGINT, &oldact, NULL);
}
int main()
{
//int sigaction(int signum, struct sigaction* act, struct sigaction* oldact);
//使用act动作替换signum原有的处理动作,并且将原有处理动作拷贝到oldact中
//struct sigaction是预先定义好的结构体,用于存放信号修改的功能的数据
act.sa_handler = sigcb;
act.sa_flags = 0;
//清空信号集合
sigemptyset(&act.sa_mask);
sigaction(SIGINT, &act, &oldact);
while(1)
{
printf("----------\n");
sleep(1);
}
}


[misaki@localhost 第七章-进程信号]$ ./signal
----------
----------
^Csignal:2
----------
----------
^C

  这段代码中我们先将SIGINT的处理方式替换为我们自定义的方式,调用一次自定义处理的同时又将其还原。

信号的阻塞

  暂时阻止信号被递达,信号依然可以注册,只是暂时不处理,解除阻塞之后才会处理。

  信号的递达:动作-信号的处理。

  信号的未决:状态-信号冲产生到处理之前的状态。

  在进程的pcb中有一个blocked集合,当某一种信号被阻塞时,会将blocked集合中对应的位置1,表示这种信号被阻塞。因此信号的阻塞过程实际就是在pcb的blocked信号阻塞集合中标记信号,信号到来之后暂时不处理。

  但是有两个信号无法被阻塞——SIGKILL-9,SIGSTOP-19无法被阻塞,无法被自定义,无法被忽略。

竞态条件

  因为运行时序而造成数据竞争——导致数据二义性。函数中所完成的操作并非原子性操作,并且操作的数据是一个全局数据。如果一个函数中操作了全局性数据,并且这个操作不是原子性操作,并且这个操作不受保护,则这个函数是一个不可重入函数。不可重入函数指不能在多个时序的运行中重复调用(重复调用有可能会造成数据二义性)。可重入函数指在多个是虚的运行中重复调用,不会造成异常影响。malloc/free就是不可重入函数。

volatile关键字

  保持内存可见性——每次操作变量都需要重新从内存中获取。防止编译器过度优化。

SIGCHLD

  自定义信号处理方式sigcb,当子进程退出,操作系统发送信号给父进程,直接触发信号回调sigcb,用户主要在sigcb中调用wait/waitpid就可以处理子进程的退出。

  SIGCHLD是一个非可靠信号,假如有多个子进程同时退出了,则有可能造成时间丢失,造成sigcb只被回调一次,只处理了一个子进程。

  必须使用非阻塞,否则没有子进程退出的时候waitpid将阻塞导致程序无法回到主控流程。

-------------本文结束感谢您的阅读!-------------
记录学习每一分,感谢您的赞助