> For the complete documentation index, see [llms.txt](https://man.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://man.bsdcn.org/man9/locking.9.md).

# locking(9)

`locking` — 内核同步原语

## 名称

`locking`

## 描述

*FreeBSD* 内核设计为可在多 CPU 上运行，因此提供了多种不同的同步原语，使开发者能够安全地访问和操作多种数据类型。

### 互斥锁

互斥锁（也称"阻塞互斥锁"）是内核中最常用的同步原语。线程在访问与其他线程（包括中断线程）共享的数据之前，会获取（加锁）一个互斥锁，访问完成后再释放（解锁）。如果无法获取互斥锁，请求该锁的线程将等待。互斥锁默认为自适应的，这意味着如果争用互斥锁的持有者当前正在另一个 CPU 上运行，那么尝试获取该互斥锁的线程将自旋，而不是让出处理器。互斥锁完全支持优先级传播。

详见 [mutex(9)](/man9/mutex.9.md)。

### 自旋互斥锁

自旋互斥锁是基本互斥锁的一种变体；两者主要区别在于自旋互斥锁从不阻塞。相反，它们在等待锁被释放时会自旋。为避免死锁，持有自旋互斥锁的线程绝不能让出其 CPU。与普通互斥锁不同，自旋互斥锁在获取时会禁用中断。由于禁用中断的开销可能较大，其获取和释放速度通常较慢。自旋互斥锁仅在绝对必要时才应使用，例如用于保护与中断过滤代码共享的数据（详见 bus\_setup\_intr(9)），或用于调度器内部实现。

### 互斥锁池

对于大多数同步原语（如互斥锁），程序员必须提供内存来保存原语。例如，互斥锁可以嵌入到它所保护的结构内部。互斥锁池提供了一组预分配的互斥锁，从而避免了这一要求。注意，来自池中的互斥锁只能用作叶锁。

详见 [mtx\_pool(9)](/man9/mtx_pool.9.md)。

### 读写锁

读写锁允许多个线程以共享方式访问受保护的数据，或由单个线程以独占方式访问。以共享方式访问的线程被称为*读者*，因为它们只应读取受保护的数据。以独占方式访问的线程被称为*写者*，因为它可以修改受保护的数据。

读写锁可以视为具有共享/独占语义的互斥锁（参见上文及 [mutex(9)](/man9/mutex.9.md)）。读写锁与互斥锁一样支持优先级传播，但优先级仅传播给独占持有者。这一限制源于共享持有者是匿名的。

详见 [rwlock(9)](/man9/rwlock.9.md)。

### 读多锁

读多锁类似于*读写锁*，但针对非常罕见的写锁定进行了优化。*读多锁*通过使用调用方提供的*追踪器*数据结构来跟踪共享持有者，从而实现完整的优先级传播。

详见 [rmlock(9)](/man9/rmlock.9.md)。

### 可睡眠读多锁

可睡眠读多锁是读多锁的一种变体。持有独占锁的线程可以睡眠，但持有共享锁的线程不能睡眠。优先级会传播给共享持有者，但不会传播给独占持有者。

### 共享/独占锁

共享/独占锁类似于读写锁；它们的主要区别在于共享/独占锁可以在无界睡眠期间持有。获取存在竞争的共享/独占锁可能会执行无界睡眠。这类锁不支持优先级传播。

详见 [sx(9)](/man9/sx.9.md)。

### 锁管理器锁

锁管理器锁是可睡眠的共享/独占锁，主要用于 [VFS(9)](/man9/vfs.9.md)（作为 [vnode(9)](/man9/vnode.9.md) 锁）和缓冲区缓存（[BUF\_LOCK(9)](/man9/buf_lock.9.md)）。它们具有其他锁类型所没有的特性，如睡眠超时、阻塞式升级、写者饥饿避免、排空以及互锁互斥锁，但这使得它们在使用和实现上都很复杂；因此应避免使用。

详见 [lock(9)](/man9/lock.9.md)。

### 非阻塞同步

内核提供了 [epoch(9)](/man9/epoch.9.md) 和 [smr(9)](/man9/smr.9.md) 两种机制，可用于在一个或多个写者并发修改数据结构时提供对数据结构的只读访问。具体而言，使用 [epoch(9)](/man9/epoch.9.md) 和 [smr(9)](/man9/smr.9.md) 进行同步的读者不会阻塞写者，这与读写锁不同；它们还有助于确保写者释放的内存不会在所有可能正在访问它的读者完成之前被重用。因此，它们是构建无锁数据结构的有用构建块。

这些机制难以正确使用，应优先采用传统的基于互斥的同步方式，除非性能或非阻塞保证是主要考虑因素。

详见 [epoch(9)](/man9/epoch.9.md) 和 [smr(9)](/man9/smr.9.md)。

### 计数信号量

计数信号量提供了一种同步资源池访问的机制。与互斥锁不同，信号量没有所有者的概念，因此在一个线程需要获取资源而另一个线程需要释放资源的情况下很有用。它们已在很大程度上被弃用。

详见 [sema(9)](/man9/sema.9.md)。

### 条件变量

条件变量与锁配合使用，用于等待某个条件变为真。线程在调用 `cv_wait` 系列函数之一之前必须持有相关联的锁。当线程在某个条件上等待时，该锁会在让出处理器之前被原子地释放，并在函数调用返回之前重新获取。条件变量可与阻塞互斥锁、读写锁、读多锁和共享/独占锁配合使用。

详见 [condvar(9)](/man9/condvar.9.md)。

### 睡眠/唤醒

函数 `tsleep`、`msleep`、`msleep_spin`、`pause`、`wakeup` 和 `wakeup_one` 也处理基于事件的线程阻塞。如果线程必须等待外部事件，它会通过 `tsleep`、`msleep`、`msleep_spin` 或 `pause` 进入睡眠。线程也可以使用锁定原语的睡眠例程 mtx\_sleep(9)、rw\_sleep(9) 或 sx\_sleep(9) 来等待。

与条件变量不同，任意地址都可作为等待通道使用，无需分配专用结构。但是，必须注意确保等待通道地址对于某个事件是唯一的。例如，像 [sx(9)](/man9/sx.9.md) 这样的可睡眠锁的内存地址不能用作睡眠通道，因为锁实现内部会使用同一地址作为等待通道。

参数 `chan` 是一个任意地址，用于唯一标识使线程进入睡眠的事件。所有在单个 `chan` 上睡眠的线程稍后会被 `wakeup`（通常从某个中断例程中调用）唤醒，以表示线程正在阻塞等待的事件已经发生。

包括 `msleep`、`msleep_spin` 在内的多个睡眠函数以及锁定原语睡眠例程都指定了一个额外的锁参数。该锁会在睡眠之前被释放，并在睡眠例程返回之前重新获取。如果 `priority` 包含 `PDROP` 标志，则该锁在返回之前不会被重新获取。该锁用于确保可以原子性地检查条件，并且当前线程可以在不遗漏条件变化或相关唤醒的情况下被挂起。此外，所有睡眠例程在线程挂起期间都会完全释放 `Giant` 互斥锁（即使被递归持有），并在函数返回之前重新获取 `Giant` 互斥锁（恢复任何递归）。

`pause` 函数是一种特殊的睡眠函数，它会等待指定的时间流逝，然后线程才恢复执行。这种睡眠不能通过显式的 `wakeup` 或信号提前终止。

详见 [sleep(9)](/man9/sleep.9.md)。

### Giant

Giant 是一个特殊的互斥锁，用于保护尚没有自己锁的数据结构。由于它提供的语义类似于旧的 spl(9) 接口，Giant 具有以下特殊特征：

* 它是递归的。
* 驱动程序可以通过不将自己标记为 MPSAFE 来请求在其周围加锁 Giant。注意，随着非 MPSAFE 驱动程序要么被正确加锁要么消失，执行此操作的基础设施正在逐渐消失。
* Giant 必须在其他不可睡眠的锁之前加锁。
* Giant 在无界睡眠期间会被释放，并在唤醒后重新获取。
* 内核中有些地方会释放 Giant 然后再重新获取。睡眠锁会在睡眠之前执行此操作。网络或 VM 代码的某些部分也可能这样做。这意味着如果你的代码睡眠，你不能指望 Giant 阻止其他代码运行，即使你希望如此。

## 交互

各种原语可以相互作用，并有若干关于如何组合及不能组合的规则。其中许多规则由 [witness(4)](/man4/witness.4.md) 检查。

### 有界睡眠与无界睡眠

在有界睡眠（也称为"阻塞"）中，恢复线程执行所需的唯一资源是线程等待获取的锁的持有者的 CPU 时间。在无界睡眠（通常简称为"睡眠"）中，线程等待外部事件或条件变为真。特别地，处于有界睡眠的线程依赖链应始终向前推进，因为 CPU 时间总是可用的。这要求处于有界睡眠的线程不会等待一个由处于无界睡眠的线程持有的锁。为避免优先级反转，处于有界睡眠的线程会将其优先级借给它所等待的锁的持有者。

以下原语执行有界睡眠：互斥锁、读写锁和读多锁。

以下原语执行无界睡眠：可睡眠读多锁、共享/独占锁、锁管理器锁、计数信号量、条件变量和睡眠/唤醒。

### 一般原则

* 持有自旋互斥锁时执行任何可能导致让出处理器的操作都是错误的。
* 持有"有界睡眠"组中的任何原语时执行任何可能导致无界睡眠的操作都是错误的。例如，持有互斥锁时尝试获取共享/独占锁是错误的，或者持有读写锁时尝试以 `M_WAITOK` 分配内存是错误的。注意，传递给某个 `sleep` 或 `cv_wait` 函数的锁在线程进入无界睡眠之前会被释放，并不违反此规则。
* 在中断过滤代码中运行时执行任何可能导致让出处理器的操作都是错误的。
* 在中断线程中运行时执行任何可能导致无界睡眠的操作都是错误的。

### 交互表

下表显示了在持有上述讨论的某个锁定原语时可以和不能执行的操作。注意，"睡眠"包括 `sema_wait`、`sema_timedwait`、任何 `cv_wait` 函数以及任何 `sleep` 函数。

| 你想要的：    | 自旋 mtx   | mutex/rw | rmlock | 可睡眠 rm   | sx/lk | 睡眠     |
| -------- | -------- | -------- | ------ | -------- | ----- | ------ |
| 你持有的：    | -------- | -------- | ------ | -------- | ----- | ------ |
| 自旋 mtx   | ok       | no       | no     | no       | no    | no-1   |
| mutex/rw | ok       | ok       | ok     | no       | no    | no-1   |
| rmlock   | ok       | ok       | ok     | no       | no    | no-1   |
| 可睡眠 rm   | ok       | ok       | ok     | ok-2     | ok-2  | ok-2/3 |
| sx       | ok       | ok       | ok     | ok       | ok    | ok-3   |
| lockmgr  | ok       | ok       | ok     | ok       | ok    | ok     |

**1** 在进入睡眠时会原子地释放此原语，并在唤醒时重新获取它的调用（`mtx_sleep`、`rw_sleep`、`msleep_spin` 等）。

**2** 这些情况仅在持有可睡眠读多锁的写锁时才允许。

**3** 虽然可以在持有此锁时睡眠，但也可以使用 `sleep` 函数在进入睡眠时原子地释放此原语，并在唤醒时重新获取。

注意，非阻塞的 try 操作在锁上始终是允许的。

### 上下文模式表

下表显示了在不同上下文中可以使用什么。目前这是一个相当容易记忆的表。

| 上下文：        | 自旋 mtx | mutex/rw | rmlock | 可睡眠 rm | sx/lk | 睡眠 |
| ----------- | ------ | -------- | ------ | ------ | ----- | -- |
| 中断过滤：       | ok     | no       | no     | no     | no    | no |
| 中断线程：       | ok     | ok       | ok     | no     | no    | no |
| callout：    | ok     | ok       | ok     | no     | no    | no |
| 直接 callout： | ok     | no       | no     | no     | no    | no |
| 系统调用：       | ok     | ok       | ok     | ok     | ok    | ok |

## 参见

[lockstat(1)](/man1/lockstat.1.md), [witness(4)](/man4/witness.4.md), [atomic(9)](/man9/atomic.9.md), [BUS\_SETUP\_INTR(9)](/man9/bus_setup_intr.9.md), [callout(9)](/man9/callout.9.md), [condvar(9)](/man9/condvar.9.md), [epoch(9)](/man9/epoch.9.md), [lock(9)](/man9/lock.9.md), [LOCK\_PROFILING(9)](/man9/lock_profiling.9.md), [mtx\_pool(9)](/man9/mtx_pool.9.md), [mutex(9)](/man9/mutex.9.md), [rmlock(9)](/man9/rmlock.9.md), [rwlock(9)](/man9/rwlock.9.md), [sema(9)](/man9/sema.9.md), [sleep(9)](/man9/sleep.9.md), [smr(9)](/man9/smr.9.md), [sx(9)](/man9/sx.9.md)

## 缺陷

可供选择的锁定原语太多了。
