捕获信号时中断系统调用


29

通过阅读read()write()调用上的手册页,可以看出这些调用被信号打断了,而不管它们是否必须阻塞。

特别地,假设

  • 进程为某些信号建立处理程序。
  • O_NONBLOCK 设置(即以阻止模式运行)打开设备(例如,终端)
  • 然后,该过程将进行read()系统调用以从设备读取数据,从而执行内核空间中的内核控制路径。
  • 当程序read()在内核空间中执行时,先前安装了处理程序的信号将传递到该进程,并调用其信号处理程序。

阅读SUSv3“系统接口卷(XSH)”中的手册页和相应部分,您会发现:

一世。如果a read()在读取任何数据之前被信号打断(即由于没有可用数据而不得不阻塞),则它将返回-1并将其errno设置为[EINTR]。

ii。如果a read()在成功读取某些数据后被信号中断(即可以立即开始处理请求),则它将返回读取的字节数。

问题A): 我是否正确地假设在任何一种情况下(块/无块)信号的传递和处理对信号都不完全透明read()

案例一 这似乎是可以理解的,因为阻塞read()通常会将进程置于TASK_INTERRUPTIBLE状态,以便在传递信号时,内核会将进程置于TASK_RUNNING状态。

但是,当read()不需要阻塞(情况ii。)并在内核空间中处理请求时,我会认为信号的到达及其处理将是透明的,就像硬件的到达和正确处理一样中断会。特别是,我会假设在传递信号后,该过程将被暂时置于用户模式下以执行其信号处理程序,然后该信号处理程序将最终返回该用户处理程序,以完成对中断read()(在内核空间中)的处理,从而read()运行其直到完成为止,然后过程返回到调用read()(在用户空间中)之后的点,结果将读取所有可用字节。

但是ii。似乎暗示read()中断了,因为数据立即可用,但是它返回时仅返回部分数据(而不是全部)。

这使我想到了第二个(也是最后一个)问题:

问题B): 如果我在A)下的假设是正确的,那么read()即使不需要阻塞,因为有可用的数据可以立即满足请求,为什么会被中断?换句话说,为什么read()在执行信号处理程序后不能恢复,最终导致所有可用数据(毕竟是可用的)被返回?

Answers:


29

简介:您是正确的,无论是在情况i(未读取任何内容而中断)还是情况ii(在部分读取后中断)时,接收信号都不是透明的。否则,我将需要对操作系统的体系结构和应用程序的体系结构进行根本性的更改。

操作系统实现视图

考虑如果系统调用被信号中断会发生什么情况。信号处理程序将执行用户模式代码。但是syscall处理程序是内核代码,并且不信任任何用户模式代码。因此,让我们探讨一下syscall处理程序的选择:

  • 终止系统调用;报告对用户代码完成了多少操作。如果需要,由应用程序代码以某种方式重新启动系统调用。这就是unix的工作方式。
  • 保存系统调用的状态,并允许用户代码继续调用。由于以下几个原因,这是有问题的:
    • 用户代码运行时,可能会发生某些事情,导致保存状态无效。例如,如果从文件中读取文件,则该文件可能会被截断。因此,内核代码将需要大量逻辑来处理这些情况。
    • 不允许保存状态保留任何锁定,因为无法保证用户代码将继续恢复系统调用,然后将永久保留该锁定。
    • 除了启动系统调用的普通接口外,内核还必须公开新的接口以恢复或取消正在进行的系统调用。在极少数情况下,这很复杂。
    • 保存的状态将需要使用资源(至少要使用内存);这些资源将需要由内核分配和保留,但要根据进程的分配进行计数。这不是无法克服的,但这是一个复杂的过程。
      • 请注意,信号处理程序可能会进行自身中断的系统调用。因此,您不能仅仅拥有涵盖所有可能的系统调用的静态资源分配。
      • 如果无法分配资源怎么办?然后,系统调用无论如何都将失败。这意味着应用程序将需要具有处理这种情况的代码,因此该设计不会简化应用程序代码。
  • 继续进行(但暂停),为信号处理程序创建一个新线程。这又是有问题的:
    • 早期的unix实现每个进程只有一个线程。
    • 信号处理程序可能会冒着超出系统调用的范围的风险。无论如何,这都是一个问题,但是在当前的unix设计中却包含了它。
    • 需要为新线程分配资源;往上看。

中断的主要区别在于中断代码是受信任的,并且受到严格限制。通常不允许分配资源,永远运行,获取锁而不释放它们或进行其他任何讨厌的事情;由于中断处理程序是由OS实现者自己编写的,因此他知道它不会做任何坏事。另一方面,应用程序代码可以执行任何操作。

应用程序设计视图

当应用程序在系统调用过程中中断时,系统调用是否应该继续完成?不总是。例如,考虑一个类似shell的程序,该程序正在从终端读取行,并且用户按下会Ctrl+C触发SIGINT。读取必须完成,这就是信号的全部含义。请注意,此示例显示了read即使尚未读取任何字节系统调用也必须是可中断的。

因此,应用程序必须有一种方法告诉内核取消系统调用。在unix设计下,这是自动发生的:信号使syscall返回。其他设计将要求应用程序有选择地恢复或取消syscall的方法。

read系统调用的方式,是因为它是有道理的原始,由于操作系统的通用设计。粗略地讲,它的意思是“尽可能多地读取数据,直到达到极限(缓冲区大小),但是如果发生其他情况,则停止读取”。实际读取一个完整的缓冲区需要read循环运行,直到读取了尽可能多的字节为止。这是一个更高级别的函数fread(3)。与read(2)系统调用不同的是,fread库函数在上的用户空间中实现read。它适用于读取文件或尝试尝试的应用程序;它不适用于命令行解释器或必须干净地限制连接的联网程序,也不适合具有并发连接且不使用线程的联网程序。

Robert Love的Linux系统编程提供了循环读取的示例:

ssize_t ret;
while (len != 0 && (ret = read (fd, buf, len)) != 0) {
  if (ret == -1) {
    if (errno == EINTR)
      continue;
    perror ("read");
    break;
  }
  len -= ret;
  buf += ret;
}

它关心case icase ii和几个。


非常感谢Gilles给出的非常简洁明了的答案,该答案证实了有关UNIX设计哲学的文章中提出的类似观点。似乎非常令人信服的是,系统调用中断行为与UNIX设计理念有关,而不是技术约束或障碍
darbehdar 2011年

@darbehdar这是全部三个:UNIX设计理念(这里主要是进程不如内核受信任,并且可以运行任意代码,并且进程和线程不是隐式创建的),技术约束(对资源分配)和应用程序设计(有是信号必须取消syscall的情况。
吉尔(Gilles)“所以,别再邪恶了”,

2

要回答问题A

是的,信号的传递和处理对并非完全透明read()

read()同时,它是由信号中断运行中途可能会占用一些资源。信号的信号处理程序也可以调用另一个read()(或任何其他异步信号安全syscall)。因此,read()必须先停止被信号中断的操作,以释放它使用的资源,否则read()从信号处理程序进行的调用将访问相同的资源并导致重入问题。

因为系统调用不同于read()可能从信号处理程序调用的系统调用,并且它们也可能占用相同的资源集read()。为了避免上述重入问题,最简单,最安全的设计是read()在运行期间每次发生信号时都停止中断。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.