6.8.3 sigqueue函数

6.8.3 sigqueue函数

在信号发送的方式当中,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]

这样就可以很清楚地看到堆栈调用的关系,方便进一步定位问题。

你可能感兴趣的:(Linux,linux)