Linux 信号机制
概述
Linux 在进程间通信时,有时候需要用到异步通讯方式,而信号机制是Linux系统本身提供的一种异步通讯.
Linux中信号的类别
Linux信号在系统中总数是有限的,信号种类如下所示:
Linux支持的信号.png
这些信号在Linux系统中各自有不同的用途.同时在Unix和Linux的不断发展中出现了两种信号,或者说由于历史遗留问题出现了两种信号:可靠信号和不可靠信号.不可靠信号是从早期的Unix继承而来,而可靠信号是后来定义的信号.
信号的处理
一个进程对信号的响应可以分为三种情况,分别为:
- 忽略信号
- 捕捉信号
- 执行系统默认操作
忽略信号
忽略信号是指在代码中进行设置后,进程不会对信号进行响应,值得注意的是有两个信号是不能忽略的这两个信号为SIGKILL和SIGSTOP.
捕捉信号
捕捉信号是指在代码中设置函数,当指定的信号发生时,调用已经设置好的处理函数,使得进程可以按照自己的意愿对信号发生时所代表的时间进行处理.
执行系统默认操作
Linux操作系统规定了很多对于信号的默认操作,这些可以通过查询获取到,但是对于实时信号来说,器系统的默认操作都是进程终止.
信号的使用
在使用信号时首先需要确认使用何种信号.然后需要进程去产生信号.
信号的产生
信号可以通过六个函数产生:
- kill函数
- raise函数
- sigqueue函数
- alarm函数
- setitimer函数
- abort函数
kill函数
kill函数原型如下:
int kill(pid_t pid,int signo)
kill函数中的pid参数可以设置为如下方式:
pid > 0: 将信号发送给指定进程ID为pid的进程
pid == 0: 将信号发送给与发送进程在同一进程组的所有进程
pid < 0: 将信号发送给进程组ID等于pid绝对值的进程,如果pid==-1,那么就将信 号发送给有权限发送信号的系统上的所有进程.
kill函数中的signo参数也可以设置为如下方式:
signo == 0:发送一个空信号,实际上不发送任何值给目标进程,但是可以检测目标进程是否存在,同时是否有权限向目标进程发送该信号.
signo != 0:向目标进程发送指定的信号
raise函数
raise函数原型如下:
int raise(int sig);
raise函数在实质上等价于kill(get_pid(),signo);因此raise函数只可以向自身进程发送信号其中signo参数的设置和kill函数相同.
sigqueue函数
sigqueue函数原型如下:
int sigqueue(pid_t pid, int sig, const union sigval value);
sigqueue函数的pid参数和sig参数和kill函数相同其功能也和kill函数类似.但是和kill函数不同sigqueue函数是较新的发送信号的函数,支持后面出现的实时信号,在发送信号的时候,也支持参数的传递,比kill函数多了一些信号的附加信息.
sigqueue函数比kill函数更加优越的地方主要在于第三个参数的使用上.第三个参数定义如下:
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
可以注意到这是一个联合体,在联合体中可以指定信号传输的参数,要么是一个四字节值,要么是一个指针.使用这个联合体时,信号的目标进程也需要使用新的信号捕捉函数sigaction,否则该参数无效,具体的内容参照下面关于sigaction函数的叙述.
alarm函数
alarm函数的原型如下所示:
unsigned int alarm(unsigned int seconds);
信号中有一个专门用来定时的信号SIGALRM信号,alarm函数就是为使用这个信号专门设计的一个函数,在alarm函数中的senconds参数中传入具体的秒数,在相应的时间到达时,就会向函数所在进程发送一个SIGALRM信号.
需要注意的一点是,每个进程只能拥有一个闹钟时间,如果一个进程已经设置过闹钟时间,且时间还未达到时,再次调用alarm函数设置闹钟时间,那么之前的值将会被新值替代,同时将闹钟时间的余留值返回.所以当新设置的闹钟时间为0时,就会取消原有的闹钟时间.
setitimer函数
setitimer函数原型如下:
int setitimer(int which, const struct itimerval *new_value,struct itimerval *old_value);
setitimer函数是对alarm函数功能的扩充,同时setitimer函数还有一个配套的查询函数:
int getitimer(int which, struct itimerval *curr_value);
setitimer函数支持三种定时器,这个选择由which参数指定,这三个定时器分别为:
- ITIMER_REAL:设定绝对时间,当设定的时间到达时内核将发送SIGALRM信号给本进程
- ITIMER_VIRTUAL :设定程序的执行时间(指程序在用户层运行的时间),当程序的执行时间到达时,内核将发送SIGALRM信号给本进程
- ITIMER_PROF :设定进程执行以及内核因本进程而消耗的时间总和,内核将发送ITIMER_VIRTUAL信号给本进程
setitimer的第二个参数是指定运行的时间,这个参数的结构体原型如下所示:
struct itimerval
{
struct timeval it_interval; /* Interval for periodic timer */
struct timeval it_value; /* Time until next expiration */
};
struct timeval
{
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
其中itimerval结构体是传入的参数,这个结构体包含了两个timerval结构体变量,这两个变量用来设定时间.
其中it_interval指定的是发送信号的周期时间,it_value中保存的是到下一次发送信号的时间.
在setitimer的第三个参数时返回之前设定的时间周期值.
abort函数
abort函数原型如下
void abort(void);
该函数向进程发送SIGABORT信号,默认情况下进程会异常退出,即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。
信号的捕捉和处理函数
目前在Linux中信号的捕捉处理函数有两个:
- signal函数
- sigaction函数
signal函数
signal函数的原型如下所示i:
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
第一个参数signum负责设定相应要捕捉的信号
第二个参数是一个函数指针,这个参数可以被指定为3个值:
SIG_IGN:忽略该信号
SIG_DFL:系统默认方式处理信号
函数指针:负责设定捕捉到信号时应采取的操作,函数原型为typedef指定的格式.
sigaction函数
sigaction函数在功能上已经彻底取代了signal函数,该函数的原型为:
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
第一个参数signum负责指定要捕捉的信号.
第二个参数和第三个参数都是一个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);
};
注意,在使用过程中,第一个元素sa_handler和第二个元素sa_sigaction不能同时指定.
这个结构体中的元素如下:
- 第一个元素sa_handler是函数指针:(可以参照signal函数中的函数指针)
负责指定关于信号的信号处理函数,但是该函数只能传入一个对应信号的信号值 - 第二个参数sa_sigaction也是一个函数指针:(可以参照signal函数中的函数指针)
负责指定相应信号的处理函数,但是该函数可以传入三个参数,第一个为信号值,第二个参数为一个siginfo_t结构体用以说明本次信号处理的各种详细信息,第三个参数现行标准中现在还没有做相关规定
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 */
long si_band; /* Band event (was int in
glibc 2.3.2 and earlier) */
int si_fd; /* File descriptor */
short si_addr_lsb; /* Least significant bit of address
(since Linux 2.6.32) */
void *si_call_addr; /* Address of system call instruction
(since Linux 3.5) */
int si_syscall; /* Number of attempted system call
(since Linux 3.5) */
unsigned int si_arch; /* Architecture of attempted system call
(since Linux 3.5) */
}
-
第三个元素是一个信号集:设定了在信号处理期间应该屏蔽的信号.
-
第四个元素:设定了修改信号行为的标识.例如当自身子进程状态改变时,不接受子进程状态改变信号等.这个flag的设定值可以通过查询获取到.
-
第五个参数也是一个函数指针:POSIX标准不再使用.
信号处理中的一些问题
- 1 信号SIGKILL和SIGSTOP不能被忽略,如果采取忽略这两个信号,那么程序会报错
- 2 对某个进程发送信号时,如果该进程是多线程的,就会出现问题.由于信号在设计时没有多线程概念,所以在设计之初就没有考虑信号和多线程一起使用的情况,虽然在后面对标准进行了重新修订,单一些较老的信号依然存在一些问题.
在信号和多线程一起使用时,信号只会被进程中的一个线程处理,其他线程是无法收到信号的.而且处理信号的线程是未知的(在ubuntu下,测试发现信号首先被主线程处理),而如果指定的信号没有被处理信号的线程重新定义操作函数,那么会采用系统默认操作,导致程序运行出现问题.最好的解决办法就是,其他线程都屏蔽要捕捉的线程,只有信号处理线程不进行屏蔽,那么该信号就会达到目标线程进行处理.
对信号阻塞进行处理的函数如下所示:
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
其中how共有三个参数:
how的取值为: - SIG_BLOCK 把参数set中的信号添加到信号屏蔽字中
- SIG_SETMASK 把信号屏蔽字设置为参数set中的信号
-
SIG_UNBLOCK 从信号屏蔽字中删除参数set中的信号
而在使用sigset之前要确保先调用int sigemptyset(sigset_t *set)或者int sigfillset(sigset_t *set); 对信号集进行初始化,否则该参数初值是未知的.
sigemptyset(sigset_t *set)初始化由set指定的信号集,信号集里面的所有信号被清空;
int sigfillset(sigset_t *set);
调用该函数后,set指向的信号集中将包含linux支持的64种信号;