在 Linux 系统开发与运维中,并发控制离不开各种类型的锁。理解 Linux 锁有哪几种,以及它们背后的实现机制,是编写稳定高并发程序的基础。不同锁在是否睡眠、是否允许重入、读写偏好等方面存在本质差异,用错类型会导致性能下降甚至死锁。

一、自旋锁(spinlock)
自旋锁是一种典型的忙等待锁。当执行单元尝试获取自旋锁而锁已被占用时,它不会立刻进入睡眠状态,而是在循环中不断检查锁状态,直到锁被释放。这种机制决定了自旋锁适用于临界区代码非常短、且不能睡眠的上下文,例如中断处理程序或持有其他自旋锁的代码路径。
在 Linux 内核中,自旋锁通过原子操作实现。早期版本仅做简单忙等,后来引入了Ticket Spinlock以解决公平性问题和避免饥饿。用户态可通过 pthread_spinlock_t 使用类似逻辑。下面的代码展示了用户态自旋锁的基本用法:
#include <pthread.h>
#include <stdio.h>
pthread_spinlock_t lock;
int counter = 0;
void* worker(void* arg) {
for (int i = 0; i < 100000; i++) {
pthread_spin_lock(&lock);
counter++;
pthread_spin_unlock(&lock);
}
return NULL;
}
int main() {
pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);
pthread_t t1, t2;
pthread_create(&t1, NULL, worker, NULL);
pthread_create(&t2, NULL, worker, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("counter=%dn", counter);
pthread_spin_destroy(&lock);
return 0;
}
自旋锁的优点是不会触发上下文切换,因此在多核机器上锁占用时间极短时效率很高。缺点也明显:若临界区较长,忙等会白白消耗 CPU 周期,在单核系统上甚至可能造成死锁(因为持锁者无法被调度运行)。
此外,自旋锁通常不可递归获取。同一执行流第二次调用获取操作会永久自旋。内核态自旋锁还会在获取时关闭内核抢占,部分类型还会禁止本地中断,使用时必须严格遵循调用约束。
二、互斥锁(mutex)
互斥锁是最常见的睡眠锁。当线程无法获取互斥锁时,它会被挂起到等待队列,让出 CPU 给其他可运行任务,直到锁释放后被唤醒。这种机制适合临界区执行时间不确定或可能进入睡眠的用户态程序。
Linux 用户态的 pthread_mutex_t 在底层依赖 futex(快速用户态互斥量)机制。无竞争时仅在用户态完成原子修改,发生竞争才陷入内核。以下示例展示基本使用:
#include <pthread.h>
#include <stdio.h>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
int shared = 0;
void* add(void* arg) {
for (int i = 0; i < 50000; i++) {
pthread_mutex_lock(&mtx);
shared++;
pthread_mutex_unlock(&mtx);
}
return NULL;
}
int main() {
pthread_t a, b;
pthread_create(&a, NULL, add, NULL);
pthread_create(&b, NULL, add, NULL);
pthread_join(a, NULL);
pthread_join(b, NULL);
printf("shared=%dn", shared);
pthread_mutex_destroy(&mtx);
return 0;
}
互斥锁相比自旋锁的最大优势是节省 CPU,但代价是上下文切换开销。若锁竞争频繁且临界区极小,频繁睡眠唤醒反而比自旋更慢。因此性能敏感场景要结合压测选择。
Linux 内核也提供 struct mutex,它与信号量不同,有严格的所有权概念,只能由持有者释放,且不支持递归(除非使用特定变种)。内核互斥锁还实现了乐观自旋,即在短等待期内先自旋再睡眠,兼顾两者优点。
三、读写锁(rwlock)
读写锁区分读操作与写操作。多个读者可同时持有读锁,但写者必须独占。它适合读多写少的数据结构,如配置缓存、路由表等。Linux 用户态对应 pthread_rwlock_t,内核态有 rwlock_t。
读写锁要注意写者饥饿问题:如果读者持续到来,写者可能长时间拿不到锁。下面代码展示读者写者模型:
#include <pthread.h>
#include <stdio.h>
pthread_rwlock_t rw;
int data = 0;
void* reader(void* arg) {
pthread_rwlock_rdlock(&rw);
printf("read data=%dn", data);
pthread_rwlock_unlock(&rw);
return NULL;
}
void* writer(void* arg) {
pthread_rwlock_wrlock(&rw);
data++;
pthread_rwlock_unlock(&rw);
return NULL;
}
int main() {
pthread_rwlock_init(&rw, NULL);
pthread_t r, w;
pthread_create(&r, NULL, reader, NULL);
pthread_create(&w, NULL, writer, NULL);
pthread_join(r, NULL);
pthread_join(w, NULL);
pthread_rwlock_destroy(&rw);
return 0;
}
读写锁在读者远多于写者时能显著提升并发度。但如果写操作频繁,其复杂性会带来额外开销,且内核态 rwlock_t 本质是自旋型读写锁,使用时同样不能睡眠。
在实际服务开发中,若写冲突不高,也可考虑 RCU(读复制更新)机制替代读写锁。RCU 让读者完全无锁,写者复制新版本再批量回收,是 Linux 内核中高性能读场景的重要方案。
四、信号量(semaphore)
信号量是一个更通用的同步原语,它维护一个计数值,允许最多 N 个执行单元同时进入临界区。二值信号量类似互斥锁,但信号量没有严格的所有权,任何单元都可执行释放操作,且可能支持跨进程。
在 Linux 内核中,信号量用 struct semaphore 表示,down 和 up 分别对应减和加。用户态可使用无名信号量 sem_t。示例如下:
#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>
sem_t sem;
int val = 0;
void* task(void* arg) {
sem_wait(&sem);
val++;
printf("val=%dn", val);
sem_post(&sem);
return NULL;
}
int main() {
sem_init(&sem, 0, 1);
pthread_t t1, t2;
pthread_create(&t1, NULL, task, NULL);
pthread_create(&t2, NULL, task, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
sem_destroy(&sem);
return 0;
}
信号量的灵活性让它可用于生产者消费者模型、资源池限制等。但它比互斥锁更重,且缺乏所有权导致调试困难,用户态新代码多推荐使用互斥锁加条件变量替代。
内核信号量在现代代码中已不鼓励使用,官方更推荐 mutex 或完成量(completion)。不过理解信号量有助于阅读旧驱动与底层同步理论。
五、其他常见锁与机制
除了上述四类,Linux 还提供多种专项机制。比如原子变量(atomic_t)通过 CPU 原子指令实现无锁计数;顺序锁(seqlock)让写者独占、读者重试,适合极少写极多读;RCU 如前文所述;以及 percpu 变量通过避免共享来消除锁。
从选型角度看,单核禁止自旋忙等,中断上下文只能用自旋类锁,用户态长临界区优先互斥锁,读多写少评估读写锁或 RCU。清楚这些锁的差异,才能在排查竞争与优化吞吐时做出正确判断。
| 锁类型 | 睡眠与否 | 典型场景 |
|---|---|---|
| 自旋锁 | 忙等不睡眠 | 中断、极短临界区 |
| 互斥锁 | 睡眠 | 用户态通用临界区 |
| 读写锁 | 读共享写独占 | 读多写少 |
| 信号量 | 可睡眠 | 资源计数、旧内核 |
综合来看,Linux 锁的种类虽多,但核心权衡始终是上下文切换成本、并发粒度与正确性。掌握它们才能让系统既安全又高效。
linux_lockmutexspinlock修改时间:2026-08-01 03:30:37