1. 什么是信号
信号是由用户、系统或者进程发送给目标进程的信息,以通知目标进程某个状态的改变或系统异常。
信号的可能来源包含三个方面:
- 用户,例如我们经常使用
Ctrl+C
来终止程序,它实际上就是给程序发送了一个中断信号;- 系统,这里包括系统某些状态的变化或者异常,比如设备就绪、内存分配失败等;
- 进程,一个进程给另一个进程发送信号,收到信号的进程然后就可以执行某些特定操作。
2. Linux信号概述
2.1 发送信号
一个进程使用
kill
函数来给其他进程发送信号。
#include <signal.h>
/* Send signal SIG to process number PID. If PID is zero,
send SIG to all processes in the current process's process group.
If PID is < -1, send SIG to all processes in process group - PID. */
#ifdef __USE_POSIX
extern int kill (__pid_t __pid, int __sig) __THROW;
#endif /* Use POSIX. */
参数说明:
- pid:目标进程号
- sig:信号值,linux中定义的信号值都大于0,如果sig取值为0,则
kill
函数不发送任何信号
返回值:
kill
函数成功时返回0,失败时则返回-1,并设置errno
2.2 信号处理方式
目标进程在收到信号时,需要定义一个接收函数来处理之。信号处理函数的原型如下:
#include <signal.h>
/* Type of a signal handler. */
typedef void (*__sighandler_t) (int);
参数说明:
- 信号处理函数只带有一个整型参数,该参数用来指示信号类型
除了用户自定义信号处理函数外,bits/signum.h头文件中还定义了信号的其他几种处理方式——
SIG_ERR
、SIG_IGN
和SIG_DEL
。
/* Fake signal functions. */
#define SIG_ERR ((__sighandler_t) -1) /* Error return. */
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
#ifdef __USE_XOPEN
# define SIG_HOLD ((__sighandler_t) 2) /* Add signal to hold mask. */
#endif
2.3 Linux信号
Linux的可用信号都定义在bits/signum.h头文件中,其中包括标准信号和POSIX实时信号。
2.4 中断系统调用
如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且errno被设置为EINTR。我们可以使用
sigaction
函数为信号设置SA_RESTART标志以自动重启被该信号中断的系统调用。
3. 信号函数
3.1 signal系统调用
可以使用
signal
系统调用为一个信号设置处理函数。
#include <signal.h>
/* Set the handler for the signal SIG to HANDLER, returning the old
handler, or SIG_ERR on error.
By default `signal' has the BSD semantic. */
extern __sighandler_t signal (int __sig, __sighandler_t __handler)
__THROW;
参数说明:
- sig:指出要捕获的信号类型
- handler:指定信号sig的处理函数
返回值:
signal
函数成功时返回一个函数指针,这个返回值是前一次调用signal
函数时传入的函数指针signal
函数失败时返回SIG_ERR,并设置errno
3.2 sigaction系统调用
设置信号处理函数的更健壮的接口是如下的系统调用:
#include <signal.h>
/* Get and/or set the action for signal SIG. */
extern int sigaction (int __sig, const struct sigaction *__restrict __act,
struct sigaction *__restrict __oact) __THROW;
参数说明:
- sig:指出要捕获的信号类型
- act:指定新的信号处理方式
- oact:输出信号之前的处理方式(如果不为NULL)
act和oact都是sigaction结构体类型的指针,其定义如下:
/* Structure describing the action to be taken when a signal arrives. */
struct sigaction
{
/* Signal handler. */
#if defined __USE_POSIX199309 || defined __USE_XOPEN_EXTENDED
union
{
/* Used if SA_SIGINFO is not set. */
__sighandler_t sa_handler;
/* Used if SA_SIGINFO is set. */
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
/* Additional set of signals to be blocked. */
__sigset_t sa_mask;
/* Special flags. */
int sa_flags;
/* Restore handler. */
void (*sa_restorer) (void);
};
成员说明:
- sa_handler:指定信号处理函数
- sa_mask:设置进程的信号掩码(在进程原有信号掩码的基础上增加信号掩码),以指定哪些信号不能发送给本进程
- sa_flags:设置程序收到信号时的行为,例如2.4小节中提到的设置SA_RESTART标志以自动重启被该信号中断的系统调用
- sa_restorer:已过时,最好不用
sa_mask是信号集__sigset_t类型,该类型指定一组信号。
4. 信号集
4.1 信号集函数
Linux使用数据结构__sigset_t来表示一组信号,其定义如下:
#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;
参数说明:
- __sigset_t结构体中只有一个长整型数组**__val**,数组的每个元素的每个位表示一个信号
Linux提供了如下一组函数来设置、修改、删除和查询信号集:
/* Clear all signals from SET. */
extern int sigemptyset (sigset_t *__set) __THROW __nonnull ((1));
/* Set all signals in SET. */
extern int sigfillset (sigset_t *__set) __THROW __nonnull ((1));
/* Add SIGNO to SET. */
extern int sigaddset (sigset_t *__set, int __signo) __THROW __nonnull ((1));
/* Remove SIGNO from SET. */
extern int sigdelset (sigset_t *__set, int __signo) __THROW __nonnull ((1));
/* Return 1 if SIGNO is in SET, 0 if not. */
extern int sigismember (const sigset_t *__set, int __signo)
__THROW __nonnull ((1));
4.2 进程信号掩码
除了可以使用
sigaction
结构体的sa_mask成员来设置进程的信号掩码,还可以使用sigprocmask
来设置或查看进程的信号掩码:
/* Get and/or change the set of blocked signals. */
extern int sigprocmask (int __how, const sigset_t *__restrict __set,
sigset_t *__restrict __oset) __THROW;
参数说明:
- how:如果set参数不为NULL,则how参数指定设置进程信号掩码的方式,其可选值如下表
- set:指定新的信号言码,如果set为NULL,则进程信号掩码不变
- oset:输出原来的信号掩码
how参数 | 含义 |
---|---|
SIG_BLOCK | 新的进程信号掩码是当前值和set指定信号集的并集 |
SIG_UNBLOCK | 新的进程信号掩码是当前值和~set指定信号集的并集,因此set指定的信号集将不被屏蔽 |
SIG_SETMASK | 直接将进程信号掩码设置成set |
返回值:
sigprocmask
成功时返回0,失败时返回-1并设置errno
4.3 被挂起的信号
设置进程信号掩码后,被屏蔽的信号将不能被进程接收。如果给进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果取消对被挂起信号的屏蔽,则它能立即被进程接收到。
sigpending
函数可以获得进程当前被挂起的信号集:
/* Put in SET all signals that are blocked and waiting to be delivered. */
extern int sigpending (sigset_t *__set) __THROW __nonnull ((1));
参数说明:
- set:保存被挂起的信号集
返回值:
sigpending
成功时返回0,失败时返回-1并设置errno
5. 示例代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
/* 信号处理函数 */
void sig_handler(int sig)
{
int save_errno = errno;
printf("start process signal: %d\n", sig);
sleep(5);
printf("end process\n");
errno = save_errno;
}
/* 为信号sig设置它的处理函数 */
void addsig(int sig)
{
struct sigaction sa;
memset(&sa, '\0', sizeof(sa));
sa.sa_handler = sig_handler;
sa.sa_flags |= SA_RESTART;
/*
sigfillset(&sa.sa_mask)的作用是:在信号处理函数执行过程中,屏蔽所有信号
(1)如果执行过程中,接收到其他信号,则把它先挂起,等此处理函数执行完毕再去处理接收到的信号
(2)如果执行过程中多次接收到同一个信号,处理函数执行完毕后只去处理接收到的信号一次
可在本程序代码触发一次的sleep的5s期间验证这两点
*/
sigfillset(&sa.sa_mask);
assert(sigaction(sig, &sa, NULL) != -1);
}
int main()
{
addsig(SIGINT); // 2
char ch;
while (ch != 'q')
{
printf("press 'Ctrl+C' to generate a SIGINT and 'q' to exit\n");
scanf("%c", &ch);
}
return 0;
}