信号的概念
信号的共性
使用信号的目的
信号的机制
信号的特质
信号的状态
信号的处理方式
信号集
未决信号集和阻塞信号集
信号的编号
信号4要素
Linux常规信号说明(1-31)
信号的产生
终端按键产生信号
硬件异常产生信号
kill函数/命令产生信号
软件条件产生信号
信号集操作函数
编辑
信号集设定
sigprocmask函数
sigpending函数
信号捕捉
signal函数
sigaction函数
信号捕捉特性
信号捕捉函数案例
内核实现信号捕捉过程
时态竞争
时序问题分析:
解决时序问题
时序问题总结
可不可重入函数
SIGCHLD信号
信号传参
中断系统调用
信号在我们的生活中随处可见,如古代烽火戏诸侯中的烽火,跑步时的使用的信号枪发出的信号。
所以说,信号是信息的载体,不能够携带大量的信息
信号的共性
简单,不能够携带大量的信息,满足某个特定条件,优先级高
使用信号的目的
1.让进程知道已经发生了一个特定的事情
2.强迫进程执行它自己代码中的信号处理程序(中断机制)
A给B发送信号。B收到信号之前执行自己的代码,收到信号后,不管执行到程序的什么位置,都要暂停运行,立即去处理信号(信号的优先级高)。与硬件中断类似,但信号是软件层实现的中断,又成为“软中断”。
信号的特质
由于信号是通过软件方法实现,其实现手段导致信号有很强的延时性。但对于用户来说,这个延迟时间非常短,不易察觉。
每一进程收到的所有信号,都是由内核负责发送的,内核处理(请记住这句话)
引发内核为进程产生信号的各类事件:
1.对于前台进程,用户可以输入特殊的终端字符来给它发送信号
ctrl+c ctrl+z ctrl+\
2.硬件发送异常
非法访问内存(段错误) 除0 内存对齐出错(总线错误)
3.系统状态变化
4.运行kill
产生信号
按键产生
系统调用
软件条件产生
硬件异常产生
命令产生
未决
产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态
递答
递送并且到达进程
信号的处理方式
1.执行默认动作
2.忽略(丢弃)
3.捕捉(调用用户处理函数)
Linux内核的进程控制块PCB是一个结构体,task_struct除了包含进程id,状态,工作目录,用户id,组id,文件描述表,还包含了信号相关的信息,主要指阻塞信号集和未决信号集
1.许多信号相关的系统调用都需要能表示一组不同的信号,多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为 sigset_t
2.在PCB中有两个非常重要的信号集。一个称为"阻塞信号集",另一个称为"未决信号集"。这两个信号集都是内核使用位图机制实现的。但操作系统不允许我们直接对这两个信号集进行位操作(我们都知道用户是不能够直接去操作内核空间里的内容的)。需自定义另一个集合,借助信号集操作函数来对PCB中的这两个信号集进行修改
3.信号的“未决”是一种状态,指的是从信号的产生到信号被处理前的这一段时间
4.信号的"阻塞"是一种开关状态,指的是阻止信号被处理,但不是阻止信号产生
5.信号的阻塞就是让系统暂时保留信号留待以后发送。由于另外有办法让系统忽略信号所以一般情况下信号的阻塞只是暂时的,只是为了防止信号打断敏感操作
将某些信号加入集合,对他们设置屏蔽,当屏蔽x信号后,在收到该信号,该信号的处理将被推迟(解除屏蔽后)。说个通俗点的,阻塞信号集可以暂时影响未决信号集,未决信号集上面有讲到是一个状态,它是处于产生到递达之间的状态(递达包括了送达并且处理)。在未决信号集中标识了暂时不处理,那么就是忽略。处理信号不是有三种方式嘛
未决信号集
1.信号的产生,未决信号集中描述该信号的位立刻翻转为1,表信号处于未决状态。当信号被处理对应的位置转回0。这一时刻往往非常短暂
2.信号产生后由某些原因(主要是阻塞)不能到达。这类信号的集合称之为未决信号集。在解除屏蔽之前一直处于未决定状态
查看信号:kill -l
说明:
不存在0号信号。
1-31:常规信号(普通信号、标准信号)
32-64: 实时信号(驱动编程于硬件有关)
每个信号有4个必备的要素:
编号 名称 事件 默认处理动作
通过 man 7 signal查看
名称 编号 默认处理动作 事件
在标准信号中,有一些信号有三个“value”。这是因为不同的操作系统定义了不同的系统信号。我们只研究Linux(中间的值),为了避免歧义,直接使用名称即可
默认处理动作
Term:终止进程
Core:终止进程,生成Core文件(查验进程死亡原因,用于gdb调试) ulimit -a/ulimit -c 1024
Ing:忽略信号
Stop:暂停信号
Cont:继续运行进程
特别说明:
9)SIGKILL和19)SIGSTOP信号,不允许忽略和捕捉,只能执行默认动作。甚至不能将其设置为阻塞
只要产生信号发送的事件发送,信号一定会产生并且递送,但是由于阻塞信号集的原因,该信号不一定会被递达,信号的产生和处理都是内核做到事情。信号的处理方式是信号递达之后的处理方式
core文件的查看
core文件需要使用gdb来查看。
gdb ./a.out
core-file core.xxxx
1) SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程
2) SIGINT:当用户按下了
组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。 3) SIGQUIT:当用户按下
组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。 4) SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件
5) SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件。
6) SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。
7) SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
8) SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
9) SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。
10) SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。
11) SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。
12) SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。
13) SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。
14) SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。
15) SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。
16) SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。
17) SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。
18) SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。
19) SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。
20) SIGTSTP:停止终端交互进程的运行。按下
组合键时发出这个信号。默认动作为暂停进程。 21) SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。
22) SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。
23) SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。
24) SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。
25) SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。
26) SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。
27) SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。
28) SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。
29) SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。
30) SIGPWR:关机。默认动作为终止进程。
31) SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。
34) SIGRTMIN ~ (64) SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。
终端按键产生信号
ctrl+c ==> 2)SIGINT 用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
ctrl+z ==> 20) SIGTSTP 停止终端交互进程的运行。默认动作为暂停进程。
ctrl+\ ==> 3) SIGQUIT 用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。
硬件异常产生信号
除0操作 ==>8) SIGFPE 在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
非法访问内存 ==>20) SIGTSTP:停止终端交互进程的运行。默认动作为暂停进程。
总线错误 ==>7) SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。
kill函数/命令产生信号
kill命令产生信号:kill -信号名称或编号 进程id (eg:kill -9(SIGKILL) 12201)
kill函数:给指定进程发送信号(不一定是杀死,根据发送什么信号决定)
int kill(pid_t pid,int sig);
成功:0
失败:-1(id非法 信号非法 权限问题)
参数:
sig:使用宏名,不要使用编号(避免歧义)
pid:
>0 ==> 发送给指定的进程
=0 ==> 发送信号给与调用kill函数进程属于同一进程组的所有进程
<0 ==> 取|pid|发送给对应的进程组
-1 ==> 发送给进程有权限发送的所有进程
一些概念说明:
进程组:
每一个进程都属于一个进程组,进程组是一个或多个进程集合,他们相互关联,共同完成一个实体任务,每一个进程都有一个进程组长,默认进程组ID与进程组长ID相同。
权限保护:
超级用户可以向任意用户发送信号,而普通用户不能向系统用户发送信号。同样普通用户无法向其他普通用户发送信号。只能向自己创建的进程发送信号
#include
#include #include #include using namespace std; int main(void) { pid_t pid; int i; int tmppid; //abort(); //raise(SIGSTOP); for(i=0;i<5;++i) { pid = fork(); if(pid == 0) { break; } if(i == 2) { tmppid = pid; } } if(i < 5) { cout< 软件条件产生信号
alarm函数
说明:设置定时器(闹钟)。在指定多少秒后,内核会给当前进程发送14)SIGALRM信号,进程收到该信号,默认动作终止进程。无论进程处于某种状态都会记时
原型:
unsigned int alarm(unsigned int seconds);
返回0或剩余的秒数
取消定时器:alarm(0)
查看当前进程执行的秒数:time 可执行程序
实际执行时间=系统时间+用户时间+等待时间(绝大多数时间都是在等待中度过)
程序运行的瓶颈在于IO,优化程序,首先优化IO
例如:
往屏幕上打印,很浪费时间。我们可以重定向往文件中输出
分别向文件和终端打印,观察1s可以计算多少个数:
#include
#include #include using namespace std; int main(void) { int i=0; alarm(1); //定时1秒,14)SIGALRM 终止进程 while(1) { ++i; //cout< #include
#include #include #include #include using namespace std; int main(void) { int i=0; int fd = open("./a.txt",O_RDWR|O_CREAT,0777); dup2(fd,STDOUT_FILENO); alarm(1); //定时1秒,14)SIGALRM 终止进程 while(1) { ++i; //cout< 大概71倍
setitimer函数
说明:设置定时器(闹钟)。可替代alarm函数,精度微妙us,可实现周期定时
原型:int setitimer(int which,const struct itimerval *new_value,struct itimerval *old_value);
成功返回 0 失败-1,并设置error
参数:
which:指定定时方式
1.自然定时:ITIMER_REAL --> 14)SIGARM
2.虚拟空间计时(用户空间):ITIMER_VIRTUAL --> 26)SIGVTALRM 占用cpu的时间
3.运行时间(用户+内存):ITIMER_PROT --> 27)SIGPROF cpu+系统调用
old_value
上一次定时的时间
it_value:定时器定时的时间
如果都为0,表示清0
案例1:
使用setitemer函数实现alarm,重复计算1s
#include
#include using namespace std; void myalarm(unsigned long sec) { itimerval it,oldit; it.it_interval.tv_sec = 0; it.it_interval.tv_usec = 0; it.it_value.tv_sec = 1; it.it_value.tv_usec = 0; int ret = setitimer(ITIMER_REAL,&it,&oldit); if(ret == -1) { perror("setitimer err"); exit(-1); } return; } int main(void) { int i; myalarm(1); while(1) { printf("%d\n",i); ++i; } return 0; } 案例2:
#include
#include #include void func(int signo) { printf("wo shi da sha bi\n"); //raise(SIGKILL); } int main(void) { signal(SIGALRM,func); //信号捕捉,当SIGALRM递达,则执行func itimerval it,oldit; it.it_interval.tv_sec = 2; it.it_interval.tv_usec = 0; it.it_value.tv_sec = 1; it.it_value.tv_usec = 0; if(setitimer(ITIMER_REAL,&it,&oldit) == -1) { perror("setitimer err"); exit(-1); } while (1) { /* code */ } return 0; }
内核通过读取未决信号集来判断信号是否应该被处理。信号屏蔽字mask可以影响未决信号集。而我们可以在应用程序中自定义set来改变mask。以达到屏蔽指定信号的目的。
信号集设定
sigset_t set; //unsigned long (8字节 64bit)
int sigemptyset(sigset_t * set); //将某个信号集清0
int sigfillset(sigset_t *set); //将某个信号集置1
int sigaddset(sigset_t *set,int signum); //将某个信号加入信号集
int sigdelset(sigset_t *set,int signum); //将某个信号清除信号集
int sigismember(const sigset_t *set,int signum); //判断某个信号是否在信号集中
sigprocmask函数
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
成功:0 失败:-1,设置error
参数:
how:假设当前信号屏蔽字为 mask
1.SIG_BLOCK:mask = mask | set
2.SIG_UNBLOCK: mask = mask | ~set
3.SIG_SETMASK: mask = set
sigpending函数
读取当前进程的未决信号集
int sigpending(sigset_t *set);
成功:0 失败:-1,设置error
#include
#include #include #include void printset(sigset_t *ped) { for(int i=1;i<32;++i) { if(sigismember(ped,i) == 1) { putchar('1'); } else { putchar('0'); } } } int main(void) { sigset_t set,ped; sigemptyset(&set); sigaddset(&set,SIGINT); sigaddset(&set,SIGQUIT); sigfillset(&set); sigprocmask(SIG_BLOCK,&set,NULL); while(1) { sigpending(&ped); printset(&ped); sleep(1); } return 0; }
signal函数
注册一个信号捕捉函数
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,.sighandler handler);
handler:
SIG_IGN 忽略此信号
SIG_DFL 使用信号默认的行为
#include
#include using namespace std; typedef void (*sighandler_t)(int); void catchsignal(int signo) { cout<<"aaa"< sigaction函数
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
成功:0 失败:-1,并设置error
act:新的处理方式
oldact:传出参数,旧的处理方式
重要的参数:
sa_handler:指定信号捕捉后的处理函数名。(也可以指定SIG_IGN SIG_DFL)
sa_mask:调用信号处理函数时,所要屏蔽的信号集合(阻塞信号集)
sa_flags:通常设置为0,表示默认属性(对正在处理的信息设置屏蔽)
信号捕捉特性
1.进程正常运行时,默认在PCB中有一个阻塞信号集和未决信号集。对于阻塞信号集,它决定自动屏蔽哪些信号。当注册了某个信号捕捉函数,捕捉到信号以后,要调用该函数。而该函数有可能执行时间很长,在这期间所需要屏蔽的信号由sa_mask来决定。当信号处理完后,又恢复为PCB中的阻塞信号集决定
2.xxx信号捕捉函数执行期间,xxx信号自动被屏蔽
3.阻塞的常规信号不支持排队,产生多次只记录一次
1.自定义一个信号捕捉函数
#include
#include #include using namespace std; void headler(int signo) { cout<<"我是你大爷..."< 2.验证在信号处理函数执行期间,该信号多次递送,那么只在处理函数支行技术和,处理一次
#include
#include #include using namespace std; void headler(int signo) { cout<<"我是你大爷..."< 3.验证sa_mask的屏蔽作用
#include
#include #include using namespace std; void headler(int signo) { cout<<"我是你大爷..."<
信号的产生和处理都是由内核进行的
频繁的在用户态和内核态进行切换是很浪费时间的
当前进程在执行期间,由于竞争的原因,导致程序执行的时序在先后两次的执行中有不同的结果
比如A准备睡觉,定了一个闹钟(定时器),在10s后叫醒他。
由于时序可能会有两种情况:
正常:10s后A被闹钟叫醒
异常:A在5s时被B叫醒,并且A跟着B一起去打牌,打了20s,那么在打牌的期间,
闹钟正常响起,但是并没有叫醒A
时序问题分析:
假设我们想要通过pause和alarm实现sleep:
1.注册SIGALARM信号处理函数(sigaction...)
2.调用alarm(1)函数设定闹钟为1秒
3.函数调用结束,开始计时倒数。正在这时,进程失去CPU(时间片轮转,进程调度算法决定),该进程处于就绪状态,等待CPU
4.但是此时时自然计时,倒数依旧在进行着,如果此时倒数结束,alarm发送信号,信号将递达,但是此时进程处于挂起状态,每一办法通过信号捕捉函数进行处理,所有处于未决状态。
5.当进程再次获得CPU,SIGALARM信号递达,执行处理函数函数
6.信号处理函数结束,回到主控制程序,执行pause(),进程将会被挂起,等待alarm唤醒,可以再没有人来唤醒他了
#include
using namespace std; void myheadler(int signo) { cout<<"aa"< 解决时序问题
可以通过设置屏蔽SIGALRM的方法来控制程序执行逻辑,但无论如何设置,程序都有可能在“解除信号屏蔽字”与“挂起等待信号”这个两个操作间隙失去CPU。除非将这两个步骤合成原子操作。sigsuspend可以实现。
int sigsuspend(const sigset_t *mask) 挂起等待信号
sigsuspend函数调用期间,进程信号屏蔽字由参数mask决定
可将某个信号从临时信号屏蔽集种删除,这样在调用sigsuspend时将解除对该信号的屏蔽,然后挂起等待,当sigsuspend返回时,信号屏蔽字恢复原来的值。
#include
using namespace std; void myheadler(int signo) { cout<<"aa"< 时序问题总结
竞态条件,跟系统负载有很紧密的关系,体现除信号的不可靠性。系统负载越严重,信号不可靠性越强。不可靠由其实现原理所致。信号是通过软件方式实现,每次系统调用结束后或中断处理结束后,需通过扫描PCB中的未决信号集来判断是否应处理某个信号。当系统负载过重时,会出现混乱
一个函数在被调用执行期间(尚未调用结束),由于某时序又被重复调用,称之为"重入"。
1.定义可重入函数,函数内不能包含全局变量集static变量,不能使用malloc、free
2.信号捕捉函数应设计为可重入函数
3.不可重入的原理:使用静态数据结构 调用了malloc和new (不是栈结构) 是标准I/O
当子进程停止或者结束时,会向父进程发送SIGCHLD信号,该信号默认处理动作是忽略,所有产生僵尸进程。
我们之前的程序是,在父进程中调用 wait或者waitpid来回收子进程,此时父进程要么阻塞等待,要么非阻塞而采用轮询的方式。那么将会导致父进程不能做其他工作,只能等着回收子进程。
我们可以利用SIGCHLD信号来捕捉,当有SIGCHLD信号产生时,父进程执行信号捕捉函数。
我们先来看一段程序:
#include
#include #include #include using namespace std; void sigchldHeadler(int signo) { pid_t pid; int status; while((pid=waitpid(-1,&status,WNOHANG))>0) { printf("回收成功:%d ok\n",pid); if(WIFEXITED(status)) { printf("退出状态:%d\n",WEXITSTATUS(status)); } if(WIFSIGNALED(status)) { printf("退出状态(信号):%d\n",WTERMSIG(status)); } } } int main(void) { //创建10个子进程 int i; for(i=0;i<10;i++) { pid_t pid = fork(); if(pid == 0) { break; } } if(i < 10) { printf("I am child:%d\n",getpid()); sleep(1); } else if(i == 10) { struct sigaction act; act.sa_handler = sigchldHeadler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(SIGCHLD,&act,NULL); while(1) { printf("I am parent:%d\n",getpid()); sleep(1); } } return 0; } 我们来思考几个问题:
当父进程在执行信号捕捉函数时,又有子进程死亡。或者有多个子进程死亡。我们知道信号集是位图机制,是不支持排队的。
当父进程的信号捕捉函数还没有注册,就已经有子进程结束了。导致僵尸进程...
或者在fork之前将SIGCHLD信号设置屏蔽
完整:
#include
#include #include #include using namespace std; void sigchldHeadler(int signo) { pid_t pid; int status; while((pid=waitpid(-1,&status,WNOHANG))>0) { printf("回收成功:%d ok\n",pid); if(WIFEXITED(status)) { printf("退出状态:%d\n",WEXITSTATUS(status)); } if(WIFSIGNALED(status)) { printf("退出状态(信号):%d\n",WTERMSIG(status)); } } } int main(void) { //设置阻塞 sigset_t set; sigemptyset(&set); sigdelset(&set,SIGCHLD); sigprocmask(SIG_BLOCK,&set,NULL); //创建10个子进程 int i; for(i=0;i<10;i++) { pid_t pid = fork(); if(pid == 0) { break; } } if(i < 10) { printf("I am child:%d\n",getpid()); //sleep(1); } else if(i == 10) { struct sigaction act; act.sa_handler = sigchldHeadler; act.sa_flags = 0; sigemptyset(&act.sa_mask); sigaction(SIGCHLD,&act,NULL); while(1) { printf("I am parent:%d\n",getpid()); sleep(1); } } return 0; }
前面讲到信号是不能携带大量数据的,一般通过 kill 来发送信号
但是信号可以携带数据,可以携带少量。
通过sigqueue函数,可在向指定进程发送信号的同时携带参数
int sigqueue(pid_t pid,int sig,const union sigval value);
成功返回0,失败-1,设置error
注意事项:
向指定进程发送指定信号的同时携带数据。不能够传地址,不能进程之间虚拟地址空间各自独立,当前进程地址传递给另一进程没有实际意义
捕捉函数:
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
成功:0 失败:-1,并设置error
act:新的处理方式
oldact:传出参数,旧的处理方式
不使用sa_handler,而使用sa_sigaction。sa_flags必须指定为SA_SIGINFO
系统调用分为两类:
慢速系统调用:可能导致进程永远阻塞,如果在阻塞期收到一个信号,该系统调用就会被断开,不再继续执行,也可以设定系统调用重启(read、write、pause、wait...)
其他系统调用:getpid,getpid、fork
sa_flags可以设置为 SA_RESTART