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 内核邮件列表中概念化,作为 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() 允许服务器在服务请求期间分离上下文的部分内容。对于大型和复杂的中间件应用程序框架,在进程创建后进行 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。

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

上下文。

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

符合

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

参见

clone(2), fork(2)

6) 高级设计

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

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

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

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

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

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

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

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

  4. 7) 低级设计

unshare() 的实现可以分为以下 4 个不同的项目

重组现有的 copy_* 函数

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

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

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

  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() 的测试应测试以下内容

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

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

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

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

  4. 9) 未来工作

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

©内核开发社区。 | 由 Sphinx 5.3.0 & Alabaster 0.7.16 提供技术支持 | 页面源代码