捕获信号时中断系统调用

dar*_*dar 34 kernel signals system-calls architecture

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

特别地,假设

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

阅读SUSv3 'System Interfaces volume (XSH)' 中的手册页和相应部分,您会发现:

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

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

问题 A): 我是否正确地假设在任何一种情况下(阻止/不阻止)信号的传递和处理对read()?

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

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

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

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

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

Gil*_*il' 35

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

操作系统实现视图

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

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

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

应用程序设计视图

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

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

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

Robert Love 的 Linux System Programming 中提供了循环读取的示例:

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;
}
Run Code Online (Sandbox Code Playgroud)

它关心case icase ii和几个。