【原创】xenomai内核解析--信号signal(一)---Linux信号机制

版权声明:本文为本文为博主原创文章,转载请注明出处。如有错误,欢迎指正。博客地址:https://www.cnblogs.com/wsg1100/

1. Linux信号

涉及硬件底层,本文以X86平台讲解。

信号是事件发生时对进程的通知机制,是操作系统提供的一种软件中断。信号提供了一种异步处理事件的方法,信号与硬件中断的相似之处在于打断了程序执行的正常流程,例如,中断用户键入中断键(Ctrl+C),会通过信号机制停止应用程序。

  1. 信号的几种产生方式:
  • 按键产生 当用户按某些按键时,引发终端产生的信号。 ctrl+C产生 SIGINT信号、ctrl+\产生SIGQUIT信号、ctrl+z产生SIGTSTP 信号
  • 系统调用产生 进程调用系统调用函数将任意信号发送给另一个进程或进程组。如:kill() 、raise()、abort()
  • 软件条件产生 当操作系统检测到某种软件条件时,使用信号通知有关进程。如:定时器函数alarm()
  • 硬件异常产生 由硬件检测到某些条件,并通知内核,然后内核为该条件发生时正在运行的进程产生适当的信号。如:非法操作内存(段错误)SIGSEGV信号、除0操作(浮点数除外)SIGFPE 信号、总线错误SIGBUS信号。
  • 命令产生 其时都是系统调用的封装,如:kill -9 pid
  1. 当某个信号产生时,进程可告诉内核其处理方式:
  • 执行系统默认动作。linux下可通过man 7 signal来查看具体信号及默认处理动作,大多数信号的系统默认动作是终止进程。
  • 忽略此信号。SIGKILL和SIGSTOP信号不能被忽略,它们用于在任何时候中断或结束某一进程。。
  • 捕捉信号。告诉内核在某种信号产生时,调用一个用户处理函数。在处理函数中,执行用户对该信号的具体处理。SIGKILL和SIGSTOP信号不能捕捉。
  1. 信号的两种状态
  • 抵达 递送并且到达进程。
  • 未决 产生和递达之间的状态。主要由于阻塞(屏蔽)导致该状态。

可以使用kill –l命令查看当前系统可使用的信号.

$ kill -l
 1) SIGHUP	 2) SIGINT	 3) SIGQUIT	 4) SIGILL	 5) SIGTRAP
 6) SIGABRT	 7) SIGBUS	 8) SIGFPE	 9) SIGKILL	10) SIGUSR1
11) SIGSEGV	12) SIGUSR2	13) SIGPIPE	14) SIGALRM	15) SIGTERM
16) SIGSTKFLT	17) SIGCHLD	18) SIGCONT	19) SIGSTOP	20) SIGTSTP
21) SIGTTIN	22) SIGTTOU	23) SIGURG	24) SIGXCPU	25) SIGXFSZ
26) SIGVTALRM	27) SIGPROF	28) SIGWINCH	29) SIGIO	30) SIGPWR
31) SIGSYS	34) SIGRTMIN	35) SIGRTMIN+1	36) SIGRTMIN+2	37) SIGRTMIN+3
38) SIGRTMIN+4	39) SIGRTMIN+5	40) SIGRTMIN+6	41) SIGRTMIN+7	42) SIGRTMIN+8
43) SIGRTMIN+9	44) SIGRTMIN+10	45) SIGRTMIN+11	46) SIGRTMIN+12	47) SIGRTMIN+13
48) SIGRTMIN+14	49) SIGRTMIN+15	50) SIGRTMAX-14	51) SIGRTMAX-13	52) SIGRTMAX-12
53) SIGRTMAX-11	54) SIGRTMAX-10	55) SIGRTMAX-9	56) SIGRTMAX-8	57) SIGRTMAX-7
58) SIGRTMAX-6	59) SIGRTMAX-5	60) SIGRTMAX-4	61) SIGRTMAX-3	62) SIGRTMAX-2
63) SIGRTMAX-1	64) SIGRTMAX

每个信号都有一个唯一的ID,其中1-31号信号为常规信号(也叫普通信号或标准信号)是不可靠信号,产生多次只记录一次;34-64称之为可靠信号,支持排队,与驱动编程等相关。可靠与不可靠在后面的内核分析中会看到。

1.1注册信号处理函数

用户进程对信号的常见操作:注册信号处理函数发送信号。如果我们不想让某个信号执行默认操作,一种方法就是对特定的信号注册相应的信号处理函数(捕捉),设置信号处理方式的是signal() 函数。注意:以下内容是基于用户进程描述的。

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

该函数由ANSI定义,由于历史原因在不同版本的Unix和不同版本的Linux中可能有不同的行为。如果我们在 Linux 下面执行 man signal 的话,会发现 Linux 不建议我们直接用这个方法,而是改用 sigaction。

int sigaction(int signum, const struct sigaction *act,
              struct sigaction *oldact);
struct sigaction {
    void (*sa_handler)(int);
    void (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t sa_mask;/* mask last for extensibility */
    int sa_flags;
    void (*sa_restorer)(void); 
};

sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略 或 SIG_DFL表执行默认动作。
sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。当注册了某个信号捕捉函数,当这个信号处理函数执行的过程中,如果再有其他信号,哪怕相同的信号到来的时候,这个信号处理函数就会被中断。如果信号处理函数中是关于全局变量的处理,执行期间,同一信号再次到来再次执行,这样的话,同步、死锁这些都要想好。sa_mask就是用来设置这个信号处理期间需要屏蔽哪些信号。阻塞的常规信号不支持排队,产生多次只记录一次,后32个实时信号支持排队。

sa_flags:通常设置为0,表使用默认属性。

所以,sigaction()与signal()的区别在于,sigaction()可以让你更加细致地控制信号处理的行为。而 signal 函数没有给你机会设置这些。需要注意的是,signal ()不是系统调用,而是 glibc 封装的一个函数。

/*glibc-2.28\signal\signal.h*/
#  define signal __sysv_signal

/*glibc-2.28\sysdeps\posix\sysv_signal.c*/
__sighandler_t
__sysv_signal (int sig, __sighandler_t handler)
{
  struct sigaction act, oact;
.....
  act.sa_handler = handler;
  __sigemptyset (&act.sa_mask);
  act.sa_flags = SA_ONESHOT | SA_NOMASK | SA_INTERRUPT;
  act.sa_flags &= ~SA_RESTART;
  if (__sigaction (sig, &act, &oact) < 0)
    return SIG_ERR;

  return oact.sa_handler;
}

可以看到signal ()的默认设置,sa_flags 设置为SA_ONESHOT | SA_NOMASK | SA_INTERRUPT并清除了SA_RESTART,SA_ONESHOT 意思是,这里设置的信号处理函数,仅仅起作用一次。用完了一次后,就设置回默认行为。这其实并不是我们想看到的。毕竟我们一旦安装了一个信号处理函数,肯定希望它一直起作用,直到我显式地关闭它。SA_NOMASK表示信号处理函数执行过程中不阻塞任何信号。

设置了SA_INTERRUPT,清除SA_RESTART,由于信号的到来是不可预期的,有可能程序正在进行漫长的系统调用,这个时候一个信号来了,会中断这个系统调用,去执行信号处理函数,那执行完了以后呢?系统调用怎么办呢?

这时候有两种处理方法,一种就是 SA_INTERRUPT,也即系统调用被中断了,就不再重试这个系统调用了,而是直接返回一个 -EINTR 常量,告诉调用方,这个系统调用被信号中断,但是怎么处理你看着办。另外一种处理方法是 SA_RESTART。这个时候系统调用会被自动重新启动,不需要调用方自己写代码。

因而,建议使用 sigaction()函数,根据自己的需要定制参数。

系统调用小节说到过,glibc 中的文件 syscalls.list定义了库函数调用哪些系统调用,这里看sigaction。

/*glibc-2.28\sysdeps\unix\syscalls.list*/
sigaction	-	sigaction	i:ipp	__sigaction	sigaction

__sigaction 会调用 __libc_sigaction,并最终调用的系统调用是rt_sigaction。

int
__sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
{
.....
  return __libc_sigaction (sig, act, oact);
}
int
__libc_sigaction (int sig, const struct sigaction *act, struct sigaction *oact)
{
  int result;

  struct kernel_sigaction kact, koact;

  if (act)
 	{
      kact.k_sa_handler = act->sa_handler;
      memcpy (&kact.sa_mask, &act->sa_mask, sizeof (sigset_t));
      kact.sa_flags = act->sa_flags;
      SET_SA_RESTORER (&kact, act);
    }
  /* XXX The size argument hopefully will have to be changed to the
     real size of the user-level sigset_t.  */
  result = INLINE_SYSCALL_CALL (rt_sigaction, sig,
				act ? &kact : NULL,
				oact ? &koact : NULL, STUB(act) _NSIG / 8);

  if (oact && result >= 0)
    {
      oact->sa_handler = koact.k_sa_handler;
      memcpy (&oact->sa_mask, &koact.sa_mask, sizeof (sigset_t));
      oact->sa_flags = koact.sa_flags;
      RESET_SA_RESTORER (oact, &koact);
    }
  return result;
}

在看系统调用前先看一下进程内核管理结构task_struct 里面关于信号处理的字段。

struct task_struct {
    ....
    /* Signal handlers: */
	struct signal_struct		*signal;
	struct sighand_struct		*sighand;
	sigset_t			blocked;
	sigset_t			real_blocked;	 
	/* Restored if set_restore_sigmask() was used: */
	sigset_t			saved_sigmask;
	struct sigpending		pending;	 
	unsigned long			sas_ss_sp;  
	size_t				sas_ss_size;
	unsigned int			sas_ss_flags;

    ......
}

blocked定义了哪些信号被阻塞暂不处理,pending表示哪些信号尚等待处理(未决),sighand表示哪些信号设置了信号处理函数。sas_ss_xxx 这三个变量用于表示信号处理函数默认使用用户态的函数栈,或开辟新的栈专门用于信号处理。另外信号需要区分进程和线程,进入 struct signal_struct *signal 可以看到,还有一个 struct sigpending shared_pendingpending是本任务的(线程也是一个轻量级任务),shared_pending线程组共享的。

回到系统调用,linux内核中为了兼容过去、兼容32位,关于信号的实现有很多种,由不同的宏控制,这里只看系统调用 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;
	....
	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;
}

在rt_sigaction里,将用户态的struct sigaction拷贝为内核态的struct k_sigaction,然后调用do_sigaction(),

每个进程内核数据结构里,struct task_struct 中有关于信号处理的几个成员,其中sighand里面有个action,是一个k_sigaction的数组,其中记录着进程的每个信号的struct k_sigaction。do_sigaction()就是将用户设置的k_sigaction保存到这个数组中,同时将该信号原来的k_sigaction保存到oact中,拷贝到用户空间。

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;
		.......
	}

	spin_unlock_irq(&p->sighand->siglock);
	return 0;
}

到此一个信号处理函数注册完成了。

set-sa

1.2 信号的发送

信号的产生多种多样,无论如何产生,都是由内核去处理的,这里以最简单的kill系统调用来看信号的发送过程。

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);
}
kill->kill_something_info->kill_pid_info->group_send_sig_info->do_send_sig_info
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;
}

同样使用tkill或者 tgkill 发送信号给某个线程,最终都是调用了 do_send_sig_info函数。

tkill->do_tkill->do_send_specific->do_send_sig_info
tgkill->do_tkill->do_send_specific->do_send_sig_info

do_send_sig_info 会调用 send_signal,进而调用 __send_signal。

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;
	.....
	if (legacy_queue(pending, sig))
		goto ret;

	......
	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);

	}
    ......
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;
}

先看是要使用哪个pending,如果是kill发送的,就是要发送给整个进程,应该使用t->signal->shared_pending,这里面的信号是整个进程所有线程共享的;如果是tkill发送的,也就是发送给某个线程,应该使用t->pending,这是线程的task_struct独享的。

struct sigpending 结构如下。

struct sigpending {
	struct list_head list;
	sigset_t signal;
};

有两个成员变量,一个sigset_t表示收到了哪些变量,另一个是一个链表,也表示接收到的信号。如果都表示收到了信号,这两者有什么区别呢?接着往下看 __send_signal 里面的代码。接下来,调用legacy_queue。如果满足条件,那就直接退出。那 legacy_queue 里面判断的是什么条件呢?我们来看它的代码。

static inline int legacy_queue(struct sigpending *signals, int sig)
{
	return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}
#define SIGRTMIN	32

当信号小于 SIGRTMIN,也即 32 的时候,如果这个信号已经在集合里面了就直接退出了。这样会造成什么现象呢?就是信号的丢失。例如,我们发送给进程 100 个SIGUSR1(对应的信号为 10),那最终能够被我们的信号处理函数处理的信号有多少呢?这就不好说了,比如总共 5 个 SIGUSR1,分别是 A、B、C、D、E。

如果这五个信来得太密。A 来了,但是信号处理函数还没来得及处理,B、C、D、E 就都来了。根据上面的逻辑,因为 A 已经将 SIGUSR1 放在 sigset_t 集合中了,因而后面四个都要丢失。 如果是另一种情况,A 来了已经被信号处理函数处理了,内核在调用信号处理函数之前,我们会将集合中的标志位清除,这个时候 B 再来,B 还是会进入集合,还是会被处理,也就不会丢。

这样信号能够处理多少,和信号处理函数什么时候被调用,信号多大频率被发送,都有关系,而且从后面的分析,我们可以知道,信号处理函数的调用时间也是不确定的。看小于 32 的信号如此不靠谱,我们就称它为不可靠信号

对于对于大于32的信号呢?接着往下看 __send_signal 里面的代码,会调用__sigqueue_alloc分配一个struct sigqueue 对象,然后通过 list_add_tail 挂在 struct sigpending 里面的链表上。这样,连续发送100个信号过来,就会在链表上挂100项,不会丢,靠谱多了。因此,大于 32 的信号我们称为可靠信号。当然,队列的长度也是有限制的,执行ulimit -a命令可以查看信号限制 pending signals (-i) 15348。

$ ulimit  -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 15348
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 15348
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

当信号挂到了 task_struct 结构之后,最后调用 complete_signal。这里面的逻辑也很简单,就是说,既然这个进程有了一个新的信号,赶紧找一个线程处理一下(在多线程的程序中,如果不做特殊的信号阻塞处理,当发送信号给进程时,由系统选择一个线程来处理这个信号)。

static void complete_signal(int sig, struct task_struct *p, int group)
{
	struct signal_struct *signal = p->signal;
	struct task_struct *t;

	/*
	 * Now find a thread we can wake up to take the signal off the queue.
	 *
	 * If the main thread wants the signal, it gets first crack.
	 * Probably the least surprising to the average bear.
	 */
	if (wants_signal(sig, p))
		t = p;
	else {
		/*
		 * Otherwise try to find a suitable thread.
		 */
		t = signal->curr_target;
		while (!wants_signal(sig, t)) {
			t = next_thread(t);
			if (t == signal->curr_target)
				/*
				 * No thread needs to be woken.
				 * Any eligible threads will see
				 * the signal in the queue soon.
				 */
				return;
		}
		signal->curr_target = t;
	}
	......
	/*
	 * 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);
	return;
}

在找到了一个进程或者线程的 task_struct 之后,我们要调用 signal_wake_up,来企图唤醒它,signal_wake_up 会调用 signal_wake_up_state。

void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
	set_tsk_thread_flag(t, TIF_SIGPENDING);
	if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
		kick_process(t);
}

signal_wake_up_state 里面主要做了两件事情。第一,就是给这个线程设置TIF_SIGPENDING,这就说明其实信号的处理和进程的调度是采取这样一种类似的机制。

当发现一个进程应该被调度的时候,我们并不直接把它赶下来,而是设置一个标识位TIF_NEED_RESCHED,表示等待调度,然后等待系统调用结束或者中断处理结束,从内核态返回用户态的时候,调用 schedule 函数进行调度。信号也是类似的,当信号来的时候,我们并不直接处理这个信号,而是设置一个标识位 TIF_SIGPENDING,来表示已经有信号等待处理。同样等待系统调用结束,或者中断处理结束,从内核态返回用户态的时候,再进行信号的处理。

signal_wake_up_state 的第二件事情,就是试图唤醒这个进程或者线程。wake_up_state 会调用 try_to_wake_up 方法。这个函数就是将这个进程或者线程设置为 TASK_RUNNING,然后放在运行队列中,这个时候,当随着时钟不断的滴答,迟早会被调用。如果 wake_up_state 返回 0,说明进程或者线程已经是 TASK_RUNNING 状态了,如果它在另外一个 CPU 上运行,则调用 kick_process 发送一个处理器间中断,强制那个进程或者线程重新调度,重新调度完毕后,会返回用户态运行。这是一个时机会检查TIF_SIGPENDING 标识位。

1.3 信号的处理

信号已经发送到位了,什么时候真正处理它呢?

就是在从系统调用或者中断返回的时候,咱们讲调度的时候讲过,无论是从系统调用返回还是从中断返回,都会调用 exit_to_usermode_loop,重点关注_TIF_SIGPENDING 标识位。

static void exit_to_usermode_loop(struct pt_regs *regs, u32 cached_flags)
{

	while (true) {
		/* We have work to do. */
		enable_local_irqs();

		if (cached_flags & _TIF_NEED_RESCHED)
			schedule(); 
        
		.....
		/* deal with pending signal delivery */
		if (cached_flags & _TIF_SIGPENDING)
			do_signal(regs);/*有信号挂起*/

		if (cached_flags & _TIF_NOTIFY_RESUME) {
			clear_thread_flag(TIF_NOTIFY_RESUME);
			tracehook_notify_resume(regs);
		}

	......
		if (!(cached_flags & EXIT_TO_USERMODE_LOOP_FLAGS))
			break;
	}
}

如果在前一个环节中,已经设置了 _TIF_SIGPENDING,我们就调用 do_signal 进行处理。

void do_signal(struct pt_regs *regs)
{
	struct ksignal ksig;
	if (get_signal(&ksig)) {
		/* Whee! Actually deliver the signal.  */
		handle_signal(&ksig, regs);
		return;
	}

	/* Did we come from a system call? */
	if (syscall_get_nr(current, regs) >= 0) {
		/* Restart the system call - no handlers present */
		switch (syscall_get_error(current, regs)) {
		case -ERESTARTNOHAND:
		case -ERESTARTSYS:/*一个系统调用因没有数据阻塞,但然被信号唤醒,但系统调用还没完成,会返回该标志*/
		case -ERESTARTNOINTR:
			regs->ax = regs->orig_ax;
			regs->ip -= 2;
			break;

		case -ERESTART_RESTARTBLOCK:
			regs->ax = get_nr_restart_syscall(regs);
			regs->ip -= 2;
			break;
		}
	}

	/*
	 * If there's no signal to deliver, we just put the saved sigmask
	 * back.
	 */
	restore_saved_sigmask();
}

do_signal 会调用 handle_signal。按说,信号处理就是调用用户提供的信号处理函数,但是这事儿没有看起来这么简单,因为信号处理函数是在用户态的。

要回答这个问题还需要回忆系统调用的过程。这个进程当时在用户态执行到某一行 Line A,调用了一个系统调用,当 syscall 指令调用的时候,会从这个寄存器里面拿出函数地址来调用,也就是调用entry_SYSCALL_64

entry_SYSCALL_64函数先使用swaps切换到内核栈,然后保存当前用户态的寄存器到内核栈,在内核栈的最高地址端,存放的是结构 pt_regs.这样,这样,在内核 pt_regs 里面就保存了用户态执行到了 Line A的指针。

syscall-64-reg

现在我们从系统调用返回用户态了,按说应该从 pt_regs 拿出 Line A,然后接着 Line A 执行下去,但是为了响应信号,我们不能回到用户态的时候返回 Line A 了,而是应该返回信号处理函数的起始地址。

static void
handle_signal(struct ksignal *ksig, struct pt_regs *regs)
{
	bool stepping, failed;
	struct fpu *fpu = &current->thread.fpu;
....
	/* Are we from a system call? */
	if (syscall_get_nr(current, regs) >= 0) {
		/* If so, check system call restarting.. */
		switch (syscall_get_error(current, regs)) {
		case -ERESTART_RESTARTBLOCK:
		case -ERESTARTNOHAND:
			regs->ax = -EINTR;
			break;
	/*当发现出现错误 ERESTARTSYS 的时候,我们就知道这是从一个没有调用完的系统调用返回的,
	设置系统调用错误码 EINTR。*/
		case -ERESTARTSYS:
			if (!(ksig->ka.sa.sa_flags & SA_RESTART)) {
				regs->ax = -EINTR;/*设置系统调用错误码 EINTR。*/
				break;
			}
		/* fall through */
		case -ERESTARTNOINTR:
			regs->ax = regs->orig_ax;
			regs->ip -= 2;
			break;
		}
	}

	......
	failed = (setup_rt_frame(ksig, regs) < 0);
	......
	signal_setup_done(failed, ksig, stepping);
}

这个时候,就需要干预和自己来定制 pt_regs 了。这个时候,要看,是否从系统调用中返回。如果是从系统调用返回的话,还要区分是从系统调用中正常返回,还是在一个非运行状态的系统调用中,因为会被信号中断而返回。

这里解析一个最复杂的场景。还记得咱们解析进程调度的时候,举的一个例子,就是从一个 tap 网卡中读取数据。当时主要关注 schedule 那一行,也即如果当发现没有数据的时候,就调用 schedule,自己进入等待状态,然后将 CPU 让给其他进程。具体的代码如下:

static ssize_t tap_do_read(struct tap_queue *q,
struct iov_iter *to,
int noblock, struct sk_buff *skb)
{
......
	while (1) {
		if (!noblock)
				prepare_to_wait(sk_sleep(&q->sk), &wait,
					TASK_INTERRUPTIBLE);
		/* Read frames from the queue */
		skb = skb_array_consume(&q->skb_array);
		if (skb)
			break;
		if (noblock) {
			ret = -EAGAIN;
			break;
		}
		if (signal_pending(current)) {
			ret = -ERESTARTSYS;
			break;
		}
		/* Nothing to read, let's sleep */
		schedule();
		}
......
}

这里我们关注和信号相关的部分。这其实是一个信号中断系统调用的典型逻辑。

首先,我们把当前进程或者线程的状态设置为 TASK_INTERRUPTIBLE,这样才能是使这个系统调用可以被中断。

其次,可以被中断的系统调用往往是比较慢的调用,并且会因为数据不就绪而通过 schedule让出 CPU 进入等待状态。在发送信号的时候,我们除了设置这个进程和线程的_TIF_SIGPENDING 标识位之外,还试图唤醒这个进程或者线程,也就是将它从等待状态中设置为 TASK_RUNNING。

当这个进程或者线程再次运行的时候,我们根据进程调度第一定律,从 schedule 函数中返回,然后再次进入 while 循环。由于这个进程或者线程是由信号唤醒的,而不是因为数据来了而唤醒的,因而是读不到数据的,但是在 signal_pending 函数中,我们检测到了_TIF_SIGPENDING 标识位,这说明系统调用没有真的做完,于是返回一个错误ERESTARTSYS,然后带着这个错误从系统调用返回。

然后,我们到了 exit_to_usermode_loop->do_signal->handle_signal。在这里面,当发现出现错误ERESTARTSYS 的时候,我们就知道这是从一个没有调用完的系统调用返回的,设置系统调用错误码 EINTR。

接下来,我们就开始折腾 pt_regs 了,主要通过调用 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 (ksig->ka.sa.sa_flags & SA_SIGINFO) {
		if (copy_siginfo_to_user(&frame->info, &ksig->info))
			return -EFAULT;
	}

	put_user_try {
		/* Create the ucontext.  */
		put_user_ex(frame_uc_flags(regs), &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);
		} ....
	} 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));
......

	/* 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;

	regs->cs = __USER_CS;

....
	return 0;
}

frame 的类型是 rt_sigframe。frame 的意思是帧。

  1. 在 get_sigframe 中会得到 pt_regs 的 sp 变量,也就是原来这个程序在用户态的栈顶指针,然后 get_sigframe 中,我们会将 sp 减去 sizeof(struct rt_sigframe),也就是把这个栈帧塞到了用户栈里面。

sig-fram-1

  1. 将sa.sa_restorer的地址保存到frame的pretcode中。这个操作比较重要。

  2. 原来的 pt_regs 不能丢,调用setup_sigcontext将原来的 pt_regs 保存在了 frame 中的 uc_mcontext 里面。

  3. 信号处理期间要屏蔽的信号set也保存到frame->uc.uc_sigmask。

  4. 在 __setup_rt_frame 把 regs->sp 设置成等于 frame。这就相当于强行在程序原来的用户态的栈里面插入了一个栈帧。

  5. 并在最后将 regs->ip 设置为用户定义的信号处理函数 sa_handler。

    sig-fram-2

这意味着,本来返回用户态应该接着原来系统调用的代码执行的,现在不了,要执行 sa_handler 了。那执行完了以后呢?按照函数栈的规则,弹出上一个栈帧来,也就是弹出了 frame。按照函数栈的规则。函数栈里面包含了函数执行完跳回去的地址。

那如果我们假设 sa_handler 成功返回了,怎么回到程序原来在用户态运行的地方呢?那就是在 __setup_rt_frame 中,通过 put_user_ex,将 sa_restorer 放到 frame->pretcode 里面。当 sa_handler 执行完之后,会执行iret指令,弹出返回地址,也就到 sa_restorer 执行。sa_restorer 这是什么呢?

sig-fram-3

咱们在 sigaction 介绍的时候就没有介绍它,在 Glibc 的 __libc_sigaction 函数中也没有注意到,它被赋值成了 restore_rt。这其实就是 sa_handler 执行完毕之后,马上要执行的函数。从名字我们就能感觉到,它将恢复原来程序运行的地方。

在 Glibc 中,我们可以找到它的定义,它竟然调用了一个系统调用,系统调用号为__NR_rt_sigreturn。

RESTORE (restore_rt, __NR_rt_sigreturn)
    
#define RESTORE(name, syscall) RESTORE2 (name, syscall)
# define RESTORE2(name, syscall) \
asm									\
  (									\
   /* `nop' for debuggers assuming `call' should not disalign the code.  */ \
   "	nop\n"								\
   ".align 16\n"							\
   ".LSTART_" #name ":\n"						\
   "	.type __" #name ",@function\n"					\
   "__" #name ":\n"							\
   "	movq $" #syscall ", %rax\n"					\
   "	syscall\n"		

sig-fram-4

在内核里面找到 __NR_rt_sigreturn 对应的系统调用。

asmlinkage long sys_rt_sigreturn(void)
{
	struct pt_regs *regs = current_pt_regs();
	struct rt_sigframe __user *frame;
	sigset_t set;
	unsigned long uc_flags;

	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;
	if (__get_user(uc_flags, &frame->uc.uc_flags))
		goto badframe;

	set_current_blocked(&set);

	if (restore_sigcontext(regs, &frame->uc.uc_mcontext, uc_flags))
		goto badframe;

	if (restore_altstack(&frame->uc.uc_stack))
		goto badframe;

	return regs->ax;
.....
}

在这里面,把上次填充的那个 rt_sigframe 拿出来,然后 restore_sigcontext 将 pt_regs恢复成为原来用户态的样子。从这个系统调用返回的时候,应用A还误以为从上次的系统调用返回的呢。

至此,整个信号处理过程才全部结束。

信号的发送与处理是一个复杂的过程,这里来总结一下。

  1. 假设我们有一个进程 A,main 函数里面调用系统调用进入内核。
  2. 按照系统调用的原理,会将用户态栈的信息保存在 pt_regs 里面,也即记住原来用户态是运行到了 line A 的地方。
  3. 在内核中执行系统调用读取数据。
  4. 当发现没有什么数据可读取的时候,只好进入睡眠状态,并且调用 schedule 让出 CPU,这是进程调度第一定律。
  5. 将进程状态设置为 TASK_INTERRUPTIBLE,可中断的睡眠状态,也即如果有信号来的话,是可以唤醒它的。
  6. 其他的进程或者 shell 发送一个信号,有四个函数可以调用 kill,tkill,tgkill,rt_sigqueueinfo
  7. 四个发送信号的函数,在内核中最终都是调用 do_send_sig_info
  8. do_send_sig_info 调用 send_signal 给进程 A 发送一个信号,其实就是找到进程 A 的task_struct,或者加入信号集合,为不可靠信号,或者加入信号链表,为可靠信号
  9. do_send_sig_info 调用 signal_wake_up 唤醒进程 A。
  10. 进程 A 重新进入运行状态 TASK_RUNNING,根据进程调度第一定律,一定会接着schedule 运行。
  11. 进程 A 被唤醒后,检查是否有信号到来,如果没有,重新循环到一开始,尝试再次读取数据,如果还是没有数据,再次进入 TASK_INTERRUPTIBLE,即可中断的睡眠状态。
  12. 当发现有信号到来的时候,就返回当前正在执行的系统调用,并返回一个错误表示系统调用被中断了。
  13. 系统调用返回的时候,会调用 exit_to_usermode_loop,这是一个处理信号的时机
  14. 调用 do_signal 开始处理信号
  15. 根据信号,得到信号处理函数 sa_handler,然后修改 pt_regs 中的用户态栈的信息,让pt_regs 指向 sa_handler。同时修改用户态的栈,插入一个栈帧 sa_restorer,里面保存了原来的指向 line A 的 pt_regs,并且设置让 sa_handler 运行完毕后,跳到 sa_restorer 运行。
  16. 返回用户态,由于 pt_regs 已经设置为 sa_handler,则返回用户态执行 sa_handler。
  17. sa_handler 执行完毕后,信号处理函数就执行完了,接着根据第 15 步对于用户态栈帧的修改,会跳到 sa_restorer 运行。
  18. sa_restorer 会调用系统调用 rt_sigreturn 再次进入内核。
  19. 在内核中,rt_sigreturn 恢复原来的 pt_regs,重新指向 line A。
  20. 从 rt_sigreturn 返回用户态,还是调用 exit_to_usermode_loop。
  21. 这次因为 pt_regs 已经指向 line A 了,于是就到了进程 A 中,接着系统调用之后运行,当然这个系统调用返回的是它被中断了,没有执行完的错误。

2 linux 多线程信号

上节说了linux信号指的是linux进程或者整个进程组,其实线程和进程在linux内核中都是task,发送信号可以是kill或tkill,对于在多线程的程序中,如果不做特殊的信号阻塞处理,当发送信号给进程时,从上面内核代码中看到,由系统选择一个线程来处理这个信号。

每个线程由自己的信号屏蔽字,但是信号的处理是进程中所有线程贡献的。这意味着单个线程可以阻止某些信号,但是当某个线程修改了给定信号的相关处理行为后,该进程下的所有线程都必须共享这个处理方式的改变。换句话说:signal或者sigaction是进程里面的概念,他们是针对整个进程进行控制的,是所有线程共享的,某个线程调用了signal或者sigaction更改了给定信号的处理方式,那么进程信号处理方式就改变。

如果一个信号与硬件故障相关,那么该信号一般会发给引起该事件的线程中去。

线程的信号操作接口如下。线程使用pthread_sigmask来阻止信号发送。

#include<signal.h>
int pthread_sigmask(int how, const sigset_t *restrict set,
                   sigset_t *restrict oset);

set参数包含线程用于修改信号屏蔽字的信号集,oset如果不为NULL,并把oset设置为siget_t结构的地址,来获取当前的信号屏蔽字。how:SIG_BLOCK将set加入到线程信号屏蔽字中,SIG_SETMASK用信号集set替换线程当前的信号屏蔽字;SUG_UNBLOCK,从当前线程信号屏蔽字中移除set中的信号。

线程可通过sigwait等待一个或多个信号的出现。

#include<signal.h>
int siwait(const sigset_t *restrict set, int *restrich signop)
int sigwaitinfo(const sigset_t *set, siginfo_t *si);
int sigtimedwait (const sigset_t *set, siginfo_t *si,
				const struct timespec *timeout);

set参数指定线程等待的信号集,signop指向整数将包含发送信号的数量。sigwait从set中选择一个未决信号(pending),从线程的未决信号集中移除该信号,并在sig中返回该信号值。如果set中的所有信号都不是pending状态,则sigwait会阻塞调用它的线程,直到set中的信号变为pending。

除了返回信息方面,sigwaitinfo的行为基本上与sigwait类似。sigwait在sig中返回触发的信号值;而sigwaitinfo的返回值就是触发的信号值,并且如果info不为NULL,则sigwaitinfo返回时,还会在siginfo_t *info中返回更多该信号的信息.

线程可通过pthread_kill把信号发送给线程。

#include<signal.h>
int pthread_kill(pthread_t thread,int signo);

可以传一个0值得signo来检查一个线程是否存在。如果信号的默认处理动作是终止该进程,那么把该信号传递给某个线程任然会杀死整个进程。

参考:
英特尔® 64 位和 IA-32 架构软件开发人员手册第 3 卷 :系统编程指南
极客时间专栏-趣谈Linux操作系统
《linux内核源代码情景分析》

posted @ 2020-07-17 20:28  木多  阅读(243)  评论(0编辑  收藏