信号和中断
“中断”是从I/O设备或协处理器发送到CPU的外部请求,它将CPU从正常执行转移到中断处理。而“信号”是要发送给进程的请求。
进程定义:一个“进程就是一系列活动”
中断是发送给进程的事件,它将“进程”从正常活动转移到其他活动,称为“中断处理”。“进程”可在完成“中断”处理后恢复正常活动。“中断”一词可以应用于任何“进程”,并不限于计算机中的CPU。每个中断都分配的有一个唯一的ID识别号,并有一个预先安装的动作函数。
进程中断
这类中断时发送给进程的中断。当某进程正在执行时,可能会受到来自3个不同来源的中断:
- 来自硬件的中断:终端、间隔定时器的"Ctrl+C"组合键等。
- 来自其他进程的中断:kill(pid,SIG#)、death_of_child等。
- 自己造成的中断:除以0、无效地址等。
每个进程中断都被转化为一个唯一的ID号,发送给进程。我们始终可限制在一个进程中的中断的数量。Unix/Linux中的进程中断称为信号,编号为1到30。进程的PROC结构体中有对应每个信号的动作函数,进程可在收到信号后执行该动作函数。
硬件中断
这类中断是发送给处理器或CPU的信号。他们也有三个可能的来源:
- 来自硬件的中断:定时器、I/O设备等。
- 来自其他处理器的中断:FFP、DMA、多处理器系统中的其他CPU。
- 自己造成的中断:除以0、保护错误、INT指令。
每个中断都有一个唯一的向量号。CPU不会自己造成中断,基本是由外部终端导致的。
进程的陷阱错误
进程可能会自己造成中断。这些中断是由被CPU识别为异常的错误引起的,例如除以0、无效地址、非法指令、越权等。当进程遇到异常时,它会陷入操作系统内核,将陷阱原因转化为信号编号,并将信号发送给自己。如果在用户模式下发生异常,则进程的默认操作是终止,并使用一个可选的内存存储进行调试。如果在内核模式下发生陷阱,原因一定是硬件错误,或者很可能是内核代码中的漏洞,在这种情况下,内核无法处理。在Unix/Linux中,内核只会打印一条PANIC错误消息,然后就停止了。
Unix/Linux信号时=示例
(1)按“Ctrl+C”组合键通常会导致当前运行的进程终止。它会产生一个键盘硬件中断,键盘中断处理程序将“Ctrl+C”组合键转换为SIGINT(2)信号,发送给终端上的所有进程,并唤醒等待键盘输入的过程。在Linux中国,exitValue的低位字节是导致进程终止的信号编号。
(2)用户可使用nohup a.out&命令在后台运行一个程序。即使在用户退出后,进程仍将继续运行。nohup命令会使sh像往常一样复刻子进程来执行程序,但是子进程将会忽略SIGNUP(1)信号。
(3)用户可执行kill pid (or kill -s 9 pid)
来杀死该进程。执行杀死的进程向目标进程发送一个SIGTERM(15)信号,请求它死亡。目标进程将会遵从请求并终止。如果进程选择忽略SIGTERM信号,它可能拒绝死亡。而kill -s 9 pid
是杀死进程的终极手段。
Unix/Linux中的信号处理
执行kill -l命令可以查看当前系统可能产生的所有信号:
信号的来源
- 来自硬件中断的信号:在进程执行过程中,一些硬件中断被转换为信号发送给进程。硬件信号的示例是:
- ①中断键(Ctrl+C),他产生一个SIGINT(2)信号。
- ②间隔定时器,产生SIGALRM(14)等信号。
- ③其他硬件错误,如总线错误、IO错误等。
- 来自异常的信号:当用户模式下的进程遇到异常时,会陷入内核模式,生成一个信号,并发送给自己。常见的陷阱信号有SIGFPE(8),表示浮点异常(除以0),最常见也是最可怕的是SIGSEGV(11),表示段错误。
- 来自其他进程的信号:进程可使用kill(pid,sig)系统调用向pid标识的目标进程发送信号。
进程PROC结构体中的信号
每个进程PROC都有一个32位向量,用来记录发送给进程的信号。在位向量中,每一位(0位除外)代表一个信号编号。此外,它还有一个信号MASK位向量,用来屏蔽相应的信号。待处理信号只有在未被屏蔽的情况下才有效。因此这样可以让进程延迟处理被屏蔽的信号,类似于CPU屏蔽某些中断。
信号处理函数
每个进程的PROC中都有一个信号处理数组 int sig[32]
,sig[32]指向对应的信号处理方式。0表示默认DEFault,1表示忽略IGNore,其他值则对应其他用户模式下预先安装的信号处理(捕捉)函数。
如果信号位向量中的位为1,则会生成一个信号1或将其发送给进程。如果屏蔽位向量的位I为1,则信号会被阻塞或屏蔽。否则,信号未被阻塞。只有当信号存在并且未被阻塞时,信号才会生效或传递给进程。
安装信号捕捉函数
int r = signal(int signal_number, void *handler);
是信号捕捉的系统调用,可以用来修改选定的信号编号的处理函数(SIGKILL(9)和SIGSTOP(19)除外),
- 第一个参数,是信号的序号,上面已经列出了信号以及对应的序号,既可以用序号,也可以使用信号名
- 第二个参数,是接收到信号以后处理信号的函数指针
- 返回值:调用成功,则返回上一次信号处理函数被调用的返回值;调用失败,返回SIG_ERR
因为存在无法找到对应的函数的情况,所以就无法搭建起信号和信号处理函数之间的联系
例如:
signal(2,signalhandler); //建立起2号信号和signalhandler函数之间的关系
//等实际收到2号信号时,就会执行signalhandler函数
已安装的信号处理函数将会进入捕捉函数入口:void catcher(int signal_number){···}
sigaction()
系统调用是更好的信号捕捉函数,它能够在执行当前捕捉函数时自动阻塞下一个信号,不会出现竞态条件,它的结果如下:
int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact);
sigaction的结构体如下:
struct sigaction{
void (*sa_handler)(int);//指向处理函数
void (*sa_sigaction)(int ,siginfo_t * ,void *);//是运行信号处理函数的另一种方法,其中signfo_t*能够接受更多的信号
sigset_t sa_mask;//可在处理函数执行期间设置要阻塞的信号
int sa_flags;//修改信号处理进程的行为,若要使用sa_sigaction处理函数,此时本参数的值应为SA_SIGINFO
void (*sa_restorer)(void);
}
sigaction()的使用示例:
代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
#include<setjmp.h>
#include<string.h>
jmp_buf env;
int count = 0;
void handler(int sig, siginfo_t *siginfo, void *context)
{
printf ("handler: sig=%d from PID=%d UID=%d count=%d\n",
sig, siginfo->si_pid, siginfo->si_uid, ++count);
if (count >= 4) // let it occur up to 4 times
longjmp(env, 1234);
}
int BAD()
{
int *ip = 0;
printf("in BAD(): try to dereference NULL pointer\n");
*ip = 123; // dereference a NULL pointer
printf("should not see this line\n");
}
int main (int argc, char *argv[])
{
int r;
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_sigaction = &handler;
act.sa_flags = SA_SIGINFO;
sigaction(SIGSEGV,&act,NULL);
if ((r = setjmp(env)) == 0)
BAD();
else
printf("proc %d survived SEGMENTATION FAULT: r=%d\n",getpid(), r);
printf("proc %d looping\n",getpid());
while(1);
}
运行结果如下:
进程处理的一些理解
进程处理的过程
进程收到某种信号的时候,并不是立即处理的。比如远处看到红绿灯变成红灯,我们会立即停下吗?并不会,我们会把看到红灯这件事记录在大脑中,等走到路口再停下。进程当前可能在执行优先级更高的东西,所以要选择合适的时候再处理这个信号。已经到来的信号会被暂时保存起来,以供在合适的时候处理,应该保存在哪里呢——进程控制块task_struct
。
信号发送的过程可以通过下面这张图来表示:
(1) 信号发送
信号发送的方式多种多样,可以是键盘发送,如Ctrl+C
发送2号信号、Ctrl+\
发送3号信号。也可以是通过命令行指令发送,如
kill -9 进程pid #给对应的进程发送9号信号
(2) 信号保存
前面也提到了,信号是保存在进程控制块里面的,由OS来保存,具体的保存方式是位图保存,我们可以用kill -l来查看具体的信号有哪些。前面已经演示过了。
我们常见的信号有31个,也就是前31个,我们可以理解为进程使用无符号32位的整型来保存我们收到的信号(实际上Linux中位图的保存没有这么简单)。当没有收到信号时,所有的比特位都是0,类似于 0000 0000 ....当我们收到了2号信号时,那就类似于 0100 0000 ....
(3) 信号捕捉和处理
后面会提到一个阻塞的概念,被阻塞的信号无法被捕捉到,既然无法被捕捉,自然就无法被处理,你托中间人给同事甲送生日礼物,中间人把礼物丢了,同事甲自然就没法处理礼物了。
参考原文链接:https://blog.csdn.net/challenglistic/article/details/124413135
几个信号的作用:
2号信号(SIGINT):作用是中止进程,快捷键是`Ctrl+C`
3号信号(SIGQUIT):作用是终止进程并Core Dump,快捷键是`Ctrl+\`
Core Dump是指进程异常中止的时候,把进程用户空间的内存数据保存到磁盘上,文件名为core
9号信号(SIGKILL):杀死进程同时无法被捕捉到(无法被捕捉到就说明,我们无法修改该信号的默认处理方式)
8号信号(SIGFPE):是浮点数异常,比如 1/0的情况