8.4 特定数据的锁策略

8.4.1 凭据

  struct ucred 是内核内部的凭据结构体, 它通常作为内核中以进程为导向的访问控制的依据。 BSD-派生的系统采用一种 “写时复制” 的模型来处理凭据数据: 同一凭据结构体可能存在多个引用, 如果需要对其进行修改, 则这个结构体将被复制、 修改, 然后替换该引用。 由于在打开时用于实现访问控制的凭据快取缓存广泛存在, 这种做法会极大地节省内存。 在迁移到细粒度的 SMP 时, 这一模型也省去了大量的锁操作, 因为只有未共享的凭据才能实施修改, 因而避免了在使用共享凭据时额外的同步操作。

  凭据结构体只有一个引用时, 被认为是可变的; 不允许改变共享的凭据结构体, 否则将可能导致发生竞态条件。 cr_mtxp mutex 用于保护 struct ucred 的引用计数, 以维护其一致性。 使用凭据结构体时, 必须在使用过程中保持有效的引用, 否则它就可能在这个不合理的消费者使用过程中被释放。

  struct ucred mutex 是一种叶 mutex, 出于性能考虑, 它通过 mutex 池实现。

  由于多用于访问控制决策, 凭据通常情况下是以只读方式访问的, 此时一般应使用 td_ucred, 因为它不需要上锁。 当更新进程凭据时, 检查和更新过程中必须持有 proc 锁。 检查和更新操作必须使用 p_ucred, 以避免检查时和使用时的竞态条件。

  如果所调系统调用将在更新进程凭据之后进行访问控制检查, 则 td_ucred 也必须刷新为当前进程的值。 这样做能够避免修改后使用过时的凭据。 内核会自动在进程进入内核时, 将线程结构体的 td_ucred 指针刷新为进程的 p_ucred, 以保证内核访问控制能用到新的凭据。

8.4.2 文件描述符和文件描述符表

  详细内容将在稍后增加。

8.4.3 Jail 结构体

  struct prison 保存了用于维护那些通过 jail(2) API 创建的 jail 所用到的管理信息。 这包括 jail 的主机名、 IP 地址, 以及一些相关的设置。 这个结构体包含引用计数, 因为指向这一结构体实例的指针会在多种凭据结构之间共享。 用了一个 mutex, pr_mtx 来保护对引用计数以及所有 jail 结构体中可变变量的读写访问。 有一些变量只会在创建 jail 的时刻发生变化, 只需持有有效的 struct prison 就可以开始读这些值了。 关于每个项目具体的上锁操作的文档, 可以在 sys/jail.h 的注释中找到。

8.4.4 MAC 框架

  TrustedBSD MAC 框架会以 struct label 的形式维护一系列内核对象的数据。 一般来说, 内核中的 label (标签) 是由与其对应的内核对象同样的锁保护的。 例如, struct vnode 上的 v_label 标签是由其所在 vnode 上的 vnode 锁保护的。

  除了嵌入到标准内核对象中的标签之外, MAC 框架也需要维护一组包含已注册的和激活策略的列表。 策略表和忙计数由一个全局 mutex (mac_policy_list_lock) 保护。 由于能够同时并行地进行许多访问控制检查, 对策略表的只读访问, 在增减忙计数时, 框架的入口处需要首先持有这个 mutex。 MAC 入口操作的过程中并不需要长时间持有此 mutex -- 有些操作, 例如文件系统对象上的标签操作 -- 是持久的。 要修改策略表, 例如在注册和解除注册策略时, 需要持有此 mutex, 而且要求引用计数为零, 以避免在用表时对其进行修改。

  对于需要等待表进入闲置状态的线程, 提供了一个条件变量 mac_policy_list_not_busy, 但这一条件变量只能在调用者没有持有其它锁时才能使用, 否则可能会引发锁逆序问题。 忙计数在整个框架中事实上还扮演了某种形式的 共享/排他 锁的作用: 与 sx 锁不同的地方在于, 等待列表进入闲置状态的线程可以饿死, 而不是允许忙计数和其它在 MAC 框架入口 (或内部) 的锁之间的逆序情况。

8.4.5 模块

  对于模块子系统, 用于保护共享数据使用了一个单独的锁, 它是一个 共享/排他 (SX) 锁, 许多情况需要获得它 (以共享或排他的方式), 因此我们提供了几个方便使用的宏来简化对这个锁的访问, 这些宏可以在 sys/module.h 中找到, 其用法都非常简单明了。 这个锁保护的主要是 module_t (当以共享方式上锁) 和全局的 modulelist_t 这两个结构体, 以及模块。 要更进一步理解这些锁策略, 需要仔细阅读 kern/kern_module.c 的源代码。

8.4.6 Newbus 设备树

  newbus 系统使用了一个 sx 锁。 读的一方应持有共享 (读) 锁 (sx_slock(9)) 而写的一方则应持有排他 (写) 锁 (sx_xlock(9))。 内部函数一般不需要进行上锁, 而外部可见的则应根据需要上锁。 有些项目不需上锁, 因为这些项目在全程是只读的, (例如 device_get_softc(9)), 因而并不会产生竞态条件。 针对 newbus 数据结构的修改相对而言非常少, 因此单个的锁已经足够使用, 而不致造成性能折损。

8.4.7 管道

  ...

8.4.8 进程和线程

  - 进程层次结构

  - proc 锁及其参考

  - 在系统调用过程中线程私有的 proc 项副本, 包括 td_ucred

  - 进程间操作

  - 进程组和会话

8.4.9 调度器

  本文在其它地方已经提供了很多关于 sched_lock 的参考和注释。

8.4.10 Select 和 Poll

  selectpoll 这两个函数允许线程阻塞并等待文件描述符上的事件 -- 最常见的情况是文件描述符是否可读或可写。

  ...

8.4.11 SIGIO

  SIGIO 服务允许进程请求在特定文件描述符的读/写状态发生变化时, 将 SIGIO 信号群发给其进程组。 任意给定内核对象上, 只允许一进程或进程组注册 SIGIO, 这个进程或进程组称为属主 (owner)。 每一支持 SIGIO 注册的对象, 都包含一指针字段, 如果对象未注册则为 NULL, 否则是一指向描述这一注册的 struct sigio 的指针。 这一字段由一全局 mutex, sigio_lock 保护。 调用 SIGIO 维护函数时, 必须以 “传引用” 方式传递这一字段, 以确保本地注册副本的中这个字段不脱离锁的保护。

  每个关联到进程或进程组的注册对象, 都会分配一 struct sigio 结构, 并包括指回该对象的指针、 属主、 信号信息、 凭据, 以及关于这一注册的一般信息。 每个进程或进程组都包含一个已注册 struct sigio 结构体的列表, 对进程来说是 p_sigiolst, 而对进程组则是 pg_sigiolst。 这些表由相应的进程或进程组锁保护。 除了用以将 struct sigio 连接到进程组上的 sio_pgsigio 字段之外, 在 struct sigio 中的多数字段在注册过程中都是不变量。 一般而言, 开发人员在实现新的支持 SIGIO 的内核对象时, 会希望避免在调用 SIGIO 支持函数, 例如 fsetownfunsetown 持有结构体锁, 以免去需要在结构体锁和全局 SIGIO 锁之间定义锁序。 通常可以通过提高结构体上的引用计数来达到这样的目的, 例如, 在进行管道操作时, 使用引用某个管道的文件描述符这样的操作, 就可以照此办理。

8.4.12 Sysctl

  sysctl MIB 服务会从内核内部, 以及用户态的应用程序以系统调用的方式触发。 这会引发至少两个和锁有关的问题: 其一是对维持命名空间的数据结构的保护, 其二是与那些通过 sysctl 接口访问的内核变量和函数之间的交互。 由于 sysctl 允许直接导出 (甚至修改) 内核统计数据以及配置参数, sysctl 机制必须知道这些变量相应的上锁语义。 目前, sysctl 使用一个全局 sx 锁来实现对 sysctl 操作的串行化; 然而, 这些是假定用全局锁保护的, 并且没有提供其它保护机制。 这一节的其余部分将详细介绍上锁和 sysctl 相关变动的语义。

  - 需要将 sysctl 更新值所进行的操作的顺序, 从原先的读旧值、 copyin 和 copyout、 写新值, 改为 copyin、 上锁、 读旧值、 写新值、 解锁、 copyout。 一般的 sysctl 只是 copyout 旧值并设置它们 copyin 所得到的新值, 仍然可以采用旧式的模型。 然而, 对所有 sysctl 处理程序采用第二种模型并避免锁操作方面, 第二种方式可能更规矩一些。

  - 对于通常的情况, sysctl 可以内嵌一个 mutex 指针到 SYSCTL_FOO 宏和结构体中。 这对多数 sysctl 都是有效的。 对于使用 sx 锁、 自旋 mutex, 或其它除单一休眠 mutex 之外的锁策略, 可以用 SYSCTL_PROC 节点来完成正确的上锁。

8.4.13 任务队列 (Taskqueue)

  任务队列 (taskqueue) 的接口包括两个与之关联的用于保护相关数据的锁。 taskqueue_queues_mutex 是用于保护 taskqueue_queues TAILQ 的锁。 与这个系统关联的另一个 mutex 锁是位于 struct taskqueue 结构体上。 在此处使用同步原语的目的在于保护 struct taskqueue 中数据的完整性。 应注意的是, 并没有单独的、 帮助用户对其自身的工作进行锁的细化用的宏, 因为这些锁基本上不会在 kern/subr_taskqueue.c 以外的地方用到。

本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.

如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系<questions@FreeBSD.org>.
关于本文档的问题请发信联系 <doc@FreeBSD.org>.