1. 信号的理解
- 信号是一种软件层次上对中断机制的一种模拟
- 信号可由内核或者进程发出到指定进程
- Linux 系统提供了 signal()系统调用可用于注册信号的处理函数
- 信号本质上是 int 类型数字编号
#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. */ #define SIGUNUSED 31
2. 信号的分类
2.1 不可靠信号(非实时信号)
上面提到的信号全为不可靠信号(1~31) , 这些信号全是系统内置的,这些信号已经有了预定义值,每个信号有了确定的用途、含义以及对应的名字,并且每种信号都有各自的系统默认操作(SIGUSR1和SIGUSR2除外)
2.2 可靠信号(实时信号)
- 命令"kill -l" 可显示出所有的信号,32-64即为可靠信号, 内核对于实时信号所采取的是队列化管理。 可靠信号支持排队,不会丢失
- 当发送一个实时信号时,可为信号指定伴随数据(一整形数据或者指针值),供接收信号的进程在
它的信号处理函数中获取如果有多个不同的实时信号处于等待状态,那么将率先传递具有最小编号的信号。换言之,信号的编号越小,其优先级越高
hxd@ubuntu:~/learn/6.信号$ 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
2.3 常见信号
2.3.1 SIGINT
当用户在终端按下中断字符(通常是 CTRL + C)时,内核将发送 SIGINT 信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行。所以通常我们都会使用 CTRL + C 来终止一个占用前台的进程,原因在于大部分的进程会将该信号交给系统去处理,从而执行该信号的系统默认操作。
2.3.2 SIGQUIT
当用户在终端按下退出字符(通常是 CTRL + \)时,内核将发送 SIGQUIT 信号给前台进程组中的每一个进程。该信号的系统默认操作是终止进程的运行、并生成可用于调试的核心转储文件。进程如果陷入无限循环、或不再响应时,使用 SIGQUIT 信号就很合适。所以对于一个前台进程,既可以在终端按下中断字符CTRL + C、也可以按下退出字符 CTRL + \来终止,当然前提条件是,此进程会将 SIGINT 信号或 SIGQUIT信号交给系统处理(也就是没有将信号忽略或捕获),进入执行该信号所对应的系统默认操作。
2.3.3 SIGABRT
当进程调用 abort()系统调用时(进程异常终止),系统会向该进程发送 SIGABRT 信号。该信号的系统默认操作是终止进程、并生成核心转储文件。
2.3.4 SIGKILL
此信号为“必杀(sure kill)”信号,用于杀死进程的终极办法,此信号无法被进程阻塞、忽略或者捕获,故而“一击必杀”,总能终止进程。使用 SIGINT 信号和 SIGQUIT 信号虽然能终止进程,但是前提条件是该进程并没有忽略或捕获这些信号,
如果使用 SIGINT 或 SIGQUIT 无法终止进程,那就使用“必杀信号”SIGKILL 吧。Linux 下有一个 kill 命令,kill 命令可用于向进程发送信号,我们会使用"kill -9 xxx"命令来终止一个进程(xxx 表示进程的 pid),这里的-9 其实指的就是发送编号为 9 的信号,也就是 SIGKILL 信号。
2.3.5 SIGUSR1 SIGUSR2
该信号供程序员自定义使用,内核绝不会为进程产生这些信号,在我们的程序中,可以使用这些信号来互通通知事件的发生,或是进程彼此同步操作。该信号的系统默认操作是终止进程。
2.3.6 SIGPIPE
涉及到管道和 socket,当进程向已经关闭的管道、FIFO 或套接字写入信息时,那么系统将发送该信号给进程。该信号的系统默认操作是终止进程。
2.3.7 SIGALRM
与系统调用 alarm()或 setitimer()有关,应用程序中可以调用 alarm()或 setitimer()函数来设置一个定时器,当定时器定时时间到,那么内核将会发送 SIGALRM 信号给该应用程序,关于 alarm()或 setitimer()函数的使用,后面将会进行讲解。该信号的系统默认操作是终止进程。
2.3.8 SIGTERM
这是用于终止进程的标准信号,也是 kill 命令所发送的默认信号(kill xxx,xxx 表示进程 pid),有时我们会直接使用"kill -9 xxx"显式向进程发送 SIGKILL 信号来终止进程,然而这一做法通常是错误的,精心设计的应用程序应该会捕获 SIGTERM 信号、并为其绑定一个处理函数,当该进程收到 SIGTERM 信号时,会在处理函数中清除临时文件以及释放其它资源,再而退出程序。如果直接使用 SIGKILL 信号终止进程,从而跳过了 SIGTERM 信号的处理函数,通常 SIGKILL 终止进程是不友好的方式、是暴力的方式,这种方式应该作为最后手段,应首先尝试使用 SIGTERM,实在不行再使用最后手段 SIGKILL。
2.3.9 SIGSTOP
这是一个“必停”信号,用于停止进程(注意停止不是终止,停止只是暂停运行、进程并没有终止),应用程序无法将该信号忽略或者捕获,故而总能停止进程。
3. 信号相关函数
3.1 signal()
该函数是 Linux 系统下设置信号处理方式最简单的接口,可将信号的处理方式设置为捕获信号、忽略信号以及系统默认操作
#include <signal.h> typedef void (*sig_t)(int); sig_t signal(int signum, sig_t handler);
- signum: 绑定的信号,建议使用宏定义
- handler: sig_t 类型的函数指针,指向信号对应的信号处理函数,当进程接收到信号后会自动执行该处理函数;参数 handler 既可以设置为用户自定义的函数,也就是捕获信号时需要执行的处理函数,也可以设置为 SIG_IGN 或 SIG_DFL,SIG_IGN 表示此进程需要忽略该信号,SIG_DFL 则表示设置为系统默认操作。sig_t 函数指针的 int 类型参数指的是,当前触发该函数的信号,可将多个信号绑定到同一个信号处理函数上,此时就可通过此参数来判断当前触发的是哪个信号。
- 返回值: 此函数的返回值也是一个 sig_t 类型的函数指针,成功情况下的返回值则是指向在此之前的信号处理函数;如果出错则返回 SIG_ERR,并会设置 errno
3.2 sigaction()
也是绑定信号处理函数,只不过该函数功能更多
#include <signal.h> // act参数一班只需要配置(sa_handler或sa_sigaction)和sa_flags; int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
- act: 如果参数 act 不为 NULL,则表示需要为信号设置新的处理方式;如果参数 act 为 NULL,则表示无需改变信号当前的处理方式。
- oldact: 如果参数不为 NULL,则会将信号之前的处理方式等信息通过参数 oldact 返回出来;如果无意获取此类信息,那么可将该参数设置为 NULL。
struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
- sa_handler: 与signal函数的handler参数相同
- sa_sigaction: 也用于指定信号处理函数,这是一个替代的信号处理函数,他提供了更多的参数,可以通过该函数获取到更多信息,这些信号通过 siginfo_t 参数获取,稍后介绍该数据结构;
sa_handler 和sa_sigaction 是互斥的,不能同时设置,对于标准信号来说,使用 sa_handler 就可以了,可通过SA_SIGINFO 标志进行选择
sa_mask: 参数 sa_mask 定义了一组信号,当进程在执行由 sa_handler 所定义的信号处理函数之前,会先将这组信号添加到进程的信号掩码字段中,当进程执行完处理函数之后再恢复信号掩码,将这组信号从信号掩码字段中删除。
参数 sa_flags 指定了一组标志,这些标志用于控制信号的处理过程,可设置为如下这些标志
(多个标志使用位或" | "组合):
SA_SIGINFO:如果设置了该标志,则表示使用 sa_sigaction 作为信号处理函数、而不是 sa_handler。
3.3 kill()
给指定的进程或进程组发送信号, 信号发送的基本规则是发送者进程的实际用户 ID 或有效用户 ID 必须等于接收者进程的实际用户 ID 或有效用户 ID。(超级用户除外)
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
pid:
- 如果 pid 为正,则信号 sig 将发送到 pid 指定的进程。
- 如果 pid 等于 0,则将 sig 发送到当前进程的进程组中的每个进程。
- 如果 pid 等于-1,则将 sig 发送到当前进程有权发送信号的每个进程,但进程 1(init)除外。
- 如果 pid 小于-1,则将 sig 发送到 ID 为-pid 的进程组中的每个进程。
- sig: 参数 sig 指定需要发送的信号,也可设置为 0,如果参数 sig 设置为 0 则表示不发送信号,但任执行错误检查,这通常可用于检查参数 pid 指定的进程是否存在。
3.4 raise()
向自身进程发送信号
#include <signal.h> int raise(int sig); // 等价于kill(getpid(), sig)
3.5 alarm()
设置一个定时器(闹钟),当定时器时间到,内核会向进程发送SIGALRM信号(每个进程只能设置一个 alarm 闹钟)
alarm 闹钟并不能循环触发,只能触发一次,若想要实现循环触发,可以在 SIGALRM 信号处理函数中再次调用 alarm()函数设置定时器
#include <unistd.h> unsigned int alarm(unsigned int seconds);
返回值: 如果在调用 alarm()时,之前已经为该进程设置了 alarm 闹钟还没有超时,则该闹钟的剩余值作为本次 alarm()函数调用的返回值,之前设置的闹钟则被新的替代;否则返回 0。
3.6 pause()
pause()系统调用可以使得进程暂停运行、进入休眠状态,直到进程捕获到一个信号为止,只有执行了信号处理函数并从其返回时,pause()才返回
#include <unistd.h> int pause(void);
3.7 sigemptyset()
初始化信号集,使其不包含任何信号
#include <signal.h> int sigemptyset(sigset_t *set); // 信号集类型,其实就是一个数组 # define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } sigset_t;
3.8 sigfillset()
初始化信号集,使其包含所有信号(包括所有实时信号)
#include <signal.h> int sigfillset(sigset_t *set); // 信号集类型,其实就是一个数组 # define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } sigset_t;
3.9 sigaddset()
向信号集添加一个信号
#include <signal.h> int sigaddset(sigset_t *set, int signum);
3.10 sigdelset()
向信号集删除一个信号
#include <signal.h> int sigdelset(sigset_t *set, int signum);
3.11 sigismember()
测试信号集中是否存在一个信号
#include <signal.h> int sigismember(const sigset_t *set, int signum);
返回值:如果信号 signum 在信号集 set 中,则返回 1;如果不在信号集 set 中,则返回 0;失败则返回- 1,并设置 errno。
3.12 strsignal()
获取信号的描述信息
#include <string.h> char *strsignal(int sig); // 函数会对参数 sig 进行检查,若传入的 sig 无效,则会返回"Unknown signal"信息。
3.13 sigprocmask()
该函数可配置当前进程的信号掩码
信号掩码: 内核为每一个进程维护了一个信号掩码(其实就是一个信号集),即一组信号。当进程接收到一个属于信号掩码中定义的信号时,该信号将会被阻塞、无法传递给进程进行处理,那么内核会将其阻塞,直到该信号从信号掩码中移除,内核才会把该信号传递给进程从而得到处理。
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
- how:
- SIG_BLOCK: 将信号集添加到信号掩码中
- SIG_UNBLOCK: 将信号集中的信号从信号掩码中移除
- SIG_SETMASK: 将信号集直接设置为信号掩码
3.14 sigsuspend()
该函数是一个原子操作。将恢复信号掩码和 pause()挂起进程这两个动作封装成一个原子操作,这sigsuspend()函数会将参数 mask 所指向的信号集来替换进程的信号掩码。
#include <signal.h> int sigsuspend(const sigset_t *mask); //等价于 sigprocmask(SIG_SETMASK, &mask, &old_mask); pause(); sigprocmask(SIG_SETMASK, &old_mask, NULL);
3.15 sigpending()
获取等待中的信号
#include <signal.h> int sigpending(sigset_t *set);
3.16 sigqueue()
发送实时信号
接收该实时信号的进程要为该信号建立一个信号处理函数,使用sigaction函数为信号建立处理函数,
并加入 SA_SIGINFO,这样信号处理函数才能够接收到实时信号以及伴随数据,也就是要使用
sa_sigaction 指针指向的处理函数,而不是 sa_handler,当然允许应用程序使用 sa_handler,但这样
就不能获取到实时信号的伴随数据了。
#include <signal.h> int sigqueue(pid_t pid, int sig, const union sigval value); // 通过测试发现使用sival_ptr传递过去没问题,但访问就会出现段错误。仔细想想也是,一个进程是不能访问另一个进程的空间的。 typedef union sigval { int sival_int; void *sival_ptr; } sigval_t;
- sig: 指定需要发送的信号。与 kill()函数一样,也可将参数 sig 设置为 0,用于检查参数 pid 所指定的进程是否存在。
3.17 abort()
- 该函数用户程序的异常退出,)通常产生 SIGABRT 信号来终止调用该函数的进程,SIGABRT 信号的系统默认操作是终止进程运行、并生成核心转储文件;当调用 abort()函数之后,内核会向进程发送 SIGABRT 信号。
- 即使在我们的程序当中捕获了 SIGABRT 信号,但是程序依然会无情的终止,无论阻
塞或忽略 SIGABRT 信号,abort()调用均不收到影响,总会成功终止进程
#include <stdlib.h> void abort(void);