深入理解 Linux 内核---信号

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/u012319493/article/details/84936406

信号的作用

信号是很短的消息,可以被发送到一个进程或一组进程。
发送给进程的唯一信息通常是一个数,来标识信号。

前缀为 SIG 的一组宏标识信号。
如,当一个进程引用无效的内存时,SIGSEGV 宏产生发送给进程的信号标识符。

使用信号的两个目的:

  • 让进程知道已经发生了一个特定的事件。
  • 强迫进程执行自己代码中的信号处理程序。

除了一些常规信号,POSIX 标准还引入了实时信号,编码范围为 32~64。
不同于常规信号,它们必须排队,以便发送的多个信号都能被接收到。
而同种类型的常规信号并不排队:如果一个常规信号被连续发送多次,则只有其中一个发送到接收进程。
Linux 内核不使用实时信号,但通过几个特定的系统调用实现了 POSIX 标准。

信号的一个重要特点是它们可以随时被发送给状态经常不可预知的进程。
发送给非运行进程的信号必须由内核保存,直到进程恢复执行。
阻塞一个信号会拖延信号的传递,直到阻塞解除。

因此,内核区分信号传递的两个不同阶段:

  • 信号产生。内核更新目标进程的数据结构,以表示一个新信号已经被发送。
  • 信号传递。内核强迫目标进程通过以下方式对信号做出反应:或改变目标进程的执行状态,或开始执行一个特定的信号处理程序,或两者都是。

每个产生的信号之多被传递一次。
信号是可消费资源:一旦已经传递出去,进程描述符中有关该信号的所有信息都被取消。

已经产生但还没有传递的信号被称为挂起信号。
任何时候,一个进程仅保存特定类型的一个挂起信号,同一进程同种类型的其他信号不被排队,只被简单地丢弃。
但对于实时信号,同种类型的挂起信号可以有好几个。

一般,信号可以保留不可预知的挂起时间,必须考虑下列因素:

  • 信号通常只被当前正在运行的进程(current)传递。
  • 给定类型的信号可以由进程选择性地阻塞。此时,在取消阻塞前进程将不接受该信号。
  • 当进程执行一个信号处理程序的函数时,通常“屏蔽”相应的信号,即自动阻塞该信号直到处理程序结束。
    因此,所处理的信号另一次出现不能中断信号处理程序,所以信号处理函数不必是可重入的。

信号的内核实现比较复杂,内核必须:

  • 记住每个进程阻塞哪些信号。
  • 当从内核态切换到用户态时,对任何一个进程都要检查是否有一个信号已经到达。这几乎在每个定时中断时都发生。
  • 确定是否可忽略该信号。发生在下列条件都满足时:
    • 目标进程没有被另一个进程跟踪(进程描述符中 ptrace 字段的 PT_PTRACED 的标志等于 0)。
    • 信号没有被目标进程阻塞。
    • 信号被目标进程忽略。
  • 处理这一信号,即信号可能在进程运行期的任意时刻请求把进程切换到一个信号处理函数,并在这个函数返回后恢复原来执行的上下文。

此外,还需考虑兼容性。

传递信号之前所执行的操作

进程以三种方式对一个信号做出应答:

  1. 显式地忽略信号。
  2. 执行与信号相关的缺省操作。由内核预定义的缺省操作取决于信号的类型:
  • Terminate,进程被终止
  • Dump,进程被终止,如果可能,创建包含进程执行上下文的核心转储文件,该文件可用于调试。
  • Ignor,信号被忽略。
  • Stop,进程被停止,即把进程设置为 TASK_STOPPED 状态。
  • Continue,如果进程被停止,就把它设置为 TASK_RUNNING 状态。
  1. 通过调用相应的信号处理函数捕获信号。

对一个信号的阻塞和忽略是不同的:
只要信号被阻塞,就不被传递;只有在信号解除阻塞后才传递。
而一个被忽略的信号总是被传递,只是没有进一步的操作。

SIGKILL 和 SIGSTOP 信号不可被显示忽略、捕获或阻塞,因此,通常必须执行它们的缺省操作。
因此,SIGKILL 和 SIGSTOP 分别允许具有适当特权的用户终止、停止任何进程,不管程序执行时采取怎样的防御措施。

如果某个信号的传递导致内核杀死一个进程,那么该信号对进程就是致命的。
致命的信号包括:

  • SIGKILL 信号
  • 缺省操作为 Terminate 的每个信号
  • 不被进程捕获的信号对于该进程是致命的

如果一个被进程捕获的信号,对应的信号处理函数终止了该进程,那么该信号就不是致命的,因为进程自己选择了终止,而不是被内核杀死。

POSIX 信号和多线程应用

POSXI 1003.1 标准对多线程应用的信号处理有一些严格的要求:

  • 信号处理程序必须在多线程应用的所有线程之间共享;不过,每个线程必须有自己的挂起信号掩码和阻塞信号掩码。
  • POSIX 库函数 kill() 和 sigqueue() 必须向所有的多线程应用而不是某个特殊的线程发送信号。所有由内核产生的信号同样如此。
  • 每个发送给多线程应用的信号仅传送给一个线程,这个线程是由内核在从不阻塞该信号的线程中随意选择出来的。
  • 如果向多线程应用发送了一个致命的信号,那么内核将被杀死该应用的所有线程,而不仅仅是杀死接收信号的那个线程。

为遵循 POSIX 标准,Linux 内核把多线程应用实现为一组属于同一个线程组的轻量级进程。

如果一个挂起信号被发送给了某个特定进程,那么该信号是私有的;如果被发送给了整个线程组,它就是共享的。

与信号相关的数据结构

在这里插入图片描述

进程描述符中的 blocked 字段存放进程当前所屏蔽的信号。
它是一个 sigset_t 位数组,每种信号类型对应一个元素:

typedef struct
{
	unsigned long sig[2];  // 每个无符号长整数由 32 位组成
}sigset_t;

信号的编号 = sigset_t 位数组中相应位的下标 + 1。
1 ~ 31 之间的编号对应于常规信号,32 ~ 64之间的编号对应于实时信号。

信号描述符和信号处理程序描述符

进程描述符的 signal 字段指向信号描述符—一个 signal_struct 类型的结构,用来跟踪共享挂起信号。
信号描述符还包括与信号处理关系不密切的一些字段,如

  • rlim,每进程的资源限制数组
  • pgrp,进程的组领头进程 PID
  • session,进程的会话领头进程 PID
    在这里插入图片描述

信号描述符被属于同一线程组的所有进程共享,即被调用 clone() 系统调用(设置 CLONE_SIGHAND 标志)创建的所有进程共享,因此,对属于同一线程组的每个进程而言,信号描述符中的字段必须都是相同的。

每个进程还有信号处理程序描述符,是一个 sighand_struct 类型的结构,用来描述每个信号必须如何被线程组处理。

在这里插入图片描述

调用 clone() 时设置 CLONE_SIGHAND 标志,信号处理程序描述符就可以被几个进程共享。

描述符的 count 字段表示共享该结构的进程个数。
在一个 POSIX 的多线程应用中,线程组中的所有轻量级进程都应该用相同的信号描述符和信号处理程序描述符。

sigaction 数据结构

字段:

  • sa_handler,指定执行操作的类型。它的值可以是指向信号处理程序的一个指针,SIG_EFL,或 SIG_IGN。
  • sa_flags,标志集,指定必须怎样处理信号。
  • sa_mask,类型为 sigset_t 的变量,指定当运行信号处理程序时要屏蔽的信号。

挂起信号队列

为了跟踪当前的挂起信号是什么,内核把两个挂起信号队列与每个进程关联:

  • 共享挂起信号队列,位于信号描述符的 shared_pending 字段,存放这个线程组的挂起信号。
  • 私有挂起信号队列,位于进程描述符的 pending 字段,存放特定进程的挂起信号。

挂起信号队列由 sigpending 数据结构组成,定义如下:

structural singpengding 
{
	struct list_head list; // 包含 sigqueue 数据结构的双向链表的头
	sigset_t signal;  // 指定挂起信号的位掩码
}

siginfo 是一个 128 字节的数据结构,存放有关出现特定信号的信息,包含下列字段:

  • si_signo,信号编号
  • si_errno,引起信号产生的指令的出错码,没有错误则为 0
  • si_code,发送信号者的代码,如:SI_USER、SI_KERNEL、SI_QUEUE、SI_TIMER 等
  • _sifields,依赖于信号类型的信息的联合体。

在信号数据结构上的操作

下面的 set 是指向 sigset_t 类型变量的一个指针,nsig 是信号的编号,mask 是无符号长整数的位掩码。

  • sigemptyset(set) 和 sigfillset(set):把 set 中的位分别置为 0 或 1。

  • sigaddset(set, nsig) 和 sigdelset(set, nsig):把 nsig 信号在 set 中对应的位分别置为 1 或 0。

    • sigaddset() 简化为:
set->sig[(nsig-1) / 32] |= 1UL << ((nsig - 1) % 32);
    • sigdelset() 简化为:
set->sig[(nsig-1) / 32] |= ~(1UL << ((nsig - 1) % 32));
  • sigaddsetmask(set, mask) 和 sigdelsetmask(set, mask):把 mask 中的位在 set 中对应的所有位分别设置为 1 或 0。仅用于编号为 1~32 之间的信号,可分别简化为:
set->sig[0] |= mask;
set->sig[0] |= ~mask;
  • sigismember(set, nsig):返回 nsig 信号在 set 中对应的值。可简化为:
return  1 & (set->sig[(nsig - 1) / 32] >> ((nsig - 1) % 32));
  • sigmask(nsig):产生 nsig 信号的位索引。如果内核需要设置、清除或测试一个特定信号在 sigset_t 类型变量中对应的位,可通过该宏得到合适的位。

  • sigandsets(d, s1, s2)、sigoresets(d, s1, s2) 和 signandsets(d, s1, s2):
    在 sigset_t 类型的 s1 和 s2 变量之间分别执行逻辑“与”、逻辑“或”即逻辑“与非”。
    结果保存在 d 指向的 sigset_t 类型的变量中。

  • sigtestsetmask(set, mask):如果 mask 在 set 中对应的任意一位被设置,就返回 1;否则返回 0,只用于编号为 1 ~ 31。

  • siginitset(set, mask):把 mask 中的位初始化为 1 ~ 32 之间的信号在 set 中对应的低位,并把 33 ~ 63 之间信号的对应位清 0。

  • siginitsetinv(set, mask):用 mask 中位的补码初始化 1 ~ 32 间的信号在 sigset_t 类型的变量中对应的低位,并把 33 ~ 63 之间信号的对应位置位。

  • signal_pending§如果 *p 进程描述符所表示的进程有非阻塞的挂起信号,就返回 1,否则返回 0。通过检查进程的 TIF_SIGPENDING 标志实现。

  • recalc_sigpending_tsk(t) 和 recalc_sigpending(): 第一个函数检查是 *t 进程描述符表示的进程有挂起信号(t->pending->signa),还是进程所属的线程组有挂起的信号(t->signal->shared_pending->signal),然后把 t->thread_info->flags 的 TIF_SIGPENDING 标志置位。第二个函数等价于 recalc_sigpending_tsk(current)。

  • rm_from_queue(mask, q):从挂起信号队列 q 中删除与 mask 位掩码相对应的挂起信号。

  • flush_sigqueue(q):从挂起信号队列 q 中删除所有的挂起信号。

  • flush_signals(t):删除发送给 *t 进程描述符所表示的进程的所有信号。
    通过清除 t->thread_info->flags 中的 TIF_SIGPENDING 标志,并在 t->pending 和 t->signal->shared_pending 队列上两次调用 flush_sigqueue() 实现。

产生信号

当发送给进程或整个线程组一个信号时,该信号可能来自内核,也可能来自另一个进程。

发送给进程的信号的函数在结束时会调用 specific_send_sig_info()。

发送给整个线程组的信号的函数在结束时会调用 group_send_sig_info()。

specific_send_sig_info()

向指定进程发送信号。

参数:

  • sig,信号编号。
  • info,或者是 siginfo_t 表的地址,或者是三个特殊值中的一个:
    • 0:信号由用户态进程发送。
    • 1:信号由内核发送。
    • 2:由内核发送的 SIGSTOP 或 SIGKILL 信号。
  • t:指向目标进程描述符的指针。

必须在关本地中断和已经获得 t->sighand->siglock 自旋锁的情况下调用该函数,执行下列步骤:

  1. 检查进程是否忽略信号,如果是就返回 0(不产生信号)。
    以下三个条件都满足时,信号被忽略:
  • 进程没有被跟踪(t->ptrace 中的 PT_PTRACED 标志被清 0)
  • 信号没有被阻塞(sigismember(&t->blocked, sig) 返回 0)
  • 或者显示地忽略信号(t->sighand->action[sig-1].sa_handler == SIG_IGN),或者隐含地忽略信号(sa_handler == SIGDFL,且信号是 SIGCONT、SIGCHLD、SIGWINCH 或 SIGURG)
  1. 如果信号是非实时的(sig < 32),且在进程的私有挂起信号队列上已经有另外一个相同的挂起信号(sigismember(&t->pending.signal, sig) 返回 1),什么都不需要做,返回0.
  2. send_signal(sig, info, t, &t->pending) 把信号添加到进程的挂起信号集合中。
  3. 如果 send_signal() 成功结束,且信号不被阻塞(sigismember(&t->blocked, sig) 返回 0),signal_wake_up() 通知进程有新的挂起信号,随后,该函数执行下述步骤:
    a. 把 t->thread_info->flags 中的 TIF_SIGPENDING 标志置位。
    b. 如果进程处于 TASK_INTERRUPTILE 或 TASK_STOPPED 状态,且信号是 SIGKILL,try_to_wake_up() 唤醒进程。
    c. 如果 try_to_wake_up() 返回 0,说明进已经是可运行的:检查进程是否已经在另外一个 CPU 上运行,如果是就像那个 CPU 发送一个处理器间中断,以强制当前进程的重新调度。
    因为从调度函数返回时,每个进程都检查是否存在挂起信号,因此,处理器间中断保证了目标进程能很快注意到新的挂起信号。
  4. 返回 1(成功产生信号)。

send_signal()

在挂起信号队列中插入一个新元素。

参数:

  • 信号编号 sig
  • siginfo_t 数据结构的地址 info
  • 目标进程描述符的地址 t
  • 挂起信号队列的地址 signals

执行下列步骤:

  1. 如果 info == 2,该信号就是 SIGKILL 或 SIGSTOP,且已经由内核通过 force_sig_specific() 产生:跳到第 9 步,内核立即强制执行与这些信号相关的操作,因此函数不用把信号添加到挂起信号队列中。
  2. 如果进程拥有者的挂起信号的数量(t->user->sigpending)小于当前进程的资源限制(t->signal->rlim[RLIMT_SIGPENDING].rlim_cur),就为新出现的信号分配 sigqueue 数据结构:
q = kmeme_cache_alloc(sigqueue_cachep, GFP_ATOMIC);
  1. 如果进程拥有者的挂起信号的数量太多,或上一步的内存分配失败,就跳转到到第 9 步。
  2. 递增拥有者挂起信号的数量(t->user->sigpending)和 t->user 所指向的每用户数据结构的引用计数器。
  3. 在挂起信号队列 signals 中增加 sigqueue 数据机构:
list_add_tail(&q->list, &signals->list);
  1. 在 sigqueue 数据结构中填充表 siginfo_t:
if((unsigned long)info == 0)
{
	q->info.si_signo = sig;
	q->info.si_errno = 0;
	q->info.si_code = SI_USER;
	q->info._sifields._kill._pid = current->pid;
	q->info._sifields._kill._uid = current->uid;
}
else if((unsigned long)info == 1)
{
	q->info.si_signo = sig;
	q->info.si_errno = 0;
	q->info.si_code = SI_KERNEL;
	q->info._sifields._kill._pid = 0;
	q->info._sifiields._kill._uid = 0;
}
ese
	copy_siginfo(&q->info, info);  // 复制由调用者传递的 siginfo_t 表
  1. 把队列位掩码中与信号相应的位置 1:
sigaddset(&signals->signal, sig);
  1. 返回 0:说明信号已经被成功追加到挂起信号队列中。
  2. 此时,不再向信号挂起队列中增加元素,因为已经有太多的挂起信号,或已经没有可以分给 sigqueue 数据结构的空闲空间,或者信号已经由内核强制立即发送。
    如果信号是实时的,并已经通过内核函数发送给队列排队,则 send_signal() 返回错误代码 -EAGIN:
if(sig >= 32 && info && (unsigned long)info != 1 && info->si_code != SI_USER)
	return -EAGIN;
  1. 设置队列的位掩码中与信号相关的位:
sigaddset(&signals->signal, sig);
  1. 返回 0:即使信号没有被追加到队列中,挂起信号掩码中相应的位也被设置。

即使在挂起队列中没有空间存放相应的挂起信号,让目标进程能接收信号也很重要。
假设一个进程正在消耗过多内存,内核必须保证即使没有空闲内存,kill() 也能成功执行。

group_send_sig_info()

向整个线程组发送信号。

参数:

  • 信号编号 sig
  • siginfo_t 表的地址 info
  • 进程描述符的地址 p

执行下列步骤:

  1. 检查 sig 是否正确
if(sig < 0 || sig > 64)
	return -EINVAL;
  1. 如果信号是由用户态进程发送的,则确定是否允许该操作。如果不允许用户态进程发送信号,返回 -EPERM。
    下列条件至少有有一个成立,信号才可被传递:
  • 发送进程的拥有者拥有适当的权限(通常意味着通过系统管理员发布信号)。
  • 信号为 SIGCONT 且目标进程与发送进程处于同一个注册会话中。
  • 两个进程属于同一个用户。
  1. 如果参数 sig == 0,不产生任何信号,立即返回:
if(!sig || !p->sighand)
	return 0;
  • 0 是无效的信号编码,说明发送进程没有向目标线程组发送信号的特权。如果目标进程正在被杀死(通过检查它的信号处理程序描述符是否被释放得知),那么函数也返回。
  1. 获取 p->sighand->siglock 自旋锁并关闭本地中断。
  2. handle_stop_signal() 检查信号的某些类型,这些类型可能使目标线程组的其他挂起信号无效。
    a. 如果线程组正在被杀死(信号描述符的 flags 字段的 SIGNAL_GROUP_EXIT 标志被设置),则返回。
    b. 如果 sig 是 SIGSTOP、SIGTSTP、SIGTTIN 或 SIGTTOU 信号,rm_from_queue() 从共享挂起信号队列 p->signal->shared_pending 和线程组所有成员的私有信号队列中删除 SIGCONT 信号。
    c. 如果 sig 是 SIGCONT 信号,rm_from_queue() 从共享挂起信号队列 p->signal->shared_pending 中删除所有的 SIGSTOP、SIGTSTP、SIGTTIN 和 SIGTTOU 信号,然后从属于线程组的进程的私有挂起信号队列中删除上述信号,并唤醒进程:
// 掩码 0x003c0000 选择以上四种停止信号
rm_from_queue(0x003c0000, &p->signal->shared_pending);
t = p;
do
{
	rm_from_queue(0x003c0000, &t->pending);
	try_to_wake_up(t, TASK_STOPPED, 0);
	t = next_thread(t);   // 返回线程组中不同轻量级进程的描述符地址
}while(t != p);
  1. 检查线程组是否忽略信号,如果是就返回 0 值(成功)。如果前一节“信号的作用”中提到的忽略信号的三个条件都满足,就忽略信号。
  2. 如果信号是非实时的,并且在线程组的共享挂起信号队列中已经有另外一个相同的信号,就什么都不做,返回 0 值(成功)。
if(sit < 32 && sigismember(&p->signal->shared_pending.signal, sig))
	return 0;
  1. send_signal() 把信号添加到共享挂起信号队列中。如果返回非 0 的错误码,终止并返回相同值。
  2. __group_complete_signal() 唤醒线程组中的一个轻量级进程。
  3. 释放 p->sighand->siglock 自旋锁并打开本地中断。
  4. 返回 0(成功)。

__group_complete_signal() 扫描线程组中的进程,查找能接收新信号的进程。满足下述所有条件的进程可能被选中:

  • 进程不阻塞信号。
  • 进程的状态不是 EXIT_ZOMBIE、EXIT_DEAD、TASK_TRACED 或 TASK_STOPPED。
  • 进程没有正在被杀死,即它的 PF_EXITING 标志没有置位。
  • 进程或者当前正在 CPU 上运行,或者它的 TIF_SIGPENDING 标志还没有设置。

一个线程组可能有很多满足上述条件的进程,函数按照下面的规则选中其中一个进程:

  • 如果 p 标识的进程(group_send_sig_info() 的参数传递的描述符地址)满足所有的优先准则,函数就选择该进程。
  • 否则,函数通过扫描线程组的成员搜索一个适当的进程,搜索从接收线程组最后一个信号的进程(p->siganl->curr_target)开始。

如果 __group_complete_signal() 成功找到一个适当的进程,就开始向被选中的进程传递信号。
首先检查信号是否是致命的,如果是,通过向线程组中的所有轻量级进程发送 SIGKILL 信号杀死整个线程组。
否则,调用 signal_wake_up() 通知被选中的进程:有新的挂起信号。

传递信号

如何确保进程的挂起信号得到处理内核所执行的操作。

在运行进程恢复用户态下的执行前,内核会检查进程 TIF_SIGPENDING 标志的值。
每当内核处理完一个中断或异常时,就检查是否存在挂起信号。

为了处理非阻塞的挂起信号,内核调用 do_signal()。参数:

  • regs,栈区的地址,当前进程在用户态下寄存器的内容存放在这个栈中。
  • oldset,变量的地址,假设函数把阻塞信号的位掩码数组存放在这个变量中。不需要保存位掩码数组时,置为 NULL。

通常只在 CPU 要返回到用户态时才调用 do_signal()。
因此,如果中断处理程序调用 do_signal(),该函数立即返回。

if((regs->xcs & 3) != 3)
	return 1;

如果 oldset 参数为 NULL,就用 current->blocked 字段的地址对它初始化:

if(!oldset)
	oldset = &current->blocked;

do_signal() 的核心是重复调用 dequeue_signal(),直到私有挂起信号队列和共享挂起信号队列中都没有非阻塞的挂起信号为止。

dequeue_signal() 的返回码存放在 signr 局部变量中,值为:

  • 0,所有挂起的信号已全部被处理,且 do_signal() 可以结束。
  • 非 0,挂起的信号正等待被处理,且 do_signal() 处理了当前信号后又调用了 dequeue_signal()。

dequeue_signal() :

  • 首先考虑私有信号队列中的所有信号,并从最低编号的挂起信号开始。
  • 然后考虑共享队列中的信号。
  • 它更新数据结构以标识信号不再是挂起的,并返回它的编号。
    这就涉及清 current->pending.signal 或 current->signal->shared_pending.signal 中对应的位,并调用 recalc_sigpending() 更新 TIF_SIGPEDING 标志的值。

do_signal() 处理每个挂起的信号,并将其编号通过 dequeue_signal() 返回:

  • 首先,检查 current 接收进程是否正受其他一些进程的监控;
    如果是,调用 do_notify_parent_cldtop() 和 schedule() 让监控进程知道进程的信号处理。
  • 然后,把要处理信号的 k_sigaction 数据结构的地址赋给局部变量 ka:
ka = &current->sig->action[signr-1];
  • 根据 ka 的内容可以执行三种操作:忽略信号、执行缺省操作或执行信号处理程序。
    如果显式忽略被传递的信号,do_signal() 仅仅继续执行循环,接着考虑另一个挂起信号:
if(ka->sa.sa_handler == SIG_IGN)
	continue;

接下来说明如何执行缺省操作和信号处理程序。

执行信号的缺省操作

如果 ka->sa.sa_handler == SIG_DFL,do_signal() 就必须执行信号的缺省操作。
但当接收进程是 init 时,该信号被丢弃:

if(current->pid == 1)
	continue;

如果接收进程是其他进程,对缺省操作是 Ignore 的信号进行简单处理:

if(signr == SIGCONT || signr == SIGCHLD || signr == SIGWINCH || signr == SIGURG)
	continue;

缺省操作是 Stop 的信号可能停止线程组中的所有进程。
因此,do_singal() 把进程的状态都设置为 TASK_STOPPED,并随后调用 schedule():

if(signr == SIGTOP || signr == SIGTSTP || signr == SIGTTIN || signr = SIGTTOU)
{
	// SIGSTOP 与其他信号的差异:SIGSTOP 总是停止线程组
	// 而其他信号只停止不在“孤儿进程组”中的线程组。
	// POSIX 标准规定,只要进程组中有一个进程有父进程,
	// 即便父进程处于不同的进程组中,但在同一个会话中,
	// 那么该进程组不是孤儿进程组
	// 因此,如果父进程死亡,但启动该进程的用户仍登录在线,
	// 那么该进程组就不是一个孤儿进程组
	if(signr != SIGSTOP && is_orphaned_pgrp(current->signal->pgrp))
		continue;

	// 检查 current 是否是线程组中第一个被停止的进程,如果是,激活“组停止”:
	// 本质上,将信号描述符中的 group_stop_count 字段设为正值
	// 并唤醒线程组中的所有进程
	// 组中的所有进程都都检查该字段以确认正在进行”组停止“
	// 然后把进程的状态设置为 TASK_STOPPED,并调用 schedule()
	// 如果线程组领头进程的父进程没有设置 SIGCHLD 的 SA_NOCLDSTOP 标志
	// 还需要向它发送 SIGCHLD 信号
	do_signal_stop(signr);
}

缺省操作位 Dump 的信号可以在进程的工作目录中创建一个”转储“文件,该关文件列出进程地址空间和 CPU 寄存器的全部内容。
do_signal() 创建了转储文件后,就杀死该线程组。

剩余 18 个信号的缺省操作时 Terminate,仅仅杀死线程组。
为了杀死整个线程组,调用 do_group_exit() 执行彻底的”组退出“过程。

捕获信号

如果信号有一个专门的处理程序,do_signal() 就执行它。
通过调用 handle_signal() 进行:

handle_signal(signr, &info, &ka, oldset, regs);

// 如果所接收信号的 SA_ONESHOT 标志被置位
// 就必须重新设置它的缺省操作
// 以便同一信号的再次出现不会再次触发信号处理程序的执行
if(ka->sa.sa_flags & SA_ONESHOT)
	ka->sa.sa_handler = SIG_DFL;  
	
// 处理了一个单独的信号后返回
// 直到下一次调用 do_signal() 时才考虑其他挂起的信号
// 确保了实时信号将以适当的顺序得到处理
return 1;

执行一个信号处理程序复杂性一:在用户态和内核态之间切换时,需要谨慎地处理栈中的内容。

handle_signal() 运行在内核态,而信号处理程序运行在用户态,当前进程恢复”正常“执行前,必须首先执行用户态的信号处理程序。
此外,当内核打算恢复进程的正常执行时,内核态堆栈不再包含被中断程序的硬件上下文,因为每当从内核态向用户态转换时,内核态堆栈都被清空。

执行一个信号处理程序复杂性二:可以调用系统调用。这种情况下,执行了系统调用的服务例程后,控制权必须返回到信号处理程序,而不是被中断程序的正常代码流。

Linux 所采用的解决方法是,把保存在内核态堆栈中的硬件上下文拷贝到当前进程的用户态堆栈中。
用户态堆栈也以同样方式修改:即当信号处理程序终止时,自动调用 sigreturn() 把这个硬件上下文拷贝回内核态堆栈中,并恢复用户态堆栈中原来的内容。
在这里插入图片描述

图 11-2 说明了有关捕获一个信号的函数的执行流:

  1. 一个非阻塞的信号发送一个进程。
  2. 中断或异常发生时,进程切换到内核态。
  3. 内核执行 do_signal(),该函数依次处理信号(handle_signal())和建立用户态堆栈(setup_frame() 或 setup_rt_frame())。
  4. 进程返回到用户态,因为信号处理程序的起始地址被强制放进程序计数器,因此开始执行信号处理程序。
  5. 处理程序终止时,setup_frame() 或 setup_rt_frame() 放在用户态堆栈中的返回代码被执行。该代码调用 sig_return() 或 rt_sigreturn() 系统调用,相应的服务例程把正常程序的用户态堆栈硬件上下文拷贝到内核态堆栈,并把用户态堆栈恢复到它原来的样子(restore_sigcontext())。
  6. 普通进程恢复执行。

下面详细讨论该种方案。

建立帧

为建立进程的用户态堆栈,handle_signal() 调用 setup_frame() 或 setup_rt_frame()。
为了在这两个函数之间进行选择,内核检查与信号相关的 sigaction 表 sa_flags 字段的 SA_SIGINFO 标志。

setup_frame() 参数:

  • sig,信号编号
  • ka,与信号相关的 k_sigaction 表的地址
  • oldset,阻塞信号的位掩码数组的地址
  • regs,用户态寄存器的内容保存在内核态堆栈区的地址

setup_frame() 把帧推入用户态堆栈中,该帧含有处理信号所需的信息,并确保正确返回到 handle_signal()。

一个帧就是包含下列字段的 sigframe 表:
在这里插入图片描述
setup_frame() 执行步骤:

  1. 调用 get_sigframe() 计算帧的第一个内存单元,通常在用户态堆栈中:
// 因为栈朝低地址方向延伸,所以通过把当前栈顶的地址减去它的大小
// 使其结果与 8 的倍数对齐,就获得了帧的起始地址
(rets->esp - sizeof(struct sigframe)) & 0xfffffff8
  1. 用 access_ok 宏对返回地址进行验证。
    如果地址有效,反复调用 __put_user() 填充帧的所有字段。
    帧的 pretcode = &__kernel_sigreturn,一些粘合代码的地址存放在 vsyscall 页中。

  2. 修改内核态堆栈的 regs 区,保证了当 current 恢复在用户态的执行时,控制权将传递给信号处理程序。

regs->esp = (unsigned long)frame;  // 而 esp 指向已推进用户态堆栈顶的帧的第一个内存单元
regs->eip = (unsigned long)ka->sa.sa_handler; //  eip 寄存器执行信号处理程序的第一条指令
regs->eax = (unsigned long)sig;
regs->edx = regs->ecx = 0;

// 把保存在内核态堆栈的段寄存器内容重新设置成它们的缺省值
regs->xds = regs->xes = regs->xss = __USER_DS;
regs->xcs = __USER_CS;

现在,信号处理程序所需的信息就在用户态堆栈的顶部。

setup_rt_frame() 与 setup_frame() 非常相似,但它把用户态堆栈存放在一个扩展帧中(rt_sigframe 数据结构中),该帧包含了与信号相关的 siginfo_t 表的内容。
此外,该函数设置 pretcode 字段以使它执行 vsyscall 页中的 __kernel_rt_sigreturn 代码。

检查信号标志

建立用户态堆栈后,handle_signal() 检查与信号相关的标志值。

// 如果信号没有设置 SA_NODEFER 标志
if(!(ka->sa.sa_flags & SA_NODEFER))
{
	spin_lock_irq(&current->sighand->siglock);

	// 在 sigaction 表中 sa_mask 字段对应的信号就必须在信号处理程序执行期间被阻塞
	sigorsets(&current->blocked, &current->blocked, &ka->sa.sa_mask);
	
	sigaddset(&current->blocked, sig);  // sig 为信号编号
	
	// 检查进程是否有非阻塞的挂起信号,并因此设置它的 TIF_SIGPENDING 标志
	recalc_sigpending(curent);
	
	spin_unlock_irq(&current->sighand->siglock);
}

然后,返回到 do_signal(),do_signal() 也立即返回。

开始执行信号处理程序

do_signal() 返回时,当前进程恢复它在用户态的执行。
由于 setup_frame() 的准备,eip 寄存器执行信号处理程序的第一条指令,而 esp 指向已推进用户态堆栈顶的帧的第一个内存单元。
因此,信号处理程序被执行。

终止信号处理程序

信号处理程序结束时,返回栈顶地址,该地址指向帧的 pretcode 字段所引用的 vsyscall 页中的代码:

__kernel_sigreturn:
	popl %eax
	movl $__NR_sigreturn, %eax
	int $0x80

信号编号(即帧的 sig 字段)被从栈中丢弃,然后调用 sigreturn() 。

sys_sigreturn() :

  1. 计算类型为 pt_regs 的 regs 的地址,pt_regs 包含用户态进程的硬件上下文。
  2. 根据存放在 esp 字段中的地址,导出并检查帧在用户态堆栈内的地址:
frame = (struct sigframe *)(regs.esp - 8);
if(verify_area(VERIFY_READ, frame, sizeof(*frame))
{
	force_sig(SIGSEGV, current);
	return 0;
}
  1. 把调用信号处理程序前所阻塞的信号的位数组从帧的 sc 字段拷贝到 current 的 blocked 字段。结果,为信号处理函数的执行而屏蔽的所有信号解除阻塞。
  2. 调用 recalc_sigpending() 。
  3. 把来自帧的 sc 字段的进程硬件上下文拷贝到内核态堆栈中,并从用户态堆栈中删除帧,这两个任务通过调用 restore_sigcontext() 完成。

rt_sigqueueinfo() 需要与信号相关的 siginfo_t 表。
扩展帧的 pretcode 指向 vsyscall 页中的 __kernel_rt_sigturn 代码,它调用 rt_sigreturn(),相应的 sys_rt_sigreturn() 服务例程把来自扩展帧的进程硬件上下文拷贝到内核态堆栈,并通过从用户态堆栈删除扩展帧以恢复用户态堆栈原来的内容。

系统调用的重新执行

内核不总是能立即满足系统调用发出的请求,这时,把发出系统调用的进程置为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 状态。

如果进程处于 TASK_INTERRUPTIBLE 状态,并且某个进程向它发送了一个信号,则内核不完成系统调用就把进程置成 TASK_RUNNING 状态。
当切换回用户态时信号被传递给进程。
这时,系统调用服务例程没有完成,但返回 EINTR、ERESTARTNOHAND、ERESTART_RESTARTBLOCK、ERESTARTSYS 或 ERESTARTNOINTR 错误码。

实际上,用户态进程获得的唯一错误码是 EINTR,表示系统调用还没有执行完。
内核内部使用剩余的错误码来指定信号处理程序结束后是否自动重新执行系统调用。

在这里插入图片描述

  • Terminate,不会自动重新执行系统调用:进程在 int $0x80 或 sysenter 指令紧接着的那条指令将恢复它在用户态的执行,这时 eax 寄存器包含的值为 -EINTR。
  • Reexecute,内核强迫用户态进程把系统调用号重新装入 eax 寄存器,并重新执行 int $0x80 或 sysenter 指令。进程意识不到这种重新执行,出错码也不传递给进程。
  • Depends,只有被传递信号的 SA_RESTART 标志被设置,才重新执行系统调用;否则,系统调用以 -EINTR 出错码结束。

传递信号时,内核在试图重新执行一个系统调用前,必须确定进程确实发出过该系统调用。
regs 硬件上下文的 orig_eax 字段起该作用。
中断或异常处理程序开始时初始化该字段:

  • 中断,与中断相关的 IRQ 号减去 256
  • 0x80 或 sysenter,系统调用号
  • 其他异常,-1

因此,orig_eax 字段中的非负数意味着信号已经唤醒了在系统调用上睡眠的 TASK_INTERRUPTIBLE 进程。
服务例程认识到系统调用曾被中断,并返回前面提到的某个错误码。

重新执行被未捕获信号中断的系统调用

如果信号被显式忽略,或者它的缺省操作被强制执行,do_signal() 就分析系统调用的出错码,并确定是否重新自动执行未完成的系统调用。
如果必须重新开始执行系统调用,do_signal() 就修改 regs 硬件上下文,以便在进程返回用户态时,eip 指向 int $0x80 或 sysenter 指令,且 eax 包含系统调用号:

if(regs->orig_eax >= 0)
{
	if(regs->eax == -ERESTARTNOHAND || regs->eax == -ERESTARTSYS || regs->eax == -ERESTARTNOINTR)
	{
		regs->eax = regs->orig_eax;   // 系统调用服务路测的返回码赋给 regs->eax
		regs->eip -= 2;  // int $0x80 和 sysreturn 的长度都是两个字节,eip 减 2 后,指向引起系统调用的指令
	}

	// 因为 eax 寄存器存放了 restart_syscall() 的系统调用号
	// 因此,用户态进程不会重新指向被信号中断的同一系统调用
	// 该错误码仅用于与时间相关的系统调用,重新指向这些系统调用时
	// 应该调整它们的用户态参数
	if(regs->eax == -ERESTART_RESTARTBLOCK)  
	{
		regs->eax = __NR_restart_syscall;
		regs->eip -= 2;
	}
}

为所捕获的信号重新执行系统调用

如果信号被捕获,那么 handle_signal() 可能分析出错码,也可能分析 sigaction 表的 SA_RESTART 标志,来决定是否必须重新执行未完成的系统调用。

如果系统调用必须被重新开始执行,handle_signal() 就与 do_signal() 完全一样继续执行;否则,向用户态进程返回一个出错码 -EINTR。

if(regs->orig_eax >= 0)
{
	switch(regs->eax)
	{
	case -ERESTART_RESTARTBLOCK:
	case -ERESTARTNOHAND:
		regs->eax = -EINTR;
		break;
	case -ERESTARTSYS:
		if(!(ka->sa.sa_flags & SA_RESTART))
		{
			regs->eax = -EINTR;
			brea;
		}
	case -ERESTARTNOINTR:
		regs->eax = regs->orig_eax;
		regs->eip -= 2;
	}
}

与信号处理相关的系统调用

kill()

kill(pid, sig) 向普通进程或多线程应用发送信号,其服务例程是 sys_kill()。pid 参数的含义取决于它的值:

  • pid > 0,把 sig 信号发送到 PID 等于 pid 的进程所属的线程组。
  • pid = 0,把 sig 信号发送到与调用进程同组的进程的所有线程组。
  • pid = -1,把信号发送到所有进程,除了 swapper(PID = 0),init(PID = 1)和 current。
  • pid < -1,把信号发送到进程组 -pid 中进程的所有线程组。

sys_kill() 为信号建立最小的 siginfo_t 表,然后调用 kill_something_info():

info.si_signo = sig;
info.si_errno = 0;
info.si_code = SI_USER;
info._sifields._kill._pid = current->tgid;
info._sifields._kill._uid = current->uid;

// 或调用 kill_proc_info()(通过 group_send_sig_info() 向一个单独的线程组发送信号)  
// 或调用 kill_pg_info()(扫描目标进程组的所有进程,并为目标进程组中的所有进程调用 send_sig_info())
// 或为系统中的所有进程反复调用 group_send_sig_info()(如果 pid  等于 -1)
return kill_something_info(sig, &info, pid);  

kill() 能发送任何信号,包括 32 ~ 64 间的实时信号。
但不能确保一个新的元素加入到目标进程的挂起信号队列,因此,挂起信号的多个实例可能被丢失。
实时信号应当通过 rt_sigqueueinfo() 进程发送。

tkill() 和 tgkill()

向线程组中的指定进程发送信号。

tkill() 的两个参数:

  • PID,信号接收进程的 pid
  • sig,信号编号

sys_tkill() 服务例程为 siginfo 表赋值、获取进程描述符地址、进行许可性检查,并调用 specific_send_sig_info() 发送信号。

tgkill() 还需要第三个参数:

  • tgid,信号接收进程组所在线程组的线程组 ID

sys_tgkill() 服务例程执行的操作与 sys_tkill() 一样,但还需要检查信号接收进程是否确实属于线程组 tgid。
该附加的检查解决了向一个正在被杀死的进程发送消息时出现的竞争条件的问题:
如果另外一个多线程应用正以足够快的速度创建轻量级级进程,信号就可能被传递给一个错误的进程。
因为线程组 ID 在多线程应用的整个生存期中是不会改变的。

改变信号的操作

sigaction(sig, act, oact) 允许用户为信号指定一个操作。
如果没有自定义的信号操作,则执行与传递的信号相关的缺省操作。

sys_sigaction() 服务例程作用于两个参数:

  • sig,信号编号
  • act,类型为 old_sigaction 的 act 表(表示新的操作)
  • oact,可选的输出参数,获得与信号相关的以前的操作。
  1. 检查 act 地址的有效性。
  2. 用 *act 的字段填充类型为 k_sigaction 的 new_ka 局部变量的 sa_handler、sa_flags 和 sa_mask 字段:
__get_user(new_ka.sa.sa_handler, &act->sa_handler);
__get_user(new_ka.sa.sa_flags, &act->sa_flags);
__get_user(mask, &act->sa_mask);
siginitset(&new_ka.sa.sa_mask, mask);
  1. 调用 do_sigaction() 把新的 new_ka 表拷贝到 current->sig->action 的 sig-1 位置的表项中(没有 0 信号):
k = &current->sig->action[sig-1];
if(act)
{
	*k = *act;
	
	// 信号处理程序从不屏蔽 SIGKILL 和 SIGSTOP 
	sigdelsetmask(&k->sa.sa_mask, sigmask(SIGKILL) | sigmask(SITSTOP));

	// POSIX 标准规定,当缺省操作是“Ignore”时
	// 把信号操作设置为 SIG_IGN 或 SIG_DFL 将引起同类型的任意挂起信号被丢弃
	if(k->sa.sa_handler == SIG_IGN || (k->sa.sa_handler == SIG_DFL && 
		(sig == SIGCONT || sig == SIGCHLD || sig == SIGWINCH || sig == SIGURG)))
	{
		rm_from_queue(sigmask(sig), &current->signal->shared_pendig);
		t = current;
		do
		{
			rm_from_queue(sigmask(sig), &current->pending);
			recalc_sigpending_tsk(t);
			t = next_thread(t);
		}while(t != current);
	}
}

sigaction() 还允许用户初始化表 sigaction 的 sa_flags 字段。

原来的 System V Unix 变体提供了 signal() 系统调用,Linux 提供了 sys_signal() 服务例程:

new_sa.sa_handler = handler;
new_sa.sa_flags = SA_ONESHOT | SA_NOMASL.
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;

检查挂起的阻塞信号

sigpending() 允许进程检查信号被阻塞时已经产生的那些信号。
服务例程 sys_sigpending() 只作用于一个参数 set,即用户变量的地址,必须将位数组拷贝到该变量中:

sigorsets(&pending, &current->pending.signal, &current->signal->shared_pending.signal);
sigandsets(&pending, &current->blocked, &pending);
copy_to_user(set, &pending, 1);

修改阻塞信号的集合

sigprocmask() 只应用于常规信号(非实时信号)。
sys_sigprocmask() 服务例程作用于三个参数:

  • oset,进程地址空间的一个指针,执行存放以前位掩码的一个位数组。
  • set,进程地址空间的一个指针,执行包含新位掩码的位数组。
  • how,一个标志,可采取如下值:
    • SIG_BLOCK,*set 位掩码数组,指定必须加到阻塞信号的位掩码数组中的信号。
    • SIG_UNBLOCK,*set 位掩码数组,指定必须从阻塞信号的位掩码数组中删除的信号。
    • SIG_SETMASK,*set 位掩码数组,指定阻塞信号新的位掩码数组。

sys_sigprocmask():

// 调用 copy_from_user() 把 set 参数拷贝到局部变量 new_set 中
if(copy_from_user(&new_set, set, sizeof(*set)))
	return -EFAULT;
	
new_set &= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));

// 把 current 标准阻塞信号的位掩码数组拷贝到 old_set 局部变量中
old_set = current->blocked.sig[0];

// 根据 how 标志进行相应操作
if(how == SIG_BLOCK)
	sigaddsetmask(&current->blocked, new_set);
else if(how == SIG_UNBLOCK)
	sigdelsetmask(&current->blocked, new_set);
else if(how == SIG_SETMASK)
	current->blocked.sig[0] = new_set;
else
	return -EINVAL;
	
recalc_sigpending(current);
if(oset && copy_to_user(oset, &old_set, sizeof(*oset)))
	return -EFAULT;
return 0;

挂起进程

sigsuspend() 把进程置为 TASK_ITERRUPTIBLE 状态,这发生在把 mask 参数指向的位掩码数组所指定的标准信号阻塞后。
只有当一个非忽略、非阻塞的信号发送到进程后,进程才被唤醒:

sys_sigsuspend() 服务例程:

mask&= ~(sigmask(SIGKILL) | sigmask(SIGSTOP));
saveset = current->blocked;
siginitset(&current->blocked, mask);
recalc_sigpending(current);
regs->eax = -EINTR;
while(1)
{
	current->state = TASK_INTERRUPTIBLE;

	schedule();  // 选择另一个进程运行

	// 当发出 sigsuspend() 的进程又开始执行时
	// do_signal() 传递唤醒了该进程的信号,返回值为 1 时,不忽略该信号
	// 因此返回 -EINTR 出错码后终止
	if(do_signal(regs, &saveset))
		return -EINTR;
}

实时信号的系统调用

实时信号的几个系统调用(rt_sigaction()、rt_sigpending()、rt_sigprocmask() 即 rt_sigsuspend()) 与前面的描述类似。

rt_sigqueueinfo():发送一个实时信号以便把它加入到目标进程的共享信号队列中。
一般通过标准库函数 sigqueue() 调用 rt_sigqueueinfo()。

rt_sigtimedwait():把阻塞的挂起信号从队列中删除而不传递它,并向调用者返回信号编号;
如果没有阻塞的信号挂起,就把当前进程挂起一个固定的时间间隔。
一般通过标准库函数 sigwaitinfo() 和 sigtimedwait() 调用 rt_sigtimedwait()。

猜你喜欢

转载自blog.csdn.net/u012319493/article/details/84936406