操作系统-信号管理
一、信号介绍
中断:当进程接收到消息后中止当前正在执行的任务,转而执行其它任务,等待其它任务执行完毕后再返回继续执行。这种执行模式称为中断,分为硬件中断和软件中断两种
信号:
- 信号是UNIX、类UNIX以及其他POSIX兼容的系统中,为了完成不同进程之间通讯的一种方式。是一种软中断,是一种异步处理机制,用于提醒进程某个事件发生了,可能要去处理。
- 当一个信号发送给一个进程,操作系统就会中断该进程的正常控制流程,如果中断前预先设置过如何处理该信号(绑定过该信号的信号处理函数),那么中断后会执行该函数,执行完返回中断点继续执行,否则就按信号的默认处理方式进行
执行 kill -l
查看当前系统可以产生的所有信号
信号 | 解释 | 产生条件 | 默认动作 |
---|---|---|---|
SIGHUP(1) | 连接断开信号 | 如果终端接口检测一个连接断开,则将此信号发送给与该终端相关的控制进程(会话首进程) | 终止 |
SIGINT(2) | 终端中断符信号 | 用户按中断键Ctrl+C ,产生此信号,并送至前台进程组的所有进程 |
终止 |
SIGQUIT(3) | 终端退出符信号 | 用户按退出键Ctrl+ ,产生此信号,并送至前台进程组的所有进程 |
终止+core |
SIGILL(4) | 非法硬件指令信号 | 进程执行了一条非法硬件指令 | 终止+core |
SIGTRAP(5) | 硬件故障信号 | 指示一个实现定义的硬件故障。常用于调试 | 终止+core |
SIGABRT(6) | 异常终止信号 | 调用abort 函数,产生此信号 |
终止+core |
SIGBUS(7) | 总线错误信号 | 指示一个实现定义的硬件故障,常用于内存故障 | 终止+core |
SIGFPE(8) | 算术异常信号 | 表示一个算术运算异常,例如除以0、浮点溢出等 | 终止+core |
SIGKILL(9) | 终止信号 | 不能被捕获或忽略。常用于杀死进程 | 终止 |
SIGUSR1(10) | 用户定义信号 | 用户定义信号,用于应用程序 | 终止 |
SIGSEGV(11) | 段错误信号 | 试图访问未分配的内存,或向没有写权限的内存写入数据 | 终止+core |
SIGUSR2(12) | 用户定义信号 | 用户定义信号,用于应用程序 | 终止 |
SIGPIPE(13) | 管道异常信号 | 写管道时读进程已终止,或写SOCK_STREAM类型套接字时连接已断开,均产生此信号 | 终止 |
SIGALRM(14) | 闹钟信号 | 以alarm 函数设置的计时器到期,或以setitimer 函数设置的间隔时间到期,均产生此信号 |
终止 |
SIGTERM(15) | 终止信号 | 由kill 命令发送的系统默认终止信号 |
终止 |
SIGSTKFLT(16) | 数协器栈故障信号 | 表示数学协处理器发生栈故障 | 终止 |
SIGCHLD(17) | 子进程状态改变信号 | 在一个进程终止或停止时,将此信号发送给其父进程 | 忽略 |
SIGCONT(18) | 使停止的进程继续 | 向处于停止状态的进程发送此信号,令其继续运行 | 继续/忽略 |
SIGSTOP(19) | 停止信号 | 不能被捕获或忽略。停止一个进程 | 停止进程 |
SIGTSTP(20) | 终端停止符信号 | 用户按停止键Ctrl+Z ,产生此信号,并送至前台进程组的所有进程 |
停止进程 |
SIGTTIN(21) | 后台读控制终端信号 | 后台进程组中的进程试图读其控制终端,产生此信号 | 停止 |
SIGTTOU(22) | 后台写控制终端信号 | 后台进程组中的进程试图写其控制终端,产生此信号 | 停止 |
SIGURG(23) | 紧急情况信号 | 有紧急情况发生,或从网络上接收到带外数据,产生此信号 | 忽略 |
SIGXCPU(24) | 超过CPU限制信号 | 进程超过了其软CPU时间限制,产生此信号 | 终止+core |
SIGXFSZ(25) | 超过文件长度限制信号 | 进程超过了其软文件长度限制,产生此信号 | 终止+core |
SIGVTALRM(26) | 虚拟闹钟信号 | 以setitimer 函数设置的虚拟间隔时间到期,产生此信号 |
终止 |
SIGPROF(27) | 虚拟梗概闹钟信号 | 以setitimer 函数设置的虚拟梗概统计间隔时间到期,产生此信号 |
终止 |
SIGWINCH(28) | 终端窗口大小改变信号 | 以ioctl 函数更改窗口大小,产生此信号 |
忽略 |
SIGIO(29) | 异步I/O信号 | 指示一个异步I/O事件 | 终止 |
SIGPWR(30) | 电源失效信号 | 电源失效,产生此信号 | 终止 |
SIGSYS(31) | 非法系统调用异常 | 指示一个无效的系统调用 | 终止+core |
不可靠信号与可靠信号
不可靠信号:
- 建立在早期信号处理机制上的信号被称为不可靠信号(1-31)
- 非实时信号,不支持排队机制,可能会丢失信号,同一个信号产生多次,进程可能只收到一次
- 当进程收到这类信号,执行了第一次用户设置的信号处理函数后,会还原会默认的处理方式。
可靠信号:
- 位于(34-64)的信号是可靠信号
- 实时信号,支持排队机制,信号只要产生就必定会被处理,因此不会丢失
信号的来源:
- 硬件异常:由硬件设备产生的信号,例如除零、非法访问内存、总线错误、未定义指令
- 软件异常:命令kill\函数产生信号
信号处理的默认动作:
- 忽略
- 终止进程
- 终止并产生core文件
- 捕获并处理
二、捕获信号
// 信号处理函数的格式
typedef void (*sighandler_t)(int);
/**
* 功能: 向内核注册一个信号和它的信号处理函数,相当于捕获该信号(绑定信号)
* @signum: 信号码(可以写整数值也可以用宏名)
*/
sighandler_t signal(int signum, sighandler_t handler);
- 在某些UNIX系统中,通过
signal
注册信号处理函数后,只有一次有效,后面会变回默认处理方式,为了在这种系统中得到持久的信号绑定,可以在信号处理函数的末尾再次通过signal
重新注册一次 - 可以通过命令
kill 信号码 进程号
给该进程发送信号 - 普通用户只能给自己的进程发送信号,只有root用户可以给任何进程发送信号
- 当信号处理完信号处理函数后是会回到产生信号的代码位置继续执行,如果我们捕获并处理的是段错误\除零这种信号,就会产生死循环,因为这些错误并没有因为捕获了信号就消失,而是一直存在并产生信号,正确做法是在他们的信号处理函数中进行数据保存然后直接结束程序。exit()
注意:9和19号信号不能被忽略也不能被不能被捕获
三、发送信号的方式
键盘:
- 给当前终端控制下的活跃进程发送信号
- Ctrl+c Ctrl+\ Ctrl+z fg
命令:
kill <-信号码> 进程号
killall <-信号码> 进程名
# 给所有的同名的进程发送信号
事件:
- 当程序执行了某种非法操作,被操作系统发现了,操作系统会给进程发送对应的信号,例如段错误、除零、总线错误、错误指令等。
函数:
/**
* 功能: 给指定的进程发送信号
* @pid: 给进程号为pid的进程发送
* pid > 0 给pid号进程发送信号sig
* pid = 0 给同组的所有进程发送信号sig
* pid = -1 给所有进程发送信号sig,前提是有向该进程发送信号的权限
* pid < -1 向进程组id等于pid的绝对值的所有进程发送信号
*/
int kill(pid_t pid, int sig);
/**
* 功能: 给调用进程自己发送信号
*/
int raise(int sig);
/**
* 功能: 给调用进程自己发送信号SIGABRT(6)
*/
void abort(void);
/**
* 功能: 让内核在seconds秒后,向调用进程发送SIGALRM(14)信号
* 返回值: 上一次alarm剩余的时间,
* 如果是正常走完时间返回0,如果时间还没走完,又再次调用alarm后,会覆盖之前的时间重新计时,并不会产生多个闹钟信号
*/
unsigned int alarm(unsigned int seconds);
四、暂停和休眠
/**
* 功能: 让调用进程进入暂停态执行,进入睡眠状态,直到有信号终止进程或者有信号被捕获,
* 会唤醒并执行信号处理函数,继续后面的执行流程。类似于不限时的sleep
* 返回值: 要么一直睡眠不返回,睡醒返回-1
*/
int pause(void);
/**
* 功能: 让调用进程睡眠seconds秒,除非有信号终止进程或者有信号被捕获,也会唤醒
* 返回值: 剩余的睡眠秒数
*/
unsigned int sleep(unsigned int seconds);
/**
* 功能: 让调用进程睡眠usec微秒,除非有信号终止进程或者有信号被捕获,也会唤醒
* 返回值: 剩余的睡眠秒数
*/
int usleep(useconds_t usec)
五、信号集与信号屏蔽
信号集:
- 是一种专门用于存储多个信号的数据类型
sigset_t
- 该类型占128字节,每个字节代表了一种信号的有或无
操作信号集的相关函数:
/**
* 功能: 将信号集set中的所有信号置零 清空信号集
*/
int sigemptyset(sigset_t *set);
/**
* 功能: 把信号集set中所有信号置1
*/
int sigfillset(sigset_t *set);
/**
* 功能: 将信号集set中的信号signum置1
*/
int sigaddset(sigset_t *set, int signum);
/**
* 功能: 将信号集set中的信号signum置0
*/
int sigdelset(sigset_t *set, int signum);
/**
* 功能: 测试信号集中是否存在signum信号
* 返回值: 存在返回1,不存在返回0 非法信号返回-1
*/
int sigismember(const sigset_t *set, int signum);
信号的递送与未决:
- 当信号产生后,系统内核会在其内部维护的进程表中,给响应信号的进程设置一个对应的标志位,着整个过程称为信号的递送
- 在信号产生到完成递送之间会存在一段时间间隔,处于这个时间间隔的信号状态是“未决”
信号屏蔽:
- 每个进程都有用一个信号掩码(
signal mask
,就是一个信号集),其中存在的信号是需要被该进程屏蔽的信号 - 让需要屏蔽的信号处于“未决状态”,当可以接收信号时,让其退出未决状态,完成递送
- 当执行一些特殊的且不想被干扰中断的操作时,例如:更新数据库敏感操作,此时可以把信号放入信号屏蔽集中,等操作完成后,再从信号屏蔽集中删除,继续处理信号,能保证敏感操作的安全性
信号屏蔽集的操作函数:
/**
* 功能: 修改当前进程的信号掩码(屏蔽集)
* @how: 修改信号掩码的方式:
* SIG_BLOCK 将set中的信号加入到信号掩码中
* SIG_UNBLOCK 从信号掩码中把set中的信号删除
* SIG_SETMASK 把set中的信号替换掉信号掩码的所有信号
* @set: 信号集 用于设置
* @oldsel: 信号集 用于获取旧信号集 NULL则不获取
*/
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);