Linux中的信号(Signal)全称为软中断信号,故又被称为软中断,常被用作进程之间进行简单通信,或系统内核用来通知进程某个事件的发生。一般情况下,进程仅能从信号中获知信号编号和少量其他信息(如信号发送者的真实用户ID/内存异常发生的地址/文件描述符等)。
1. 信号类型
Linux系统中,支持POSIX标准的规则信号(regular signal,编号1-31)和实时信号(real-time signal,编号32-63)。对于regular signal来说,无论发送多少次,在接收进程处理之前,重复的信号会被合并为一个(每一种regular signal对应于系统进程表项中软中断字段的一个比特,因此不同的信号可以同时存在,同一信号仅能表示有或无而不能表示重复的次数);而real-time signal发送多少次,就会在接收进程的信号队列中出现多少次。
Linux在i386上的31个规则信号(regular signal)
编号 | 信号名称 | 缺省动作 | 说明 |
---|---|---|---|
1 | SIGHUP | 终止 | 终止控制终端或进程 |
2 | SIGINT | 终止 | 键盘产生的中断(Ctrl-C) |
3 | SIGQUIT | dump | 键盘产生的退出 |
4 | SIGILL | dump | 非法指令 |
5 | SIGTRAP | dump | debug中断 |
6 | SIGABRT/SIGIOT | dump | 异常中止 |
7 | SIGBUS/SIGEMT | dump | 总线异常/EMT指令 |
8 | SIGFPE | dump | 浮点运算溢出 |
9 | SIGKILL | 终止 | 强制进程终止 |
10 | SIGUSR1 | 终止 | 用户信号,进程可自定义用途 |
11 | SIGSEGV | dump | 非法内存地址引用 |
12 | SIGUSR2 | 终止 | 用户信号,进程可自定义用途 |
13 | SIGPIPE | 终止 | 向某个没有读取的管道中写入数据 |
14 | SIGALRM | 终止 | 时钟中断(闹钟) |
15 | SIGTERM | 终止 | 进程终止 |
16 | SIGSTKFLT | 终止 | 协处理器栈错误 |
17 | SIGCHLD | 忽略 | 子进程退出或中断 |
18 | SIGCONT | 继续 | 如进程停止状态则开始运行 |
19 | SIGSTOP | 停止 | 停止进程运行 |
20 | SIGSTP | 停止 | 键盘产生的停止 |
21 | SIGTTIN | 停止 | 后台进程请求输入 |
22 | SIGTTOU | 停止 | 后台进程请求输出 |
23 | SIGURG | 忽略 | socket发生紧急情况 |
24 | SIGXCPU | dump | CPU时间限制被打破 |
25 | SIGXFSZ | dump | 文件大小限制被打破 |
26 | SIGVTALRM | 终止 | 虚拟定时时钟 |
27 | SIGPROF | 终止 | profile timer clock |
28 | SIGWINCH | 忽略 | 窗口尺寸调整 |
29 | SIGIO/SIGPOLL | 终止 | I/O可用 |
30 | SIGPWR | 终止 | 电源异常 |
31 | SIGSYS/SYSUNUSED | dump | 系统调用异常 |
编号为0的信号,用以测试进程是否拥有信号发送的权限,并不会被实际发送。
同一信号在不同系统中的值可能会不一样(或者说在不同系统中同一数值可能会代表不同的信号),因此,建议使用信号名而不是信号值。
信号的数值越小,则优先级越高。当进程收到多个待处理信号时,总是先处理优先级别高的信号。
2. 信号处理
通常,进程对信号的处理方式可以是以下三种方式之一:默认方式(default,交由系统默认信号处理函数处理);忽略(ignore,不进行任何处理);进程捕获信号并处理(capture)。
SIGKILL,SIGSTOP不能被用户程序捕获,也不能被忽略。也就是说,SIGKILL/SIGSTOP总是会由系统默认的处理函数进行处理(最终结果是进程被中止)。
3. 信号相关系统调用函数
Linux中提供了kill()/tkill()/tgkill()/pause/alarm/setitimer()/signal()/sigaction()/sigpending/sigprocmask()/sigsuspend()/rt_sigaction()/rt_sigpending()/rt_sigprocmask()/rt_sigqueueinfo()/rt_sigsuspend()/rt_sigtimedwait()等函数供进程使用。
3.1 kill()/tkill()/tgkill()
kill()/tkill()/tgkill()分别用于向进程(进程组)/线程(线程组)发送信号。
3.1.1 进程信号发送:kill()
函数声明
#include <signal.h>
int kill(pid_t pid, int sig);
glibc功能测试宏定义:
参数及返回值
- 入参pid:指定信号发送的目标进程
- pid>0:指定目标进程的ID
- pid=0:目标为当前进程(函数调用进程)所在进程组内的所有进程
- pid=-1:目标为当前进程有发送信号权限的所有进程*(除自身和进程1(init)外**)
- pid<-1:目标进程ID值为-pid
- 入参sig:指定要发送的信号值。sig=0表示仅执行错误检查而不实际发送任何信号,常用于检查目标进程/进程组是否存在。
- 返回值:0表示成功;-1表示失败,失败原因由errno给出:
- EINVAL:无效信号
- EPERM:发送进程没有权限进行操作
- ESRCH:目标进程/进程组不存在(“存在”的进程则可以是僵尸进程(zombie),或已经提交退出申请的进程)
*发送进程拥有向目标进程发送信号的权限,是指发送进程为特权进程(拥有CAP_KILL权限),或者发送进程的有效用户ID/真实用户ID(effective UID/real UID)等于目标进程的真实用户ID(real UID)或saved set-user-ID。对于SIGCONT信号,发送进程和目标属于同一个会话(session),就拥有发送的权限。
**不允许向进程0(swapper)发送任何信号;进程1(init)仅处理那些自己感兴趣的信号(capture),而丢弃其他信号。
3.1.2 线程信号发送:tkill()和tgkill()
tkill与tgkill用于向指定线程发送信号。相比较而言,kill()则是向进程发送信号,如果进程拥有多条线程,则无法确定究竟哪一条线程会进行信号响应)。
函数声明
int tgkill(int tgid, int tid, int sig);
由于tkill()仅通过线程ID来指定目标,有可能发送给错误的线程(比如欲发送的目标线程已退出,而另一条新线程使用了同一ID),因此,tkill()已废弃。推荐使用tgkill()。
当线程组ID(tgid)为-1时,tgkill()等同于tkill()。
3.2 挂起操作:pause()
pause()用于挂起进程,直到收到信号后继续执行。
函数声明
int pause(void);
3.3 signal()
signal()用于设置自定义的信号处理响应函数。
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
参数及返回值
- signum:要捕获处理的信号值
- handler:指定信号处理函数。handler可以是
- 自定义的信号处理函数:入参为int(信号值),无返回值(void)
- SIG_IGN:表示忽略此信号
- SIG_DFT:表示使用系统默认处理
- 返回值:
- SIG_ERR:错误,错误码由errno给出:EINVAL表示指定的信号值错误
- 此前的信号处理函数
例:进程捕获SIGINT/SIGUSR1/SIGUSR2信号并打印相应信息,使用命令kill发送信号进行测试或退出。
- #include <stdio.h>
- #include <signal.h>
- #include <errno.h>
- #include <stdlib.h>
- typedef void (*sighandler_t)(int);
- void sighandler(int signo)
- {
- switch (signo)
- {
- case SIGINT:
- printf("Got a SIGINT\n");
- break;
- case SIGUSR1:
- printf("Got a SIGUSR1\n");
- break;
- case SIGUSR2:
- printf("Got a SIGUSR2\n");
- break;
- default:
- break;
- }
- }
- int main(void)
- {
- sighandler_t oldhandler = signal(SIGINT, sighandler);
- if ( oldhandler == SIG_ERR )
- {
- printf("set signal handler error! errno = %d\n", errno);
- return -1;
- }
- signal(SIGUSR1, sighandler);
- signal(SIGUSR2, sighandler);
- printf("pid = %d\n", (int)getpid());
- while (1)
- {
- usleep(1000000);
- }
- return 0;
- }
3.4 sigaction()
使用signal()可以自定义信号处理方式,但通过signal()所设置的信号处理函数所能获取的信息太少-仅仅只有信号类型/值,过于简单。某些情况下进程需要获取与此信号相关的更详尽的信息,如对于因内存访问异常而产生的SIGSEGV信号,非法访问的地值址;再如信号的发送者是谁等等。这时候,就需要sigaction()。
函数声明
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
glibc功能测试宏:
结构体sigaction
- struct sigaction {
- void (*sa_handler)(int);
- void (*sa_sigaction)(int, siginfo_t *, void *);
- sigset_t sa_mask;
- int sa_flags;
- void (*sa_restorer)(void);
- };
结构体sigaction字段:
- sa_handler:与signal()中相同,指定一个入参为int(信号值)/无返回值的自定义函数作为信号处理函数;也可以是SIG_DFT/SIG_IGN分别表示使用系统默认处理/忽略
- sa_sigaction:入参为int(信号值)/siginfo_t*(siginfo_t指针,给出与信号相关的其他信息)/void*(被转换的ucontext_t指针,给出信号相关的用户上下文信息),无返回值的用户信号处理函数地址。
sa_handler与sa_sigaction均可以指定用户信号处理函数。当sa_flags字段中设置了SA_SIGINFO位时,系统使用sa_sigaction指定的函数;否则使用sa_handler。考虑到不同系统兼容性的问题(sa_handler和sa_sigaction共用一个union),不要同时指定两个字段。 - sa_mask:信号处理期间需要阻塞的信号掩码。此外,触发了信号处理函数的信号本身也会被阻塞,除非设置了SA_NODEFER标志
- sa_flags:信号行为标识,可以是以下标置位(bit)或组合:
- SA_NOCLDSTOP:仅当信号值(signum)为SIGCHLD时有效(为SIGCHLD设置自定义处理函数时),表示不接收子进程停/启(stop/resume)信号
- SA_NOCLDWAIT:当信号值(sginum)为SIGCHILD时,表示当子进程终止时不要将子进程变为僵尸进程(zombie)。此标志位仅当为SIGCHLD设置自定义处理函数或将SIGCHLD处理函数恢复为系统缺省时(SIG_DFL)时有效。
- SA_NODEFER:在触发处理函数的信号处理过程中,不要阻止再次接收此信号(缺省情况下,正在处理中的信号会被系统阻塞)。SA_NOMASK含义与之相同,但已废弃。
- SA_ONSTACK:调用使用sigaltstack()设置的另一个信号处理函数,若其不可用则调用缺省栈内处理函数。
- SA_RESETHAND:在某个信号触发过自定义处理函数后,将此信号的处理方式恢复为系统默认。SA_ONSHOT含义相同,但已废弃
- SA_RESTART:与BSD兼容,在信号处理中,将某些系统调用标记为可重用
- SA_SIGINFO:使用sa_sigaction指定的3参数信号处理函数而非sa_handler指定的单参数处理函数。
- sa_restorer:已废弃
结构体siginfo_t
- siginfo_t {
- int si_signo; /* Signal number */
- int si_errno; /* An errno value */
- int si_code; /* Signal code */
- int si_trapno; /* Trap number that caused
- hardware-generated signal
- (unused on most architectures) */
- pid_t si_pid; /* Sending process ID */
- uid_t si_uid; /* Real user ID of sending process */
- int si_status; /* Exit value or signal */
- clock_t si_utime; /* User time consumed */
- clock_t si_stime; /* System time consumed */
- sigval_t si_value; /* Signal value */
- int si_int; /* POSIX.1b signal */
- void *si_ptr; /* POSIX.1b signal */
- int si_overrun; /* Timer overrun count; POSIX.1b timers */
- int si_timerid; /* Timer ID; POSIX.1b timers */
- void *si_addr; /* Memory location which caused fault */
- int si_band; /* Band event */
- int si_fd; /* File descriptor */
- }
siginfo_t字段说明:
- si_signo/si_sigerrno/si_sigcode字段对所有信号均有效(Linux中未使用si_sigerrno)
- POSIX.1b信号和SIGCHLD会填充si_pid和si_uid字段
- POSIX.1b定时器会填充si_overrun和si_timerid(内核用于标识定时器的ID,与timer_create()所返回ID并不相同)
SIGCHLD填充si_status/si_utime/si_stime字段,单位为sysconf(_SC_CLK_TCK)
- si_int/si_ptr由POSIX.1b信号发送方指定。
- SIGILL, SIGFPE, SIGSEGV, SIGBUS信号会填充si_addr字段; SIGPOLL填充si_band和si_fd;
- si_code字段值给出信号发送的原因:
- SI_USER:由kill()或raise()发送
- SI_KERNEL:由系统内核发送
- SI_QUEUE:sigqueue()
- SI_TIMER:POSIX定时器超时
- SI_MESGQ:POSIX消息队列状态改变
- SI_ASYNCIO:AIO完成
- SI_SIGIO:排队的SIGIO
- SI_TKILL:tkill()或tgkill()
对SIGILL/SIGFPE/SIGSEGV/SIGBUS/SIGTRAP/SIGPOLL/SIGCHLD等信号,有各自相应的si_code值
返回值:0:成功;-1:失败
例:使用sigaction()设置信号SIGSEGV的的自定义处理。在主程序中通过非法内存访问触发SIGSEGV信号,在信号处理中给出非法访问的地址。
- #include <stdio.h>
- #include <signal.h>
- #include <errno.h>
- #include <string.h>
- #include <stdlib.h>
- void sigact(int signo, siginfo_t *siginfo, void *utx)
- {
- printf("signo = %d\n", signo);
- if ( signo == SIGSEGV )
- {
- printf("siginfo.si_addr = %p\n", siginfo->si_addr);
- printf("siginfo.si_code = %d\n", siginfo->si_code);
- }
- exit(-1);
- }
- int main(void)
- {
- int ret = 0;
- char *ptr = (char *)0x80000000;
- struct sigaction sanew, saold;
- memset(&sanew, 0, sizeof(struct sigaction));
- memset(&saold, 0, sizeof(struct sigaction));
- sanew.sa_sigaction = sigact;
- sanew.sa_flags = SA_SIGINFO;
- ret = sigaction(SIGSEGV, &sanew, &saold);
- if ( ret )
- {
- printf("sigaction() error! errno = %d\n", errno);
- return ret;
- }
- *ptr = 0; /*to trigger a signal - SIGSEGV */
- return ret;
- }
3.4 定时器:alarm()/setitimer()
alarm()/setitimer()用于为进程设置定时器,按照指定的时间系统发送指定信号给进程。
3.4.1 alarm()
函数声明
unsigned int alarm(unsigned int seconds);
入参及返回值
- 入参seconds:指定定时器时延,单位为秒。若为0,不设置定时器,用于获取返回值;若不为0,设置延迟时长,到指定时间后系统发送SIGALRM信号给进程
- 返回值:如进程已设置有定时器,函数返回值为距定时器被触发所剩余的秒数;若进程还未设置定时,则返回0
alarm()仅产生一次定时,定时信号一旦发送定时器即失效。如果在一次定时后仍需获取定时信号,则需要在定时信号处理函数中再次调用alarm()。
3.4.2 setitimer()/getitimer()
setitimer()/getitimer()用于设置/获取固定间隔的定时信号。
函数声明
int getitimer(int which, struct itimerval *curr_value);
int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);
结构体itimerval定义:
- struct itimerval {
- struct timeval it_interval; /* 第一次超时后的时间间隔值 */
- struct timeval it_value; /* 首次超时时长 */
- };
- struct timeval {
- long tv_sec; /* 秒 */
- long tv_usec; /* 微秒(us), 必须<1s*/
- };
参数及返回值
- which:指定定时间隔类型
- ITIMER_REAL:定时器按照真实时间运行,超时后发送SIGALRM信号
- ITIMER_VIRTUAL:定时器仅在进程执行时运行,超时后发送SIGVALRM信号
- ITIMER_PROF:定时器在进程运行和系统为进程工作时运行(进程所使用的用户时间和内核时间),超时后发送SIGPROF信号
- newvalue: 设定定时器时间间隔值。其中it_value指定从设定开始至首次发送超时信号的时间间隔值;it_interval指定以后的定时器时间间隔(>0)
- old_value/curr_value:当前定时设置。old_value可以为NULL,表示不需要返回旧的设置
- 返回值:0表示成功;-1表示失败,失败原因由errno给出:
- EFAULT:newvalue/old_value/curr_value指针无效
- EINVAL:which值无效;或tv_usec值不在允许范围之内(0-999999)
例,设置定时器5秒之后发送第一个超时信号SIGALRM,以后每隔1秒发送一次
- #include <stdio.h>
- #include <unistd.h>
- #include <signal.h>
- #include <sys/time.h>
- #include <errno.h>
- #include <stdlib.h>
- static long int g_cnt = 0;
- void tmhandler(int signum)
- {
- printf("cnt = %ld, Received signal %d\n", g_cnt, signum);
- signal(SIGALRM, tmhandler);
- ++g_cnt;
- }
- int main(void)
- {
- int ret = 0;
- struct itimerval itmv = {0};
- signal(SIGALRM, tmhandler);
- itmv.it_interval.tv_sec = 1;
- itmv.it_value.tv_sec = 5;
- ret = setitimer(ITIMER_REAL, &itmv, NULL);
- if ( ret )
- {
- printf("setitimer() failed, errno = %d\n", errno);
- exit(ret);
- }
- while (1)
- {
- usleep(10000000);
- }
- return 0;
- }