linux系统上信号发送和信号接收讲解
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux系统上信号发送和信号接收讲解相关的知识,希望对你有一定的参考价值。
参考技术A用于进程间通信,通信机制由操作系统保证,比较稳定。
在linux中可以通过kill -l查看所有信号的类型。
kill -信号类型 进程ID
int kill(pid_t pid, int sig);
入参pid :
pid > 0: 发送信号给指定的进程。
pid = 0: 发送信号给 与调用kill函数进程属于同一进程组的所有进程。
pid < 0: 取|pid|发给对应进程组。
pid = -1:发送给进程有权限发送的系统中所有进程。
sig :信号类型。
返回值 :成功:0;失败:-1 (ID非法,信号非法,普通用户杀init进程等权级问题),设置errno
以OpenHarmony源码为例,应用ANR后,AbilityManagerService会通知应用dump堆栈信息,就是通过信号量做的。
头文件位置 :
include <signal.h>
函数解释 :
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
当接收到指定的信号signum时,就会跳转到参数handler指定的函数执行。其中handler的入参是信号值。
函数原型 :
signum参数指出要捕获的信号类型,act参数指定新的信号处理方式,oldact参数输出先前信号的处理方式(如果不为NULL的话)。
sigaction结构体
sa_handler 信号处理函数
sa_mask 在处理该信号时可以暂时将sa_mask 指定的信号集搁置
sa_flags 指定一组修改信号行为的标志。 它由以下零个或多个的按位或组成
SA_RESETHAND:当调用信号处理函数时,将信号的处理函数重置为缺省值SIG_DFL
SA_RESTART:如果信号中断了进程的某个系统调用,则系统自动启动该系统调用
SA_NODEFER :一般情况下, 当信号处理函数运行时,内核将阻塞该给定信号。但是如果设置了 SA_NODEFER标记, 那么在该信号处理函数运行时,内核将不会阻塞该信号
sa_restorer 是一个替代的信号处理程序,当设置SA_SIGINFO时才会用它。
相关函数
int sigemptyset( sigset_t *set);
sigemptyset()用来将参数set信号集初始化并清空。
执行成功则返回0,如果有错误则返回-1。
完整示例
kill信号由谁接收处理
通常信号发送都是使用kill系统调用来实现,这个功能其实相对粗糙一些,它的第一个参数指明了接受者,但是这个接受者在多线程中并不总是最终的处理者。那么通过这个现象可以解释这个参数的意义:那就是首选(prefer)这个线程,但是如果这个线程实在是有些难言之隐,那么它所在的线程组中其它线程也可以代劳。这一点和之后新添加的tkill系统调用不同,从使用场景上看,这个tkill是为了支持POSIX 线程库中pthread_kill而添加的内核支持函数,所以它是定向的信号发送,所以即使是目标线程现在不方便,信号还是只能等目标线程在适当的时候来处理这个信号,而其它线程没有办法染指。
在很多时候,我们都希望能够在信号发送的时候使用tkill,从而减少不确定性,但是这个美好的愿望并不是每次都能奏效的。例如,当子进程退出的时候,SIGCHLD信号到底发给谁,这个通常是确定的,那就是执行fork/vfork的线程,而且通常情况下最终由哪个线程来执行信号处理函数可能都不重要。但是有时候这个确实很重要的,最明显的场景就是对线程私有数据的访问是线程相关的。当然这并不是我在工程中遇到的问题,工程中遇到的问题可能比这个情况更为复杂,使用线程私有数据说明这个问题只是觉得更加典型而已。
二、kill信号接受者何时确定
在信号发送的时候,信号接受者已经选中:
sys_kill-->>>kill_something_info---->>>kill_pid_info--->>>group_send_sig_info--->>>__group_send_sig_info--->>>__group_complete_signal
__group_complete_signal(int sig, struct task_struct *p) {
……
if (wants_signal(sig, p))
t = p;
else if (thread_group_empty(p))
return;
else {
/*
* Otherwise try to find a suitable thread.
*/
……
}
……
/*
* The signal is already in the shared-pending queue.
* Tell the chosen thread to wake up and dequeue it.
*/
signal_wake_up(t, sig == SIGKILL);
}
这个kill在很多时候还是非常忠实的尊重调用者的意愿,如果发送者可以接收信号,那么就铁定选择这个信号,不含糊,但是如果说目标线程不方便(由wants_signal确定),那么就在线程组中其它线程选择一个来处理这个信号。
static inline int wants_signal(int sig, struct task_struct *p)
{
if (sigismember(&p->blocked, sig)) 如果目标线程屏蔽了信号,那么不方便。
return 0;
if (p->flags & PF_EXITING)正在退出,不方便。
return 0;
if (sig == SIGKILL) 杀死信号,必须处理。
return 1;
if (p->state & (TASK_STOPPED | TASK_TRACED))被调试或者暂定线程不方便。
return 0;
return task_curr(p) || !signal_pending(p);
}
由于通常那些希望处理这些信号的线程都是能通过这个检测的,所以kill的目标线程通常(例如对于SIGCHLD)是如愿以偿的处理这个信号的,由于没有执行__group_complete_signal函数最后的signal_wake_up(t, sig == SIGKILL);,所以其它线程的TIF_SIGPENDING未被置位,可以认为这个信号如果首选目标可以处理,那么这个信号对其它线程透明。
三、节外生枝
那么是不是在这里确定这个信号的接收者之后,这个信号一定会在这个线程的上下文下执行呢?答案同样是不确定。我们看一下当一个线程去去信号的处理函数
get_signal_to_deliver--->>dequeue_signal
{
int signr = __dequeue_signal(&tsk->pending, mask, info);
if (!signr) {
signr = __dequeue_signal(&tsk->signal->shared_pending,
mask, info);
……
}
recalc_sigpending_tsk(tsk);
……
}
fastcall void recalc_sigpending_tsk(struct task_struct *t)
{
if (t->signal->group_stop_count > 0 ||
(freezing(t)) ||
PENDING(&t->pending, &t->blocked) ||
PENDING(&t->signal->shared_pending, &t->blocked))
set_tsk_thread_flag(t, TIF_SIGPENDING);
else
clear_tsk_thread_flag(t, TIF_SIGPENDING);
}
从这个信号处理函数中可以看到,当一个线程处理信号的时候,它首先从自己的私有延迟队列中取信号,如果取到则返回,娶不到则到共享队列中取信号。当信号取出之后,它会自觉的通过recalc_sigpending_tsk来再次检测是否有信号可以处理,同样是检测了私有和共享延迟队列。
现在假设在第二节的kill调用时发送信号SIGX选择了线程A,但是A一直没有机会得到调度,然后线程B收到一个信号SIGY被唤醒,那么线程B在处理完SIGY这个信号之后将会再次从共享队列中看到本来确定给A处理的SIGX信号,所以他会先于A线程来消耗掉这个信号,然后A线程真正获得执行的时候这个信号已经消失。
四、验证
上节描述的场景不太容易复现,因为线程A和线程B的内核态抢占不太容易模拟,它通常在可抢占实时系统中偶现,但是这个场景在理论上是存在(如果有不同意的同学可以指点一下)。所以我们使用一个必现的场景来模拟展示一下这种情况。
[[email protected] Uncertain]$ cat Uncertain.c
/*
* Author: [email protected]
* Date :2012.05.08
* Desc :Illuminate signal-handling race
*/
#include <sched.h>
#include <sys/types.h>
#include <signal.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
//简单信号处理函数,主要用来打印信号值和处理线程。
void sighandler(int sig)
{
printf("signo is %d, my threadid is %d ",sig,syscall(SYS_gettid));
}
//简单线程,用来作为真正处理信号处理线程。
void * reaper(void * arg)
{
while (1) sleep(100000);
}
//主函数。
int main()
{
pthread_t uncle;
//安装SIGCHLD信号和SIGINT处理函数。
signal(SIGCHLD,sighandler);
signal(SIGINT,sighandler);
printf("main thread pid is %d ",getpid());
//创建线程。
pthread_create(&uncle,NULL,reaper,NULL);
//创建子进程,休息10s,等待父进程中所有线程创建完成并开始执行。然后退出,退出之后产生SIGCHLD发送给主线程。
if(0 == fork())
{
sleep(10);
printf("forked child will exit ");
exit(0);
}
//通过vfork创建无限休眠子进程,从而让主进程进入不可唤醒休眠中,这个状态满足wants_signal判断。
if(0 == vfork())
{
sleep(10000);
_exit(0);
}
printf("parent exit ");
}
//生成可执行文件
[[email protected] Uncertain]$ gcc Uncertain.c -o Uncertain.c.exe -lpthread -static
/usr/lib/gcc/i686-redhat-linux/4.4.2/../../../libpthread.a(libpthread.o): In function `sem_open‘:
(.text+0x6d1a): warning: the use of `mktemp‘ is dangerous, better use `mkstemp‘
//后台运行可执行程序
[[email protected] Uncertain]$ ./Uncertain.c.exe &
main thread pid is 19204
[2] 19204
//一段时间后,子进程退出,向父进程发送SIGCHLD信号,注意:此时uncle线程未被唤醒。
[[email protected] Uncertain]$ forked child will exit
//主动发送一个信号SIGINT到uncle线程,从而触发它的信号处理函数。
[[email protected] Uncertain]$ kill -INT 19205
//uncle线程同时处理了两个信号,它抢先处理了本来发送给主线程的SIGCHLD(17)号信号,然后处理了主动发送的SIGINT。
[[email protected] Uncertain]$ signo is 17, my threadid is 19205
signo is 2, my threadid is 19205
[[email protected] Uncertain]$
五、为什么先处理大数值的SIGCHLD后处理小数值的SIGINT
这里有一个细节,就是信号处理函数执行的时候是先处理17号信号,然后处理2号信号,但是在信号摘取函数中是从小到大取信号的:
dequeue_signal--->>__dequeue_signal---->>next_signal
for (i = 0; i < _NSIG_WORDS; ++i, ++s, ++m)
if ((x = *s &~ *m) != 0) {
sig = ffz(~x) + i*_NSIG_BPW + 1;
break;
}
break;
其中都是通过ffz来选择第一置位的bit,所以理论上升或应该是先执行2号信号,然后才是17信号。为了确认这一点,我同样调试了内核进行了验证,的确是先取到2号信号,然后是17号,这点大家不用怀疑。
所以要从其它地方看,那就是内核对信号处理函数的判断。
#ifdef CONFIG_VM86
#define resume_userspace_sig check_userspace
#else
#define resume_userspace_sig resume_userspace
#endif
work_pending:
testb $_TIF_NEED_RESCHED, %cl
jz work_notifysig
……
call do_notify_resume 从信号处理返回,再次跳转到resume_userspace_sig处,通过前面宏知道它等价于resume_userspace
jmp resume_userspace_sig
END(work_pending)
……
ENTRY(resume_userspace) do_notify_resume函数之后跳转到这个地方。
DISABLE_INTERRUPTS(CLBR_ANY) # make sure we don‘t miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done on
# int/exception return?
jne work_pending 再次跳转到前面判断是否有信号。
jmp restore_all
END(ret_from_exception)
可以看到,在内核返回到用户态之前,线程所有的信号堆栈都已经被压到用户态堆栈中,它们的执行顺序是和get_signal_to_deliver中取到的信号顺序刚好相反。这个用户态堆栈的创建是在do_notify_resume--->>>do_signal--->>handle_signal--->>setup_rt_frame中完成,所以上面每执行一次work_pending循环测试,用户态堆栈就会多出一些信号处理函数堆栈(这一点对于一些线程堆栈受限系统可能要考虑一下)。
以上是关于linux系统上信号发送和信号接收讲解的主要内容,如果未能解决你的问题,请参考以下文章