概述
- 信号是由用户 、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。
- Linux信号产生条件:
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。
比如Ctrl+C
通常会给进程发送一个中断信号。 - 系统异常。
比如浮点数异常和非法内存段访问。 - 系统状态变化
比如alarm定时器到期将引起SIGALRM信号。 - 运行kill命令或调起kill函数
- 对于前台进程,用户可以通过输入特殊的终端字符来给它发送信号。
- 服务器程序必须处理(或至少处理)一些常见的信号,以免异常终止。
Linux信号概述
发送信号
- Linux下,一个进程给其他进程发送信号的API是kill函数。
该函数把sig信号发送给目标进程pid。
1 2 3 4 |
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig); |
- Linux的信号值都大于0,如果sig取值为0,则kill函数不发生任何信号。
但将sig设置为0可以用来检测目标进程或进程组是否存在,因为检查工作总是在信号发送之前就执行。
不过这种检测方式是不可靠的。一方面由于进程PID的回绕,可能导致被检测的PID不是我们期望的进程PID;另一方面是这种检测不是原子操作。 - kill调用成功时返回0,失败返回-1并设置errno。
信号处理方式
- 目标进程在收到信号时,需要定义一个接收函数来处理这个信号。
- 参数用来指示信号类型。
信号处理函数应该是可重入的,否则很容易引发一些竞态条件。
1 2 3 |
#include <signal.h> typedef void (*__sighandler_t)(int); |
- 除了用户自定义的信号处理函数外,
bits/signum.h
头文件中还定义了信号的两种其他处理方式:SIG_IGN和SIG_DEL。- SIG_IGN表示忽略目标信号
- SIG_DFL表示信号的默认处理方式:
结束进程(Term)
忽略信号(Ign)
结束进程并生成核心转储文件(Core)
暂停进程(Stop)
继续进程(Cont)
1 2 3 4 |
#include <bits/signum.h> #define SIG_DEL ((__sighandler_t) 0) #define SIG_IGN ((__sighandler_t) 1) |
Linux信号
- Linux的可用信号都定义在
bits/signum.h
头文件中,其中包括标准信号和POSIX实时信号。
中断系统调用
- 如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。
- 我们可以用sigaction函数为信号设置SA_RESTART标志以自动重启被信号中断的系统调用。
- 对于默认行为是暂停进程的信号(比如SIGSTOP,SIGTTIN),如果我们没有为它们设置信号处理函数,则它们也可以中断某些系统调用(比如connect,epoll_wait)。
POSIX没有规定这种行为,这是Linux独有的。
信号函数
signal系统调用
- 要为一个信号设置处理函数,可以使用下面的signal系统调用:
- sig参数指出要捕获的信号类型。handler参数是_sighandler_t类型的函数指针,用于指定信号sig的处理函数。
- 成功时返回一个函数指针,这个返回值是前一次调用signal函数时传入的函数指针,或者是信号sig对应的默认处理函数指针SIG_DEF。
- signal系统调用出错时返回SIG_ERR,并设置errno。
1 2 3 |
#include <signal.h> _sighandler_t signal(int sig, _sighandler_t _handler) |
sigaction系统调用
- 设置信号处理函数的更健壮的接口:
- sig参数指出要捕获的信号类型
- act参数指定新的信号处理方式
- oact参数则输出信号先前的处理方式(如果不为NULL)的话
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct sigaction { #ifdef __USE_POSIX199309 union { _sighandler_t sa_handler; void (*sa_sigaction)(int, siginfo_t*, void*); } _sigaction_handler; #define sa_handler __sigaction_handler.sa_handler #define sa_sigaction __sigaction_handler.sa_sigaction #else _sighandler_t sa_handler; #endif _sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; |
1 2 3 |
#include <signal.h> int sigaction(int sig, const struct sigaction* act, struct sigaction* oact); |
信号集
信号集函数
- Linux使用数据结构sigset_t来表示一组信号。
- sigset_t实际上是一个长整型数组,数组的每个位表示一个信号。
1 2 3 4 5 6 |
#include <bits/sigset.h> #define _SIGSET_NWORDS (1024/(8*sizeof(unsigned long int))) typedef struct { unsigned long int __val[_SIGSET_NWORDS]; } __sigset_t; |
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <signal.h> // 清空信号集 int sigemptyset(sigset_t* _set); // 在信号集中设置所有信号 int sigfillset(sigset_t* _set); // 将信号_signo添加至信号集中 int sigaddset(sigset_t* _set, int _signo); // 将信号_signo从信号集中删除 int sigdelset(sigset_t* _set, int _signo); // 测试_signo是否在信号集中 int sigismember(_const sigset_t* _set, int _signo); |
进程信号掩码
- 可以利用sigaction结构体的sa_mask成员来设置进程的信号掩码
- _set指定新的信号掩码,_oset输出原来的信号掩码(如果不为NULL的话)
1 2 3 |
#include <signal.h> int sigprocmask(int _how, _const sigset_t* _set, sigset_t* _oset); |
- 如果_set不为NULL的话,_how参数指定设置进程的信号掩码的方式
如果_set参数为NULL,则进程信号掩码不变,此时我们仍然可以利用_oset参数来获取进程的当前的信号掩码
被挂起的信号
- 设置进程信号掩码后,被屏蔽的信号将不能被进程接收。
- 如果我们给进程发一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。
如果我们取消对被挂起信号的屏蔽,则它立即能被进程接收到。 - 获取进程当前被挂起的信号集:
- set参数用于保存被挂起的信号集。
- 进程即使多次接收到同一个被挂起的信号,sigpending函数也只能反映一次。
并且,当我们再次使用sigprocmask使能接收该挂起的信号时,该信号的处理函数也只能被触发一次。
- 成功时返回0,失败返回-1并设置errno。
1 2 3 |
#include <signal.h> int sigpending(sigset_t* set); |
统一事件源
- 信号是一种异步事件:
- 信号处理函数和程序的主循环是两条不同的执行路线。
- 显然,信号处理函数需要尽可能快地执行完毕,以确保信号不被屏蔽太久。一种解决方案:
- 把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环在根据收到的信号值执行目标信号对应的逻辑代码。
- 信号处理函数通常使用管道来将信号“传递”给主循环:
- 信号处理函数往管道的写端写入信号值
- 主循环则从管道的读端读出该信号值
- 主循环使用I/O复用系统监听管道的读端文件描述符上的可读事件从而知道管道上何时有数据可读。
网络编程相关信号
SIGHUP
- 当挂起进程的控制终端时,SIGHUP信号将被触发。
- 对于没有控制终端的网络后台程序而言,它们通常利用SIGHUP信号来强制服务器重读配置文件。
- 比如xinetd超级服务程序:
- xinetd程序在接收到SIGHUP信号之后将调用hard_reconfig函数,它循环读取
/etc/xinetd.d/
目录下的每个子配置文件,并检测其变化。 - 如果某个正在运行的子服务的配置文件被修改以停止服务,则xinetd主进程将给该子服务进程发送SIGTERM信号以结束它。
- 如果某个子服务的配置文件被修改以开启服务,则xinetd将创建新的socket并将其绑定到该服务对应的端口上。
- xinetd程序在接收到SIGHUP信号之后将调用hard_reconfig函数,它循环读取
strace
- strace命令能跟踪程序执行时调用的系统调用和接收到的信号。
SIGPIPE
- 默认情况下,往一个读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。
- 我们需要在代码中捕获并处理该信号,或者至少忽略它,因为程序接收到SIGPIPE信号的默认行为是结束进程,而我们绝对不希望因为错误的写操作而导致程序退出。
- 引起SIGPIPE信号的写操作将设置errno为EPIPE。
SIGURG
- 在Linux环境下,内核通知应用程序带外数据到达主要有两种方法:
- I/O复用技术,select等系统调用在接收到带外数据时将返回。
- 使用SIGURG信号
本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Linux 内核空间&&用户空间03/30
- ♥ Linux 基于文件描述符的文件操作(非缓冲)03/23
- ♥ Linux目录的作用03/16
- ♥ Make&&Makefile03/23
- ♥ Shell 语法记述 第四篇09/05
- ♥ Linux 信号处理二03/27