一.基本概念
信号是Linux系统提供的一种,向指定进程发送特定事件的方式。
通过kill -l指令。即可查看系统中所有的信号,其中我们只关注1-31号,其他信号为实时信号不作考虑。
信号产生是异步的。
二.信号处理
- 默认动作
- 忽略动作
- 自定义处理-信号捕捉
进程处理信号,都是默认的,默认动作通常是:终止、暂停、忽略等。
自定义捕捉指定信号函数:
#include<signal.h>
sighandler_t signal(int signum,sighandler_t handler);
参数:
signum:信号值
handler:类型为void (*sighandler_t)(int)的函数指针。
该函数通过自定义一个函数操作,将其传入signal函数,随后通过信号signum执行该函数操作。
其中signum会作为handler函数的参数传入。
通过该函数修改指定信号的捕捉方式,再次调用该信号时,就会执行对应的handler函数。
同时可以对不同的信号传递相同的handler函数。
理解进程的发送与保存(浅度):
在进程的task_struct内部,存在能够保存信号的位图成员变量。
向进程发送信号,就是修改其PCB中的信号的指定位图,只有OS有这个权利。
三.信号产生
- 通过kill命令,向指定进程发送指定的信号。
- 键盘可以产生信号,如ctrl + c,即终止进程。
- 系统调用。
1.系统调用函数
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
向指定进程,发送指定信号。
#include<signal.h>
int raise(int sig);
向调用该函数的进程发送指定信号。
#include<stdlib.h>
void abort(void);
向调用该函数的进程发送6号信号,即异常终止。
6号信号可以被自定义捕捉,但是仍然会执行其异常终止的功能。
9号信号不允许被自定义捕捉,其默认为终止进程的信号。
#include<unistd.h>
unsigned int alarm(unsigned int seconds);闹钟函数,可以定时终止进程。参数为闹钟时间,返回值为上一个闹钟的剩余时间。
在OS中会存在很多个闹钟,这就要求OS要把所有的闹钟组织其中共同管理。
闹钟的执行顺序采用最小堆的方式,闹钟的剩余时间越短,就越靠上。
2.异常信号
当我们的进程执行时发生了非法访问等异常操作时,OS就会向进程发送异常信号从而终止进程,这就是通常所谓的程序崩溃。
常见的异常信号有:
8号SIGFPE:除0错误
11号SIGSEGV:野指针访问
如果我们把异常信号捕捉了,也是可以不让进程终止的,但是程序也不会继续往下执行。
当我们的程序出现异常时,OS作为软硬件资源的管理者,要随时处理这些问题,尤其是除0错误而引发的硬件错误,即向目标进程发送信号。
此时,寄存器开始发挥作用,寄存器只有一套,但是寄存器里的数据属于每一个进程,我们需要通过寄存器来完成硬件上下文的保存和恢复。
进程的终止状态有两种:
- term:异常终止
- core:异常终止,但是会帮我们形成一个debug文件,保存进程异常时的核心数据,协助我们找到错误。
四.阻塞信号
实际执行信号的处理动作称为信号递达。包括默认,忽略和自定义捕捉。
信号从产生到递达之间的状态,称为信号未决。
进程可以选择阻塞某个信号,阻塞一个信号,对应的信号一旦产生,永不递达,一直未决,直到主动解除阻塞。
一个信号如果阻塞,和它有没有未决无关。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
在进程的task_struct内部,存在三张表:
- handler表:函数指针数组,其下标就是信号的编号,由此来索引信号处理方法。
- pending表:位图,未决信号集,比特位的位置代表信号编号,内容代表信号是否收到。
- block表:位图,阻塞信号集,比特位的位置代表信号编号,内容代表信号是否阻塞。
通过这三张表,就可以让进程完成对信号的识别。
五.信号保存
信号集操作函数
#include <signal.h>int sigemptyset(sigset_t *set);//初始化set指向的信号集,全部比特位置0int sigfillset(sigset_t *set);//初始化set指向的信号集,全部比特位置1int sigaddset (sigset_t *set, int signo);//在set指向的信号集中添加信号int sigdelset(sigset_t *set, int signo);//在set指向的信号集中删除信号int sigismember(const sigset_t *set, int signo);
读取或更改进程的信号屏蔽字(阻塞信号集)
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数:
how:有三个选项:
set: 输入型参数,即要设置的信号屏蔽字的值。
oldset:输出型参数,保存原始的信号屏蔽字,返回给用户。
成功返回0,失败返回-1。
获取当前进程的pending位图
#include <signal.h>
int sigpending(sigset_t *set);//输出型参数
成功返回0,失败返回-1。
六.信号处理
信号捕捉
进程的地址空间大小为4G,其中3G归用户所用,称为用户空间,而剩余的1G大小称为内核空间,归OS所有。实际上,在地址空间和OS之间,还可以存在一张内核级页表,可以将OS映射到内核空间,从而使OS存在于进程的地址空间中。
系统同时运行多个进程时,所有的进程共同维护一份内核级页表,这就是为什么不管进程如何切换,我们都能够找到OS的原因。
OS如何从键盘读取数据:
当键盘向OS写入数据时,会发生硬件中断,此时数据会被写入CPU的寄存器中进行保存, 在内存中,存在专门存放外设管理的函数指针数组,其中数组的下标,对应每一种外设的中断号,数据在写入寄存器时,寄存器也会保存中断号,此时内存就可以通过中断号,将寄存器里的数据读入内存。
信号捕捉函数:
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
参数:
signum:信号编号。
act:输入型参数,结构体类型,其内部包含一个函数指针对象,指向信号要执行的handler方法。
oldact:输出型参数,保存信号原本的结构体信息。
struct sigaction
{
void(*sa_handler)(int);//handler方法
void(*sa_sigaction)(int, siginfo_t *, void *);//不关心
sigset_t sa_mask;//进程正在处理某信号,同时我们还想屏蔽其他信号,则通过sigaddset函数将其他信号填入sa_mask中。
int sa_flags;//不关心,置0。
void(*sa_restorer)(void);//不关心
};
当进程正在对某信号进行处理时,默认该信号会被自动屏蔽,直至该信号被处理完成时,会自动解除对该信号的屏蔽。
七.其他信号知识
子进程在退出时,会向父进程发送退出信号——SIGCHLD,父进程可以通过对该信号进行处理,从而对子进程进行某些管理。