在信号发送的方式当中,sigqueue算是后起之秀,传统的信号多用signal/kill这两个函数搭配,完成信号处理函数的安装和信号的发送。
后来因为signal函数的表达力有限,控制不够精准,所以引入了sigaction函数来负责信号的安装
与其对应的是,引入了sigqueue函数来完成实时信号的发送。
当然了,sigqueue函数也能发送非实时信号。
sigqueue函数的接口定义如下:
int sigqueue(pid_t pid, int sig, const union sigval value);
sigqueue函数拥有和kill函数类似的语义,也可以发送空信号(信号0)来检查进程是否存在。
和kill函数不同的地方在于,它不能通过将pid指定为负值而向整个进程组发送信号。
比较有意思的是函数的第三个入参,它指定了信号的伴随数据(或者称为有效载荷,payload),该参数的数据类型是联合体,定义代码如下:
union sigval {
int sival_int;
void *sival_ptr;
};
通过指定sigqueue函数的第三个参数,可以传递一个int值或指针给目标进程。
考虑到不同的进程有各自独立的地址空间,传递指针到另一个进程几乎没有任何意义。
因此sigqueue函数很少传递指针(sival_ptr),大多是传递整型(sival_int)。
注意 :
尽管跨进程使用sigval中的指针sival_ptr没有任何意义,但sival_ptr字段并非百无一用。该字段可用于使用sigval联合体的其他函数中,如POSIX计时器的timer_create函数和POSIX消息队列中的mq_notify函数。
sigval联合体的存在,扩展了信号的通信能力。
一些简单的消息传递完全可以使用sigqueue函数来进行。
比如,通信双方事先定义某些事件为不同的int值,通过sigval联合体,将事件发送给目标进程。目标进程根据联合体中的int值来区分不同的事件,做出不同的响应。但是这种方法传递的消息内容受到了限制,不容易扩展,所以不宜作为常规的通信手段。
下面的例子会使用sigqueue函数向目标进程发送信号,其中目标进程、信号值和发送次数都可指定,发送信号的同时,也发送了伴随数据。
void usage()
{
fprintf(stderr, "sigqueue_send sig pid [times]\n");
}
int main(int argc, char *argv[])
{
pid_t pid;
int sig;
int times = 0;
union sigval mysigval;
if (argc < 3)
{
usage();
return -1;
}
pid = atoi(argv[1]);
sig = atoi(argv[2]);
if (argc > 4)
{
times = atoi(argv[3]);
}
mysigval.sival_int = 123;
if (sig < 0 || sig > 64 || times < 0)
{
usage();
return -2;
}
int i;
for(i = 0; i < times; i++)
{
if ((sigqueue(pid, sig, mysigval) == -1))
{
fprintf(stderr, "sigqueue failed (%s)\n", strerror(errno));
return -3;
}
}
return 0;
}
一般来说,sigqueue函数的黄金搭档是sigaction函数。
在使用sigaction函数时,只要给成员变量sa_flags置上SA_SIGINFO的标志位,就可以使用三参数的信号处理函数来处理实时信号。
struct sigaction act;
act.sa_flags |= SA_SIGINFO;
三参数的信号处理函数如下:
void handle(int, siginfo_t *info, void *ucontext);
siginfo_t结构体存在以下成员:
siginfo_t {
int si_signo;
int si_errno;
int si_code;
int si_trapno;
pid_t si_pid;
uid_t si_uid;
union sigval si_value;
void *si_addr
...
}
这个结构体包含很多信息,目标进程可以通过该数据结构获取到如下的信息:
si_signo:信号的值。
si_code:信号来源,可以通过这个值来判断信号的来源,具体见表6-13。
表6-13 si_code的值及其含义
除此之外,一些特殊的信号会产生一些独特的si_code,来表示信号产生的根源或来源。
例如,如果无效地址对齐引发SIGBUS信号,si_code就会被置为BUS_ADRALN等。想进一步了解详情,可以查看glibc的bits/siginfo.h头文件。
si_value:sigqueue函数发送信号时所带的伴随数据。
si_pid:信号发送进程的进程ID。
si_uid:信号发送进程的真实用户ID。
si_addr:仅针对硬件产生的信号SIGBUS、SIGFPE、SIGILL和SIGSEGV设置该字段,该字段表示无效的内存地址(SIGBUS和SIGSEGV)或导致信号产生的程序的指令地址(SIGFPE和SIGILL)。
三参数信号处理函数的第三个参数是void*类型的,其实它是一个ucontext_t类型的变量。
typedef struct ucontext
{
unsigned long int uc_flags;
struct ucontext *uc_link;stack_t uc_stack;
mcontext_t uc_mcontext;
__sigset_t uc_sigmask;
struct _libc_fpstate __fpregs_mem;
} ucontext_t;
这个结构体提供了进程上下文的信息,用于描述进程执行信号处理函数之前进程所处的状态。
通常情况下信号处理函数很少会用到这个变量,但是该变量也有很精妙的应用,如下面的例子。
对于C程序员而言,基本每个人都会遇到段错误。一般情况下,段错误出现的原因是程序访问了非法的内存地址。当段错误发生时,操作系统会发送一个SIGSEGV信号给进程,导致进程产生核心转储文件并且退出。如何才能让进程先捕捉SIGSEGV信号,打印出有用的方便定位问题的信息,然后再优雅地退出呢?可以通过给SIGSEGV注册信号处理函数来实现,代码如下所示:
#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif
#ifndef __USE_GNU
#define __USE_GNU
#endif
#include
#include
#include
#include
#include
#include
#include
typedef struct _sig_ucontext {
unsigned long uc_flags;
struct ucontext *uc_link;
stack_t uc_stack;
struct sigcontext uc_mcontext;
sigset_t uc_sigmask;
} sig_ucontext_t;
void crit_err_hdlr(int sig_num, siginfo_t * info, void * ucontext)
{
void * array[50];
void * caller_address;
char ** messages;
int size, i;
sig_ucontext_t * uc;
uc = (sig_ucontext_t *)ucontext;
caller_address = (void *) uc->uc_mcontext.rip;
fprintf(stderr, "signal %d (%s), address is %p from %p\n", sig_num, strsignal(sig_num), info->si_addr, (void *)caller_address);
size = backtrace(array, 50);
array[1] = caller_address;
messages = backtrace_symbols(array, size);
/* 跳过第一个栈帧 */
for (i = 1; i < size && messages != NULL; ++i)
{
fprintf(stderr, "[bt]: (%d) %s\n", i, messages[i]);
}
free(messages);
exit(EXIT_FAILURE);
}
int crash()
{
char * p = NULL;
*p = 0;
return 0;
}
int foo4()
{
crash();
return 0;
}
int foo3()
{
foo4();
return 0;
}
int foo2()
{
foo3();
return 0;
}
int foo1()
{
foo2();
return 0;
}
int main(int argc, char ** argv)
{
struct sigaction sigact;
sigact.sa_sigaction = crit_err_hdlr;
sigact.sa_flags = SA_RESTART | SA_SIGINFO;
if (sigaction(SIGSEGV, &sigact, (struct sigaction *)NULL) != 0)
{
fprintf(stderr, "error setting signal handler for %d (%s)\n", SIGSEGV, strsignal(SIGSEGV));
exit(EXIT_FAILURE);
}
foo1();
exit(EXIT_SUCCESS);
}
上面的函数利用了第三个参数里面的ucontext->uc_mcontext.rip字段,获取到了收到信号前的EIP寄存器的值,根据该值,可以将堆栈信息打印出来,输出如下:
baby@ubuntu:~/linux/sig$ sudo ./print
signal 11 (Segmentation fault), address is (nil) from 0x561272027384
[bt]: (1) ./print(+0x1384) [0x561272027384]
[bt]: (2) ./print(+0x1384) [0x561272027384]
[bt]: (3) ./print(+0x13a0) [0x5612720273a0]
[bt]: (4) ./print(+0x13b9) [0x5612720273b9]
[bt]: (5) ./print(+0x13d2) [0x5612720273d2]
[bt]: (6) ./print(+0x13eb) [0x5612720273eb]
[bt]: (7) ./print(+0x1493) [0x561272027493]
[bt]: (8) /lib/x86_64-linux-gnu/libc.so.6(+0x29fd0) [0x7fa3e910efd0]
[bt]: (9) /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x7d) [0x7fa3e910f07d]
[bt]: (10) ./print(+0x1145) [0x561272027145]
缺点是没有打印出函数名,只打印了指令的地址。我们固然可以使用objdump得到汇编文件,根据地址查找到各自的函数名,但是手工干预太多,效率太低。如果在编译的时候,带上-rdynamic选项,就可打印出函数的地址了,代码如下所示:
baby@ubuntu:~/linux/sig$ sudo gcc print.c -o print_bt -rdynamic
baby@ubuntu:~/linux/sig$ sudo ./print_bt
signal 11 (Segmentation fault), address is (nil) from 0x55ef419fc384
[bt]: (1) ./print_bt(crash+0x14) [0x55ef419fc384]
[bt]: (2) ./print_bt(crash+0x14) [0x55ef419fc384]
[bt]: (3) ./print_bt(foo4+0x12) [0x55ef419fc3a0]
[bt]: (4) ./print_bt(foo3+0x12) [0x55ef419fc3b9]
[bt]: (5) ./print_bt(foo2+0x12) [0x55ef419fc3d2]
[bt]: (6) ./print_bt(foo1+0x12) [0x55ef419fc3eb]
[bt]: (7) ./print_bt(main+0xa1) [0x55ef419fc493]
[bt]: (8) /lib/x86_64-linux-gnu/libc.so.6(+0x29fd0) [0x7f4691a36fd0]
[bt]: (9) /lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0x7d) [0x7f4691a3707d]
[bt]: (10) ./print_bt(_start+0x25) [0x55ef419fc145]
这样就可以很清楚地看到堆栈调用的关系,方便进一步定位问题。