在上一节中我们介绍了linux下的五种IO模型:
- 阻塞IO;
- 非阻塞IO;
- IO多路复用模型;
- 信号驱动IO模型;
- 异步IO;
并介绍了poll机制在按键驱动中的使用,这一节我们将重点介绍信号驱动IO模型如何在按键驱动的例子中的使用。
一、信号驱动IO模型
1.1 什么是信号驱动IO模型
我们举个例子,我们在钓鱼的时候需要不停的查看鱼竿,看看有没有鱼咬鱼浮了,这导致我们不能去做其他的事情,显然这是一个阻塞IO模型。
如果我们给鱼竿安装一个报警器,当鱼浮下沉的时候,也就说明有鱼儿咬钩了,就立刻报警。我们收到报警信号之后,去把鱼钓上来。映射到linux操作系统中,这就是信号驱动IO模型。
其实这个就有点类似事件监听模型。应用程序预先在内核中注册一个信号处理函数,本质就是一个回调函数,当IO准备好之后(读:有数据可读;写:有空间可写),内核就会给进程发送一个预先预定号的信号,进程在收到信号后,就调用信号对应的处理函数。
1.2 什么是信号
信号是linux系统响应某些条件而产生的一个事件,它可以用于进程之间的通信。
信号是在软件层次上对中断机制的一种模拟,在原理上,一个信号接收到一个信号与处理器接收到一个中断请求可以说是一样的。
信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底什么时候到达。那么,进程是如何发现和接受信号呢?
实际上,信号的接收不是由用户进程来完成的,而是由内核代理。当一个进程P2向另一个进程P1发送信号后,内核接受到信号,并将其放在P1的信号队列当中。当P1再次进入内核态时,会检查信号队列,并根据相应的信号调取相应的信号处理函数。
信号是进程间通信机制中唯一的异步通信机制,可以看做是异步通知,通知接收信号的进程有哪些事情发生了。信号机制经过POSIX实时扩展后,功能更加强大,除了基本通知功能外,还可以传递附加信息。
1.3 信号来源
信号事件的发生有两个来源:
- 硬件来源(比如我们按下了键盘或者其它硬件故障);
- 软件来源,最常用发送信号的系统函数是kill, raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作;
1.4 linux产生信号的条件
(1) 当用户按下某些终端键时,将产生信号
- 终端上按“CTRL+C”组合键通常产生中断信号 SIGINT;
- 终端上按“CTRL+\”键通常产生中断信号 SIGQUIT;
- 终端上按“Ctrl+Z”键通常产生中断信号 SIGSTOP 等;
(2) 硬件异常将产生信号
比如数据运算时,除数为0;或者无效的内存访问等;这些条件通常由硬件检测到,并通知内核,然后内核为该条件发生时正在运行的进程产生适当的信号;
(3) 软件异常将产生信号
当检测到某种软件条件已发生,并将其通知有关进程时,产生信号;
(4) 调用 kill函数将发送信号。
注意:接收信号进程和发送信号进程的所有者必须相同,或发送信号进程的所有者必须是超级用户;
(5) 运行shell kill 命令将发送信号
此程序实际上是使用 kill 函数来发送信号。也常用此命令终止一个失控的后台进程;
1.5 信号的捕获和处理
刚才我们说,当进程P1再次进入内核时,会检查信号队列。那么,进程P1什么时候会再次进入内核呢?进入内核后在什么时机会检测信号队列呢?
- 当前进程由于系统调用、中断或异常而进入内核空间以后,从内核空间返回到用户空间的前夕;
- 当前进程在内核中进入睡眠以后刚被唤醒的时候(必定是在系统调用中),或者由于不可忽略信号的存在而提前返回到用户空间;
发现信号后,根据信号向量,就找到了信号处理函数,然后从内核态跑到用户态去执行信号处理程序,那么用户进程一般如何处理信号?用户进程可按照下列3中方式来处理:
- 忽略信号:即对信号不做任何处理,大多数信号都可以使用这种方式处理,但信号SIGKILL和SIGSTOP绝不能被忽略,因为它们向超级用户提供了一种使进程终止的可靠方法;
- 缺省动作:执行信号的默认动作,大多数信号的系统默认动作是终止进程.;
- 捕捉信号:定义信号处理函数,当信号发生时,执行相应的处理函数;
Linux究竟采用上述三种方式的哪一个来响应信号,取决于传递给相应API函数的参数。
二、linux中的信号
2.1 系统支持信号
那么我们就需要知道在linux系统中有哪些可用的信号,在linux终端输入kill -l可以查看系统所支持的信号,可以看出,每个信号的名字都是以SIG开头:
root@zhengyang:/work/sambashare/linux-5.2.8# kill -l 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX
在头文件/usr/include/bits/signum.h中,这些信号都被定义为正整数,即每个信号和一个数字编码相对应:
/* Signals. */ #define SIGHUP 1 /* Hangup (POSIX). */ #define SIGINT 2 /* Interrupt (ANSI). */ #define SIGQUIT 3 /* Quit (POSIX). */ #define SIGILL 4 /* Illegal instruction (ANSI). */ #define SIGTRAP 5 /* Trace trap (POSIX). */ #define SIGABRT 6 /* Abort (ANSI). */ #define SIGIOT 6 /* IOT trap (4.2 BSD). */ #define SIGBUS 7 /* BUS error (4.2 BSD). */ #define SIGFPE 8 /* Floating-point exception (ANSI). */ #define SIGKILL 9 /* Kill, unblockable (POSIX). */ #define SIGUSR1 10 /* User-defined signal 1 (POSIX). */ #define SIGSEGV 11 /* Segmentation violation (ANSI). */ #define SIGUSR2 12 /* User-defined signal 2 (POSIX). */ #define SIGPIPE 13 /* Broken pipe (POSIX). */ #define SIGALRM 14 /* Alarm clock (POSIX). */ #define SIGTERM 15 /* Termination (ANSI). */ #define SIGSTKFLT 16 /* Stack fault. */ #define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */ #define SIGCHLD 17 /* Child status has changed (POSIX). */ #define SIGCONT 18 /* Continue (POSIX). */ #define SIGSTOP 19 /* Stop, unblockable (POSIX). */ #define SIGTSTP 20 /* Keyboard stop (POSIX). */ #define SIGTTIN 21 /* Background read from tty (POSIX). */ #define SIGTTOU 22 /* Background write to tty (POSIX). */ #define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */ #define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */ #define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */ #define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */ #define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */ #define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */ #define SIGPOLL SIGIO /* Pollable event occurred (System V). */ #define SIGIO 29 /* I/O now possible (4.2 BSD). */ #define SIGPWR 30 /* Power failure restart (System V). */ #define SIGSYS 31 /* Bad system call. */
下面给出一些信号的描述信息:
信号名称 | 值 | 说明 |
---|---|---|
SIGHUP | 1 | 连接挂断 |
SIGINT | 2 | 终端中断 |
SIGQUIT | 3 | 终端退出 |
SIGILL | 4 | 非法指令 |
SIGABRT | 5 | 跟踪陷阱 |
SIGIOT | 6 | IOT陷阱 |
SIGBUS | 7 | BUS错误 |
SIGFPE | 8 | 浮点运算异常 |
SIGKILL | 9 | 终止进程(不能被捕获或忽略) |
SIGUSR1 | 10 | 用户定义信号1 |
SIGSEGV | 11 | 无效内存段访问 |
SIGUSR2 | 12 | 用户定义信号2 |
SIGPIPE | 13 | 向无读进程的管道写数据 |
SIGALRM | 14 | 超时警告 |
SIGTERM | 15 | 终止 |
SIGSTKFLT | 16 | 堆栈错误 |
SIGCHLD | 17 | 子进程已经停止或退出 |
SIGCONT | 18 | 如果停止了,继续执行 |
SIGSTOP | 19 | 停止执行(不能被捕获或忽略) |
SIGTSTP | 20 | 终端停止信号 |
SIGTTIN | 21 | 后台进程需要从终端读取命令 |
SIGTTOU | 22 | 后台进程需要从终端写出 |
SIGURG | 23 | 紧急的套接字事件 |
SIGXCPU | 24 | 超额使用CPU分配的时间 |
SIGXFSZ | 25 | 文件尺寸超额 |
SIGVTALRM | 26 | 虚拟时钟信号 |
SIGPROF | 27 | 时钟信号描述 |
SIGWINCH | 28 | 窗口尺寸变化 |
SIGIO | 29 | I/O |
SIGPWR | 30 | 断电重启 |
SIGSYS | 31 | 系统调用错误 |
除了SIGSTOP和SIGKILL两个信号外,进程能够忽略或捕获其他的全部信号。一个信号被捕获的意思是当一个信号到达时有相应的代码处理它,如果一个信号没有被这个进程所捕获,内核将采用默认行为处理。
2.2 信号分类
linux信号又可以按照可靠性以及实时性两个维度进行分类。
按照可靠性分类:
- 可靠信号:支持信号队列;
- 不可靠信息:不支持信号队列;
在早期的UNIX中信号是不可靠的,不可靠在这里指的是:信号可能丢失,一个信号发生了,但进程却可能一直不知道这一点。
现在Linux 在SIGRTMIN信号之前的都叫不可靠信号,这里的不可靠主要是不支持信号队列,就是当多个信号发生在进程中的时候(收到信号的速度超过进程处理的速度的时候),这些没来的及处理的信号就会被丢掉,仅仅留下一个信号。
可靠信号是多个信号发送到进程的时候(收到信号的速度超过进程处理信号的速度的时候),这些没来的及处理的信号就会排入进程的信号队列。等进程有机会来处理的时候,依次再处理,信号不丢失。
按照实时性分类:
- 实时信号;
- 非实时信号;
非实时信号都不支持信号队列,都是不可靠信号;实时信号都支持信号队列,都是可靠信号。
三、信号的安装
在用户进程中,如果想要处理某一种信号,就需要在进程中安装该信号。安装信号主要用来确定信号值及进程针对该信号值的动作之间的映射关系,即进程将要处理哪个信号;该信号被传递给进程时,将执行何种操作。
linux中有两个函数实现信号的安装:
-
signal:它只有两个参数,不支持信号传递消息,主要用于SIGRTMIN之前的非实时信号的安装;
-
sigaction:sigaction是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue系统调用配合使用,当然,sigqueue同样支持非实时信号的安装。sigaction优于signal主要体现在支持信号带有参数;
3.1 signal函数
在用户进程中,可以使用signal函数来设置对应信号的处理函数:
typedef void (*__sighandler_t) (int); __sighandler_t signal (int __sig, __sighandler_t __handler);
首先定义一个函数指针sighandler_t,参数类型为int,没有返回值;
然后定义signal函数,可以看到该函数:
- __sig:第一个参数指定信号的值,int类型,SIGRTMIN之前的信号,并且排除SIGKILL及SIGSTOP;
- __handler:第二个参数指定信号值对应的处理函数,__sighandler_t类型,函数的参数为信号值;如果参数设置为SIG_IGN 忽略参数__sig指定的信号,参数设置为SIG_DFL将参数__sig指定的信号重设为系统默认的信号处理方式;
比如SIG_DEF定义为,即指向地址0:
#define SIG_DFL ((__sighandler_t) 0)
函数返回一个函数指针,类型为sighandler_t。如果signal调用成功,返回上一次为安装信号__sig而调用signal时指定的__handler值;失败则返回SIG_ERR。
然后演示如何使用,我们编写一个示例程序,在程序中,我们捕获SIGINT信号,然后输出"catch the signal SIGINT 信号值":
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <stdlib.h> void sig_handler(int signo); int main(void) { printf("mian is waiting for a signal\n"); if(signal(SIGINT,sig_handler) == SIG_ERR){ perror("signal errror"); exit(EXIT_FAILURE); } for(; ;);//有时间让我们发送信号 return 0; } void sig_handler(int signo) { printf("catch the signal SIGINT %d\n",signo); }
使用gcc编译,然后在ubuntu系统运行:
root@zhengyang:/work/sambashare# ./hello mian is waiting for a signal ^Ccatch the signal SIGINT 2 ^Ccatch the signal SIGINT 2 ^Ccatch the signal SIGINT 2 ^Ccatch the signal SIGINT 2 ^Ccatch the signal SIGINT 2 已杀死
每当我们按下CTRL+C就会发送SIGINT信号,从而执行我们安装的信号处理函数,而当我们重新打开一个终端执行kill -9 进程号时,发送信号值为9的信号SIGKILL,执行系统默认处理函数,退出进程。
3.2 sigaction函数
int sigaction (int __sig, const struct sigaction * __act, struct sigaction * __oact)
其中:
- __sig:第一个参数指定信号的值,int类型,可以为除SIGKILL及SIGSTOP外的任何一个特定有效的信号(为这两个信号定义自己的处理函数,将导致信号安装错误);
- __act:第二个参数是指向结构sigaction的一个实例的指针,在结构sigaction的实例中,指定了对特定信号的处理,可以为NULL,进程会以缺省方式对信号处理;
- _oact:第三个参数_oact指向的对象用来保存原来对相应信号的处理,可指定_oact为NULL。
如果把第二、第三个参数都设为NULL,那么该函数可用于检查信号的有效性;
第二个参数最为重要,其中包含了对指定信号的处理、信号所传递的信息、信号处理函数执行过程中应屏蔽掉哪些函数等等。sigaction结构定义如下:
struct sigaction { union{ void (*sa_handler)(int);//信号处理程序 不接受额外数据 void (*sa_sigaction)(int, siginfo_t *, void *);//信号处理程序,能接受额外数据,可以和sigqueue配合使用 }_u; sigset_t sa_mask; int sa_flags;//影响信号的行为SA_SIGINFO表示能接受数据 void (*sa_restorer)(void);//废弃 };
其中:
- sa_restorer,已过时,POSIX不支持它,不应再被使用。
- _sa_handler指定信号对应的处理函数,和signal函数第二个参数一样,除了可以是用户自定义的处理函数外,还可以为SIG_DFL(采用缺省的处理方式),也可以为SIG_IGN(忽略信号)。
- _sa_handler指定的处理函数只有一个参数,即信号值,所以信号不能传递除信号值之外的任何信息;
- _sa_sigaction指定的信号处理函数带有三个参数,是为实时信号而设的(当然同样支持非实时信号),它指定一个3参数信号处理函数。第一个参数为信号值,第三个参数没有使用(posix没有规范使用该参数的标准),第二个参数是指向siginfo_t结构的指针,结构中包含信号携带的数据值。
更多相关内容参考linux系统编程之信号(六):信号发送函数sigqueue和信号安装函数sigaction和Linux 改进捕捉信号机制(sigaction,sigqueue)。
四、信号的发送
发送信号的主要函数有:kill、raise、sigqueue、alarm、setitimer以及abort。
在第三节中我们已经介绍了如何给一个用户进程安装信号以及信号对应的处理函数,在这一节我们将介绍进程P2如何向进程P1发送信号。
4.1 kill
4.2 raise
4.3 sigqueue
4.4 alarm
4.5 setitimer
4.6 abort
参考文章:
[3]linux内核剖析(九)进程间通信之-信号signal(部分转载)
[5]linux 信号安装、signal、kill,arise讲解