我读了几本Linux的书和教程,他们都说内核在内核从内核模式转换到用户模式的时候处理信号,这是完全有道理的,直到我看到并实验了following code:
>cat sig_timing.c
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <stdbool.h>
volatile bool done = false;
static void sig_handler(int signo)
{
printf("Received signal %d (%s), current errno:%s\n", signo, strsignal(signo), strerror(errno));
done = true;
}
int main(int argc, char **argv)
{
signal(SIGALRM, sig_handler);
alarm(3);
while (!done) {
strlen("Hello World!");
}
return 0;
}
>gcc sig_timing.c
>./a.out
Received signal 14 (Alarm clock), current errno:Success
所以主函数在注册信号后进入死循环,循环不调用任何系统调用,所以没有机会进入内核,然后没有从内核模式转换到用户模式,然后应该没有机会调用信号处理程序,对吗?
Later on,谈话主持人解释了发生了什么(我做了一些调整):
发送方内核线程发送CPU间消息,在运行目标进程的CPU上引起硬件中断,使其进入内核处理中断,并返回用户模式。
我并不那么确信:这个解释似乎是说信号发送器和信号接收器在两个CPU硬件线程上运行。2但是没有超线程的CPU怎么办?3进程只在一个CPU线程上运行。4在这种情况下,当用户代码运行一个死循环时,信号是否有机会得到处理?
1条答案
按热度按时间ubby3x7f1#
这将是一个复杂的答案,所以请耐心等待。
优先调度
所有现代操作系统都使用抢占式调度(与协作调度相反),其中硬件定时器被编程为定期引发定时器中断(称为OS“tick”),然后进入OS调度程序。因此,即使用户线程正在运行无限循环,也会偶尔(可能10ms),硬件将引发异常,导致内核陷阱。这允许操作系统运行调度程序,以决定下一个运行哪个用户任务。在您的特定示例中,这使得内核不仅可以调度你的程序(
sig_timing
),而且还可以注意到用户空间警报应该在什么时候被触发。抢先式调度是您即使在单核计算机上也能获得交互式多编程体验的主要原因。CPU在任务之间是分时的,调度程序在每个节拍运行,以便调度不同的任务。
请注意,操作系统计时器周期(10ms)比用户报警时间(3s)小得多,操作系统几乎可以在3s内将信号发送给进程。另一个相关的结果是,如果有另一个高优先级用户空间任务占用CPU,不允许操作系统调度您的程序,则操作系统可以在3s之后发送SIGALRM。
旁注:合作调度
在协作调度中,任务被要求显式地将控制权交给调度程序(通常使用类似
yield
的调用)。在这样的系统中,你的程序可能永远不会收到信号。精确异常和信号
与某些信号(SIGSEGV、SIGILL、SIGBUS)不同,报警信号(SIGALRM)与特定的用户空间指令无关,也不需要“精确”。换句话说,内核不需要非常精确地确定何时发送信号。请记住,对于CPU内核来说,一秒非常长。
然而,精确的异常是由特定指令引起的。SIGSEGV是由权限错误引起的,SIGILL是由非法指令引起的,等等。这些指令会导致硬件直接在指令上引发陷阱,操作系统开始运行。操作系统处理故障,并将相应的信号转发给应用程序,其中包含指令上的精确状态。