捕获信号时中断系统调用

捕获信号时中断系统调用

read()从阅读有关和调用的手册页来看,write()这些调用似乎会被信号中断,无论它们是否必须阻塞。

特别是,假设

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

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

我。如果 aread()在读取任何数据之前被信号中断(即由于没有可用数据而必须阻塞),则返回 -1 并errno设置为 [EINTR]。

二.如果 aread()在成功读取一些数据后被信号中断(即可以立即开始为请求提供服务),则它返回读取的字节数。

问题一): 我是否正确地假设在任何一种情况下(阻止/不阻止)信号的传递和处理对于 来说并不完全透明read()

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

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

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

这引出了我的第二个问题(也是最后一个):

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

答案1

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

操作系统实现视图

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

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

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

应用程序设计视图

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

所以应用程序必须有一种方法告诉内核取消系统调用。在 Unix 设计下,这会自动发生:信号使系统调用返回。其他设计需要一种让应用程序可以随意恢复或取消系统调用的方法。

系统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 i以及case ii其他一些事情。

答案2

回答问题A:

是的,信号的传递和处理对于read().

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

因为除了可以从信号处理程序调用之外的系统调用read(),它们也可能占用相同的资源集read()。为了避免上述的重入问题,最简单、最安全的设计是read()在运行过程中每次有信号发生时就停止中断。

相关内容