信号集及相关函数
信号集(sigset_t
)是表示多个信号的数据类型。
不同的信号编号可能会超过一个整型量的位数,所以不能用整型量中的一位代表一个信号。
POSIX.1定义数据类型sigset_t
用以表示信号集,并定义了信号的操作函数。
信号集的操作函数
下面的函数可以用来对信号集初始化、增删、检测等:
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
// 上面四个函数,成功返回0;出错返回-1
int sigismember(const sigset_t *set, int signum);
// signum信号存在返回1,不存在返回0
sigemptyset
函数初始化set
指向的信号集,清除其中所有信号。
sigfillset
函数也初始化set
指向的信号集,使其包含所有信号。
sigaddset
函数将signum
指定的信号添加到信号集set
中。
sigdelset
函数从信号集set
中删除signum
指定的信号。
sigismember
函数signum
指定的信号是否在信号集set
中。
信号集的应用函数
sigpromask
函数
调用sigpromask
函数可以检测或更改进程的信号屏蔽字。
#include <signal.h>
int sigprocmask(int how, const sigset_t * restrict set,
sigset_t * restrict oldset);
// 返回值:成功返回0, 失败返回-1
无论其他参数取何值,oldset
参数均用于返回进程的当前信号屏蔽字。
如果set
是空指针,则不改变该进程的信号屏蔽字,how
参数也无意义。
如果set
非空,则根据how
参数来决定如何修改当前信号屏蔽字。
how
参数取值如下:
how | 说明 |
---|---|
SIG_BLOCK | 其当前信号屏蔽字与set 指向的信号屏蔽字并集产生该进程新的信号屏蔽字。set 中包含了需要阻塞的新信号。 |
SIG_UNBLOCK | 该进程新的信号屏蔽字是其当前信号屏蔽字与set 所指向信号屏蔽字的交集。set 中包含了需要解除阻塞的信号。 |
SIG_SETMASK | 该进程新的信号屏蔽字是set 所指向的值。 |
在调用sigprocmask
后如果有未决的、不在阻塞的信号,则在该函数返回前,就会至少将其中之一递送给该进程。这说明sigprocmask
函数可能会被信号处理函数中断。
示例
下面的示例会调用sigprocmask
函数检测进程当前信号屏蔽字中的信号并打印。
#include <errno.h>
#include <error.h>
#include <signal.h>
#include <stdio.h>
static void pr_mask(const char* str)
{
sigset_t sigset;
int errno_save;
errno_save = errno;
if (sigprocmask(0, NULL, &sigset) != 0)
{
perror("sigprocmask");
return;
}
else
{
printf("%s", str);
if (sigismember(&sigset, SIGINT))
printf(" SIGINT");
if (sigismember(&sigset, SIGQUIT))
printf(" SIGQUIT");
if (sigismember(&sigset, SIGUSR1))
printf(" SIGUSR1");
if (sigismember(&sigset, SIGALRM))
printf(" SIGALRM");
if (sigismember(&sigset, SIGTSTP))
printf(" SIGTSTP");
printf("\n");
}
errno = errno_save;
}
int main()
{
sigset_t newsig_set;
sigemptyset(&newsig_set);
sigaddset(&newsig_set, SIGALRM);
// 阻塞SIGALRM信号
printf("block signal SIGALRM\n");
if (sigprocmask(SIG_BLOCK, &newsig_set, NULL) != 0)
{
perror("sigprocmask");
return -1;
}
pr_mask("current sig mask:");
// 取消阻塞SIGALRM信号
printf("unblcok signal SIGALRM\n");
if (sigprocmask(SIG_UNBLOCK, &newsig_set, NULL) != 0)
{
perror("sigprocmask");
return -1;
}
pr_mask("current sig mask:");
}
// $ ./pr_mask
// block signal SIGALRM
// current sig mask: SIGALRM
// unblcok signal SIGALRM
// current sig mask:
在这个例子中,先是在信号屏蔽字中增加了SIGALRM
信号,随后又删除了SIGALRM
信号。
使用这种技术,也可以保护不希望由信号中断的代码临界区。
下面的示例展示了如何使用sigprocmaks
函数来创建一个不被信号中断的代码临界区:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
volatile sig_atomic_t flag = 0;
void signal_handler(int signum) {
flag = 1;
}
int main() {
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGINT, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
while (1) {
// 进入临界区前屏蔽SIGINT信号
sigset_t mask, prev_mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigprocmask(SIG_BLOCK, &mask, &prev_mask);
// 在临界区内执行一些操作
printf("进入临界区\n");
sleep(5); // 模拟一些耗时操作
printf("离开临界区\n");
// 恢复信号屏蔽集合
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
// 检查是否收到SIGINT信号,如果是则退出循环
if (flag) {
printf("收到SIGINT信号,退出循环\n");
break;
}
}
return 0;
}
sigpending
函数
sigpending
函数返回当前进程阻塞不能递送的信号集,其中的信号都是未决的。
#include <signal.h>
int sigpending(sigset_t *set);
// 成功返回0, 失败返回-1
下面的例子展示了sigpending
函数的用法:
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
static void sig_quit(int signo);
int main()
{
sigset_t newmask, oldmask, pendmask;
// 捕捉退出信号,终端按键Ctrl+\;
if (signal(SIGQUIT, sig_quit) == SIG_ERR)
{
perror("signal");
exit(-1);
}
// 阻塞SIGQUIT,并保存信号屏蔽字在oldmask中
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) != 0)
{
perror("sigprocmask");
exit(-1);
}
// 休眠5s,此时产生的SIGQUIT会被阻塞
sleep(5);
// 检测SIGQUIT是否正在阻塞中
if (sigpending(&pendmask) != 0)
{
perror("sigpending");
exit(-1);
}
if (sigismember(&pendmask, SIGQUIT))
printf("\nSIGQUIT pending\n");
// 恢复之前的信号屏蔽字,即解除SIGQUIT阻塞,之前的信号屏蔽字保存在oldmask中
// 这里不适用SIG_UNBLOCK的原因是在执行这段代码之前,SIGQUIT也有可能是阻塞,所以这里不能直接解除阻塞
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) != 0)
{
perror("sigprocmask");
exit(-1);
}
// 如果已经SIGQUIT信号是未决的,则在sigprocmask返回之前就会被递送,继而执行信号处理函数
// 因此sig_quit函数执行在printf之前
printf("SIGQUIT unblocked\n");
// 此时在产生SIG_QUIT就会退出进程,因为sig_quit函数中将信号处理程序设置为默认
sleep(5);
exit(0);
}
static void sig_quit(int signo)
{
printf("Caught SIGQUIT\n");
if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
{
perror("signal");
exit(-1);
}
}
执行结果如下:
$ ./sigpend
^\ # 产生一次SIGQUIT信号,在第一次sleep(5)之间
SIGQUIT pending # 从sleep返回后使用sigpending函数检测到该信号阻塞
Caught SIGQUIT # 解除阻塞后,sigprocmask返回之前先递送阻塞的信号给进程,执行信号处理程序
SIGQUIT unblocked
^\Quit (core dumped) # 信号处理程序重置为默认时,信号会使进程直接退出
$ ./sigpend
^\^\^\^\^\^\^\^\^\^\^\
SIGQUIT pending
Caught SIGQUIT # 信号处理程序仅执行一次,可见系统没有将SIGQUIT信号进行排队,并仅传递一次
SIGQUIT unblocked
^\Quit (core dumped)
$
sigaction
函数
sigaction
函数用于检查或修改与指定信号相关的处理动作。与之前的signal
函数功能相同,但更加强大。
#include <signal.h>
int sigaction(int signum,
const struct sigaction * restrict act,
struct sigaction * restrict oldact);
// 返回值:成功返回0,失败返回-1
signum
参数指定要检测或修改的信号编号。
如果act
参数非空,则修改signum
指定的信号的处理动作。
如果oldact
参数非空,则用于返回signum
指定的信号的之前的处理动作。
sigaction
结构体说明:
struct sigaction
{
// 信号处理函数地址
void (*sa_handler)(int);
// 如果sa_flags取值为SA_SIGINFO,则用sa_sigaction函数代替sa_handler
void (*sa_sigaction)(int, siginfo_t*, void*);
// 在执行信号处理函数期间,被阻塞信号的屏蔽字
sigset_t sa_mask;
// 信号的处理行为标志
int sa_flags;
};
当修改信号处理动作时,如果sa_handler
为信号捕捉函数地址而非SIG_DFL
或SIG_IGN
,并且sa_mask
指定了一个信号集,则在调用sa_handler
指定的信号处理函数之前,会将sa_mask
指定的信号集加入到进程的信号屏蔽字中,直到该信号处理函数执行结束再恢复为原先值。
在信号处理程序被调用执行期间,操作系统建立的新信号屏蔽字包括当前正在被递送的信号。即使该信号没有被sa_mask
参数指定的信号集包含。
一旦对给定的信号设置了一个动作,那么在调用sigaction
显式改变其之前,该设置就一直有效。
sa_flags
参数指定对信号进行处理的各个选项,说明如下:
选项 | 说明 |
---|---|
SA_NOCLDSTOP | 如果信号是SIGCHLD ,则子进程停止或继续时不产生SIGCHLD 信号,子进程终止时会产生。 |
SA_NOCLDWAIT | 如果信号是SIGCHLD ,则调用进程的子进程终止时不创建僵死进程。并且POSIX.1未定义是否会产生SIGCHLD 信号,Linux下会产生。同时如果在调用进程中调用wait 或waitpid ,则会一直阻塞到其所有子进程都终止,然后返回-1,并且设置errno 为ECHLD 。 |
SA_NODEFER | 在信号处理程序执行期间,不将当前信号添加到进程的信号掩码中(除非在sa_mask 中显式包含)。这样可以允许同一信号在处理程序执行期间再次被触发。 |
SA_RESETHAND | 在此信号捕捉函数入口处,将此信号的处理方式重置为SIG_DFL ,并清除SA_SIGINFO 标志。 |
SA_RESTART | 由此信号中断的系统调用自动重启动。 |
SA_SIGINFO | 指定使用sa_sigaction 字段中带有附加信息的信号处理程序。如果未设置,则使用sa_handler 指定的信号从处理程序。 |
如果sa_flags
参数使用了SA_SIGINFO
标志,则使用sa_sigaction
指向的信号处理程序。
sa_sigaction函数说明
sa_sigaction
指向的信号处理程序原型如下:
void handler(int signo, siginfo_t* info, void* context);
siginfo_t
结构包含了信号产生原因的相关信息, POSIX.1规定实现至少包括si_signo
与si_code
字段。
struct siginfo_t
{
int si_signo; // 信号编号
int si_errno; // 如果非0,则等同于errno
int si_code; // 信号码,指示发送信号的原因
pid_t si_pid; // 发送信号的进程ID,适用于SIGCHLD或kill
uid_t si_uid; // 发送信号的进程的实际用户ID,条件同上
int si_status; // 发送信号的进程的退出状态,适用于SIGCHLD
clock_t si_utime; // 发送信号的进程使用的用户时间,适用于SIGCHLD
clock_t si_stime; // 发送信号的进程使用的系统时间,适用于SIGCHLD
void* si_addr; // 造成SIGBUS、SIGILL、SIGFPE、SIGSEGV等信号的错误地址
union sigval si_value; // 自定义值
...
}
union sigval{
int sival_int;
void* sigval_ptr;
}
应用程序在使用sigqueue
函数递送信号时,可以使用sigval
联合向信号处理程序传递整数或者指针。
si_code
参数对于特定信号用于指示其的产生原因,取值如下:
context
参数可以被强制转换为ucontext_t
结构类型,表示信号传递时进程的上下文,可以使用man 3 getcontext
查看详细内容。
typedef struct ucontext_t {
struct ucontext_t *uc_link; // 保存当前上下文切换后的下一个上下文,当前上下文执行完毕后会切换到uc_link指向的上下文环境
sigset_t uc_sigmask; // 保存当前上下文中的信号屏蔽字,切换到当前上下文时会设置该信号屏蔽字
// 保存当前上下文的栈信息,栈的起始地址和大小等信息。切换到当前上下文时,会将uc_stack中的栈信息设置到当前线程中
stack_t uc_stack;
// 保存当前上下文的机器相关的寄存器状态,程序计数器(PC)、栈指针(SP)、通用寄存器等的值。切换到当前上下文时,会将uc_mcontext中的寄存器状态设置到当前线程中
mcontext_t uc_mcontext;
} ucontext_t;
typedef struct {
void* ss_sp; // 栈的起始地址指针
size_t ss_size; // 栈的大小
int ss_flags; // 栈的标志位
} stack_t;
示例
下面的示例使用sigaction
函数来实现signal
函数。
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef void (*SigFunc)(int signo);
// 返回之前的信号处理函数,使用新的信号处理函数
SigFunc mysignal(int signo, SigFunc func)
{
struct sigaction act, oact;
act.sa_handler = func;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
if (signo == SIGALRM)
{
#ifdef SA_INTERRUPT
act.sa_flags |= SA_INTERRUPT;
#endif
}
else
{
// 除SIGALRM之外的信号,都设置系统调用自动重启动,SIGALRM信号用于对I/O操作进行时间限制
act.sa_flags |= SA_RESTART;
}
if (sigaction(signo, &act, &oact) != 0)
return SIG_ERR;
return (oact.sa_handler);
}
static void sig_alrm(int signo)
{
printf("SIGALRM=%d\n", signo);
}
int main()
{
if (mysignal(SIGALRM, sig_alrm) == SIG_ERR)
{
perror("signal");
exit(-1);
}
alarm(5);
pause();
}
// 执行结果如下:
// $ ./my_signal
// SIGALRM=14
在重启启动被中断的系统调用的环境中,使用SA_INTERRUPT
标志可以使系统调用被中断后不再重启动。
sigsetjmp与siglongjmp函数
sigsetjmp
与siglongjmp
函数是setjmp
与longjmp
函数的替代品,用于在信号处理程序中进行非局部跳转。
原因是使用longjmp
函数跳出信号处理程序后,并不能保证会恢复信号屏蔽字,因为在执行信号处理程序时,当前信号会被加入到当前信号屏蔽字中。
如果在中断了其他信号处理程序的信号的处理程序中调用siglongjmp
函数,那和调用longjmp
函数存在的问题是一样的,都是会提前终止第一个信号处理程序,这种问题只能通过可靠信号的阻塞机制来解决。
#include <setjmp.h>
int sigsetjmp(sigjmp_buf env, int savesigs);
// 返回值:直接调用返回0,从siglongjmp调用返回非0
[[noreturn]] void siglongjmp(sigjmp_buf env, int val);
这两个函数与setjmp
和longjmp
函数的唯一区别就是增加了一个参数savesigs
。
如果savesigs
非0,则sigsetjmp
函数会在env
参数中保存当前信号屏蔽字。
调用siglongjmp
函数时,如果env
中保存有当前信号屏蔽字,则从其中恢复。
示例
下面的例子中显示了系统会将正在执行的信号处理程序对应的信号加入的当前信号屏蔽字中,同时也展示了sigsetjmp
与siglongjmp
函数的用法。
#include <errno.h>
#include <setjmp.h>
#include <signal.h>
#include <stdio.h>
#include <time.h>
#include <unistd.h>
static sigjmp_buf jmpbuf;
// 用于判定是否能够跳转,防止在还没调用sigsetjmp初始化jmpbuf之前进行跳转
// sig_atomic_t数据类型,ISO C标准定义,写时不会被中断,意味着可以用一条机器指令来对其进行访问
// volatile修饰符表示该变量不需要编译器优化,同时每次都从内存中读取,而不是缓存在寄存器中
static volatile sig_atomic_t canjump;
static void pr_mask(const char* str)
{
sigset_t sigset;
int errno_save;
errno_save = errno;
if (sigprocmask(0, NULL, &sigset) != 0)
{
perror("sigprocmask");
return;
}
else
{
printf("%s", str);
if (sigismember(&sigset, SIGINT))
printf(" SIGINT");
if (sigismember(&sigset, SIGQUIT))
printf(" SIGQUIT");
if (sigismember(&sigset, SIGUSR1))
printf(" SIGUSR1");
if (sigismember(&sigset, SIGALRM))
printf(" SIGALRM");
if (sigismember(&sigset, SIGTSTP))
printf(" SIGTSTP");
printf("\n");
}
errno = errno_save;
}
static void sig_usr1(int signo)
{
time_t starttime;
// 检测是否可以跳转
if (canjump == 0)
return; // 执行到这里表示有未预期的SIGUSR1信号,因为canjmp还没初始化
pr_mask("starting sig_usr1: "); // 在这里打印的信号屏蔽字中会出现SIGUSR1
//
alarm(3); // 设置3s时钟
starttime = time(NULL);
for (;;) // 忙等5s,在这期间会触发SIGALRM信号
if (time(NULL) > starttime + 5)
break;
pr_mask("finishing sig_usr1: ");
canjump = 0;
siglongjmp(jmpbuf, 1); // 跳转回main函数,并设置sigsetjmp函数返回值为1
}
static void sig_alrm(int signo)
{
// 此时信号屏蔽字应该为SIGUSR1|SIGALRM,因为递送中的信号会被加入到当前信号屏蔽字中
pr_mask("in sig_alrm: ");
}
int main()
{
if (signal(SIGUSR1, sig_usr1) == SIG_ERR)
{
perror("signal: ");
_exit(-1);
}
if (signal(SIGALRM, sig_alrm) == SIG_ERR)
{
perror("signal: ");
_exit(-1);
}
pr_mask("staring main: "); // 此时信号屏蔽字为空
if (sigsetjmp(jmpbuf, 1)) // 1 表示保存当前信号屏蔽字,并恢复
{
pr_mask("ending main: "); // 此时信号屏蔽字为空
_exit(0);
}
canjump = 1; // 原子操作,并表示现在可以跳转了
for (;;)
pause();
}
执行结果如下:
$ ps -ef | grep sig
blduan 24494 23109 0 20:37 pts/0 00:00:00 ./sigsetjmp_demo
$ kill -USR1 24494
$ ./sigsetjmp_demo
staring main:
starting sig_usr1: SIGUSR1
in sig_alrm: SIGUSR1 SIGALRM
finishing sig_usr1: SIGUSR1
ending main:
从执行结果可以看出,当调用一个信号处理程序时,被捕捉到的信号就被加入到进程的当前信号屏蔽字中。
sigsuspend
函数
sigsuspend()
函数会设置一个临时的信号屏蔽字,然后阻塞进程,直到收到一个可递送的未屏蔽的信号。
sigsuspend()
函数返回之后会将信号屏蔽字设置为调用之前的信号屏蔽字。
基于以上两点,sigsuspend()
函数通常用于要将对某些信号解除阻塞并使进程阻塞作为一个原子操作的情况。
#include <signal.h>
int sigsuspend(const sigset_t *mask);
// 返回值: -1, 并将errno设置为EINTR
sigsuspend
函数的作用是将解除信号阻塞与使进程休眠合并为一个原子操作, 可用于等待特定信号。
下面的三个例子中分别实现了三个版本sleep
函数,说明了sigsuspend
函数的作用。
static void sig_alrm1(int signo)
{
}
static unsigned int sleep1(unsigned int seconds)
{
struct sigaction newact, oldact;
// 1. 设置SIGALRM的信号处理程序
newact.sa_handler = sig_alrm1;
sigemptyset(&newact.sa_mask);
if (sigaction(SIGALRM, &newact, &oldact) != 0)
return seconds;
// 2. 设置时钟,seconds秒后产生SIGALRM信号
alarm(seconds);
// 3. 休眠,等待从sig_alrm1信号函数中返回则返回。
// 此处存在时间窗口
pause();
// 4. 检测休眠剩余描述
unsigned int unslept = alarm(0);
// 5. 恢复之前的信号处理程序
sigaction(SIGALRM, &oldact, NULL);
return unslept;
}
该sleep1
函数存在的问题在于,如果第2步调用完alarm
函数之后CPU被其他进程占用,直到seconds
指定的秒数之后,该进程都没能抢占到CPU,那么该进程在重新运行时,就会先被SIGALRM
信号中断,先运行SIGALRM
信号的处理函数,然后执行pause
就会导致进程一直休眠。
对于这个问题如何优化呢?可以在调用alarm
函数之前将SIGALRM
信号阻塞,然后在pause
之前在解除阻塞,使pause
函数等待信号,如函数sleep2
。
static unsigned int sleep2(unsigned int seconds)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask;
// 1. 设置SIGALRM的信号处理程序
newact.sa_handler = sig_alrm1;
sigemptyset(&newact.sa_mask);
if (sigaction(SIGALRM, &newact, &oldact) != 0)
return seconds;
// 2. 阻塞SIGALRM信号
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) != 0)
return seconds;
// 3. 设置时钟,seconds秒后产生SIGALRM信号
alarm(seconds);
// 4. 解除阻塞SIGALRM信号,该函数返回前如果有未决的、不阻塞的信号,会先执行信号处理程序
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) != 0)
return alarm(0);
// 5. 休眠,等待从sig_alrm1信号函数中返回则返回。
// 此处存在时间窗口,可能会被SIGALRM信号中断
pause();
// 6. 检测休眠剩余描述
unsigned int unslept = alarm(0);
// 7. 恢复之前的信号处理程序
sigaction(SIGALRM, &oldact, NULL);
return unslept;
}
然而sleep2
函数也存在问题,即 sigpromask
函数在返回之前,系统就会将一个未决的并且不阻塞的信号递送给当前进程,这意味着sigprocmask
函数还未返回,就会先被某个信号处理程序中断,如果仅有一个信号并且该信号是SIGALRM
,那么就会导致下面的pause
函数无法返回,进程永久休眠。
所以需要将解除阻塞SIGALRM
信号与pause
休眠等待该信号合并为一个原子动作,不会被信号中断,这就是sigsuspend
函数的作用。
下面的示例中使用sigsuspend
函数来实现sleep3
函数。
unsigned int sleep3(unsigned int seconds)
{
struct sigaction newact, oldact;
sigset_t newmask, oldmask, suspmask;
unsigned int unslept;
newact.sa_handler = sig_alrm1;
sigemptyset(&newact.sa_mask);
// 1. 设置SIGALRM的信号处理程序
if (sigaction(SIGALRM, &newact, &oldact) != 0)
return seconds;
sigemptyset(&newmask);
sigaddset(&newmask, SIGALRM);
// 2. 阻塞SIGALRM信号
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) != 0)
return seconds;
// 3. 设置时钟,seconds秒后产生SIGALRM信号
alarm(seconds);
// 4. 从原有的信号屏蔽字中去除SIGALRM,解除SIGALRM阻塞,并并暂停进程等待接收信号
suspmask = oldmask;
sigdelset(&suspmask, SIGALRM);
sigsuspend(&suspmask); // 原子操作,解除阻塞到进程休眠是原子操作,不会导致信号丢失
// 5. 检测休眠剩余描述
unslept = alarm(0);
// 6. 恢复之前的信号处理程序
sigaction(SIGALRM, &oldact, NULL);
// 7. 恢复之前的信号屏蔽字
sigprocmask(SIG_SETMASK, &oldmask, NULL);
return unslept;
}
示例
该函数常用于临时设置一个新的信号屏蔽字,并等待特定信号的到来,以实现临时阻塞其他信号的目的。
下面的例子中,会捕捉中断信号与退出信号,但仅当捕捉到退出信号时,进程才会继续执行。
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
static volatile sig_atomic_t quitflag = 0;
static void sig_intr(int signo)
{
if (signo == SIGQUIT)
quitflag = 1;
else
{
printf("\ninterrupt\n");
quitflag = 0;
}
}
int main()
{
sigset_t newmask, oldmask, suspmask;
if (signal(SIGINT, sig_intr) == SIG_ERR)
{
perror("signal");
_exit(-1);
}
if (signal(SIGQUIT, sig_intr) == SIG_ERR)
{
perror("signal");
_exit(-1);
}
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
// 阻塞SIGQUIT,目的在于在调用sigsuspend之前不会被SIGQUIT信号中断
// 如果SIGQUIT信号仅产生一次并且在sigsuspend之前产生,那么下面sigsuspend就会永久阻塞
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) != 0)
{
perror("sigprocmask");
_exit(-1);
}
sigemptyset(&suspmask);
// 只有捕捉到SIGQUIT信号才会退出循环,与条件变量的用法类似
while (quitflag == 0)
// 设置信号屏蔽字为susmask,并使进程休眠
sigsuspend(&suspmask);
// 已捕捉到SIGQUIT信号
quitflag = 0;
// 恢复信号屏蔽字,并解除阻塞SIGQUIT
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) != 0)
{
perror("sigprocmask");
_exit(-1);
}
return 0;
}
执行结果如下:
$ ./sigsuspend_demo1
^C
interrupt
^C
interrupt
^C
interrupt
^C
interrupt
$
如果在等待信号期间需要进程去休眠,可以调用sigsuspend
;如果在等待信号期间需要调用其他系统函数,那么解决方法是使用多线程,在单独的下线程中处理信号。
sigqueue
函数
sigqueue
函数可以在发送信号时携带整形或指针参数,其他与kill
函数一致。
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
// 返回值:成功返回0,失败返回-1
如果接收进程想要获取sigqueue
函数发送的信号的额外参数,就要对sig
参数指定的信号使用sigaction
函数安装信号处理程序,同时指定SA_SIGINFO
标志,然后使用sa_sigaction
指定的信号处理程序。
下面是使用sigqueue
函数发送带自定义参数的示例
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
// 1. 发送10个SIGUSR信号
for (int i = 0; i < 10; i++)
{
union sigval val1;
val1.sival_int = i;
sigqueue(atoi(argv[1]), SIGUSR1, val1);
printf("send SIGUSR1 %d time\n", i);
}
// 发送10个SIGRTMIN实时信号,发送优先级高,阻塞时不会被合并为一个
for (int i = 0; i < 10; i++)
{
union sigval val1;
val1.sival_int = i;
sigqueue(atoi(argv[1]), SIGRTMIN + 1, val1);
printf("send SIGRTMIN %d time\n", i);
}
// 循环交叉发送SIGINT和SIGALRM
for (int i = 0; i < 10; i++)
{
union sigval val1;
val1.sival_int = i;
sigqueue(atoi(argv[1]), i % 2 == 0 ? SIGINT : SIGALRM, val1);
printf("send %s %d time\n", strsignal(i % 2 == 0 ? SIGINT : SIGALRM), i);
}
}
// $ ./sigqueue_send 39062
// send SIGUSR1 0 time
// send SIGUSR1 1 time
// send SIGUSR1 2 time
// send SIGUSR1 3 time
// send SIGUSR1 4 time
// send SIGUSR1 5 time
// send SIGUSR1 6 time
// send SIGUSR1 7 time
// send SIGUSR1 8 time
// send SIGUSR1 9 time
// send SIGRTMIN 0 time
// send SIGRTMIN 1 time
// send SIGRTMIN 2 time
// send SIGRTMIN 3 time
// send SIGRTMIN 4 time
// send SIGRTMIN 5 time
// send SIGRTMIN 6 time
// send SIGRTMIN 7 time
// send SIGRTMIN 8 time
// send SIGRTMIN 9 time
// send Interrupt 0 time
// send Alarm clock 1 time
// send Interrupt 2 time
// send Alarm clock 3 time
// send Interrupt 4 time
// send Alarm clock 5 time
// send Interrupt 6 time
// send Alarm clock 7 time
// send Interrupt 8 time
// send Alarm clock 9 time
接收信号示例如下:
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static void sig_act(int signo, siginfo_t* siginfo, void* context)
{
// 在执行信号处理程序时,相应的信号会被阻塞
// 阻塞期间产生的标准信号仅递送一次,实时信号会全部排队递送
if (signo == SIGUSR1)
printf("caught SIGUSR1 ");
else if (signo == SIGALRM)
printf("caught SIGALRM ");
else if (signo == SIGINT)
printf("caught SIGINT ");
else if (signo == (SIGRTMIN + 1))
printf("caught SIGRTMIN");
printf("%d\n", siginfo->si_value.sival_int);
}
int main()
{
struct sigaction newact, oldact;
sigemptyset(&newact.sa_mask);
newact.sa_flags = SA_SIGINFO;
newact.sa_sigaction = sig_act;
if (sigaction(SIGUSR1, &newact, &oldact) != 0)
{
perror("sigaction");
_exit(-1);
}
if (sigaction(SIGINT, &newact, &oldact) != 0)
{
perror("sigaction");
_exit(-1);
}
if (sigaction(SIGALRM, &newact, &oldact) != 0)
{
perror("sigaction");
_exit(-1);
}
if (sigaction(SIGRTMIN + 1, &newact, &oldact) != 0)
{
perror("sigaction");
_exit(-1);
}
while (1)
sleep(1);
}
// $ ./sigqueue_recv
// caught SIGUSR1 0 执行信号处理程序阻塞期间,后续SIGUSR1并合并为一个
// caught SIGRTMIN0 实时信号不会被合并
// caught SIGRTMIN1
// caught SIGUSR1 1
// caught SIGRTMIN2
// caught SIGRTMIN3
// caught SIGRTMIN4
// caught SIGRTMIN5
// caught SIGRTMIN6
// caught SIGRTMIN7
// caught SIGRTMIN8
// caught SIGRTMIN9
// caught SIGALRM 1 执行信号处理程序阻塞期间,后续SIGALRM并合并为一个
// caught SIGINT 0 执行信号处理程序阻塞期间,后续SIGINT并合并为一个
// caught SIGALRM 3
// caught SIGINT 2
上面的实例中展示了如何使用sigqueue
发送信号时如何携带参数,同时也说明了一点非实时信号在被阻塞期间产生了一次或多次,解除阻塞之后仅会被递送一次,例如SIGUSR1, SIGARLRM, SIGINT
在第一次处理期间是阻塞的,后面产生的多个信号都被合并为一个进行递送,而实时信号则会进行排队。
- 原文作者:生如夏花
- 原文链接:https://blduan.top/post/%E8%AF%BB%E4%B9%A6%E7%AC%94%E8%AE%B0/apue/%E4%BF%A1%E5%8F%B7%E9%9B%86%E5%8F%8A%E7%9B%B8%E5%85%B3%E5%87%BD%E6%95%B0/
- 版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。