Skip to content

Latest commit

 

History

History
870 lines (699 loc) · 37.6 KB

File metadata and controls

870 lines (699 loc) · 37.6 KB

三、信号管理

信号提供了一个基本的基础设施,在该基础设施中,任何进程都可以被异步通知系统事件。 它们还可以用作进程之间的通信机制。 了解内核如何提供和管理整个信号处理机制的平稳吞吐量,可以让我们对内核有更多的了解。 在本章中,我们将详细介绍我们对信号的理解,从进程如何引导它们到内核如何巧妙地管理例程以确保信号事件正常运行。 我们将非常详细地讨论以下主题:

  • 信号及其类型概述
  • 流程级信号管理调用
  • 过程描述符中的信号数据结构
  • 内核的信号生成和传递机制

信号

信号是传递给进程或进程组的短消息。 内核使用信号通知进程系统事件的发生;信号还用于进程之间的通信。 Linux 将信号分为两组,即通用 POSIX(经典 Unix 信号)和实时信号。 每组由 32 个不同的信号组成,由唯一 ID 标识:

#define _NSIG 64
#define _NSIG_BPW __BITS_PER_LONG
#define _NSIG_WORDS (_NSIG / _NSIG_BPW)

#define SIGHUP 1
#define SIGINT 2
#define SIGQUIT 3
#define SIGILL 4
#define SIGTRAP 5
#define SIGABRT 6
#define SIGIOT 6
#define SIGBUS 7
#define SIGFPE 8
#define SIGKILL 9
#define SIGUSR1 10
#define SIGSEGV 11
#define SIGUSR2 12
#define SIGPIPE 13
#define SIGALRM 14
#define SIGTERM 15
#define SIGSTKFLT 16
#define SIGCHLD 17
#define SIGCONT 18
#define SIGSTOP 19
#define SIGTSTP 20
#define SIGTTIN 21
#define SIGTTOU 22
#define SIGURG 23
#define SIGXCPU 24
#define SIGXFSZ 25
#define SIGVTALRM 26
#define SIGPROF 27
#define SIGWINCH 28
#define SIGIO 29
#define SIGPOLL SIGIO
/*
#define SIGLOST 29
*/
#define SIGPWR 30
#define SIGSYS 31
#define SIGUNUSED 31

/* These should not be considered constants from userland. */
#define SIGRTMIN 32
#ifndef SIGRTMAX
#define SIGRTMAX _NSIG
#endif

通用类别中的信号绑定到特定的系统事件,并通过宏进行适当的命名。 实时类别中的进程不受特定事件的限制,应用可以自由参与进程通信;内核使用通用名称SIGRTMINSIGRTMAX来引用它们。

在生成信号时,内核将信号事件传递给目标进程,目标进程反过来可以根据配置的操作(称为信号处置)响应信号。

以下是进程可以设置为其信号处置的操作列表。 流程可以在某个时间点将任何一个动作设置为其信号处理,但它可以在这些动作之间进行任意次数的切换,而不受任何限制。

  • 内核处理程序:内核为每个信号实现一个默认处理程序。 进程可以通过其任务结构的信号处理程序表使用这些处理程序。 在接收到信号时,进程可以请求执行适当的信号处理程序。 这是默认配置。

  • **流程定义的处理程序:**允许流程实现自己的信号处理程序,并将其设置为响应信号事件而执行。 这可以通过适当的系统调用接口实现,该接口允许进程将其处理程序例程与信号绑定。 在信号出现时,流程处理程序将被异步调用。

  • **忽略:**进程也可以忽略信号的发生,但它需要通过调用适当的系统调用来声明其忽略意图。

内核定义的默认处理程序例程可以执行以下任何操作:

  • 忽略:不发生任何事情。
  • 终止:终止进程,即该组中的所有线程(类似于exit_group)。 组长(仅)向其父级报告WIFSIGNALED状态。
  • 核心转储:编写一个核心转储文件,描述使用相同mm的所有线程,然后杀死所有这些线程
  • 停止:停止该组中的所有线程,即TASK_STOPPED状态。

下表列出了默认处理程序执行的操作:

 +--------------------+------------------+
 * | POSIX signal     | default action |
 * +------------------+------------------+
 * | SIGHUP           | terminate 
 * | SIGINT           | terminate 
 * | SIGQUIT          | coredump 
 * | SIGILL           | coredump 
 * | SIGTRAP          | coredump 
 * | SIGABRT/SIGIOT   | coredump 
 * | SIGBUS           | coredump 
 * | SIGFPE           | coredump 
 * | SIGKILL          | terminate
 * | SIGUSR1          | terminate 
 * | SIGSEGV          | coredump 
 * | SIGUSR2          | terminate
 * | SIGPIPE          | terminate 
 * | SIGALRM          | terminate 
 * | SIGTERM          | terminate 
 * | SIGCHLD          | ignore 
 * | SIGCONT          | ignore 
 * | SIGSTOP          | stop
 * | SIGTSTP          | stop
 * | SIGTTIN          | stop
 * | SIGTTOU          | stop
 * | SIGURG           | ignore 
 * | SIGXCPU          | coredump 
 * | SIGXFSZ          | coredump 
 * | SIGVTALRM        | terminate 
 * | SIGPROF          | terminate 
 * | SIGPOLL/SIGIO    | terminate 
 * | SIGSYS/SIGUNUSED | coredump 
 * | SIGSTKFLT        | terminate 
 * | SIGWINCH         | ignore 
 * | SIGPWR           | terminate 
 * | SIGRTMIN-SIGRTMAX| terminate 
 * +------------------+------------------+
 * | non-POSIX signal | default action |
 * +------------------+------------------+
 * | SIGEMT           | coredump |
 * +--------------------+------------------+

信号管理 API

应用提供了用于管理信号的各种 API;我们将看看其中几个重要的 API:

  1. Sigaction():用户模式进程使用 POSIX APIsigaction()检查或更改信号的处置。 此 API 提供了各种属性标志,可以进一步定义信号的行为:
 #include <signal.h>
 int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

 The sigaction structure is defined as something like:

 struct sigaction {
 void (*sa_handler)(int);
 void (*sa_sigaction)(int, siginfo_t *, void *);
 sigset_t sa_mask;
 int sa_flags;
 void (*sa_restorer)(void);
 };
  • int signum是识别出的signal的标识符号。 sigaction()检查并设置与该信号相关联的动作。
  • 可以为const struct sigaction *act分配struct sigaction实例的地址。 此结构中指定的操作将成为绑定到信号的新操作。 当act指针保持未初始化(NULL)时,当前处理保持不变。
  • struct sigaction *oldact是一个输出参数,需要使用未初始化的sigaction实例的地址进行初始化;sigaction()通过此参数返回当前与信号关联的操作。
  • 以下是各种flag选项:
  • SA_NOCLDSTOP:此标志仅在绑定SIGCHLD的处理程序时才相关。 它用于禁用子进程上的停止(SIGSTP)和恢复(SIGCONT)事件的SIGCHLD通知。
  • SA_NOCLDWAIT:此标志仅在绑定SIGCHLD的处理程序或将其处置设置为SIG_DFL时才相关。 设置此标志会导致子进程在终止时立即销毁,而不是使其处于僵尸状态。
  • SA_NODEFER:设置此标志会导致即使相应的处理程序正在执行,也会传递生成的信号。
  • SA_ONSTACK:此标志仅在绑定信号处理程序时才相关。 设置此标志会导致信号处理程序使用备用堆栈;备用堆栈必须由调用者进程通过sigaltstack()API 设置。 在没有备用堆栈的情况下,将在当前堆栈上调用处理程序。
  • SA_RESETHAND:当此标志与sigaction()一起应用时,它会使信号处理程序变为一次触发,也就是说,指定信号的操作将重置为SIG_DFL,以备以后出现此信号时使用。
  • SA_RESTART:此标志允许重新进入系统调用操作,由当前信号处理程序中断。
  • SA_SIGINFO:此标志用于向系统指示信号处理程序已分配--sigaction结构的sa_sigaction指针,而不是sa_handler。 分配给sa_sigaction的处理程序接收两个附加参数:
      void handler_fn(int signo, siginfo_t *info, void *context);

第一个参数是signum,处理程序绑定到该参数。 第二个参数是一个 outparam,它是指向类型为siginfo_t的对象的指针,该对象提供有关信号源的附加信息。 以下是siginfo_t的完整定义:

 siginfo_t {
 int si_signo; /* Signal number */
 int si_errno; /* An errno value */
 int si_code; /* Signal code */
 int si_trapno; /* Trap number that caused hardware-generated signal (unused on most           architectures) */
 pid_t si_pid; /* Sending process ID */
 uid_t si_uid; /* Real user ID of sending process */
 int si_status; /* Exit value or signal */
 clock_t si_utime; /* User time consumed */
 clock_t si_stime; /* System time consumed */
 sigval_t si_value; /* Signal value */
 int si_int; /* POSIX.1b signal */
 void *si_ptr; /* POSIX.1b signal */
 int si_overrun; /* Timer overrun count; POSIX.1b timers */
 int si_timerid; /* Timer ID; POSIX.1b timers */
 void *si_addr; /* Memory location which caused fault */
 long si_band; /* Band event (was int in glibc 2.3.2 and earlier) */
 int si_fd; /* File descriptor */
 short si_addr_lsb; /* Least significant bit of address (since Linux 2.6.32) */
 void *si_call_addr; /* Address of system call instruction (since Linux 3.5) */
 int si_syscall; /* Number of attempted system call (since Linux 3.5) */
 unsigned int si_arch; /* Architecture of attempted system call (since Linux 3.5) */
 }
  1. Sigprocmask():除了更改信号处理(指定接收到信号时要执行的操作)外,还允许应用阻塞或解除阻塞信号传输。 应用可能需要在执行关键代码块时执行此类操作,而不需要异步信号处理程序抢占。 例如,网络通信应用可能不想在进入发起与其对等设备的连接的代码块时处理信号:
    • sigprocmask()是一个 POSIX API,用于检查、阻止和取消阻止信号。
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldset); 

任何阻塞信号的出现都会在每个进程的挂起信号列表中排队。 挂起队列被设计为在对实时信号的每次出现进行排队时,保持阻塞的通用信号的一次出现。 用户模式进程可以使用sigpending()rt_sigpending()API 探测挂起信号。 这些例程将挂起信号列表返回到sigset_t指针指向的实例中。

    int sigpending(sigset_t *set);

The operations are applicable for all signals except SIGKILL and SIGSTOP; in other words, processes are not allowed to alter the default disposition or block SIGSTOP and SIGKILL signals.

从程序中发出信号

kill()sigqueue()是 POSIXAPI,进程可以通过它们为另一个进程或进程组发出信号。 这些 API 便于将信号用作进程通信机制:

 int kill(pid_t pid, int sig);
 int sigqueue(pid_t pid, int sig, const union sigval value);

 union sigval {
 int sival_int;
 void *sival_ptr;
 };

虽然这两个 API 都提供了参数来指定要引发的接收器PIDsignum,但sigqueue()提供了一个附加参数(联合信号),通过该参数可以将数据与信号一起发送到接收器进程。 目标进程可以通过struct siginfo_t(si_value)实例访问数据。 Linux 使用本机 API 扩展了这些函数,这些 API 可以将信号排队到线程组,甚至可以排队到线程组中的轻量级进程(LWP):

/* queue signal to specific thread in a thread group */
int tgkill(int tgid, int tid, int sig);

/* queue signal and data to a thread group */
int rt_sigqueueinfo(pid_t tgid, int sig, siginfo_t *uinfo);

/* queue signal and data to specific thread in a thread group */
int rt_tgsigqueueinfo(pid_t tgid, pid_t tid, int sig, siginfo_t *uinfo);

等待排队信号

在将信号应用于进程通信时,进程可能更适合挂起自身,直到出现特定信号,然后在来自另一个进程的信号到达时恢复执行。 POSIX 调用sigsuspend()sigwaitinfo()sigtimedwait()提供此功能:

int sigsuspend(const sigset_t *mask);
int sigwaitinfo(const sigset_t *set, siginfo_t *info);
int sigtimedwait(const sigset_t *set, siginfo_t *info, const struct timespec *timeout);

虽然所有这些 API 都允许进程等待指定信号的出现,但sigwaitinfo()通过通过info指针返回的siginfo_t实例提供有关该信号的附加数据。 sigtimedwait()通过提供允许操作超时的附加参数来扩展功能,使其成为有界等待调用。 Linux 内核提供了另一个 API,它允许通过名为signalfd()的特殊文件描述符向进程通知信号的发生:

 #include <sys/signalfd.h>
 int signalfd(int fd, const sigset_t *mask, int flags);

如果成功,signalfd()将返回一个文件描述符,进程需要在该文件描述符上调用read(),该描述符会一直阻塞,直到出现掩码中指定的任何信号。

信号数据结构

内核维护每进程信号数据结构以跟踪信号处置、**阻塞信号挂起信号队列。 流程任务结构包含对以下数据结构的适当引用:

struct task_struct {

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

 unsigned long sas_ss_sp;
 size_t sas_ss_size;
 unsigned sas_ss_flags;
  ....
  ....
  ....
  ....

};

信号描述符

回想一下我们在第一章前面的讨论,Linux 通过轻量级进程支持多线程应用。 线程化应用的所有 LWP 都是进程组的一部分,并共享信号处理程序;每个 LWP(线程)维护自己的挂起和阻塞的信号队列。

任务结构的信号指针引用作为信号描述符的类型signal_struct的实例。 此结构由线程组的所有 LWP 共享,并维护共享挂起信号队列(用于排队到线程组的信号)等元素,这对进程组中的所有线程都是通用的。

下图表示维护共享挂起信号所涉及的数据结构:

以下是signal_struct的几个重要字段:

struct signal_struct {
 atomic_t sigcnt;
 atomic_t live;
 int nr_threads;
 struct list_head thread_head;

 wait_queue_head_t wait_chldexit; /* for wait4() */

 /* current thread group signal load-balancing target: */
 struct task_struct *curr_target;

 /* shared signal handling: */
 struct sigpending shared_pending; 
 /* thread group exit support */
 int group_exit_code;
 /* overloaded:
 * - notify group_exit_task when ->count is equal to notify_count
 * - everyone except group_exit_task is stopped during signal delivery
 * of fatal signals, group_exit_task processes the signal.
 */
 int notify_count;
 struct task_struct *group_exit_task;

 /* thread group stop support, overloads group_exit_code too */
 int group_stop_count;
 unsigned int flags; /* see SIGNAL_* flags below */

阻塞和挂起的队列

任务结构中的blockedreal_blocked实例是阻塞信号的位掩码;这些队列是每个进程的。 因此,线程组中的每个 LWP 都有其自己的阻塞信号掩码。 任务结构的pending实例用于对专用挂起信号进行排队;所有排队到正常进程和线程组中的特定 LWP 的信号都将排队到该列表中:

struct sigpending {
 struct list_head list; // head to double linked list of struct sigqueue
 sigset_t signal; // bit mask of pending signals
};

下图表示维护私有挂起信号所涉及的数据结构:

信号处理程序描述符

任务结构的sighand指针引用 structsighand_struct的实例,它是由线程组中的所有进程共享的信号处理程序描述符。 此结构也由使用clone()CLONE_SIGHAND标志创建的所有进程共享。 此结构包含一个由k_sigaction个实例组成的数组,每个实例包装一个描述每个信号当前处理情况的sigaction实例:

struct k_sigaction {
 struct sigaction sa;
#ifdef __ARCH_HAS_KA_RESTORER 
 __sigrestore_t ka_restorer;
#endif
};

struct sighand_struct {
 atomic_t count;
 struct k_sigaction action[_NSIG];
 spinlock_t siglock;
 wait_queue_head_t signalfd_wqh;
};

下图表示信号处理程序描述符:

信号产生和传输

在接收器进程的任务结构中的待决信号列表中,当信号的出现被排队时,称为生成。 该信号是根据来自用户模式进程、内核或任何内核服务的请求(在进程或组上)生成的。 当一个或多个接收器进程知道信号的发生并被强制执行适当的响应处理程序时,信号被认为是传递的;换句话说,信号传递等于相应处理程序的初始化。 理想情况下,假设生成的每个信号都是即时传递的;然而,在信号生成和最终传递之间存在延迟的可能性。 为了方便可能的延迟传递,内核为信号生成和传递提供了单独的功能。

信号生成呼叫

内核提供了两组单独的信号生成函数:一组用于在单个进程上生成信号,另一组用于进程线程组。

  • 以下是在进程上生成信号的重要函数列表: send_sig():在进程上生成指定的信号;此函数被内核服务广泛使用
  • end_sig_info():使用其他siginfo_t实例扩展send_sig()
  • force_sig():用于生成不可忽略或阻挡的优先级不可屏蔽信号
  • force_sig_info():使用其他siginfo_t实例扩展force_sig()

所有这些例程最终都会调用核心内核函数send_signal(),该函数被编程为生成指定的信号。

以下是在进程组上生成信号的重要函数列表:

  • kill_pgrp():在进程组中的所有线程组上生成指定信号
  • kill_pid():向由 PID 标识的线程组生成指定信号
  • kill_pid_info():使用其他*siginfo_t*实例扩展kill_pid()

所有这些例程都调用函数group_send_sig_info(),该函数最终使用适当的参数调用send_signal()send_signal()函数是核心信号生成函数;它使用适当的参数调用__send_signal()例程:

 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()执行的重要步骤:

  1. 检查来自info参数的信号源。 如果信号生成是由内核为不可屏蔽的SIGKILLSIGSTOP发起的,它会立即设置签名位掩码的适当位,设置TIF_SIGPENDING标志,并通过唤醒目标线程来启动传递过程:
 /*
 * fast-pathed signals for kernel-internal things like SIGSTOP
 * or SIGKILL.
 */
 if (info == SEND_SIG_FORCED)
 goto out_set;
....
....
....
out_set:
 signalfd_notify(t, sig);
 sigaddset(&pending->signal, sig);
 complete_signal(sig, t, group);
  1. 调用__sigqeueue_alloc()函数,该函数检查接收器进程的挂起信号数量是否小于资源限制。 如果为真,则递增挂起信号计数器并返回struct sigqueue实例的地址:
 q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
 override_rlimit);
  1. sigqueue实例排队到挂起列表中,并将信号信息填写到siginfo_t中:
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;
 }
  1. 在挂起信号的位掩码中设置适当的信号位,并通过调用complete_signal(),来尝试信号传输,进而设置TIF_SIGPENDING标志:
 sigaddset(&pending->signal, sig);
 complete_signal(sig, t, group);

信号传递

在通过前面提到的任何信号生成调用更新接收器的任务结构中的适当条目来生成信号之后,内核进入传送模式。 如果接收器进程在 CPU 上并且没有阻塞指定的信号,则信号会立即传送。 即使接收器不在 CPU 上,也会通过唤醒进程来传送优先级信号SIGSTOPSIGKILL;然而,对于其余信号,传送将推迟到进程准备好接收信号。 为了便于延迟传递,内核在允许进程恢复用户模式执行之前,在从中断系统调用返回时检查进程的非阻塞挂起信号。 当进程调度器(在从中断和异常返回时调用)发现设置了TIF_SIGPENDING标志时,它会调用内核函数do_signal()在恢复进程的用户模式上下文之前启动挂起信号的传递。 进入内核模式后,进程的用户模式寄存器状态存储在进程内核堆栈中,结构称为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 rbp;
 unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
 unsigned long r11;
 unsigned long r10;
 unsigned long r9;
 unsigned long r8;
 unsigned long rax;
 unsigned long rcx;
 unsigned long rdx;
 unsigned long rsi;
 unsigned long rdi;
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
 unsigned long orig_rax;
/* Return frame for iretq */
 unsigned long rip;
 unsigned long cs;
 unsigned long eflags;
 unsigned long rsp;
 unsigned long ss;
/* top of stack page */
};

使用内核堆栈中的地址pt_regs调用do_signal()例程。 虽然do_signal()旨在传递非阻塞的挂起信号,但其实现是特定于体系结构的。

以下是do_signal()的 x86 版本:

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()使用类型为struct ksignal的实例的地址调用get_signal()函数(我们将简要考虑此例程的重要步骤,跳过其他细节)。 此函数包含一个循环,该循环调用dequeue_signal(),直到来自私有和共享挂起列表的所有非阻塞挂起信号都出列。 它从查找专用挂起信号队列开始,从编号最低的信号开始,然后进入共享队列中的挂起信号,然后更新数据结构以指示信号不再挂起并返回其编号:

 signr = dequeue_signal(current, &current->blocked, &ksig->info);

对于dequeue_signal())返回的每个挂起信号,get_signal()通过类型为struct ksigaction *ka:的指针检索当前信号处理

ka = &sighand->action[signr-1]; 

如果信号处置设置为SIG_IGN,它将静默忽略当前信号,并继续迭代以检索另一个挂起信号:

if (ka->sa.sa_handler == SIG_IGN) /* Do nothing. */
 continue;

如果 disposition 不等于SIG_DFL,则它检索sigaction的地址,并将其初始化为参数ksig->ka,以便进一步执行用户模式处理程序。 它进一步检查用户的sigaction中的SA_ONESHOT (SA_RESETHAND)标志,如果设置,则将信号处理重置为SIG_DFL,中断循环,并返回给调用者。 do_signal()现在调用handle_signal()例程来执行用户模式处理程序(我们将在下一节详细讨论这一点)。

  if (ka->sa.sa_handler != SIG_DFL) {
 /* Run the handler. */
 ksig->ka = *ka;

 if (ka->sa.sa_flags & SA_ONESHOT)
 ka->sa.sa_handler = SIG_DFL;

 break; /* will return non-zero "signr" value */
 }

如果 Disposition 设置为SIG_DFL,它将调用一组宏来检查内核处理程序的默认操作。 可能的默认操作包括:

  • 术语:默认操作是终止进程
  • IGN:默认操作是忽略信号
  • 核心:默认操作是终止进程并转储核心
  • 停止:默认操作是停止进程
  • cont:默认操作是继续进程(如果该进程当前已停止

下面是get_signal()中的一段代码片段,它根据设置的处置启动默认操作:

/*
 * Now we are doing the default action for this signal.
 */
 if (sig_kernel_ignore(signr)) /* Default is nothing. */
 continue;

 /*
 * Global init gets no signals it doesn't want.
 * Container-init gets no signals it doesn't want from same
 * container.
 *
 * Note that if global/container-init sees a sig_kernel_only()
 * signal here, the signal must have been generated internally
 * or must have come from an ancestor namespace. In either
 * case, the signal cannot be dropped.
 */
 if (unlikely(signal->flags & SIGNAL_UNKILLABLE) &&
 !sig_kernel_only(signr))
 continue;

 if (sig_kernel_stop(signr)) {
 /*
 * The default action is to stop all threads in
 * the thread group. The job control signals
 * do nothing in an orphaned pgrp, but SIGSTOP
 * always works. Note that siglock needs to be
 * dropped during the call to is_orphaned_pgrp()
 * because of lock ordering with tasklist_lock.
 * This allows an intervening SIGCONT to be posted.
 * We need to check for that and bail out if necessary.
 */
 if (signr != SIGSTOP) {
 spin_unlock_irq(&sighand->siglock);

 /* signals can be posted during this window */

 if (is_current_pgrp_orphaned())
 goto relock;

 spin_lock_irq(&sighand->siglock);
 }

 if (likely(do_signal_stop(ksig->info.si_signo))) {
 /* It released the siglock. */
 goto relock;
 }

 /*
 * We didn't actually stop, due to a race
 * with SIGCONT or something like that.
 */
 continue;
 }

 spin_unlock_irq(&sighand->siglock);

 /*
 * Anything else is fatal, maybe with a core dump.
 */
 current->flags |= PF_SIGNALED;

 if (sig_kernel_coredump(signr)) {
 if (print_fatal_signals)
 print_fatal_signal(ksig->info.si_signo);
 proc_coredump_connector(current);
 /*
 * If it was able to dump core, this kills all
 * other threads in the group and synchronizes with
 * their demise. If we lost the race with another
 * thread getting here, it set group_exit_code
 * first and our do_group_exit call below will use
 * that value and ignore the one we pass it.
 */
 do_coredump(&ksig->info);
 }

 /*
 * Death signals, no core dump.
 */
 do_group_exit(ksig->info.si_signo);
 /* NOTREACHED */
 }

首先,宏sig_kernel_ignore检查默认操作 Ignore。 如果为真,则继续循环迭代以查找下一个挂起信号。 第二个宏sig_kernel_stop检查默认的操作 STOP;如果为真,它将调用do_signal_stop()例程,该例程将进程组中的每个线程置于TASK_STOPPED状态。 第三个宏sig_kernel_coredump检查默认操作转储;如果为真,它将调用do_coredump()例程,该例程生成核心转储二进制文件并终止线程组中的所有进程。 接下来,对于默认操作为 Terminate 的信号,通过调用do_group_exit()例程终止组中的所有线程。

执行用户模式处理程序

回想一下我们在上一节中的讨论,do_signal()调用handle_signal()例程来传递其处理设置为用户处理程序的挂起信号。 用户模式信号处理程序驻留在进程代码段中,需要访问进程的用户模式堆栈;因此,内核需要切换到用户模式堆栈来执行信号处理程序。 要从信号处理程序成功返回,需要切换回内核堆栈以恢复正常用户模式执行的用户上下文,但这样的操作将失败,因为内核堆栈将不再包含用户上下文(struct pt_regs),因为进程从用户模式到内核模式的每个条目都清空了它。

为了确保进程在用户模式下正常执行的平稳过渡(从信号处理程序返回时),handle_signal()将内核堆栈中的用户模式硬件上下文(struct pt_regs)移动到用户模式堆栈(struct ucontext)中,并设置处理程序帧以在返回期间调用_kernel_rt_sigreturn()例程;此函数将硬件上下文复制回内核堆栈,并恢复用户模式上下文以恢复当前进程的正常执行。

下图描述了用户模式信号处理程序的执行情况:

设置用户模式处理程序帧

要为用户模式处理程序设置堆栈帧,handle_signal()使用ksignal实例的地址调用setup_rt_frame(),该地址包含与当前进程的内核堆栈中的信号关联的k_sigaction和指向struct pt_regs的指针。 下面是setup_rt_frame()的 x86 实现:

setup_rt_frame(struct ksignal *ksig, struct pt_regs *regs)
{
 int usig = ksig->sig;
 sigset_t *set = sigmask_to_save();
 compat_sigset_t *cset = (compat_sigset_t *) set;

 /* Set up the stack frame */
 if (is_ia32_frame(ksig)) {
 if (ksig->ka.sa.sa_flags & SA_SIGINFO)
 return ia32_setup_rt_frame(usig, ksig, cset, regs); // for 32bit systems with SA_SIGINFO
 else
 return ia32_setup_frame(usig, ksig, cset, regs); // for 32bit systems without SA_SIGINFO
 } else if (is_x32_frame(ksig)) {
 return x32_setup_rt_frame(ksig, cset, regs);// for systems with x32 ABI
 } else {
 return __setup_rt_frame(ksig->sig, ksig, set, regs);// Other variants of x86
 }
}

它检查 x86 的特定变体并调用适当的帧设置例程。 为了进一步讨论,我们将重点讨论适用于 x86-64 的__setup_rt_frame()。 此函数使用处理信号所需的信息填充名为struct rt_sigframe的结构的实例,设置返回路径(通过_kernel_rt_sigreturn()函数),并将其推入用户模式堆栈:

/*arch/x86/include/asm/sigframe.h */
#ifdef CONFIG_X86_64

struct rt_sigframe {
 char __user *pretcode;
 struct ucontext uc;
 struct siginfo info;
 /* fp state follows here */
};

-----------------------  

/*arch/x86/kernel/signal.c */
static int __setup_rt_frame(int sig, struct ksignal *ksig,
 sigset_t *set, struct pt_regs *regs)
{
 struct rt_sigframe __user *frame;
 void __user *restorer;
 int err = 0;
 void __user *fpstate = NULL;

 /* setup frame with Floating Point state */
 frame = get_sigframe(&ksig->ka, regs, sizeof(*frame), &fpstate);

 if (!access_ok(VERIFY_WRITE, frame, sizeof(*frame)))
 return -EFAULT;

 put_user_try {
 put_user_ex(sig, &frame->sig);
 put_user_ex(&frame->info, &frame->pinfo);
 put_user_ex(&frame->uc, &frame->puc);

 /* Create the ucontext. */
 if (boot_cpu_has(X86_FEATURE_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. */
 restorer = current->mm->context.vdso +
 vdso_image_32.sym___kernel_rt_sigreturn;
 if (ksig->ka.sa.sa_flags & SA_RESTORER)
 restorer = ksig->ka.sa.sa_restorer;
 put_user_ex(restorer, &frame->pretcode);

 /*
 * This is movl $__NR_rt_sigreturn, %ax ; int $0x80
 *
 * WE DO NOT USE IT ANY MORE! It's only left here for historical
 * reasons and because gdb uses it as a signature to notice
 * signal handler stack frames.
 */
 put_user_ex(*((u64 *)&rt_retcode), (u64 *)frame->retcode);
 } put_user_catch(err);

 err |= copy_siginfo_to_user(&frame->info, &ksig->info);
 err |= setup_sigcontext(&frame->uc.uc_mcontext, fpstate,
 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->sp = (unsigned long)frame;
 regs->ip = (unsigned long)ksig->ka.sa.sa_handler;
 regs->ax = (unsigned long)sig;
 regs->dx = (unsigned long)&frame->info;
 regs->cx = (unsigned long)&frame->uc;

 regs->ds = __USER_DS;
 regs->es = __USER_DS;
 regs->ss = __USER_DS;
 regs->cs = __USER_CS;

 return 0;
}

rt_sigframe结构的*pretcode字段分配信号处理程序函数的返回地址,该函数是_kernel_rt_sigreturn()例程。 用sigcontext初始化struct ucontext uc,它包含从内核堆栈的pt_regs复制的用户模式上下文、常规阻塞信号的位数组和浮点状态。 在设置frame实例并将其推送到用户模式堆栈之后,__setup_rt_frame()更改内核堆栈中进程的pt_regs,以便在当前进程恢复执行时将控制权移交给信号处理程序。 将**指令指针(IP)设置为信号处理程序的基地址,并将堆栈****指针(Sp)**设置为先前推送的帧的顶部地址;这些更改会导致信号处理程序执行。

重新启动中断的系统调用

我们在第 1 章理解进程、地址空间和线程中了解到,用户模式进程调用系统调用来切换到内核模式以执行内核服务。 当进程进入内核服务例程时,该例程可能会因资源可用性(例如,等待排除锁定)或发生事件(例如中断)而被阻塞。 这样的阻塞操作要求调用方进程进入TASK_INTERRUPTIBLE,``TASK_UNINTERRUPTIBLETASK_KILLABLE状态。 实现的特定状态取决于对系统调用中调用的阻塞调用的选择。

如果调用方任务进入TASK_UNINTERRUPTIBLE状态,则会生成该任务上出现的信号,使它们进入挂起列表,并且只有在服务例程完成后(在返回到用户模式的路径上)才会将其传递给进程。 但是,如果任务被置于TASK_INTERRUPTIBLE状态,则会生成该任务上的信号,并通过将其状态更改为TASK_RUNNING来尝试立即交付,这会导致该任务甚至在系统调用完成之前就在阻塞的系统调用上被唤醒(导致系统调用操作失败)。 通过返回相应的故障代码来指示此类中断。 信号对处于TASK_KILLABLE状态的任务的影响类似于TASK_INTERRUPTIBLE,不同之处在于唤醒仅在发生致命的SIGKILL信号时生效。

EINTRERESTARTNOHANDERESTART_RESTARTBLOCKERESTARTSYSERESTARTNOINTR是各种内核定义的故障代码;系统调用被编程为在故障时返回适当的错误标志。 错误代码的选择决定了在处理中断信号后是否重新启动失败的系统调用操作:

(include/uapi/asm-generic/errno-base.h)
 #define EPERM 1 /* Operation not permitted */
 #define ENOENT 2 /* No such file or directory */
 #define ESRCH 3 /* No such process */
 #define EINTR 4 /* Interrupted system call */
 #define EIO 5 /* I/O error */
 #define ENXIO 6 /* No such device or address */
 #define E2BIG 7 /* Argument list too long */
 #define ENOEXEC 8 /* Exec format error */
 #define EBADF 9 /* Bad file number */
 #define ECHILD 10 /* No child processes */
 #define EAGAIN 11 /* Try again */
 #define ENOMEM 12 /* Out of memory */
 #define EACCES 13 /* Permission denied */
 #define EFAULT 14 /* Bad address */
 #define ENOTBLK 15 /* Block device required */
 #define EBUSY 16 /* Device or resource busy */
 #define EEXIST 17 /* File exists */
 #define EXDEV 18 /* Cross-device link */
 #define ENODEV 19 /* No such device */
 #define ENOTDIR 20 /* Not a directory */
 #define EISDIR 21 /* Is a directory */
 #define EINVAL 22 /* Invalid argument */
 #define ENFILE 23 /* File table overflow */
 #define EMFILE 24 /* Too many open files */
 #define ENOTTY 25 /* Not a typewriter */
 #define ETXTBSY 26 /* Text file busy */
 #define EFBIG 27 /* File too large */
 #define ENOSPC 28 /* No space left on device */
 #define ESPIPE 29 /* Illegal seek */
 #define EROFS 30 /* Read-only file system */
 #define EMLINK 31 /* Too many links */
 #define EPIPE 32 /* Broken pipe */
 #define EDOM 33 /* Math argument out of domain of func */
 #define ERANGE 34 /* Math result not representable */
 linux/errno.h)
 #define ERESTARTSYS 512
 #define ERESTARTNOINTR 513
 #define ERESTARTNOHAND 514 /* restart if no handler.. */
 #define ENOIOCTLCMD 515 /* No ioctl command */
 #define ERESTART_RESTARTBLOCK 516 /* restart by calling sys_restart_syscall */
 #define EPROBE_DEFER 517 /* Driver requests probe retry */
 #define EOPENSTALE 518 /* open found a stale dentry */

从中断的系统调用返回时,用户模式 API 总是返回EINTR错误代码,而不考虑底层内核服务例程返回的特定错误代码。 内核的信号传递例程使用剩余的错误代码来确定中断的系统调用是否可以在从信号处理程序返回时重新启动。下表显示了系统调用执行中断时的错误代码及其对各种信号处理的影响:

这就是他们的意思:

  • No Restart:系统调用不会重启。 该进程将从系统调用(int$0x80 或 sysenter)之后的指令恢复在用户模式下执行。
  • 自动重启:内核通过将相应的 syscall 标识符加载到eax并执行 syscall 指令(int$0x80 或 sysenter),强制用户进程重新启动系统调用操作。
  • 显式重新启动:仅当进程在为中断信号设置处理程序(通过 sigaction)时启用了SA_RESTART标志时,系统调用才会重新启动。

简略的 / 概括的 / 简易判罪的 / 简易的

虽然信号是进程和内核服务参与的基本通信形式,但它提供了一种简单而有效的方法,可以在发生各种事件时从运行中的进程获得异步响应。 通过了解信号使用的所有核心方面、它们的表示、数据结构以及用于信号生成和传递的内核例程,我们现在更了解内核,也更好地准备在本书后面的部分中查看进程之间更复杂的通信方式。 在花了前三章讨论进程及其相关方面之后,我们现在将深入研究内核的其他子系统,以提高我们的可见性。 在下一章中,我们将建立对内核核心方面之一--内存子系统的理解。

在下一章中,我们将逐步了解内存管理的许多关键方面,如内存初始化、分页和保护以及内核内存分配算法等。**