在用户空间实现可取消的系统调用

10
我正在尝试在Linux上实现pthread取消,但不希望出现任何“不愉快的行为”(一些人可能会说是错误),这是我最近其他一些问题的讨论。到目前为止,Linux/glibc对pthread取消的处理方式是将其视为不需要内核支持并且可以纯粹通过在syscall调用之前启用异步取消,并在syscall返回后恢复之前的取消状态来在库级别处理。这至少有两个问题,其中一个非常严重:
  1. 如果syscall分配了资源,取消操作可能在从内核空间返回但用户空间尚未保存返回值之前执行。这会导致资源泄漏,而没有方法使用取消处理程序进行修补。
  2. 如果线程在可取消的syscall处阻塞时处理了信号,则整个信号处理程序将在启用异步取消的情况下运行。这可能非常危险,因为信号处理程序可能调用异步信号安全但不是异步取消安全的函数。

我解决问题的第一个想法是设置一个标志,表示线程处于取消点,而不是启用异步取消,当设置该标志时,取消信号处理程序将检查保存的指令指针是否指向syscall指令(特定于体系结构)。如果是,则表示syscall尚未完成,当信号处理程序返回时将重新启动,因此我们可以取消。如果不是,则我认为syscall已经返回,并延迟取消操作。然而,还存在竞争条件——线程可能根本没有到达syscall指令,这种情况下syscall可能会阻塞并永远不会响应取消操作。另一个小问题是,如果在进入信号处理程序时设置了取消点标志,则从信号处理程序中执行的非可取消的syscalls错误地变成了可取消的。

我正在研究一种新方法,并寻求反馈意见。必须满足以下条件:

  • 在syscall完成之前收到的任何取消请求必须在其阻塞任何显着时间之前被处理,但不能在由于信号处理程序中断而挂起重新启动时处理。
  • 在syscall完成后收到的任何取消请求都必须延迟到下一个取消点。
我心中的想法需要为可取消的系统调用包装器编写专门的汇编代码。基本思路如下:
  1. 将即将到来的系统调用指令的地址推入堆栈。
  2. 将堆栈指针存储在线程本地存储中。
  3. 从线程本地存储中测试取消标志;如果设置了,则跳转到取消程序。
  4. 进行系统调用。
  5. 清除线程本地存储中保存的指针。

然后,取消操作将涉及以下步骤:

  1. 在目标线程的线程本地存储中设置取消标志。
  2. 测试目标线程的线程本地存储中的指针;如果不为空,向目标线程发送取消信号。

然后,取消信号处理程序会执行以下操作:

  1. 检查信号上下文中保存的堆栈指针是否等于线程本地存储中保存的指针。如果不相等,则取消点被信号处理程序打断,现在无需进行任何操作。
  2. 检查程序计数器寄存器(保存在信号上下文中)是否小于等于保存在保存的堆栈指针处的地址。如果是,则表示系统调用尚未完成,我们会执行取消操作。

到目前为止,我唯一看到的问题在于信号处理程序的第1步:如果它决定不执行,则在信号处理程序返回后,线程可能会被阻塞在系统调用上,忽略挂起的取消请求。对此,我看到两个潜在的解决方案:

  1. 在这种情况下,安装一个计时器以向特定线程传递信号,基本上每秒钟重试一次,直到我们走运。
  2. 再次引发取消信号,但在取消信号处理程序返回时不解除取消信号的屏蔽。当中断的信号处理程序返回时,它将自动解除屏蔽,然后我们可以再试一次。但是,这可能会干扰信号处理程序内的取消点行为。

您认为哪种方法最好,或者是否有其他更基本的缺陷我没有注意到?

1个回答

4
解决方案2似乎不像一个hack。我认为它不会引起你所说的问题,因为在系统调用处理程序中调用可取消的系统调用将检查TLS中的取消标志,如果取消信号处理程序已经运行并且已经通过信号掩码进行了更改,则必须已经设置了该标志。
(如果每个阻塞系统调用都采用类似于pselect()的sigmask参数,则实现者可能会更容易些)。

你的括号注释正是理想的解决方案。整个问题源于缺乏任何机制来原子性地取消阻塞和进行系统调用,这需要在用户空间中进行粗略的黑客处理(看起来像内核空间),以解决它。 - R.. GitHub STOP HELPING ICE
你说得对,我的担忧是错误的。在取消信号被“错误地”阻塞的时候,取消标志早已被设置,由信号处理程序执行的任何取消点都将立即生效,无需信号传递。除非发现了意外问题,否则我倾向于将此答案标记为已接受。 - R.. GitHub STOP HELPING ICE
@R.: 我能想到的另一个问题是,您需要在取消操作和系统调用包装器之间设置内存屏障。 - caf
再次感谢!实现似乎按预期工作。 - R.. GitHub STOP HELPING ICE

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接