unshare 系统调用

本文档描述了新的系统调用 unshare()。本文档概述了该功能,为什么需要它,如何使用它,它的接口规范、设计、实现以及如何测试它。

变更日志

版本 0.1 初始文档,Janak Desai (janak@us.ibm.com), 2006 年 1 月 11 日

目录

  1. 概述

  2. 优点

  3. 成本

  4. 要求

  5. 功能规范

  6. 高层设计

  7. 底层设计

  8. 测试规范

  9. 未来工作

1) 概述

大多数传统的操作系统内核都支持将线程抽象为进程内的多个执行上下文。这些内核提供特殊的资源和机制来维护这些“线程”。Linux 内核以一种巧妙而简单的方式,不区分进程和“线程”。内核允许进程共享资源,因此它们可以实现传统的“线程”行为,而无需在内核中额外的的数据结构和机制。以这种方式实现线程的力量不仅来自于它的简单性,还来自于允许应用程序程序员在传统线程的全有或全无共享资源的限制之外工作。在 Linux 上,在通过 clone 系统调用创建线程时,应用程序可以选择在线程之间共享哪些资源。

unshare() 系统调用为 Linux 线程模型添加了一个原语,允许线程选择性地“取消共享”在创建时正在共享的任何资源。unshare() 是 Al Viro 在 2000 年 8 月在 Linux-Kernel 邮件列表中,作为关于 Linux 上 POSIX 线程的讨论的一部分而概念化的。对于希望在不创建新进程的情况下控制共享资源的应用程序,unshare() 增强了 Linux 线程的实用性。unshare() 是 Linux 上实现进程/线程概念为虚拟机的可用原语集合的自然补充。

2) 优点

unshare() 对于大型应用程序框架(如 PAM)非常有用,在这些框架中,创建新进程来控制进程资源的共享/取消共享是不可能的。由于在使用 fork 或 clone 创建新进程时默认共享命名空间,因此如果非线程应用程序需要与默认共享命名空间分离,则 unshare() 甚至可以使它们受益。以下列出了两个可以使用 unshare() 的用例。

2.1 每个安全上下文的命名空间

unshare() 可用于使用内核的每个进程命名空间机制实现多实例化目录。多实例化目录(例如每个用户和/或每个安全上下文的 /tmp、/var/tmp 实例,或者每个安全上下文的用户主目录实例)在处理这些目录时隔离用户进程。使用 unshare(),PAM 模块可以在登录时轻松地为用户设置私有命名空间。带有标签系统保护配置文件的通用标准认证需要多实例化目录,但是,随着 Linux 内核中共享树功能的可用性,即使是普通的 Linux 系统也可以通过在登录时设置私有命名空间并多实例化 /tmp、/var/tmp 和系统管理员认为合适的其他目录中受益。

2.2 取消共享虚拟内存和/或打开的文件

考虑一个客户端/服务器应用程序,其中服务器通过创建共享虚拟内存和打开的文件等资源的进程来处理客户端请求。如果没有 unshare(),服务器必须在创建服务请求的进程时决定需要共享什么。unshare() 允许服务器在服务请求期间取消关联上下文的一部分。对于大型和复杂的中间件应用程序框架,在创建进程后取消共享() 的能力非常有用。

3) 成本

为了不重复代码并处理 unshare() 在活动任务上工作(而不是 clone/fork 在新分配的非活动任务上工作)的事实,unshare() 必须对 clone/fork 系统调用使用的 copy_* 函数进行小的重组更改。更改现有、经过充分测试和稳定的代码以实现可能在一开始不会被广泛使用的新功能是有成本的。但是,通过对更改进行适当的设计和代码审查,并为 LTP 创建 unshare() 测试,此新功能的优势可以超过其成本。

4) 要求

unshare() 反转使用 clone(2) 系统调用完成的共享,因此 unshare() 应该具有与 clone(2) 类似的接口。也就是说,由于 clone(int flags, void *stack) 中的标志指定了应该共享什么,因此 unshare(int flags) 中的类似标志应该指定应该取消共享什么。不幸的是,这似乎会颠倒标志的含义,而不是在 clone(2) 中使用它们的方式。但是,没有更简单的解决方案,既不那么令人困惑,又可以在未来无需更改 ABI 的情况下实现增量上下文取消共享。

unshare() 接口应适应将来可能添加的新上下文标志,而无需重新构建旧应用程序。如果添加了新的上下文标志,unshare() 的设计应允许根据需要在需要的基础上逐步取消共享这些资源。

5) 功能规范

名称

unshare - 解除进程执行上下文的部分关联

概要

#include <sched.h>

int unshare(int flags);

描述

unshare() 允许进程解除其当前与其他进程共享的执行上下文部分的关联。当使用 fork(2) 创建新进程时,默认情况下会共享执行上下文的一部分(例如命名空间),而其他部分(例如虚拟内存、打开的文件描述符等)可以通过显式请求在通过 clone(2) 创建进程时共享它们。

unshare() 的主要用途是允许进程控制其共享执行上下文,而无需创建新进程。

flags 参数指定以下常量之一或按位或运算的组合。

CLONE_FS

如果设置了 CLONE_FS,则调用者的文件系统信息将与共享的文件系统信息解除关联。

CLONE_FILES

如果设置了 CLONE_FILES,则调用者的文件描述符表将与共享的文件描述符表解除关联。

CLONE_NEWNS

如果设置了 CLONE_NEWNS,则调用者的命名空间将与共享的命名空间解除关联。

CLONE_VM

如果设置了 CLONE_VM,则调用者的虚拟内存将与共享的虚拟内存解除关联。

返回值

成功时,返回零。失败时,返回 -1,并设置 errno

错误
EPERM 非 root 进程(没有 CAP_SYS_ADMIN 的进程)指定了 CLONE_NEWNS。

没有 CAP_SYS_ADMIN。

ENOMEM 无法分配足够的内存来复制需要取消共享的调用者上下文部分。

需要取消共享的调用者上下文部分。

EINVAL 指定了无效的标志作为参数。

符合

unshare() 调用是 Linux 特有的,不应在打算可移植的程序中使用。

另请参阅

clone(2), fork(2)

6) 高层设计

根据 flags 参数,unshare() 系统调用会分配适当的进程上下文结构,使用当前共享版本的值填充它,将新复制的结构与当前任务结构关联起来,并释放相应的共享版本。由于以下两个原因,clone 的辅助函数 (copy_*) 不能直接被 unshare() 使用。

  1. clone 在新分配的尚未激活的任务结构上操作,而 unshare() 在当前活动的任务上操作。因此,unshare() 必须在关联新复制的上下文结构之前获取适当的 task_lock()。

  2. unshare() 必须在将所有要取消共享的上下文结构与当前任务关联并释放较旧的共享结构之前,分配并复制它们。否则,在尝试因错误而回滚时会产生竞争条件和/或 oops。考虑取消共享虚拟内存和命名空间的情况。在成功取消共享 vm 后,如果在分配新的命名空间结构时系统调用遇到错误,则错误返回代码必须反转 vm 的取消共享。作为反转的一部分,系统调用必须返回到较旧的共享 vm 结构,该结构可能不再存在。

因此,从 copy_* 函数中分配和复制当前上下文结构的代码被移到了新的 dup_* 函数中。现在,copy_* 函数调用 dup_* 函数来分配和复制适当的上下文结构,然后将其与正在构建的任务结构关联。另一方面,unshare() 系统调用执行以下操作:

  1. 检查标志以强制执行缺失但隐含的标志

  2. 对于每个上下文结构,如果 flags 参数中设置了相应的位,则调用相应的 unshare() 辅助函数来分配和复制新的上下文结构。

  3. 如果在分配和复制过程中没有错误,并且存在新的上下文结构,则锁定当前任务结构,将新的上下文结构与当前任务结构关联,并释放当前任务结构上的锁。

  4. 适当释放较旧的、共享的上下文结构。

7) 底层设计

unshare() 的实现可以分为以下 4 个不同的部分:

  1. 现有 copy_* 函数的重组

  2. unshare() 系统调用服务函数

  3. 每个不同进程上下文的 unshare() 辅助函数

  4. 不同架构的系统调用号注册

7.1) copy_* 函数的重组

每个 copy 函数(如 copy_mm、copy_namespace、copy_files 等)大致包含两个组件。第一个组件分配和复制相应的结构,第二个组件将其链接到作为参数传递给 copy 函数的任务结构。第一个组件被拆分成了自己的函数。这些 dup_* 函数分配并复制了相应的上下文结构。重组后的 copy_* 函数调用其对应的 dup_* 函数,然后将新复制的结构链接到调用 copy 函数的任务结构。

7.2) unshare() 系统调用服务函数

  • 检查标志,强制执行隐含标志。如果设置了 CLONE_THREAD,则强制设置 CLONE_VM。如果设置了 CLONE_VM,则强制设置 CLONE_SIGHAND。如果设置了 CLONE_SIGHAND 并且信号也被共享,则强制设置 CLONE_THREAD。如果设置了 CLONE_NEWNS,则强制设置 CLONE_FS。

  • 对于每个上下文标志,使用传递到系统调用的标志和指向新取消共享结构的指针的引用,调用相应的 unshare_* 辅助例程。

  • 如果 unshare_* 辅助函数创建了任何新的结构,则在当前任务上获取 task_lock(),修改适当的上下文指针,并释放任务锁。

  • 对于所有新取消共享的结构,释放相应的较旧的共享结构。

7.3) unshare_* 辅助函数

对于对应于 CLONE_SYSVSEM、CLONE_SIGHAND 和 CLONE_THREAD 的 unshare_* 辅助函数,返回 -EINVAL,因为它们尚未实现。对于其他辅助函数,检查标志值以查看是否需要为该结构取消共享。如果是,则调用相应的 dup_* 函数来分配和复制该结构,并返回指向该结构的指针。

7.4) 最后

适当修改特定于架构的代码以注册新的系统调用。

8) 测试规范

unshare() 的测试应测试以下内容:

  1. 有效标志:测试以检查信号和信号处理程序的 clone 标志(尚未实现取消共享)是否返回 -EINVAL。

  2. 缺失/隐含标志:测试以确保在未指定取消共享文件系统的情况下取消共享命名空间,能够正确地取消共享命名空间和文件系统信息。

  3. 对于四个支持的取消共享(命名空间、文件系统、文件和 vm),验证系统调用是否正确地取消共享相应的结构。验证单独取消共享它们以及彼此组合使用是否按预期工作。

  4. 并发执行:在 shm 段中的地址上使用共享内存段和 futex 来同步大约 10 个线程的执行。让几个线程执行 execve,几个线程执行 _exit,其余线程使用不同的标志组合执行 unshare。验证是否按预期执行了取消共享,并且没有发生 oops 或挂起。

9) 未来工作

unshare() 的当前实现不允许取消共享信号和信号处理程序。信号本身就很复杂,并且取消共享当前正在运行的进程的信号和/或信号处理程序更加复杂。如果将来有特定的需求来允许取消共享信号和/或信号处理程序,则可以将其逐步添加到 unshare(),而不会影响使用 unshare() 的遗留应用程序。