理论部分
简单来说
信号是用来通知进程发生了异步事件
比如,在终端运行程序,按下ctrl+c就产生一个中端信号,大概过程是这样的:
- 用户从shell下启动一个进程;
- 用户按下ctrl+c,产生一个硬件中端信号;
- cpu从用户态切换到进程态,处理中端;(这个过程当年读书的时候在《计算机组成原理》看过)
- 终端驱动程序将ctrl+c解释成一个SIGINT信号,记在该进程的PCB中(也可以说发送了一个SIGINT信号给该进程)。
- 当某个时刻要从内核返回到该进程的用户空间代码继续执行之前,首先处理PCB中记录的信号,发现有一个SIGINT信号待处理,而这个信号的默认处理动作是终止进程,所以直接终止进程而不再返回它的用户空间代码执行。
前台进程在运行过程中用户随时可能按下ctrl+c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。
kill -l 可以查看系统定义的信号,这些定义是在signal.h中。
1.png也可以通过man 7 signal来查看。
2.png其中Action代表默认处理动作,
Term : 终止当前进程
Core : 终止并且Core Dump
Ing : 忽略
Stop : 停止当前进程
Cont: 继续执行先前停止的进程
注:当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。
产生信号的条件主要是:
- 终端的按键,如ctrl+c产生SIGINT信号,ctrl+\产生SIGQUIT信号,ctrl+z产生SIGTSTP信号。
- 硬件异常产生信号。
- 一个进程调用kill (man 2 kill) 可以发送信号给另一个进程。
- 可以调用kill (man 1 kill)。
- 当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号,向读端已关闭的管道写数据时产生SIGPIPE信号。
用户程序可以调用sigaction(man 2 sigaction)函数告诉内核如何处理某种信号,可选的操作有
- 忽略
- 执行默认动作
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号。
发送信号可以用:
#include <signal.h>
int kill(pid_t pid, int signo);//向其他进程发信号
int raise(int signo);//向自己发信号
#include <stdlib.h>
void abort(void);//使当前进程接收到SIGABRT信号而异常终止
#include <unistd.h>
unsigned int alarm(unsigned int seconds);//内核在seconds秒之后给当前进程发SIGALRM信号
信号从产生到递达之间的状态,称为信号未决。
每个信号都有两个标志位分别表示阻塞和未决,还有一个函数指针表示处理动作
信号集操作函数
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作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);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。注意,在使用sigset_t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
#include<signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);//读取或更改进程的信号屏蔽字
3.png
#include<signal.h>
int sigpending(sigset_t *set);//读取当前进程的未决信号集,通过set参数传出
信号补捉过程
4.png#include<signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);//读取和修改与指定信号相关联的处理动作
act和oact指向sigaction结构体
struct sigaction {
void (*sa_handler)(int); /* addr of signal handler, */
/* or SIG_IGN, or SIG_DFL */
sigset_t sa_mask; /* additional signals to block */
int sa_flags; /* signal options, Figure 10.16 */
/* alternate handler */
void (*sa_sigaction)(int, siginfo_t *, void *);
};
将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被main函数调用,而是被系统所调用。
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
#include<unistd.h>
int pause(void);//使调用进程挂起直到有信号递达
当捕捉到信号时,不论进程的主控制流程当前执行到哪儿,都会先跳到信号处理函数中执行,从信号处理函数返回后再继续执行主控制流程。信号处理函数是一个单独的控制流程,因为它和主控制流程是异步的,二者不存在调用和被调用的关系,并且使用不同的堆栈空间。引入了信号处理函数使得一个进程具有多个控制流程,如果这些控制流程访问相同的全局资源(全局变量、硬件资源等),就有可能出现冲突。
理解部分
关于信号,之前接触的比较多的是kill这个命令,现在看来,发送信号给进程是一种交互方式。当进程中实现了补捉信号以及处理的函数时,就可以与用户进行简单的交互,之所以简单是因为信号数量很少。所以一般用做进程的启动,重启,停止,错误处理等等。若使用其他的信息传递方式我想也是可以实现的,就是麻烦了一些。
下面来实现一个简单的例子,后台跑一个进程,打印出发送给该进程的信号。
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void show_handler(int sig)
{
printf("I got signal %d\n", sig);
}
int main(void)
{
int i = 0;
struct sigaction act, oldact;
act.sa_handler = show_handler;
sigaddset(&act.sa_mask, SIGQUIT);
act.sa_flags = 0;
sigaction(SIGINT, &act, &oldact);
sigaction(SIGCONT, &act, &oldact);
while(1) {
sleep(1);
i++;
}
}
贴上运行图
s.png注:sigaction 是不能接收SIGKILL 和 SIGSTOP的信号
网友评论