Linux Kernel信号处理机制源码分析
信号(Signal)是一种比较原始的IPC(Inter-Process Communication,进程间通信)机制。本文主要是进行源码的分析,阅读本文的前提是对Linux的信号机制有所了解。
术语概览
- 信号(Signal)
- 信号屏蔽/阻塞(Block):一个进程可以选择阻塞/屏蔽一个信号。然后对于其他进程向自己发送的这个信号,就直接忽略。直到不再阻塞,才能接收到新的信号。
- 经典/不可靠(Regular)信号:编号范围在[1,31]的信号。
- 实时/可靠(Realtime)信号:编号在[SIGRTMIN, SIGRTMAX]之间的信号。其中,SIGRTMIN=34,SIGRTMAX=64。
- 挂起/未决(Pending)信号:当一个信号被发送到一个进程,而该进程尚未处理该信号时,则称该信号是Pending的。信号的处理是在某些时机下进行的,而不像硬件终端可以立即执行。所以一个信号刚刚发送到一个进程,该进程不会立即响应。
- 忽略(Ignore)信号:一个信号的处理函数可以被设置为SIG_IGN。这相当于一个空函数,表示直接忽略该信号,不做任何处理。
- 默认信号处理函数:一个信号的处理函数可以被设置为SIG_DFL。表示选用系统默认的信号处理函数。
数据结构
要深入分析Signal机制,需要先了解下面的数据结构。
Sigset结构
Sigset结构是一个信号集合结构。其定义在arch/x86/include/asm/signal.h当中。
#ifdef __i386__
# define _NSIG_BPW 32
#else
# define _NSIG_BPW 64
#endif
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)
typedef unsigned long old_sigset_t; /* at least 32 bits */
typedef struct {
unsigned long sig[_NSIG_WORDS];
} sigset_t;
可以看出,Linux使用位掩码(Bit mask)来存储信号。这里BPW是Bit Per Word的意思,表示体系结构对应的字长是多少比特。_NSIG表示信号的总数目(为64)。我们知道,unsigned long数据类型是一字长(32位下是32bit,64位下是64bit),所以,通过_NSIG / _NSIG_BPW个unsigned long类型的数字,即可表示集合。每一个信号对应了某一个数字中的某一位。
Linux内核提供了很多操纵sigset的方式。在include/linux/signal.h当中有定义,可以直接去找。
双向循环链表
信号处理机制中大量应用Linux中的链表数据结构。Linux内核中官方提供的链表结构是双向循环链表。在进行内核开发的时候,建议使用Linux内部提供的链表。
Linux中的链表类型在include/linux/types.h当中定义:
struct list_head {
struct list_head *next, *prev;
};
这种链表结构如何使用呢?其实我们只需要把它包含到我们自己的需要串成链表的类型中即可。这也是Linux内核链表的一个非常精妙的地方——是数据里面包含链表,而不是链表里面放数据!比如,我们需要一个LinkedList<foo>这样的结构,只需要如下定义:
struct foo_list {
struct list_head list;
foo val;
}
val则是foo_list的实际的数据域。此外,我们称foo_list的每一个实例,为里面的list的container(容器)。为了初始化链表,Linux在include/linux/list.h当中定义了一些实用的宏和函数。
#define LIST_HEAD_INIT(name) { &(name), &(name) }
#define LIST_HEAD(name) \
struct list_head name = LIST_HEAD_INIT(name)
static inline void INIT_LIST_HEAD(struct list_head *list)
{
list->next = list;
list->prev = list;
}
下面的例程展示了如何动态创建一个链表:
struct foo_list* foos;
struct foo_list* first_foo;
first_foo = kmalloc(sizeof(*node0), GFP_KERNEL);
first_foo->val = NULL; // 为val字段赋值
INIT_LIST_HEAD(&first_foo->list);
foos = first_foo;
通过INIT_LIST_HEAD,我们将表头表尾都指向自身,构成了元素数量为1的循环链表。因为是环形链表,从任意一个节点开始都可以遍历到整个链表。当然这么做其实是不符合规范的。我们应该给环形链表一个索引节点。可以这样做:
static LIST_HEAD(foos)
根据宏定义,其实等价于
static struct list_head foos = {&foos, &foos};
这么做其实是创建了一个伪”头节点“。注意到static,可以让在函数体内定义的这个变量被分配在静态存储区。防止退出函数体之后链表表头中的next、prev域指针失效。
链表操纵函数
- 添加一个节点到head节点之后:list_add(struct list_head *new, struct list_head *head)
- 添加一个节点到head节点之前:list_add_tail(struct list_head* new, struct list_head* head)
- 从链表当中删除一个节点:list_del(struct list_head *entry)
- 从链表中删除一个节点并重新初始化它:list_del_init(struct list_head *entry)
- 判断链表是否为空:list_empty(struct list_head *head)
- 连接两个链表(把list插入到head之后):list_splice(struct list_head *list, struct list_head *head)
遍历链表
遍历链表可以使用list_for_each宏。其定义如下:
#define list_for_each(pos, head) \
for (pos = (head)->next; pos != (head); pos = pos->next)
有索引节点的list都可以用list_for_each来遍历。将索引节点传给list_for_each的head参数即可。pos是循环变量,表示链表中的每一个元素。可以看下面:
struct list_head* p;
struct foo_list* node;
list_for_each(p, &foos) {
node = list_entry(p, struct foo_list, list);
}
等等,这个list_entry是什么?这是Linux内核最精彩的几个地方之一。我们知道,我们现在在遍历的其实是list_head结构,而不是foo_list。然而list_head是包含在foo_list当中的。那我们如果遍历到某一个list_head,我们怎么知道它对应的foo_list是谁呢?这看似是无法做到的。
仔细分析以下。我们知道,某一个list_head变量相对于包含它的foo_list的偏移,是在编译时期就确定的!我们只需要将当前list_head的地址加上这个偏移,不就可以实现了吗?没错,Linux kernel里面有一个container_of宏(在include/linux/kernel.h),就是这么干的:
#define container_of(ptr, type, member) ({ \
const typeof( ((type *)0)->member ) *__mptr = (ptr); \
(type *)( (char *)__mptr - offsetof(type,member) );})
代码还是非常精妙的。仔细看一下,typeof( ((type *)0)->member )表示member成员的类型。_mptr是一个这个类型的指针,然后将ptr复制给__mptr。那可能要问,为什么还要用mptr这玩意呢?直接用ptr不好吗?这里是为了当ptr的类型和member的类型不匹配的时候,编译器产生警告的Warning,以提示程序员。
第二行,就是将ptr的地址减去在结构体内的偏移——offsetof(type, member)。所以它可以实现container_of的目的。
而list_entry就是一个container_of的wrapper:
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
每次把链表遍历写成上面那样不够方便。所以,还提供了——list_for_each_entry(pos, head, member) 宏。上面的代码可以改写成
struct foo_list* node;
list_for_each_entry(node, &foos, list) {
...
}
进程和信号的绑定数据结构
每一个进程都对应了一个task_struct。对一个进程发送信号,以及安装信号,本质上都是操纵这个进程task_struct当中的项。在task_struct当中,有下列字段是与信号相关的:
(include/linux/sched.h)
/* signal handlers */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask; /* restored if set_restore_sigmask() was used */
struct sigpending pending;
我们重点关注sighand、pending、blocked。
sighand_struct
每一个进程对应一个sighand_struct。主要是存储信号与处理函数之间的映射。定义如下
(include/linux/sched.h)
struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG];
spinlock_t siglock;
wait_queue_head_t signalfd_wqh;
};
其中的action是我们最需要关注的。它是一个长度为_NSIG的数组。下标为k的元素,就代表编号为k的信号的处理函数。k_sigaction实际上就是在内核态中对于sigaction的一个包装。
(linux/signal.h)
struct k_sigaction {
struct sigaction sa;
};
pending
每一个进程都有一个挂起信号等待队列。我们需要注意,信号和硬中断不一样。信号处理程序只会在一些特定的时机(从内核态回到用户态的时候)被调用。所以,如果发送信号的频率比较高,对于一个进程来说,可能有一个信号还没处理,又接受到了另一个相同种类的信号。在旧的UNIX操作系统中,这种情况新的信号就会被丢弃。在Linux内核中,增加了可靠信号的支持。其实现方案很简单,就是为每个进程使用一个队列,接收到信号,则先将信号放入队列中。然而Linux内核有保证了对于1-31号信号的前向兼容性。具体来说,Linux内核对于可靠信号的处理如下:
- 当一个进程接收到regular signal([1,31]),首先判断该进程当前是否已经接收过但未处理这种signal。如果是的话,则什么也不干。否则,将该信号加入到未处理信号队列中。
- 当一个进程接收到realtime signal的时候,无论如何都会把该信号加入未处理信号队列中。
从这个行为来看,为了快速判断/设定某个信号是否在未处理信号队列中,可以使用上面的sigset数据结构来实现。然后,队列则使用链表来实现。事实上,内核就是这么做的。
(linux/signal.h)
struct sigpending {
struct list_head list;
sigset_t signal;
};
这里的signal,就是用来记录当前在未处理信号队列中的信号集合。而list,则表示信号队列链表的表头。这个list的container是sigqueue。
(linux/signal.h)
struct sigqueue {
struct list_head list;
int flags;
siginfo_t info;
struct user_struct *user;
};
我们重点关注里面的info域。
(include/uapi/asm-generic/siginfo.h)
typedef struct siginfo {
int si_signo;
int si_errno;
int si_code;
union {
int _pad[SI_PAD_SIZE];
/* kill() */
struct {
__kernel_pid_t _pid; /* sender's pid */
__ARCH_SI_UID_T _uid; /* sender's uid */
} _kill;
/* POSIX.1b timers */
struct {
__kernel_timer_t _tid; /* timer id */
int _overrun; /* overrun count */
char _pad[sizeof( __ARCH_SI_UID_T) - sizeof(int)];
sigval_t _sigval; /* same as be信号的信息low */
int _sys_private; /* not to be passed to user */
} _timer;
/* POSIX.1b signals */
struct {
__kernel_pid_t _pid; /* sender's pid */
__ARCH_SI_UID_T _uid; /* sender's uid */
sigval_t _sigval;
} _rt;
/* SIGCHLD */
struct {
__kernel_pid_t _pid; /* which child */
__ARCH_SI_UID_T _uid; /* sender's uid */
int _status; /* exit code */
__ARCH_SI_CLOCK_T _utime;
__ARCH_SI_CLOCK_T _stime;
} _sigchld;
/* SIGILL, SIGFPE, SIGSEGV, SIGBUS */
struct {
void __user *_addr; /* faulting insn/memory ref. */
#ifdef __ARCH_SI_TRAPNO
int _trapno; /* TRAP # which caused the signal */
#endif
short _addr_lsb; /* LSB of the reported address */
struct {
void __user *_lower;
void __user *_upper;
} _addr_bnd;
} _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;
可以看到,这里记录了接收到信号的信息。
blocked
一个进程可以选择阻塞某些信号。对于这些被阻塞的信号,其他进程仍可以发送阻塞信号给这个进程,但是这个进程将不会接收到这个信号。blocked也是一个sigset,表示被阻塞的信号集合。需要注意的是,SIGKILL信号无法被阻塞。
pt_regs结构
pt_regs是一个结构体。定义如下:
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_ax;
/* Return frame for iretq */
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
这个结构体实际上起到这样的作用。在执行系统调用的时候,需要陷入到内核态。在进入内核态之前,内核会先将用户态下各个寄存器的情况保存下来。在准备回到用户态的时候,会将这些寄存器的值恢复。pt_regs往往是存储在内核栈当中的。
首先我们需要明确这些寄存器在内核栈中的内存分布。
高地址
ss
sp
flags
cs
ip
orig_ax
di
si
...
r13
r14
r15
低地址
在恢复这些寄存器的时候,首先通过mov指令将前面的(ip寄存器以下)的部分全部恢复。这时,栈中只剩下(从高到低)ss、sp、flags、cs、ip。这些剩余寄存器是不能通过mov恢复的。而是直接通过一条iret指令。
iret指令将会按顺序执行下面操作:
- 从栈顶弹出并恢复RIP
- 从栈顶弹出并恢复CS
- 从栈顶弹出并恢复标志寄存器
- 从栈顶弹出并恢复RSP
- 从栈顶弹出并恢复SS
刚好符合pt_regs的内存分布。
信号处理过程
一次完整的信号处理过程,由下面的几个部分组成。
安装信号处理函数
首先,用户可以自定义信号的处理函数。看下面的例程:
#include <stdio.h>
#include <signal.h>
void handler(int sig) {
printf("Received signal: %u\n", sig);
}
int main() {
struct sigaction sa;
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGTERM, &sa, NULL);
sigaction(SIGKILL, &sa, NULL);
while (1) {
sigsuspend(&sa.sa_mask);
printf("Haha\n");
}
return 0;
}
glibc库将Linux提供的和信号有关的系统调用进行了包装(Wrap)。我们可以便捷地调用sigaction函数来安装信号处理函数。首先,使用sigaction结构体设置信号处理函数的信息。
sigaction结构体定义于内核源码的include/linux/signal.h。
struct sigaction {
__sighandler_t sa_handler;
unsigned long sa_flags;
__sigrestore_t sa_restorer;
sigset_t sa_mask;
};
sa_handler是处理函数的函数指针。其类型__sighandler_t如下
typedef void __signalfn_t(int);
typedef __signalfn_t __user *__sighandler_t;
可知sa_handler是用户态下的函数,接收一个int变量,表示信号的编号。sa_flags这里不谈。sa_restorer表示信号处理函数执行结束之后,去往哪一个地址当中。GLIBC库会自动设置这一项为rs_sigreturn系统调用。后面会作具体介绍。sa_mask可以指定阻塞信号集合。当该sa_handler在执行中,进程会暂时屏蔽(阻塞)这个集合内的信号。(注意:当sa_handler不在执行的时候,仍然可以接收到这些信号)。
在设置好sigaction的参数之后,通过sigaction来安装设定。sigaction是GLIBC库中的一个Wrapper函数,其本质是rt_sigaction系统调用。
rt_sigaction的定义位于kernel/signal.c当中。
SYSCALL_DEFINE4(rt_sigaction, int, sig,
const struct sigaction __user *, act,
struct sigaction __user *, oact,
size_t, sigsetsize)
{
struct k_sigaction new_sa, old_sa;
int ret = -EINVAL;
/* XXX: Don't preclude handling different sized sigset_t's. */
if (sigsetsize != sizeof(sigset_t))
goto out;
if (act) {
if (copy_from_user(&new_sa.sa, act, sizeof(new_sa.sa)))
return -EFAULT;
}
ret = do_sigaction(sig, act ? &new_sa : NULL, oact ? &old_sa : NULL);
if (!ret && oact) {
if (copy_to_user(oact, &old_sa.sa, sizeof(old_sa.sa)))
return -EFAULT;
}
out:
return ret;
}
sig参数表示对应的信号。act参数表示新安装的sigaction结构体,是一个指向用户空间sigaction的指针。稍后该系统调用会将这个sigaction结构体拷贝到内核空间。oact可以用来查询之前在该信号上安装的sigaction结构体。sigsetsize需要被设置为sizeof(sigset_t)(目的不清楚,sigset_t在前面有详细介绍)。
首先,通过copy_from_user函数,将用户空间内的act拷贝到局部变量new_sa当中。
然后调用do_sigaction。该函数定义如下
int do_sigaction(int sig, struct k_sigaction *act, struct k_sigaction *oact)
{
struct task_struct *p = current, *t;
struct k_sigaction *k;
sigset_t mask;
if (!valid_signal(sig) || sig < 1 || (act && sig_kernel_only(sig)))
return -EINVAL;
k = &p->sighand->action[sig-1];
spin_lock_irq(&p->sighand->siglock);
if (oact)
*oact = *k;
if (act) {
sigdelsetmask(&act->sa.sa_mask,
sigmask(SIGKILL) | sigmask(SIGSTOP));
*k = *act;
/*
* POSIX 3.3.1.3:
* "Setting a signal action to SIG_IGN for a signal that is
* pending shall cause the pending signal to be discarded,
* whether or not it is blocked."
*
* "Setting a signal action to SIG_DFL for a signal that is
* pending and whose default action is to ignore the signal
* (for example, SIGCHLD), shall cause the pending signal to
* be discarded, whether or not it is blocked"
*/
if (sig_handler_ignored(sig_handler(p, sig), sig)) {
sigemptyset(&mask);
sigaddset(&mask, sig);
flush_sigqueue_mask(&mask, &p->signal->shared_pending);
for_each_thread(p, t)
flush_sigqueue_mask(&mask, &t->pending);
}
}
spin_unlock_irq(&p->sighand->siglock);
return 0;
}
首先通过if语句检查信号是否合法。如果不合法,返回EINVAL错误号码。具体检查项有:
- 信号数值是否在[1,64]之间。
- sig_kernel_only检查信号是否为SIGKILL或SIGSTOP。这意味着你不能为SIGKILL或者SIGSTOP绑定处理函数。
然后通过sigdelsetmask强制删除sa_mask当中的SIGKILL和SIGSTOP。这意味着你无法在一个信号处理函数执行的时候屏蔽调SIGKILL和SIGSTOP信号。
然后是最重要的一句话:*k = *act。这样,就把处理的sigaction结构体直接绑定到当前进程task_struct当中的action数组中(前面数据结构部分有详细介绍)。
sig_handler_ignored可以判断一个信号是否是被忽略的信号。其定义如下:
static int sig_handler_ignored(void __user *handler, int sig)
{
/* Is it explicitly or implicitly ignored? */
return handler == SIG_IGN ||
(handler == SIG_DFL && sig_kernel_ignore(sig));
}
有两种情况,一个信号是被忽略的:
- 显式忽略:信号处理函数是SIG_IGN。
- 隐式忽略:信号处理函数是SIG_DFL,但是默认操作就是忽略。
如果一个信号是被忽略的,那么就将通过flush_sigqueue_mask函数,将该信号从当前进程的等待队列中移除。这个实现较为简单,不多介绍。
其他进程向当前进程发送信号
在信号安装完毕之后,程序就正常执行。现在,有某一个进程向自己发送了某一种信号。内核会怎么处理呢?系统调用kill就可以让我们实现这一目的。
(kernel/signal.c)
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct siginfo info;
info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info.si_pid = task_tgid_vnr(current);
info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
return kill_something_info(sig, &info, pid);
}
我们知道siginfo就是用来存储接收到的信号的。我们可以大概猜想发送信号的核心,就是先构建一个siginfo,然后把这个siginfo结构体给放入目标进程task_struct当中的pending队列当中。
在这里,我们看到了siginfo的构建。然后,调用了kill_something_info函数。
(kernel/signal.c)
static int kill_something_info(int sig, struct siginfo *info, pid_t pid)
{
int ret;
if (pid > 0) {
rcu_read_lock();
ret = kill_pid_info(sig, info, find_vpid(pid));
rcu_read_unlock();
return ret;
}
......
}
关注最关键的部分。就是pid>0的if语句块当中。我们发现,又调用了kill_pid_info。
(kernel/signal.c)
int kill_pid_info(int sig, struct siginfo *info, struct pid *pid)
{
int error = -ESRCH;
struct task_struct *p;
for (;;) {
rcu_read_lock();
p = pid_task(pid, PIDTYPE_PID);
if (p)
error = group_send_sig_info(sig, info, p);
rcu_read_unlock();
if (likely(!p || error != -ESRCH))
return error;
/*
* The task was unhashed in between, try again. If it
* is dead, pid_task() will return NULL, if we race with
* de_thread() it will find the new leader.
*/
}
}
pid_task函数的目的是根据pid获取到对应的task_struct。然后,调用group_send_sig_info函数。
(kernel/signal.c)
int group_send_sig_info(int sig, struct siginfo *info, struct task_struct *p)
{
int ret;
rcu_read_lock();
ret = check_kill_permission(sig, info, p);
rcu_read_unlock();
if (!ret && sig)
ret = do_send_sig_info(sig, info, p, true);
return ret;
}
然后进一步调用do_send_sig_info函数
(kernel/signal.c)
int do_send_sig_info(int sig, struct siginfo *info, struct task_struct *p,
bool group)
{
unsigned long flags;
int ret = -ESRCH;
if (lock_task_sighand(p, &flags)) {
ret = send_signal(sig, info, p, group);
unlock_task_sighand(p, &flags);
}
return ret;
}
这一层一层的太复杂了。然后调用的是send_signal函数
(kernal/signal.c)
static int send_signal(int sig, struct siginfo *info, struct task_struct *t,
int group)
{
int from_ancestor_ns = 0;
#ifdef CONFIG_PID_NS
from_ancestor_ns = si_fromuser(info) &&
!task_pid_nr_ns(current, task_active_pid_ns(t));
#endif
return __send_signal(sig, info, t, group, from_ancestor_ns);
}
最终调用的是__send_signal:
(kernel/signal.c)
static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
int group, int from_ancestor_ns)
{
struct sigpending *pending;
struct sigqueue *q;
int override_rlimit;
int ret = 0, result;
assert_spin_locked(&t->sighand->siglock);
result = TRACE_SIGNAL_IGNORED;
if (!prepare_signal(sig, t,
from_ancestor_ns || (info == SEND_SIG_FORCED)))
goto ret;
pending = group ? &t->signal->shared_pending : &t->pending;
/*
* Short-circuit ignored signals and support queuing
* exactly one non-rt signal, so that we can get more
* detailed information about the cause of the signal.
*/
result = TRACE_SIGNAL_ALREADY_PENDING;
if (legacy_queue(pending, sig))
goto ret;
result = TRACE_SIGNAL_DELIVERED;
/*
* fast-pathed signals for kernel-internal things like SIGSTOP
* or SIGKILL.
*/
if (info == SEND_SIG_FORCED)
goto out_set;
/*
* Real-time signals must be queued if sent by sigqueue, or
* some other real-time mechanism. It is implementation
* defined whether kill() does so. We attempt to do so, on
* the principle of least surprise, but since kill is not
* allowed to fail with EAGAIN when low on memory we just
* make sure at least one signal gets delivered and don't
* pass on the info struct.
*/
if (sig < SIGRTMIN)
override_rlimit = (is_si_special(info) || info->si_code >= 0);
else
override_rlimit = 0;
q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
override_rlimit);
if (q) {
list_add_tail(&q->list, &pending->list);
switch ((unsigned long) info) {
case (unsigned long) SEND_SIG_NOINFO:
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info.si_pid = task_tgid_nr_ns(current,
task_active_pid_ns(t));
q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
break;
case (unsigned long) SEND_SIG_PRIV:
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_KERNEL;
q->info.si_pid = 0;
q->info.si_uid = 0;
break;
default:
copy_siginfo(&q->info, info);
if (from_ancestor_ns)
q->info.si_pid = 0;
break;
}
userns_fixup_signal_uid(&q->info, t);
} else if (!is_si_special(info)) {
if (sig >= SIGRTMIN && info->si_code != SI_USER) {
/*
* Queue overflow, abort. We may abort if the
* signal was rt and sent by user using something
* other than kill().
*/
result = TRACE_SIGNAL_OVERFLOW_FAIL;
ret = -EAGAIN;
goto ret;
} else {
/*
* This is a silent loss of information. We still
* send the signal, but the *info bits are lost.
*/
result = TRACE_SIGNAL_LOSE_INFO;
}
}
out_set:
signalfd_notify(t, sig);
sigaddset(&pending->signal, sig);
complete_signal(sig, t, group);
ret:
trace_signal_generate(sig, info, t, group, result);
return ret;
}
代码太长了。我们分成几个部分来看。
result = TRACE_SIGNAL_ALREADY_PENDING;
if (legacy_queue(pending, sig))
goto ret;
这里legacy_queue适用于检测当前的信号是否满足下面两个条件:
- 是regular signal(不可靠信号)
- 已经在挂起信号队列中了
static inline int legacy_queue(struct sigpending *signals, int sig)
{
return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}
也就是说,在发送信号的时候,对于已经存在于队列的不可靠信号,直接忽略。
然后看
q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
override_rlimit);
if (q) {
list_add_tail(&q->list, &pending->list);
switch ((unsigned long) info) {
case (unsigned long) SEND_SIG_NOINFO:
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_USER;
q->info.si_pid = task_tgid_nr_ns(current,
task_active_pid_ns(t));
q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
break;
case (unsigned long) SEND_SIG_PRIV:
q->info.si_signo = sig;
q->info.si_errno = 0;
q->info.si_code = SI_KERNEL;
q->info.si_pid = 0;
q->info.si_uid = 0;
break;
default:
copy_siginfo(&q->info, info);
if (from_ancestor_ns)
q->info.si_pid = 0;
break;
}
userns_fixup_signal_uid(&q->info, t);
首先创建一个sigqueue节点。然后,一般情况下,将调用copy_siginfo函数,将信息复制到当前节点的info域内。实际上这是完全符合刚才的猜想的。
还需要提一下,接下来调用的complete_signal函数会调用signal_wake_up。这个函数会将线程的TIF_SIGPENDING标志设为1。这样后面就可以快速检测是否有未处理的信号了。
当前进程陷入内核态,并准备返回用户态时处理信号
现在,当前进程正在正常执行。刚才已经有进程发送信号,通过send_signal将信号存储在了当前进程的Pending queue当中。当前进程显然不会立刻处理这个信号。处理信号的时机,实际上是当前进程因为一些原因陷入内核态,然后返回用户态的时候。
现在,假设当前进程因为下面的原因进入内核态:
- 中断
- 系统调用
- 异常
执行完内核态的操作之后,返回用户态。返回用户态内核内部将会使用这个函数:exit_to_usermode_loop。该函数的代码就不放了,但是该函数中有一个重要操作:
if (cached_flags & _TIF_SIGPENDING)
do_signal(regs);
返回用户态的时候,将会判断当前线程是否被设置了TIF_SIGPENDING标志。如果设定,证明存在未处理的信号。此时,就需要调用do_signal函数,来处理未处理的信号。
下面是do_signal函数
(arch/x86/kernel/signal.c)
void do_signal(struct pt_regs *regs)
{
struct ksignal ksig;
if (get_signal(&ksig)) {
/* Whee! Actually deliver the signal. */
handle_signal(&ksig, regs);
return;
}
...
}
可以看出,do_signal的核心是handle_signal。如下
static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
...
/*
* If TF is set due to a debugger (TIF_FORCED_TF), clear TF now
* so that register information in the sigcontext is correct and
* then notify the tracer before entering the signal handler.
*/
stepping = test_thread_flag(TIF_SINGLESTEP);
if (stepping)
user_disable_single_step(current);
failed = (setup_rt_frame(ksig, regs) < 0);
if (!failed) {
/*
* Clear the direction flag as per the ABI for function entry.
*
* Clear RF when entering the signal handler, because
* it might disable possible debug exception from the
* signal handler.
*
* Clear TF for the case when it wasn't set by debugger to
* avoid the recursive send_sigtrap() in SIGTRAP handler.
*/
regs->flags &= ~(X86_EFLAGS_DF|X86_EFLAGS_RF|X86_EFLAGS_TF);
/*
* Ensure the signal handler starts with the new fpu state.
*/
if (fpu->fpstate_active)
fpu__clear(fpu);
}
signal_setup_done(failed, ksig, stepping);
}
主要的handle_signal操作,是通过setup_rt_frame来设定的。而setup_rt_frame的核心是__setup_rt_frame。
static int __setup_rt_frame(int sig, struct ksignal *ksig,
sigset_t *set, struct pt_regs *regs)
{
struct rt_sigframe __user *frame;
void __user *fp = NULL;
int err = 0;
frame = get_sigframe(&ksig->ka, regs, sizeof(struct rt_sigframe), &fp);
if (!access_ok(VERIFY_WRITE, frame, sizeof(*frame)))
return -EFAULT;
if (ksig->ka.sa.sa_flags & SA_SIGINFO) {
if (copy_siginfo_to_user(&frame->info, &ksig->info))
return -EFAULT;
}
put_user_try {
/* Create the ucontext. */
if (cpu_has_xsave)
put_user_ex(UC_FP_XSTATE, &frame->uc.uc_flags);
else
put_user_ex(0, &frame->uc.uc_flags);
put_user_ex(0, &frame->uc.uc_link);
save_altstack_ex(&frame->uc.uc_stack, regs->sp);
/* Set up to return from userspace. If provided, use a stub
already in userspace. */
/* x86-64 should always use SA_RESTORER. */
if (ksig->ka.sa.sa_flags & SA_RESTORER) {
put_user_ex(ksig->ka.sa.sa_restorer, &frame->pretcode);
} else {
/* could use a vstub here */
err |= -EFAULT;
}
} put_user_catch(err);
err |= setup_sigcontext(&frame->uc.uc_mcontext, fp, regs, set->sig[0]);
err |= __copy_to_user(&frame->uc.uc_sigmask, set, sizeof(*set));
if (err)
return -EFAULT;
/* Set up registers for signal handler */
regs->di = sig;
/* In case the signal handler was declared without prototypes */
regs->ax = 0;
/* This also works for non SA_SIGINFO handlers because they expect the
next argument after the signal number on the stack. */
regs->si = (unsigned long)&frame->info;
regs->dx = (unsigned long)&frame->uc;
regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
regs->sp = (unsigned long)frame;
/* Set up the CS register to run signal handlers in 64-bit mode,
even if the handler happens to be interrupting 32-bit code. */
regs->cs = __USER_CS;
return 0;
}
先看get_sigframe函数。
static void __user *
get_sigframe(struct k_sigaction *ka, struct pt_regs *regs, size_t frame_size,
void __user **fpstate)
{
/* Default to using normal stack */
unsigned long math_size = 0;
unsigned long sp = regs->sp;
unsigned long buf_fx = 0;
int onsigstack = on_sig_stack(sp);
struct fpu *fpu = ¤t->thread.fpu;
/* redzone */
if (config_enabled(CONFIG_X86_64))
sp -= 128;
...
sp = align_sigframe(sp - frame_size);
...
return (void __user *)sp;
}
我们只需要关注两个部分就可以了。
首先是redzone部分。
/* redzone */
if (config_enabled(CONFIG_X86_64))
sp -= 128;
红色区域(Red zone)是X86-64 调用规范中的一个重要标准。它指出,在%rsp指向的栈顶之后的128字节被保留,不能被信号和中断处理程序使用(注意:只是不被信号和中断处理程序使用,而不表示不被其他函数调用使用)。
所以,如果体系结构是x86_64,那么就把sp,也就是用户态的栈顶指针减去128,保留那128字节的空间。
然后,我们注意到get_sigframe将会将栈顶指针sp减掉frame_size。这其实是相当于在栈中保留frame_size字节的空间。(然后通过align_sigframe确保栈顶指针的16字节对齐)。然后,返回新的栈顶指针。
这个空间用来放什么呢?我们看get_sigframe的调用者,
struct rt_sigframe __user *frame;
frame = get_sigframe(&ksig->ka, regs, sizeof(struct rt_sigframe), &fp);
可以看到,新的栈顶指针指向的sizeof(struct rt_sigframe)字节的内存空间,被复制给了frame。也就是说,刚才的get_sigframe实际上就是为了保留好rt_sigframe的空间。
rt_sigframe可谓非常重要。其定义如下:
(x86/include/asm/sigframe.h)
struct rt_sigframe {
char __user *pretcode;
struct ucontext uc;
struct siginfo info;
};
用户栈上的内存分布:
高地址
info
uc
pretcode
低地址
pretcode域记录了原本用户态的执行位置。你可能会问,现在陷入在内核态中,那么这个位置不是被保存在内核栈的pt_regs里面了马?因为这个pt_regs我们会修改(后面会看到),所以用户态原本的执行位置将被保存在用户栈上的pretcode域内!
然后就是uc。ucontext是用户上下文的意思。ucontext结构体定义如下:
struct ucontext {
unsigned long uc_flags;
struct ucontext *uc_link;
stack_t uc_stack;
struct sigcontext uc_mcontext;
sigset_t uc_sigmask; /* mask last for extensibility */
};
这里uc_mcontext是一个sigcontext结构体。
struct sigcontext_64 {
__u64 r8;
__u64 r9;
__u64 r10;
__u64 r11;
__u64 r12;
__u64 r13;
__u64 r14;
__u64 r15;
__u64 di;
__u64 si;
__u64 bp;
__u64 bx;
__u64 dx;
__u64 ax;
__u64 cx;
__u64 sp;
__u64 ip;
__u64 flags;
__u16 cs;
__u16 gs;
__u16 fs;
__u16 __pad0;
__u64 err;
__u64 trapno;
__u64 oldmask;
__u64 cr2;
/*
* fpstate is really (struct _fpstate *) or (struct _xstate *)
* depending on the FP_XSTATE_MAGIC1 encoded in the SW reserved
* bytes of (struct _fpstate) and FP_XSTATE_MAGIC2 present at the end
* of extended memory layout. See comments at the definition of
* (struct _fpx_sw_bytes)
*/
__u64 fpstate; /* Zero when no FPU/extended context */
__u64 reserved1[8];
};
很明显这个结构体是用来保存用户态程序寄存器状态的。
然后,__setup_rt_frame函数会做的工作包括但不限于(可以对照着代码看):
- 检查为rt_sigframe保存的栈区域是否可写
- 填入ucontext中的内容
- 填入rt_sigframe中的pretcode(put_user_ex(ksig->ka.sa.sa_restorer, &frame->pretcode);)
- 保存原用户态的寄存器情况到uc_mcontext当中
最后,可以看到最关键的一部分:
/* Set up registers for signal handler */
regs->di = sig;
/* In case the signal handler was declared without prototypes */
regs->ax = 0;
/* This also works for non SA_SIGINFO handlers because they expect the
next argument after the signal number on the stack. */
regs->si = (unsigned long)&frame->info;
regs->dx = (unsigned long)&frame->uc;
regs->ip = (unsigned long) ksig->ka.sa.sa_handler;
regs->sp = (unsigned long)frame;
/* Set up the CS register to run signal handlers in 64-bit mode,
even if the handler happens to be interrupting 32-bit code. */
regs->cs = __USER_CS;
注意,这里将会修改pt_regs。首先将sig传递给di,这样一来,rdi寄存器中保存着sig的值。就可以实现传入参数给信号处理函数。
IP寄存器也会被修改成sa_handler。sa_handler是一个指向用户空间下信号处理函数的指针。代码段寄存器CS也会被修改成用户空间下的代码段寄存器。这样一来,在回到用户态时,iret将会设置IP和CS到用户空间下信号处理函数对应的位置。
然后就到了用户空间函数了!有一个细节,用户空间函数返回的时候,根据栈分布,是会返回到pretcode位置的。在Glibc中,sa_restorer会自动设定为一个调用rt_sigreturn的函数。于是,最终,rt_sigreturn系统调用被调用。
asmlinkage long sys_rt_sigreturn(void)
{
struct pt_regs *regs = current_pt_regs();
struct rt_sigframe __user *frame;
sigset_t set;
frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));
if (!access_ok(VERIFY_READ, frame, sizeof(*frame)))
goto badframe;
if (__copy_from_user(&set, &frame->uc.uc_sigmask, sizeof(set)))
goto badframe;
set_current_blocked(&set);
if (restore_sigcontext(regs, &frame->uc.uc_mcontext))
goto badframe;
if (restore_altstack(&frame->uc.uc_stack))
goto badframe;
return regs->ax;
badframe:
signal_fault(regs, frame, "rt_sigreturn");
return 0;
}
这里代码含义非常清晰。进行一系列地恢复操作即可。首先恢复寄存器到陷入内核态之前的状态,然后恢复栈。这就是完整的信号生命周期。