一、信号的基本概念
为了更加清晰的了解信号,那么就拿我们最熟悉的场景切入:
1>用户输入命令,在shell下启动一个前台进程。
2>用户按下ctrl-c,这个键盘输入就是一个硬件中断。
3>如果Cpu当前正在执行这个代码,则该进程的用户空间代码暂停执行,CPU从用户态切换到内核态处理硬件中断。
4>终端驱动将Ctrl-C解释成一个SIGINT信号,,记在该进程的PCB中(也可以说发送一个SIGINT信号给该进程)。
5>当某个时刻要从内核态返回到该进程的用户态之时,首先处理PCB中记录的信号,发现有一个SIGINT信号带处理,而这个信号的默认动作是终制进程,所以直接终止进程而不在返回用户空间代码执行。
注:
1>Ctrl-C产生的信号只能发给前台进程,一个命令加个&可以放到后台运行,这样shellbubi等待进程结束就可以 接受新的命令,启动新的进程。
2>shell可以同时运行一个前台进程与任意多个后台进程,只是前台进程才能接到像Ctrl-C这样控制键产生的信号/
3>前台进程在运行过程中用户随时可能按下Ctrl-C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制来说是异步的。
二、信号的查看
用Kill-l 命令可以查看系统定义的信号列表:
每个信号都有一个编号和一个宏定义名称,这个可以在signal.h中查找
三、产生信号的方式
1>用户在终端按下某些键时,终端驱动程序会发送给前台进程《例如:Ctrl-C ; Ctrl-Z ;Ctrl-\》
2>硬件异常产生信号,这些条件有硬件检测并通知内核,然后内核向当前进程发送适当的信号,例如当前进程执行了除0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
3>一个进程调用KILL(2)函数可以发送信号给另一个进程。
4>软件条件产生。
总而言之:信号的产生可以分为以下三种:(1.通过终端产生信号;2.调用系统函数向进程发信号;3.由软件条件产生。)
四、信号处理常见方式
1>忽略此信号。
2>执行此信号的默认处理动作。
3>提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。
五、常见信号
1>sigset_t(信号集)
sigset_t类型对于每种信号用一个bit表示”有效“或”无效“,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不关心的,使用者只能调用以下函数来操作sigset_t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
#include<signal.h>
int sigemptyset(sigset_t * set);
int sigfillset(sigset_t * set);
int sigaddset(sigset_t * set,int signo);
int sigdelset(sigset_t* set,int signo);
int sigismember(const sigset_t * set,int signo);
2>sigpending
用法:
#include<signal.h>
sigpending(sigset_t *set)
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1.
3>pause
#include<signal.h>
int pause(void);
pause函数使调用进程挂起直到有信号递达。
4>sigaction
#include<signal.h>
int sigaction(int signo,const struct sigaction *act,struct sigaction *oact);
注:相当于signal;
5>sigchld
子进程在终止时会给父进程发SIGCHLGD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程就只需专心处理自己的工作,不必关系子进程了,子进程结束时通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
六、代码的实现
例1:
检测结果:
例2:
检测结果:
例3:
检测结果:
例4:
检测结果: