ICode9

精准搜索请尝试: 精确搜索
首页 > 系统相关> 文章详细

Linux Kernel信号处理机制源码分析

2021-01-21 23:35:01  阅读:378  来源: 互联网

标签:__ Kernel info struct signal list 源码 sig Linux


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 = &current->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;
}

这里代码含义非常清晰。进行一系列地恢复操作即可。首先恢复寄存器到陷入内核态之前的状态,然后恢复栈。这就是完整的信号生命周期。

标签:__,Kernel,info,struct,signal,list,源码,sig,Linux
来源: https://www.cnblogs.com/gnuemacs/p/14311120.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有