【问题标题】:How do unix signals work?unix 信号是如何工作的?
【发布时间】:2011-05-11 14:47:08
【问题描述】:

信号在 unix 中是如何工作的?我通过 W.R. Stevens 但无法理解。请帮帮我。

【问题讨论】:

标签: c unix signals


【解决方案1】:

信号只不过是进程执行中的一个中断。一个进程可以给自己发信号,也可以将一个信号传递给另一个进程。也许父母可以向其孩子发送信号以终止它,等等。

查看以下链接了解。

https://unix.stackexchange.com/questions/80044/how-signals-work-internally

http://www.linuxjournal.com/article/3985

http://www.linuxprogrammingblog.com/all-about-linux-signals?page=show

【讨论】:

  • 谢谢你。@shivam
【解决方案2】:

将信号设施视为由操作系统(而不是硬件)实现的中断。

当您的程序愉快地遍历其根植于 main() 的执行轨迹时,可能会发生这些中断,导致程序被分派到一个向量(处理程序),在那里运行代码,然后返回到它得到的位置被打断了。

这些中断(信号)可以来自多种来源,例如硬件错误,例如访问错误或未对齐的地址、子进程死亡、用户使用kill 命令生成的信号,或来自其他进程使用kill 系统调用的信号。使用信号的方式是为它们指定处理程序,这些处理程序在信号发生时由操作系统调度。请注意,其中一些信号无法处理,并导致进程直接死亡。

但那些可以处理的,可能非常有用。您可以将它们用于进程间通信,即一个进程向另一个进程发送信号,另一个进程处理它,并在处理程序中做一些有用的事情。如果您向它们发送正确的信号,许多守护进程会做一些有用的事情,例如重新读取配置文件。

【讨论】:

    【解决方案3】:

    上述所有陈述中未解决的一些问题是多核、在接收信号时在内核空间中运行、在接收信号时在内核空间中休眠、系统调用重启和信号处理程序延迟。

    这里有几个问题需要考虑:

    • 如果内核知道需要将信号传递给在 CPU_X 上运行的进程 X,但内核在 CPU_Y 上运行时获悉该信号 (CPU_X!=CPU_Y)。所以内核需要阻止进程在不同的内核上运行。
    • 如果进程在接收信号时在内核空间中运行怎么办?每次进程进行系统调用时,它都会进入内核空间并修改内核空间中的数据结构和内存分配。所有这些黑客攻击是否也发生在内核空间中?
    • 如果进程在内核空间休眠等待其他事件怎么办? (读、写、信号、轮询、互斥只是一些选项)。

    答案:

    • 如果进程在另一个 CPU 上运行,内核将通过跨 CPU 通信向另一个 CPU 传递中断并为其发送消息。另一个 CPU 将在硬件中保存状态并跳转到另一个 CPU 上的内核,然后将在另一个 CPU 上传递信号。这是尽量不在另一个 CPU 上执行进程的信号处理程序的一部分,这会破坏缓存局部性。
    • 如果进程在内核空间中运行,它不会被中断。相反,记录该过程已收到信号。当进程退出内核空间时(在每个系统调用结束时),内核将设置蹦床来执行信号处理程序。
    • 如果进程在内核空间中运行时,在收到信号后到达睡眠函数,则该睡眠函数(这对内核中的所有睡眠函数都是通用的)将检查进程是否有信号未决.如果是这样,它不会使进程进入睡眠状态,而是会在进入内核时取消所有已完成的操作,并在设置蹦床以执行信号处理程序时退出到用户空间,然后重新启动系统称呼。您实际上可以使用siginterrupt(2) 系统调用来控制要中断系统调用的信号以及不希望中断系统调用的信号。当您使用带有SA_RESTART 标志的sigaction(2) 注册信号时,您可以决定是否要为某个信号重新启动系统调用。如果发出系统调用并被信号切断并且没有自动重新启动,您将获得EINTR(中断)返回值,您必须处理该值。您还可以查看restart_syscall(2) 系统调用了解更多详情。
    • 如果进程已经在内核空间休眠/等待(实际上所有休眠/等待总是在内核空间),它会从休眠中唤醒,内核代码会自行清理并在返回用户空间后跳转到信号处理程序如果用户需要,系统调用会自动重新启动(非常类似于前面对进程在内核空间中运行时会发生什么的解释)。

    关于这一切为何如此复杂的几点说明:

    • 您不能仅仅停止在内核空间中运行的进程,因为内核开发人员会分配内存、处理数据结构等等。如果你只是拿走控制权,你会破坏内核状态并导致机器挂起。必须以受控方式通知内核代码,它必须停止运行,返回用户空间并允许用户空间处理信号。这是通过内核中所有(嗯,几乎所有)休眠函数的返回值来完成的。内核程序员应该尊重这些返回值并采取相应的行动。
    • 信号是异步的。这意味着它们应该尽快交付。想象一个只有一个线程的进程,休眠了一个小时,然后收到了一个信号。睡眠在内核内部。所以你除了唤醒内核代码,自行清理,返回用户空间并执行信号处理程序,可能在信号处理程序完成后重新启动系统调用。您当然不希望该进程仅在一小时后执行信号处理程序。然后你期望睡眠恢复。用户空间和内核人员为此付出了巨大的努力。
    • 总而言之,信号类似于中断处理程序,但用于用户空间。这是一个很好的类比,但并不完美。虽然中断处理程序由硬件生成,但一些信号处理程序源自硬件,但大多数只是软件(有关子进程死亡的信号,来自另一个进程使用 kill(2) 系统调用的信号等等)。

    那么信号处理的延迟是多少?

    • 如果收到信号时,其他进程正在运行,则由内核调度程序决定是否让其他进程完成其时间片,然后才传递信号。如果您使用的是常规 Linux/Unix 系统,这意味着您可能会在收到信号之前延迟 1 个或多个时间片(这意味着毫秒,相当于永恒)。
    • 当您收到信号时,如果您的进程是高优先级或其他进程已经获得了它们的时间片,您将很快收到信号。如果您在用户空间运行,您将“立即”获得它,如果您在内核空间运行,您将很快到达睡眠功能或从内核返回,在这种情况下,当您返回用户空间时,您的信号处理程序将被调用。这通常是很短的时间,因为在内核中花费的时间并不多。
    • 如果您正在内核中休眠,并且没有其他任何东西高于您的优先级或需要运行,则处理您的系统调用的内核线程将被唤醒,并在它在进入内核的过程中所做的所有事情之后进行清理,返回用户空间并执行您的信号。这不会花费太长时间(这里说的是微秒)。
    • 如果您正在运行实时版本的 Linux,并且您的进程具有最高的实时优先级,那么您将在触发后很快收到信号。说话时长为 50 微秒甚至更好(取决于我无法进入的其他因素)。

    【讨论】:

      【解决方案4】:

      下面的解释并不准确,它的工作方式在不同系统之间的几个方面有所不同(甚至可能在某些部分的不同硬件上使用相同的操作系统),但我认为它通常很好 足够满足你的好奇心来使用它们。大多数人开始在编程中使用信号时甚至没有这种程度的理解,但在我习惯使用它们之前,我想了解它们。

      信号传递

      操作系统内核有一个称为进程控制块的数据结构,用于每个运行的进程,其中包含有关该进程的数据。这可以通过进程 ID (PID) 进行查找,并包含一个信号操作和待处理信号表。

      当一个信号被发送到一个进程时,操作系统内核将查找该进程的进程控制块并检查信号动作表来定位正在发送的特定信号的动作。如果信号动作值为SIG_IGN,则内核会忘记新信号。如果信号动作值为SIG_DFL,那么内核会在另一个表中查找该信号的默认信号处理动作并执行该动作。如果值是其他任何值,则假定它是进程中的函数地址,信号被发送到应该被调用的进程。 SIG_IGNSIG_DFL 的值是转换为函数指针的数字,其值不是进程地址空间内的有效地址(例如 0 和 1,它们都在第 0 页中,从不映射到进程中)。

      如果进程注册了一个信号处理函数(信号动作值既不是 SIG_IGN 也不是 SIG_DFL),则在挂起的信号表中为该信号创建一个条目,并且该进程被标记为准备好运行(它可能有一直在等待某些事情,例如数据可用于调用 read、等待信号或其他一些事情)。

      现在,下一次运行该进程时,操作系统内核将首先将一些数据添加到堆栈中并更改该进程的指令指针,使其看起来几乎就像该进程本身刚刚调用了信号处理程序一样。这并不完全正确,实际上与实际发生的情况有很大的偏差,我稍后再谈。

      信号处理函数可以做它所做的任何事情(它是代表它被调用的进程的一部分,所以它是在了解程序应该如何处理该信号的情况下编写的)。当信号处理程序返回时,该进程的常规代码再次开始执行。 (再次,不准确,但更多关于下一个)

      好的,以上内容应该已经让您很好地了解了信号是如何传递给进程的。我认为需要这个pretty good idea 版本才能掌握完整的想法,其中包括一些更复杂的东西。

      操作系统内核通常需要知道信号处理程序何时返回。这是因为信号处理程序需要一个参数(这可能需要堆栈空间),您可以在信号处理程序执行期间阻止同一信号被传递两次,和/或在传递信号后重新启动系统调用。要完成这一点,除了堆栈和指令指针的变化之外。

      必须发生的是内核需要让进程告诉它它已经完成了信号处理函数的执行。这可以通过将一段 RAM 映射到进程的地址空间来完成,该地址空间包含进行此系统调用的代码,并使信号处理函数的返回地址(该函数开始运行时堆栈上的顶部值)为这段代码。我认为这就是它在 Linux 中的完成方式(至少是较新的版本)。实现这一点的另一种方法(我不知道这是否完成,但它可能会这样做)将信号处理函数的返回地址设为无效地址(例如 NULL),这将在大多数系统上导致中断,这将再次给予操作系统内核控制权。这如何发生并不重要,但内核必须再次获得控制权以修复堆栈并知道信号处理程序已完成。

      在研究我学到的另一个问题时

      Linux 内核确实为此将一个页面映射到进程中,但是用于注册信号处理程序的实际系统调用(什么 sigaction 调用 )需要一个参数 sa_restore 参数,该地址是应该用作信号处理程序的返回地址,内核只是确保它放在那里。该地址的代码发出I'm done 系统调用(sigreturn),内核知道信号处理程序已经完成。

      信号生成

      我主要假设您首先知道信号是如何产生的。由于某些事情发生,操作系统可以代表进程生成它们,例如计时器到期、子进程死亡、访问不应访问的内存或发出不应访问的指令(不存在的指令)或享有特权的人),或许多其他事情。计时器情况在功能上与其他情况略有不同,因为它可能在进程未运行时发生,因此更像是通过kill 系统调用发送的信号。对于代表当前进程发送的与定时器无关的信号,这些信号是在发生中断时生成的,因为当前进程做错了。这个中断给了内核控制权(就像一个系统调用一样),内核生成要传递给当前进程的信号。

      【讨论】:

      • 太棒了!你从哪里学来的?信号传递,最后一段:“为信号处理函数制作返回值”=您的意思是返回地址,对吗?然后:“Linux内核确实将一个页面映射到进程中”=您的意思是“没有”,对吗?
      • 是的。我通过使用strace、阅读代码并最终编写一个从从未发送过的信号返回的程序了解到这一点。
      • 只有当进程的线程(要传递信号的对象)在准备执行的队列上时才由操作系统传递信号吗?这是有道理的,但我只是想确定
      • @Curious:不。当操作系统决定向进程传递未决信号时可能会有所不同,但信号通常用于唤醒睡眠进程或在等待其他进程时中断进程。在这些情况下,信号传递可能会导致进程移动到就绪队列。
      猜你喜欢
      • 2010-11-16
      • 1970-01-01
      • 2016-07-23
      • 1970-01-01
      • 2016-03-31
      • 1970-01-01
      • 2011-05-30
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多