本文介绍了 SMPng 架构的当前设计和实现。首先介绍了基本原语和工具。接下来,概述了 FreeBSD 内核的同步和执行模型的一般架构。然后讨论了特定子系统的锁定策略,记录了为每个子系统引入细粒度同步和并行性所采取的方法。最后,提供了详细的实施说明,以激励设计选择,并使读者了解涉及特定原语使用的重要影响。
本文是一个正在进行的工作,将根据与 SMPng 项目相关的正在进行的设计和实施活动进行更新。许多部分目前仅以概述形式存在,但随着工作的进行将会逐步完善。关于本文档的更新或建议可直接发送给文档编辑。
SMPng 的目标是允许内核并发。内核基本上是一个相当庞大和复杂的程序。为了使内核多线程化,我们使用了一些用于使其他程序多线程化的工具。这些工具包括互斥锁、共享/独占锁、信号量和条件变量。有关这些和其他与 SMP 相关术语的定义,请参阅本文的术语表部分。
存在几种处理内存屏障和原子指令的现有方法,所以本部分不会包含太多细节。简单地说,如果在保护对该变量的写入时使用锁定,则不能在没有锁的情况下读取变量。当你考虑到内存屏障仅仅确定了内存操作的相对顺序时,这一点就变得很明显;它们并不对内存操作的时间作出任何保证。也就是说,内存屏障不会强制 CPU 的本地缓存或存储缓冲区刷新。相反,在释放锁时的内存屏障仅确保对受保护数据的所有写入对其他 CPU 或设备可见,如果用于释放锁的写操作可见的话。CPU 可以自由地将数据保留在其缓存或存储缓冲区中,只要它愿意。然而,如果另一个 CPU 对同一数据执行原子指令,那么第一个 CPU 必须保证更新的值对第二个 CPU 可见,并且还需处理内存屏障可能需要的其他任何操作。
例如,假设一个简单的模型,其中数据在进入主内存(或全局缓存)时被视为可见,当一个 CPU 上触发了原子指令时,其他 CPU 的存储缓冲区和缓存必须刷新对该相同缓存行的任何写入,以及在内存屏障后面等待的任何操作。
使用受原子指令保护的项目时需要特别小心。例如,在睡眠互斥锁实现中,我们必须使用 atomic_cmpset 而不是 atomic_set 来打开 MTX_CONTESTED 位。原因是,我们将 mtx_lock 的值读入一个变量,然后根据该读取做出决定。然而,我们读取的值可能过时,或者在我们做决定时可能会更改。因此,当执行 atomic_set 时,它可能设置另一个值上的位,而不是我们做出决定的值。因此,我们必须使用 atomic_cmpset ,只有当我们做出决定的值是最新的和有效的时,才设置该值。
最后,原子指令只允许更新或读取一个项目。如果需要原子地更新多个项目,则必须改用锁。例如,如果必须读取两个计数器,并且这些计数器的值相对一致,则这些计数器必须受到锁的保护,而不是通过单独的原子指令。
读锁不需要像写锁那样强大。两种类型的锁都需要确保它们正在访问的数据不是陈旧的。但是,只有写访问需要独占访问。多个线程可以安全地读取一个值。可以通过多种方式为读取和写入使用不同类型的锁。
首先,通过在写入时使用独占锁,在读取时使用共享锁可以使用 sx 锁。这种方法非常直接。
第二种方法有点更加隐晦。您可以用多个锁来保护数据。然后,对于读取该数据,您只需要拥有其中一个锁的读取锁。但是,要写入数据,您需要拥有所有锁的写锁。这可能会使写入变得非常昂贵,但在数据以各种方式访问时可能会很有用。例如,父进程指针由 proctree_lock sx 锁和每个进程互斥锁保护。有时 proc 锁会更容易,因为我们只是检查我们已经锁定的进程的父进程是谁。然而,其他地方如 inferior 需要通过父指针遍历进程树,并且锁定每个进程既是困难又是痛苦,无法保证您正在检查的条件对于检查和采取的操作都保持有效。
如果您需要一个锁来检查变量的状态,以便根据您读取的状态采取行动,您不能只在读取变量时保持锁定,然后在对您读取的值采取行动之前放弃锁定。一旦您放弃锁定,变量可能会发生更改,使您的决定无效。因此,您必须在读取变量和执行检验结果的操作时保持锁定。
跟随其他多线程 UNIX®内核的模式,FreeBSD 通过为中断处理程序提供自己的线程上下文来处理中断处理程序。为中断处理程序提供上下文允许它们在锁上阻塞。然而,为了避免延迟,中断线程以实时内核优先级运行。因此,中断处理程序不应该执行很长时间,以避免使其他内核线程饥饿。此外,由于多个处理程序可能共享一个中断线程,中断处理程序不应该休眠或使用可休眠锁,以避免使另一个中断处理程序饥饿。
FreeBSD 中当前的中断线程被称为重量级中断线程。之所以这样称呼是因为切换到中断线程涉及完整的上下文切换。在最初的实现中,内核不是抢占式的,因此中断会在中断了内核线程时等待,直到内核线程阻塞或返回用户空间才有机会运行。
为了解决延迟问题,FreeBSD 内核已经被改为抢占式。目前,我们只在释放睡眠互斥锁或中断到来时抢占内核线程。然而,计划是使 FreeBSD 内核完全抢占式,如下所述。
并非所有中断处理程序都在线程上下文中执行。相反,某些处理程序直接在主中断上下文中执行。这些中断处理程序目前被错误地命名为“快速”中断处理程序,因为内核早期版本中使用的 INTR_FAST 标志用于标记这些处理程序。当前仅使用这些类型的中断处理程序的中断是时钟中断和串行 I/O 设备中断。由于这些处理程序没有自己的上下文,因此它们可能不会获取阻塞锁,因此只能使用自旋互斥体。
最后,在 MD 代码中可以增加一种名为轻量级上下文切换的可选优化。由于中断线程在内核上下文中执行,它可以借用任何进程的 vmspace。因此,在轻量级上下文切换中,切换到中断线程不会切换 vmspaces,而是借用被中断线程的 vmspace。为了确保被中断线程的 vmspace 不会在我们之下消失,不允许被中断线程执行,直到中断线程不再借用其 vmspace。当中断线程阻塞或完成时,这种情况可能发生。如果中断线程阻塞,那么当其再次可运行时,它将使用自己的上下文。因此,它可以释放被中断线程。
这种优化的缺点是它们非常机器特定和复杂,因此只有在有大的性能改进时才值得努力。目前还为时过早,实际上,它可能会降低性能,因为几乎所有中断处理程序将立即在 Giant 上阻塞并在阻塞时需要进行线程修复。此外,Mike Smith 提出了一种另一种中断处理的替代方法,是这样工作的:
每个中断处理程序都有两个部分:在主中断上下文中运行的谓词和在自己的线程上下文中运行的处理程序。
如果中断处理程序有一个谓词,那么当触发中断时,将运行谓词。如果谓词返回 true,则假定中断已完全处理,并且内核从中断返回。如果谓词返回 false 或没有谓词,则计划运行线程处理程序。
将轻量级上下文切换适应这种方案可能会相当复杂。由于我们可能希望在将来某个时候转换到这种方案,最好推迟对轻量级上下文切换的工作,直到我们就最终中断处理架构达成一致,并确定轻量级上下文切换如何适应或不适应其中。
内核抢占相当简单。基本思想是 CPU 应始终执行最高优先级的可用工作。至少这是理想状态。有几种情况下,实现理想状态的成本不值得完美。
实现完整的内核抢占非常简单:当您将一个线程调度到运行队列时,您检查其优先级是否高于当前正在执行的线程。如果是,您会启动到该线程的上下文切换。
虽然锁可以保护大多数数据在抢占的情况下,但并非所有内核都是抢占安全的。例如,如果一个持有自旋互斥锁的线程被抢占,新线程尝试获取相同的自旋互斥锁,那么新线程可能会永远自旋,因为被中断的线程可能永远没有机会执行。此外,一些代码,例如在 Alpha 上为进程分配地址空间号的代码在 exec 期间需要不被抢占,因为它支持实际的上下文切换代码。通过使用临界区,这些代码部分禁用了抢占。
临界区 API 的责任是防止在临界区内发生上下文切换。对于一个完全抢占的内核,当前线程以外的每个线程的 setrunqueue 都是一个抢占点。一种实现方法是 critical_enter 设置一个每线程标志,该标志由其对应的部分清除。如果在设置了此标志的情况下调用 setrunqueue ,则无论新线程相对于当前线程的优先级如何,它都不会抢占。但是,由于临界区用于自旋互斥锁以防止上下文切换,并且可以获取多个自旋互斥锁,临界区 API 必须支持嵌套。为此,当前实现使用嵌套计数而不是单一的每线程标志。
为了尽量减少延迟,临界区内的抢占是被推迟而不是丢弃的。如果一个通常会被抢占的线程在当前线程处于临界区时变为可运行状态,则会设置一个每线程标志,以指示存在待处理的抢占。当最外层的临界区退出时,将检查此标志。如果标志被设置,则当前线程将被抢占以允许更高优先级的线程运行。
中断对于自旋互斥锁构成问题。如果低级中断处理程序需要一个锁,它需要不中断任何需要该锁的代码,以避免可能的数据结构损坏。目前,通过临界区 API 的 cpu_critical_enter 和 cpu_critical_exit 函数实现了这种机制。目前,该 API 在 FreeBSD 的所有平台上禁用和重新启用中断。这种方法可能并非完全最佳,但易于理解且易于正确实现。从理论上讲,第二个 API 只需要用于主中断上下文中使用的自旋互斥锁。然而,为了使代码更简单,它用于所有自旋互斥锁甚至所有临界区。将 MD API 与 MI API 分离并仅在自旋互斥锁实现中与 MI API 一起使用可能是可取的。如果采用这种方法,则很可能需要对 MD API 进行重命名以显示它是一个单独的 API。
正如前面提到的,已经做出了一些权衡,牺牲了完美抢占可能并不总是提供最佳性能的情况。
第一个折衷是抢占代码不考虑其他 CPU。假设我们有两个 CPU A 和 B,A 的线程优先级为 4,B 的线程优先级为 2。如果 CPU B 使优先级为 1 的线程可运行,则从理论上讲,我们希望 CPU A 切换到新线程,这样我们将运行两个最高优先级的可运行线程。然而,确定要在哪个 CPU 强制执行抢占以及实际通过 IPI 信号传递给该 CPU 以及所需的同步成本将是巨大的。因此,当前代码将强制 CPU B 切换到更高优先级的线程。请注意,这仍然使系统处于更好的位置,因为 CPU B 正在执行优先级为 1 的线程,而不是优先级为 2 的线程。
第二个折衷限制立即内核抢占为实时优先级内核线程。在上面定义的抢占的简单情况下,如果一个更高优先级的线程变为可运行状态,线程将立即被抢占(或者在关键部分退出时)。然而,许多在内核中执行的线程只在内核环境中短暂执行一段时间,然后阻塞或返回到用户空间。因此,如果内核抢占这些线程以运行另一个非实时内核线程,那么内核可能会在执行线程准备睡眠或执行之前切换出执行线程。CPU 上的缓存必须调整到新线程。当内核返回到被抢占的线程时,它必须重新填充丢失的所有缓存信息。此外,执行了两次额外的上下文切换,如果内核延迟抢占直到第一个线程阻塞或返回到用户空间,则可以避免这两次额外的上下文切换。因此,默认情况下,如果更高优先级的线程是实时优先级线程,抢占代码将立即抢占。
打开所有内核线程的完全内核抢占对于调试工具具有价值,因为它暴露了更多的竞态条件。在单处理器系统上,这尤其有用,否则很难模拟许多竞态条件。因此,有一个内核选项 FULL_PREEMPTION 可以启用所有内核线程的抢占,可用于调试目的。
简而言之,当线程从一个 CPU 移动到另一个 CPU 时发生线程迁移。在非抢占内核中,这只能发生在明确定义的点,比如调用 msleep 或返回到用户空间时。然而,在抢占内核中,中断可以在任何时候强制执行抢占并可能发生迁移。这可能会对每个 CPU 数据产生负面影响,因为除了 curthread 和 curpcb 之外,在你迁移时数据可能会随时更改。由于你可能随时迁移,这使得未受保护的每个 CPU 数据访问变得相当无用。因此,希望能够禁用对需要每个 CPU 数据保持稳定的代码段的迁移。
当前临界区阻止了迁移,因为它们不允许上下文切换。然而,在某些情况下,这可能是一种太强的要求,因为临界区实际上还会阻止当前处理器上的中断线程。因此,另一个 API 已经提供,允许当前线程指示,如果它被抢占,则不应迁移到另一个 CPU。
此 API 被称为线程固定,由调度程序提供。该 API 包括两个函数: sched_pin 和 sched_unpin 。这些函数管理每个线程的嵌套计数 td_pinned 。当嵌套计数大于零时,线程被固定,线程初始时未固定,嵌套计数为零。每个调度程序实现都需要确保只有在第一次调用 sched_pin 时,固定的线程才会在执行时所在的 CPU 上执行。由于嵌套计数只有线程本身编写,并且只有在固定线程未执行但持有 sched_lock 时由其他线程读取时, td_pinned 不需要任何锁定。 sched_pin 函数增加嵌套计数, sched_unpin 函数减少嵌套计数。请注意,这些函数仅在当前线程上操作,并将当前线程绑定到其在执行时所在的 CPU。要将任意线程绑定到特定 CPU,应改用 sched_bind 和 sched_unbind 函数。
内核设施允许内核服务注册函数以作为软件中断的一部分执行。事件根据所需的时钟滴答数进行调度,并在大约正确的时间调用消费者提供的函数的回调。
挂起超时事件的全局列表由全局自旋互斥锁保护;所有对超时列表的访问都必须在持有此互斥锁的情况下执行。当唤醒时,它扫描挂起超时列表以查找应触发的超时。为避免锁定顺序颠倒,当调用提供的回调函数时,线程将释放互斥锁。如果在注册期间未设置标志,则在调用调用之前将抓取 Giant,然后在之后释放。在继续之前,将重新抓取互斥锁。代码在释放互斥锁时小心地保持列表处于一致状态。如果启用了,则会测量执行每个函数所需的时间,并在超过阈值时生成警告。
struct ucred 是内核的内部凭据结构,通常用作内核内的基础进程驱动访问控制。BSD 派生系统使用“写时复制”模型来处理凭据数据:凭据结构可能存在多个引用,当需要进行更改时,结构会被复制,修改,然后替换引用。由于广泛缓存凭据以实现对打开的访问控制,这导致了大量的内存节省。随着向细粒度 SMP 的转变,这种模型还通过要求仅在未共享的凭据上进行修改,从而避免在使用已知共享凭据时需要显式同步,大大节省了锁操作。
具有单个引用的凭据结构被视为可变的;共享凭据结构不得修改,否则会存在竞争条件。一个互斥体, cr_mtxp 保护 struct ucred 的引用计数,以保持一致性。对结构的任何使用都需要在使用期间具有有效引用,否则结构可能会在非法使用者的情况下被释放。
struct ucred 互斥锁是一个叶子互斥锁,通过互斥锁池实现,以提高性能。
通常,凭据在访问控制决策中以只读方式使用,在这种情况下,通常优先选择 td_ucred ,因为它不需要锁定。当进程的凭据更新时,必须跨检查和更新操作保持 proc 锁,从而避免竞争。进程凭据 p_ucred 必须用于检查和更新操作,以防止时间检查、时间使用竞争。
如果系统调用在更新进程凭据后执行访问控制,那么 td_ucred 的值也必须刷新为当前进程值。这将防止在更改后使用过时的凭据。内核会自动从进程 p_ucred 刷新线程结构中的 td_ucred 指针,每当进程进入内核时,允许使用新的凭据进行内核访问控制。
细节待定。
struct prison 存储与使用 jail(2) API 创建的 jails 的维护相关的管理详细信息。这包括每个 jail 主机名、IP 地址和相关设置。由于结构实例的指针被许多凭据结构共享,因此此结构是引用计数的。一个互斥锁 pr_mtx 保护对引用计数和结构 jail 内所有可变变量的读写访问。一些变量仅在创建 jail 时设置,对 struct prison 的有效引用足以读取这些值。每个条目的精确锁定通过 sys/jail.h 中的注释记录。
TrustedBSD MAC 框架以各种内核对象的形式维护数据,如 struct label 所示。一般来说,内核对象中的标签受到与内核对象的其余部分相同的锁的保护。例如,在 struct vnode 中的 v_label 标签受 vnode 锁的保护。
除了标准内核对象中维护的标签之外,MAC 框架还维护着注册的活跃策略列表。该策略列表受全局互斥锁( mac_policy_list_lock )和一个繁忙计数(也受互斥锁保护)的保护。由于许多访问控制检查可能同时发生,只读访问策略列表需要持有互斥锁同时递增(后来递减)繁忙计数。在 MAC 条目操作期间不必一直持有互斥锁——一些操作,例如对文件系统对象上的标签操作,是长时间的。要修改策略列表,比如在策略注册和注销期间,必须持有互斥锁并且引用计数必须为零,以防止在使用列表时对其进行修改。
控制变量 mac_policy_list_not_busy 可用于需要等待列表变得不繁忙的线程,但只有在调用者未持有其他锁时才能等待此条件变量,否则可能会发生锁顺序违规问题。实际上,繁忙计数充当了对框架访问的一种共享/互斥锁:不同的是,与 sx 锁不同,等待列表变得不繁忙的消费者可能会被饿死,而不是允许锁顺序问题关于繁忙计数和可能在进入(或内部)MAC 框架时持有的其他锁。
对于模块子系统,存在一个用于保护共享数据的单个锁。此锁是一个共享/排他(SX)锁,并且很有可能需要被获取(共享或排他),因此添加了一些宏以使对锁的访问更加容易。这些宏可以在 sys/module.h 中找到,从使用角度来看相当基础。在此锁保护之下的主要结构是 module_t 结构(在共享时)和全局 modulelist_t 结构,模块。应该查看 kern/kern_module.c 中的相关源代码以进一步了解锁定策略。
新总线系统将有一个 SX 锁。读取者将持有共享(读取)锁(sx_slock(9)),写入者将持有排他(写入)锁(sx_xlock(9))。内部函数将根本不进行锁定。外部可见的函数将根据需要进行锁定。那些无论比赛赢还是输都无关紧要的项目将不被锁定,因为它们往往在各个地方被读取(例如 device_get_softc(9))。对新总线数据结构的更改将相对较少,因此一个单独的锁应该足够,并且不会造成性能损失。
…
进程层次结构
进程锁,引用
系统调用期间要冻结的进程条目的线程特定副本,包括 td_ucred
进程间操作
进程组和会话
对 sched_lock 的大量引用,以及指向文档中特定原语和相关魔法的注释。
select 和 poll 函数允许线程阻塞,等待文件描述符上的事件发生,最常见的是文件描述符是否可读或可写。
…
SIGIO 服务允许进程请求在指定文件描述符的读/写状态发生变化时向其进程组传递 SIGIO 信号。在任何给定内核对象上,最多允许有一个进程或进程组注册 SIGIO,该进程或组被称为所有者。支持 SIGIO 注册的每个对象都包含一个指针字段,如果对象未注册,则该字段值为 NULL ,或者指向描述注册的 struct sigio 。该字段受全局互斥锁 sigio_lock 保护。调用 SIGIO 维护函数时,必须按引用传递此字段,以便在未受锁保护时不会生成字段的本地注册副本。
为与任何进程或进程组关联的每个注册对象分配一个 struct sigio ,并包含指向对象、所有者、信号信息、凭证和注册的一般处理的反向指针。每个进程或进程组包含一个注册 struct sigio 结构的列表, p_sigiolst 用于进程, pg_sigiolst 用于进程组。这些列表分别由进程或进程组锁保护。每个 struct sigio 中的大多数字段在注册期间保持不变,但 sio_pgsigio 字段除外,它将 struct sigio 链接到进程或进程组列表中。实现支持 SIGIO 的新内核对象的开发人员通常希望在调用支持 SIGIO 函数时避免持有结构锁,例如 fsetown 或 funsetown ,以避免在结构锁和全局 SIGIO 锁之间定义锁定顺序。通过在结构上使用提升的引用计数,例如在管道操作期间依赖于文件描述符引用到管道上,通常可以实现这一点。
sysctl MIB 服务既可以从内核内部调用,也可以从用户空间应用程序使用系统调用调用。锁定引发至少两个问题:首先,保护维护命名空间的结构,其次,与通过 sysctl 接口访问的内核变量和函数的交互。由于 sysctl 允许直接导出(和修改)内核统计信息和配置参数,因此 sysctl 机制必须了解这些变量的适当锁定语义。目前,sysctl 使用单个全局 sx 锁来串行使用 sysctl ;但是,它被假定在 Giant 下运行,并且不提供其他保护措施。本节的其余部分推测了 sysctl 的锁定和语义更改。
需要更改 sysctl 更新值的操作顺序,从读取旧值、复制进和复制出、写入新值改为复制进、锁定、读取旧值和写入新值、解锁、复制出。普通的 sysctl 只需复制出旧值并设置新值(复制进)的可能仍然能够遵循旧模型。不过,对于所有 sysctl 处理程序使用第二个模型可能更为简洁,以避免锁操作。
为了允许常见情况,一个 sysctl 可以在 SYSCTL_FOO 宏和结构体中嵌入一个互斥体的指针。这对大多数 sysctl 是有效的。对于由 sx 锁、自旋互斥体或其他除了单个睡眠互斥体之外的锁定策略保护的值,可以使用 SYSCTL_PROC 节点来正确获取锁定。
任务队列的接口有两个基本锁与其关联,以保护相关的共享数据。 taskqueue_queues_mutex 旨在作为一个用于保护 taskqueue_queues TAILQ 的锁。与该系统关联的另一个互斥锁是 struct taskqueue 数据结构中的一个。使用同步原语的目的是保护 struct taskqueue 中数据的完整性。值得注意的是,没有单独的宏来帮助用户锁定他/她自己的工作,因为这些锁很可能不会在 kern/subr_taskqueue.c 之外使用。
一个睡眠队列是一个结构,它保存了在等待通道上睡眠的线程列表。每个没有在等待通道上睡眠的线程都携带着一个睡眠队列结构。当一个线程在等待通道上阻塞时,它将自己的睡眠队列结构捐赠给该等待通道。与等待通道关联的睡眠队列存储在哈希表中。
睡眠队列哈希表保存了至少有一个阻塞线程的等待通道的睡眠队列。哈希表中的每个条目称为睡眠队列链。该链包含一个睡眠队列的链表和一个自旋互斥锁。自旋互斥锁保护睡眠队列列表以及列表上睡眠队列结构的内容。一个给定等待通道只关联一个睡眠队列。如果多个线程在等待通道上阻塞,那么除第一个线程外,所有其他线程关联的睡眠队列都存储在主睡眠队列的空闲睡眠队列列表中。当一个线程从睡眠队列中移除时,如果它不是队列上唯一睡眠的线程,则它将从主队列的空闲列表中获得一个睡眠队列结构。当最后一个线程恢复时,它将获得主睡眠队列。由于线程可能以不同于添加顺序的顺序从睡眠队列中移除,因此线程可能以不同的睡眠队列结构离开睡眠队列。
sleepq_lock 函数锁定映射到特定等待通道的睡眠队列链的自旋互斥锁。 sleepq_lookup 函数在哈希表中查找与给定等待通道关联的主睡眠队列。如果找不到主睡眠队列,则返回 NULL 。 sleepq_release 函数解锁与给定等待通道关联的自旋互斥锁。
通过 sleepq_add 函数将线程添加到休眠队列中。此函数接受等待通道、保护等待通道的互斥量的指针、等待消息描述字符串和标志掩码。在调用此函数之前,应该通过 sleepq_lock 锁定休眠队列链。如果没有互斥量保护等待通道(或者由巨人保护),那么互斥量指针参数应该是 NULL 。标志参数包含一个类型字段,指示线程正在添加到哪种类型的休眠队列,以及一个指示休眠是否可中断的标志( SLEEPQ_INTERRUPTIBLE )。当前只有两种类型的休眠队列:通过 msleep 和 wakeup 函数管理的传统休眠队列( SLEEPQ_MSLEEP )和条件变量休眠队列( SLEEPQ_CONDVAR )。休眠队列类型和锁指针参数仅用于内部断言检查。调用 sleepq_add 的代码应该在通过 sleepq_lock 锁定相关休眠队列链并在通过其中一个等待函数阻塞之前,明确解锁任何保护等待通道的互斥量。
通过调用 sleepq_set_timeout 设置休眠的超时。该函数接受等待通道和超时时间(相对滴答计数)作为参数。如果需要通过到达信号中断休眠,则还应调用 sleepq_catch_signals 函数。此函数仅接受等待通道作为其唯一参数。如果这个线程已经有一个挂起的信号,那么 sleepq_catch_signals 将返回一个信号号码;否则,它将返回 0。
一旦线程被添加到睡眠队列中,它就会使用其中一个 sleepq_wait 函数阻塞。根据调用者是否希望使用超时或由捕获的信号或用户线程调度器的中断终止睡眠,有四种等待函数。 sleepq_wait 函数只是等待,直到当前线程被其中一个唤醒函数显式恢复。 sleepq_timedwait 函数等待,直到线程被显式恢复或先前调用 sleepq_set_timeout 设置的超时到期。 sleepq_wait_sig 函数等待,直到线程被显式恢复或其睡眠被终止。 sleepq_timedwait_sig 函数等待,直到线程被显式恢复、先前调用 sleepq_set_timeout 设置的超时到期或线程的睡眠被终止。所有等待函数都接受等待通道作为它们的第一个参数。此外, sleepq_timedwait_sig 函数接受第二个布尔参数,以指示先前调用 sleepq_catch_signals 是否发现挂起的信号。
如果线程被显式恢复或被信号终止,则等待函数返回值为零以表示成功睡眠。如果线程因超时或用户线程调度器的中断而恢复,则返回适当的 errno 值。请注意,由于 sleepq_wait 只能返回 0,因此它不返回任何内容,调用者应假定成功睡眠。此外,如果线程的睡眠超时并同时被终止,则 sleepq_timedwait_sig 将返回一个错误,表示发生了超时。如果返回错误值 0,并且使用了 sleepq_wait_sig 或 sleepq_timedwait_sig 进行阻塞,则应调用 sleepq_calc_signal_retval 函数检查是否有任何挂起的信号,并计算适当的返回值(如果有)。先前调用 sleepq_catch_signals 返回的信号号应作为唯一参数传递给 sleepq_calc_signal_retval 。
被阻塞在等待通道上的线程由 sleepq_broadcast 和 sleepq_signal 函数显式恢复。 这两个函数都接受要恢复线程的等待通道,要提升线程到的优先级以及指示正在恢复哪种类型的睡眠队列的标志参数。 优先级参数被视为最低优先级。 如果正在恢复的线程的优先级(数字较低)已经比优先级参数高,则其优先级不会被调整。 标志参数用于内部断言,以确保睡眠队列不被错误地视为其他类型。 例如,条件变量函数不应该恢复传统睡眠队列上的线程。 sleepq_broadcast 函数恢复所有阻塞在指定等待通道上的线程,而 sleepq_signal 仅恢复阻塞在等待通道上的最高优先级线程。 在调用这些函数之前,应先通过 sleepq_lock 函数锁定睡眠队列链。
睡眠线程可能会被调用 sleepq_abort 函数中断其睡眠。 必须在 sched_lock 保持的情况下调用此函数,并且线程必须排队等候在一个睡眠队列上。 也可以通过 sleepq_remove 函数从特定的睡眠队列中移除线程。 此函数接受线程和等待通道作为参数,并仅在线程位于指定等待通道的睡眠队列上时唤醒线程。 如果线程不在睡眠队列上或者线程位于不同等待通道的睡眠队列上,则此函数不执行任何操作。
与睡眠队列进行比较/对比。
查找/等待/释放。-描述 TDF_TSNOBLOCK 竞争情况。
优先级传播。
我们是否应该要求在调用 mtx_destroy()时拥有互斥锁,因为我们无法安全地断言它们不被其他任何人拥有?
使用临界区…
描述有争议的互斥体竞争
当持有转门链锁时,安全地读取有争议的互斥锁的 mtx_lock 是为什么。
它是做什么的
它是如何工作的
结构 isrc
图 Drivers
我们应该将一个互锁传入 sema_wait 吗?
我们应该有非可休眠 sx 锁吗?
添加有关引用计数正确使用的信息。
如果遵循适当的访问协议,操作是原子的,如果其所有效果对其他 CPU 一起可见。在极端情况下,原子指令直接由机器体系结构提供。在更高的层次上,如果一个结构的几个成员受到锁的保护,那么如果在持有锁的情况下执行所有操作而不在任何操作之间释放锁,则一组操作是原子的。
另请参见操作。
当一个线程在等待锁、资源或条件时,它被阻塞。不幸的是,由于这个原因,这个术语有点多义性。
参见 sleep。
临界区是一段不允许被抢占的代码。使用 critical_enter(9) API 进入和退出临界区。
机器相关。
参见 MI。
内存操作内存操作读取和/或写入内存位置。
机器无关。
参见 MD。
操作请参见内存操作。
主中断上下文主中断上下文是指在中断发生时运行的代码。此代码可以直接运行中断处理程序,也可以调度一个异步中断线程来执行给定中断源的中断处理程序。
实时内核线程高优先级的内核线程。目前,唯一的实时优先级内核线程是中断线程。
另请参见线程。
当线程被阻塞在条件变量或通过 msleep 或 tsleep 阻塞在睡眠队列上时,线程处于睡眠状态。
参见块。
可睡眠锁可睡眠锁是一个可以被处于睡眠状态的线程持有的锁。Lockmgr 锁和 sx 锁目前是 FreeBSD 中唯一的可睡眠锁。最终,一些 sx 锁,如 allproc 和 proctree 锁,可能会变成不可睡眠锁。
参见 sleep。
线程 A 内核线程由结构线程表示。线程拥有锁并持有单个执行上下文。
等待通道一个线程可以在其上休眠的内核虚拟地址。