目录
信号简介
简单应用程序事例与API介绍
信号的常见系统调用
rt_sigaction
kill
sigaltstack
sigprocmask
与信号相关的内核数据结构
内核中信号发送
核心发送函数__send_signal
__send_signal的常见封装
force_sig_info
do_tkill
kill_something_info
信号传递(处理)
信号处理执行时机
获取信号信息
切换执行到用户态信号处理函数
信号处理完成后再次回到内核态
学以致用
本文参考资料
趣味问答
信号在最早的UNIX系统引入,用于进程间通信,是内核的一种软件机制,通过内核代码实现的。
内核对信号的响应机制有点像中断,信号来了后需要打断当前进程的执行,去执行信号处理函数,
执行完毕后,再恢复原来的上下文继续执行。
内核对信号是如何管理的?信号在内核中是如何响应的?当前进程执行过程中,代码执行流是如何
跳转到信号处理函数的?执行完信号处理函数后,又是如何再跳回来的?
请看下文,下文是基于X86处理器,4.18的内核为基础写的。
信号最原始的作用
信号是很短的消息,可以被发送到一个进程或者一组进程。
使用信号主要是两个目的:
依靠信号机制,内核实现了如异常处理、进程状态管理,同时给用户程序提供了使用信号的支持。
所以,信号的作用说起来简单,其实在内核中的实现并不简单,属于内核的重要模块。
信号的分类
1~31为常规信号(regular signal),除了SIGUSR1和SIGUSR2之外,系统都赋予其特殊功能,如
总线错误SIGBUS、非法内存访问SIGSEGV,常规信号不具有信号缓存特性,当一个信号在处理
过程中,再来一个同样的信号,新来的信号会丢失,不会被缓存到队列。
32~64 为实时信号(real-time signal),用户可以自定义功能,具有信号缓存特性。
信号的状态
信号挂起(pending): 信号发送接口调用后,pending状态在进程(线程)的
task_struct结构中更新,并把信号信息siginfo_t挂入队列。
当信号处理时,从队列中dequeue队列,获取siginfo_t后,
删除对应信号的pending状态位。
详情见内核send_signal 、dequeue_synchronous_signal、
dequeue_signal函数。
信号阻塞(blocked):当某个信号的信号处理函数正在执行且这个信号的sa_flags没有被
设置为SA_NODEFER时,这时内核会设置当前信号为blocked状态,
此外,用户程序可以显式的通过sigprocmask修改blocked状态。
SIGSTOP和SIGKILL不能被设置blocked状态。
设置阻塞某一个信号后,信号来了不会被处理,但是信号的pending
状态会被设置,这个时候查看信号挂起sigpending可以查到这个信
号。当阻塞状态被清除后,pengding状态的信号可以被重新处理。
详情见内核send_signal 、dequeue_synchronous_signal、
dequeue_signal、recalc_sigpending函数。
信号传递(delivered):也可以叫信号处理状态,当信号成功发送时,信号并不会被立即
处理,只有当被发送进程处于正在运行状态且当前运行CPU出现
从内核态到用户态切换时机时,信号处理过程才会被执行到。
信号忽略(ignore): 当信号被判断为ignored状态时,信号发送流程跳过设置pending状态,
信号被drop掉。判断信号是否可被ignore可以查看内核函数
sig_task_ignored,一般来说,当信号handler被设置为 SIG_IGN或者这
个信号默认行为是ignore且handler 被设置为SIG_DFL时,这个信号会
被忽略处理。还有一种特殊情况,就是init进程,只有在极少情况下才
不会忽略信号。详情请看sig_task_ignored函数。
SIGKILL和SIGSTOP的特殊性
构造一个简单的应用事例,看一下Linux信号的使用:
测试代码如下:signal_test.c
#define _GNU_SOURCE
#include
#include
#include
#include
#include
#include
#include
#include
#define gettid() syscall(__NR_gettid)
#define SIG_STACK_LEN (4096*8)
#define SIG_STACK_POOL_SIZE (10)
char sig_stack_buffer[SIG_STACK_POOL_SIZE][SIG_STACK_LEN];
int sigsegv_produce()
{
printf("%s\n",__func__);
*(volatile char *)0x5555 = 0x33;
return 0;
}
int siginfo_printf(siginfo_t *si)
{
printf("siginfo_printf:\n");
printf(" si_signo = %d\n",si->si_signo);
printf(" si_errno = %d\n",si->si_errno);
printf(" si_code = %d\n",si->si_code);
if(si->si_code == SI_USER)
{
printf(" sender pid = %d\n",si->si_pid);
printf(" sender uid = %d\n",si->si_uid);
}
/*这种情况时,si->si_addr 为异常操作的地址*/
if((si->si_signo == SIGSEGV)&&(si->si_code == SEGV_MAPERR))
{
printf(" fault addr = 0x%lx\n",(long int)si->si_addr);
}
}
static void signal_process(int sig, siginfo_t *si, void *ctx_void)
{
long int sp_get;
printf("\nsp get = 0x%lx\n",(long int)&sp_get);
printf("siginfo_t addr = 0x%lx\n",(long int)si);
printf("ctx_void addr = 0x%lx\n",(long int)ctx_void);
if(sig == SIGUSR1)
{
printf("==>recv signal SIGUSR1<==\n");
siginfo_printf(si);
printf("send SIGFPE to pid %d\n",getpid());
kill(getpid(),SIGFPE);
printf("send SIGUSR2 to pid %d\n",getpid());
kill(getpid(), SIGUSR2);
}
if(sig == SIGUSR2)
{
printf("==>recv signal SIGUSR2<==\n");
siginfo_printf(si);
sigsegv_produce();
}
if(sig == SIGSEGV)
{
printf("==>recv signal SIGSEGV<==\n");
siginfo_printf(si);
/*如果此处不加exit,程序会进入无限循环*/
exit(1);
}
if(sig == SIGFPE)
{
printf("==>recv signal SIGFPE<==\n");
}
}
int set_handler(int signo, void (*handler)(int, siginfo_t *, void *),
int flags)
{
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = handler;
sa.sa_flags = SA_RESTART|SA_SIGINFO|flags;
sigemptyset(&sa.sa_mask);
if (sigaction(signo, &sa, 0))
printf("sigaction failed\n");
}
int set_signal_handle_onstack(int signo,void (*handler)(int, siginfo_t *, void *),
int stack_id)
{
stack_t stack;
int err;
stack.ss_sp = sig_stack_buffer[stack_id];
stack.ss_size = SIG_STACK_LEN;
stack.ss_flags = SS_ONSTACK;
printf("set sig:%d signal stack sp = 0x%lx\n",
signo,
(long int)stack.ss_sp + SIG_STACK_LEN);
err = sigaltstack(&stack, NULL);
if (err != 0)
printf("sigaltstack failed - %s\n",strerror(errno));
set_handler(signo,handler,SA_ONSTACK);
return 0;
}
int main(int argc ,char **argv)
{
sigset_t mask;
/*设置SIGFPE 为阻塞状态*/
sigemptyset(&mask);
sigaddset(&mask, SIGFPE);
sigprocmask(SIG_BLOCK, &mask,NULL);
set_handler(SIGUSR1,signal_process,0);
set_handler(SIGUSR2,signal_process,0);
/*设置SIGSEGV信号处理函数为signal_process ,且有自己的信号处理备用栈*/
set_signal_handle_onstack(SIGSEGV,signal_process,0);
while(1)
sleep(1);
return 0;
}
执行结果打印如下:
root@intel-x86-64:/ramDisk# ./signal_test &
set sig:11 signal stack sp = 0x55555575e040
./signal_test进程的pid号为1400,通过kill命令给进程发信号SIGUSR1(10)
root@intel-x86-64:/ramDisk# kill -10 1400
./signal_test进程收到
sp get = 0x7fffffffe620
siginfo_t addr = 0x7fffffffe770
ctx_void addr = 0x7fffffffe640
==>recv signal SIGUSR1<==
siginfo_printf:
si_signo = 10
si_errno = 0
si_code = 0 0对应SI_USER,sent by kill, sigsend, raise
sender pid = 1386
sender uid = 0
send SIGFPE to pid 1400
send SIGUSR2 to pid 1400
sp get = 0x7fffffffe120
siginfo_t addr = 0x7fffffffe270
ctx_void addr = 0x7fffffffe140
==>recv signal SIGUSR2<==
siginfo_printf:
si_signo = 12
si_errno = 0
si_code = 0
sender pid = 1400
sender uid = 0
sigsegv_produce
sp get = 0x55555575dbe0
siginfo_t addr = 0x55555575dd30
ctx_void addr = 0x55555575dc00
==>recv signal SIGSEGV<==
siginfo_printf:
si_signo = 11
si_errno = 0
si_code = 1
fault addr = 0x5555
上面signal_test.c中用到的应用函数接口主要有
函数原型为:int sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
通过这个接口设置内核对某个信号的处理,设置的信息都在struct sigaction *act中,
获取的信息在struct sigaction *oact中。主要包括信号的处理函数sa_sigaction、
信号处理的flag设置sa_flags、信号处理过程中屏蔽的信号集合sa_mask。
其中sa_flags可以设置的值有:
SA_RESTART:当系统调用被信号打断执行后,信号处理完毕后,自动重新执行系统调用
SA_SIGINFO:信号处理函数中的siginfo_t *si有信息。
SA_NODEFER:信号处理过程中,不屏蔽(blocked)对应的信号
SA_ONSTACK:信号处理使用私有的备用栈
还有一些sa_flags的值就不一一介绍了。
用法事例请看signal_test.c中的set_handler、set_signal_handle_onstack函数
2. kill
函数原型为int kill (__pid_t __pid, int __sig)
给进程发送一个信号
3. sigaltstack
函数原型为int sigstack (struct sigstack *ss, struct sigstack *oss)
设置、获取当前线程的信号处理备用栈信息,包括栈起始地址、栈size、flag。
用法事例请看signal_test.c中的set_signal_handle_onstack函数
4. sigprocmask
函数原型为int sigprocmask (int how, const sigset_t *set, sigset_t *oset)
设置、获取当前进程信号的阻塞配置
sigset_t的本质是一个64bit的变量,一个信号对应其中的一个bit,对应bit置位,
代表这个信号被阻塞。how的参数可以是SIG_SETMASK、SIG_BLOCK、
SIG_UNBLOCK。
对应的系统调用为SYSCALL_DEFINE4(rt_sigprocmask, int, how, sigset_t __user *, nset,
sigset_t __user *, oset, size_t, sigsetsize)
通过strace 跟踪上面事例程序signal_test执行的系统调用。signal_test.c中用到的信号方面的系统
调用主要有:
用strace抓到sigaction应用函数接口抓到的系统调用接口为:
rt_sigaction(SIGUSR1, {sa_handler=0x555555554b73, sa_mask=[],
sa_flags=SA_RESTORER|SA_RESTART|SA_SIGINFO,
sa_restorer=0x7ffff7e40120}, NULL, 8)
rt_sigaction在内核中系统调用的定义为:
kernel/signal.c
SYSCALL_DEFINE4(rt_sigaction, int, sig,
const struct sigaction __user *, act,
struct sigaction __user *, oact,
size_t, sigsetsize)
主要做的工作有:
(1) 使用copy_from_user 拷贝用户态act指向的struct sigaction的信息到内核态的
struct k_sigaction new_sa变量
(2) 把new_sa变量的内容copy到当前进程p->sighand->action[sig-1]对应的成员中。
每一个信号对应一个action,action的类型为struct k_sigaction
(3) 如果当前信号的handler被设置为ignore,那么清除当前进程信号共享信号pengding与进程下
线程私有信号pengding中的对应的bit位,同时删除其对应链表上的struct sigqueue
(4) 如果oact参数不为NULL,那么copy设置前的内核的struct sigaction信息到用户态的指针。
SIGKILL 和SIGSTOP的信号处理函数不能通过rt_sigaction设置
kill在内核中系统调用的定义为:
kernel/signal.c
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
主要做的工作有:
(1) 填充struct siginfo结构的信息,包括si_signo、si_code、si_pid、si_uid
(2) 当pid大于0时,调用__kill_pgrp_info函数,向pid对应的进程发送信号
(3) 当pid小于-1时,比如pid=-n,调用__kill_pgrp_info函数,向n所在的进程组发送信号
(4) 当pid等于0时,向当前进程所在的进程组发送信号
(5) 当pid等于-1时,向系统中除了进程1、进程0和当前进程之外的进程发送信号
用strace抓到sigaltstack应用函数接口抓到的系统调用接口为:
sigaltstack({ss_sp=0x555555756040, ss_flags=SS_ONSTACK, ss_size=32768}, NULL)
sigaltstack应用函数接口对应系统调用:
kernel/signal.c
SYSCALL_DEFINE2(sigaltstack,const stack_t __user *,uss, stack_t __user *,uoss)
主要做的工作有:
(1) 如果uss不为空,把用户态传入的stack_t类型的数据copy到内核态的stack_t类型的变量new
中
(2) 设置current进程的t->sas_ss_sp、t->sas_ss_size、t->sas_ss_flags
(3) 如果uoss不为空,返回设置前的current进程的t->sas_ss_sp、t->sas_ss_size、
t->sas_ss_flags到uoss指向的空间。
用strace抓到sigprocmask应用函数接口抓到的系统调用接口为:
rt_sigprocmask(SIG_BLOCK, [FPE], NULL, 8)
sigprocmask应用函数接口对应系统调用:
kernel/signal.c
SYSCALL_DEFINE4(rt_sigprocmask, int, how, sigset_t __user *, nset,
sigset_t __user *, oset, size_t, sigsetsize)
主要做的工作有:
(1) copy 用户态传入的nset的信息到内核态的new_set变量
(2) new_set变量中清除SIGKILL、SIGSTOP信号对应的bit,因为这两个信号不能设置阻塞
(3) 调用函数sigprocmask做真正的设置:
如果how为SIG_BLOCK,设置current进程的blocked中new_set中置位的bit位;
如果how为SIG_UNBLOCK,清除current进程的blocked中new_set中置位的bit位;
如果how为SIG_SETMASK,赋值new_set到current进程的blocked。
(4) 如果oset不为null,copy 当前进程current->blocked到oset指向的用户态内存
图解内核中与信号相关的内核数据结构直接的关系,参考了《深入理解LINUX内核》书中的
图11-1,在学习过程中,曽多次回看这个图,所以我觉得这个图不错,值得再画一次。
图1 信号主要内核数据结构关系图
上图中相关数据结构就是Linux内核信号相关最关键的数据结构,下面是这些结构简单的注释,有些我也没搞懂,可供简单查阅,最细节的地方还是要看代码。
sched.h (include\linux)
struct task_struct
{
…
/*
信号描述符,用来描述共享挂起信号,
如果是给组发信号,pending状态会设置到signal->shared_pending
每个进程的资源限制数组rlim也在这个结构里
*/
struct signal_struct *signal;
/*信号处理描述符*/
struct sighand_struct *sighand;
/*
描述线程私有挂起信号,
如果给指定线程发信号,pending状态会设置到这里的pending
*/
struct sigpending pending;
/*被阻塞信号的掩码*/
sigset_t blocked;
…
}
signal_types.h (include\linux)
struct sigpending {
struct list_head list; /*链表,用于挂接一个或多个struct sigqueue */
sigset_t signal;/*一个信号对应一个bit,从0到63,对应sig 1~64*/
};
struct sigqueue {
struct list_head list; /*链表,用于挂接struct sigpending的list */
int flags;
siginfo_t info;/*信号信息,包含什么信号,信号从哪来等信息*/
struct user_struct *user;
};
struct k_sigaction {
struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER
__sigrestore_t ka_restorer; /*4.18内核中搜到的都设置为NULL*/
#endif
};
typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;
typedef void __restorefn_t(void);
typedef __restorefn_t __user *__sigrestore_t;
/*一种类型的信号对应一个struct sigaction */
struct sigaction {
unsigned int sa_flags; /*信号对应的flags,如SA_RESTART、SA_SIGINFO 、 SA_ONSTACK */
__sighandler_t sa_handler; /*信号处理函数指针*/
__sigrestore_t sa_restorer; /*当用户态信号处理函数执行完后,跳转回内核态执行的函数指针*/
sigset_t sa_mask; /* 指定这个信号的处理函数运行时,要屏蔽的信号*/
};
signal.h (arch\x86\include\asm)
typedef struct {
unsigned long sig[_NSIG_WORDS];/* 一个信号对应一个bit,从0到63,对应sig 1~64*/
} sigset_t;
signal.h (include\linux\sched)
struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG]; /*每一种信号对应一个struct k_sigaction */
spinlock_t siglock; /*在信号处理相关内核代码中,经常要获取这把锁*/
wait_queue_head_t signalfd_wqh;
};
siginfo.h (include\uapi\asm-generic)
typedef struct siginfo {
int si_signo; /*信号的编号*/
/*
信号的发送者来源、原因
SI_USER:用户通过kill、raise发送
SI_KERNEL:一般内核信号发送函数发送的
SI_QUEUE:
SI_TIMER;
SI_ASYNCIO:
SI_TKILL:
*/
int si_code;
int si_errno;/*引起信号产生的出错码,在内核中搜了下,一般都设成了0*/
/*
_sifields :不同的si_code的对应的更多的信息,由于不同的类型信息不同,
所以里面这是一个联合体结构
*/
union {
int _pad[SI_PAD_SIZE];
/* kill() */
struct {
__kernel_pid_t _pid; /* sender's pid */
__kernel_uid32_t _uid; /* sender's uid */
} _kill;
/* POSIX.1b timers */
struct {
__kernel_timer_t _tid; /* timer id */
int _overrun; /* overrun count */
sigval_t _sigval; /* same as below */
int _sys_private; /* not to be passed to user */
} _timer;
/* POSIX.1b signals */
struct {
__kernel_pid_t _pid; /* sender's pid */
__kernel_uid32_t _uid; /* sender's uid */
sigval_t _sigval;
} _rt;
/* SIGCHLD */
struct {
__kernel_pid_t _pid; /* which child */
__kernel_uid32_t _uid; /* sender's uid */
int _status; /* exit code */
__ARCH_SI_CLOCK_T _utime;
__ARCH_SI_CLOCK_T _stime;
} _sigchld;
/* SIGILL, SIGFPE, SIGSEGV, SIGBUS, SIGTRAP, SIGEMT */
struct {
void __user *_addr; /* faulting insn/memory ref. */
#ifdef __ARCH_SI_TRAPNO
int _trapno; /* TRAP # which caused the signal */
#endif
#ifdef __ia64__
int _imm; /* immediate value for "break" */
unsigned int _flags; /* see ia64 si_flags */
unsigned long _isr; /* isr */
#endif
#define __ADDR_BND_PKEY_PAD (__alignof__(void *) < sizeof(short) ? \
sizeof(short) : __alignof__(void *))
union {
/*
* used when si_code=BUS_MCEERR_AR or
* used when si_code=BUS_MCEERR_AO
*/
short _addr_lsb; /* LSB of the reported address */
/* used when si_code=SEGV_BNDERR */
struct {
char _dummy_bnd[__ADDR_BND_PKEY_PAD];
void __user *_lower;
void __user *_upper;
} _addr_bnd;
/* used when si_code=SEGV_PKUERR */
struct {
char _dummy_pkey[__ADDR_BND_PKEY_PAD];
__u32 _pkey;
} _addr_pkey;
};
} _sigfault;
/* SIGPOLL */
struct {
__ARCH_SI_BAND_T _band; /* POLL_IN, POLL_OUT, POLL_MSG */
int _fd;
} _sigpoll;
/* SIGSYS */
struct {
void __user *_call_addr; /* calling user insn */
int _syscall; /* triggering system call number */
unsigned int _arch; /* AUDIT_ARCH_* of syscall */
} _sigsys;
} _sifields;
} __ARCH_SI_ATTRIBUTES siginfo_t;
内核中信号发送最终都会调到__send_signal 和send_sigqueue函数,其中send_sigqueue只被posix_timer_event函数调用到,所以我们平时常见的kill、异常信号的发送,都是经过__send_signal函数发送。
__send_signal函数在kernel/signal.c中,原型为:
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
int group, int from_ancestor_ns)
参数有:
sig 指定发送的信号编号
info指定信号的信息,包括信号编号、信号来源
t指定信号发送的目标进程或线程的struct task_struct描述符
group指定这信号发给某个指定的线程还是某个线程组(进程)
from_ancestor_ns指定当前发送进程是不是属于祖先namespace
__send_signal做的主要工作就是分配一个struct sigqueue,把参数info中的信息写入struct sigqueue,再根据group的值,把struct sigqueue挂入t->signal->shared_pending或者t->pending,
的list,并且更新t->signal->shared_pendind.signal或者t->pending. signal中sig对应的bit。最后选择线程组中的一个线程设置其TIF_SIGPENDING标志,并wake_up。
内核代码复杂的地方就在于,一个函数在完成主要工作的流程中,加入了很多的判断、保护、特殊情况处理。__send_signal函数也一下。
__send_signal在主流程之外做的特殊判断流程有:
原型:int force_sig_info(int sig, struct siginfo *info, struct task_struct *t)
当出现非法内存访问时由page_fault异常处理调用,
page_fault >>do_page_fault >>__do_page_fault >>bad_area_access_error
>> force_sig_info_fault >> force_sig_info >>specific_send_sig_info >>__send_signal
force_sig_info调用__send_signal的提前处理:
如果信号被设置为SIG_IGN忽略状态或者处于blocked状态,那么恢复handler为
SIG_DFL,并清除其blocked状态
原型:static int do_tkill(pid_t tgid, pid_t pid, int sig) kernel/signal.c
系统调用tkill、tgkill调用的是do_kill
do_tkillà do_send_specificà do_send_sig_infoà send_signalà__send_signal
原型:static int kill_something_info(int sig, struct siginfo *info, pid_t pid)
系统调用kill调用的是kill_something_info
kill_something_info >> __kill_pgrp_info >> group_send_sig_info >>do_send_sig_info
>> send_signal >> __send_signal
kill_something_info发信号的对象是整个进程或者进程组
信号发送后,信号并不会被立即处理,只有当信号发送的目标进程正在运行,且运行的CPU出现如中断返回、系统调用返回这样的内核态到用户态切换的时机时,pending的信号才会有机会被处理。
信号处理执行时机在内核中的函数调用关系如下:
正常系统调用路径:
entry_SYSCALL_64
>>do_syscall_64
>>syscall_return_slowpath
>>prepare_exit_to_usermode
>>exit_to_usermode_loop 判断是否有_TIF_SIGPENDING
>>exit_to_usermode_loop
>>do_signal
新进程创建返回路径:
ret_from_fork
>> syscall_return_slowpath
>>prepare_exit_to_usermode
>>exit_to_usermode_loop 判断是否有_TIF_SIGPENDING
>>exit_to_usermode_loop
>>do_signal
中断返回路径
retint_user
>>prepare_exit_to_usermode
>>exit_to_usermode_loop 判断是否有_TIF_SIGPENDING
>>exit_to_usermode_loop
>>do_signal
获取待处理的信号信息主要在get_signal内核函数中,这个函数流程比较复杂,因为内核中的进程有多种状态,信号也有多种处理方式,组合起来就复杂了。
int get_signal(struct ksignal *ksig)
获取信号信息主要的流程就是:
除了主要流程外,get_signal中有很多特殊的分支处理,如:
在获取到需要执行信号处理函数的信号后,调用handle_signal进行处理。
handle_signal其实主要只做了两件事:
1. 把进程被信号打断的上下文信息保存起来
2. 修改当前struct pt_regs *regs指向的寄存器内容为跳转到信号处理函数的寄存器内容。
在内核态跳转到用户态执行前,会把struct pt_regs *regs中的寄存器信息设置到寄存器里,然后执行汇编指令iretq或者sysretq,处理器就会跳转到用户态,并且执行rip寄存器中指向的代码段。
setup_rt_frame函数就是实际干上诉两件事的,
图2 信号处理时栈信息排布
上图是信号处理函数切换到用户态前,通过setup_rt_frame建立的栈信息排布,最上面是fpstate信息,在我的环境中size是0x280, 然后是struct rt_sigframe信息,size是0x1b8,里面主要包含信号打断前的寄存器上下文、信号的信息和从用户态信号处理函数切回到内核态的代码段地址pretcode。
在前面signal_test.c中用户态处理函数void signal_process(int sig, siginfo_t *si, void *ctx_void)的si指针与ctx_void指针,就指向图2中栈排布的地址。默认信号处理的栈共用线程的栈,但是内核支持给信号处理设置自己私有的独立栈,如前面signal_test.c中使用SA_ONSTACK机制后。对SIGSEGV信号的设置,设置后图2中栈的高低地址范围就是用户态设置的stack.ss_sp~stack.ss_size地址范围。
handle_signal最后执行了signal_setup_done函数,
这个函数主要作用是设置信号的blocked bit状态:
做完上诉处理后,函数执行回到retint_user或者ret_from_fork或者entry_SYSCALL_64,在后面的汇编中,我们可以找到设置内核栈中struct pt_regs保存的寄存器信息到真正的寄存器,并执行INTERRUPT_RETURN或者USERGS_SYSRET64返回到用户态。具体代码可以查看
entry_64.S (arch\x86\entry)
当进入用户态指向信号处理函数时,sp指针指向的是图2中struct rt_sigframe所在的地址,struct rt_sigframe中的第一个成员是pretcode。
当用户态信号处理函数返回的时候,retq指令执行前,sp指针重新指向struct rt_sigframe所在的地址,然后执行retq指令,pop 栈中的frame->pretcode到rip寄存器,执行frame->pretcode指向的代码,回到内核态。
当调用用户态函数sigaction时,glibc自动添加SA_RESTORER 到sa_flags,并且设置sa_restorer为触发__NR_rt_sigreturn的系统调用的代码段地址。在内核信号处理__setup_rt_frame中,设置frame->pretcode为sa_restorer的值。
系统调用SYSCALL_DEFINE0(rt_sigreturn) 恢复信号处理前的寄存器上下文,恢复之前的blocked sigset_t,然后系统调用正常返回,代码执行流就会切回到最开始的地方。
信号与进程的异常、退出、状态变化,息息相关,在懂得了信号的内核原理后,可以依靠其中的tracepoint做一个工具,监视系统中某个或某些进程的信号收发。当系统中出现奇怪的想象、问题时,从信号收发这个角度分析一下,没准对我们分析问题有所帮助。
了解Linux内核原理是一件非常划算的事,知其然并知其所以然,才能举一反三。只要你做软件开发,就可以持续受益。毕竟Linux系统不太可能被淘汰,即使linux系统不行了,这些原理代表的思想,也可以用在别的操作系统上。
可能也没那么有趣,但是我觉得学习过程中,以问答的思维方式学习,对掌握知识有好处,所以从这个角度列一些问答的问题。
1. 实时信号与普通信号的区别是啥?
答:
(1)普通信号一个进程仅存在给定类型的一个挂起信号,同进程同类型的其他信号不被排队,只能简单丢弃。但是实时信号不同,同种类型的挂起信号可以有多个。
(2)普通信号一般都被系统赋予特殊作用。如总线错误SIGBUS、非法内存访问SIGSEGV。
2. 进程已经设置自定义异常信号处理函数后,仍然想生成coredump文件怎么办?
答:
默认设置了自定义异常信号处理函数后,内核信号处理流程不会生成coredump文件,会去执行用户定义的信号处理。但是,当一个SIGSEGV信号的处理过程中,由于栈越界或者处理函数访问非法地址再次产生一个SIGSEGV信号时,内核认为这种情况是致命的问题,会杀死进程,并生成coredump文件。
3. 哪些进程的t->signal->flags状态是SIGNAL_UNKILLABLE?
答:
init进程,容器的init进程。
4. SIGCHLD信号什么时候产生?
答:
(1) 子进程终止时
(2)子进程收到SIGSTOP信号停止时
(3)子进程处于停止态,接收到SIGCONT后
5. 不开启信号处理备用栈时,为什么线程栈越界后,进程会退出?
答:
当线程栈越界时,触发 CPU segment fault异常,发送信号到线程,线程准备跳转到用户态执行信号处理函数时,需要在栈中建立frame,frame中包含跳转的信息,这时由于栈已经越界,在内核建立frame阶段,就会异常,导致用户态的信号处理函数根本没有机会执行。二次异常后,内核调用force_sigsegv函数,设置SIGSEGV信号为默认处理,然后杀死进程退出。
6. 在一个SIGSEGV异常处理函数中再产生一次SIGSEGV,会怎么样?
答:
进程会退出。当SIGSEGV信号处理中再次产生非法内存访问时,触发CPU异常,内核调用force_sig_info再次发送SIGSEGV信号到线程,这时SIGSEGV信号是 blocked状态,所以设置sa_handler = SIG_DFL,在进行信号处理时,执行默认的SIGSEGV信号处理流程,杀死进程,如果满足生成coredump条件,会生成coredump文件。