Linux percpu_rw_semaphoreper-CPU读写信号量与rcu_sync
2026/6/14 12:25:07 网站建设 项目流程

Linux percpu_rw_semaphore per-CPU读写信号量与rcu_sync

percpu_rw_semaphore是Linux内核中一种高性能的读写信号量,其读路径使用per-CPU计数器避免原子操作和缓存行bouncing,写路径借助rcu_sync机制实现高效的读者静默等待.

一、使用场景与设计目标

percpu_rw_semaphore专为读操作极为频繁而写操作极少的场景设计(如全局系统配置的读取和修改).读者几乎不产生任何原子操作开销,写者需要等待所有读者释放读锁.

```c
struct percpu_rw_semaphore {
struct rcu_sync rss; /* RCU同步机制 */
unsigned int __percpu *read_count; /* per-CPU读者计数 */
struct rcuwait writer; /* 写者等待队列 */
wait_queue_head_t wait_writer; /* 写者等待队列头 */
atomic_t block; /* 写者阻塞标志 */
/* 锁依赖跟踪 */
raw_spinlock_t lock; /* 保护refcount */
atomic_t readers_block;
};
```

二、读者快路径

读者获取锁时,仅做per-CPU变量的原子递增:

```c
static inline void percpu_down_read(struct percpu_rw_semaphore *sem)
{
/*
* 检查是否有写者在等待(block标志)
* 无写者: 直接递增per-CPU计数器, 零开销
* 有写者: 走慢路径
*/
if (likely(!atomic_read(&sem->block))) {
this_cpu_inc(*sem->read_count);
/*
* 读内存屏障: 确保读锁获取前的所有读操作不会重排到获取之后
* 同时对写入者可见
*/
smp_mb();
} else {
/* 有写者竞争, 走慢路径 */
__percpu_down_read(sem);
}
}

static inline void percpu_up_read(struct percpu_rw_semaphore *sem)
{
/*
* 如果block标志没有被设置, 直接递减per-CPU计数器
* 如果block标志被设置, 可能需要唤醒写者
*/
if (likely(!atomic_read(&sem->block))) {
smp_mb(); /* 确保临界区的所有读操作在递减前完成 */
this_cpu_dec(*sem->read_count);
} else {
__percpu_up_read(sem);
}
}
```

三、读者慢路径: 有写者竞争时

当检测到有写者等待时,读者走慢路径,确保写者不会被永久饥饿:

```c
static void __percpu_down_read(struct percpu_rw_semaphore *sem)
{
/* 增加per-CPU计数器 */
this_cpu_inc(*sem->read_count);
smp_mb(); /* 确保inc在block检查之前全局可见 */

/*
* 再次检查block标志.
* 如果block已清除(写者已完成), 读者可以继续.
* 如果block仍置位, 读者需要等待.
*/
if (likely(!atomic_read(&sem->block)))
return;

/*
* 写者仍在等待, 先递减计数器(不持有读锁)
* 等待写者完成后再重新获取
*/
this_cpu_dec(*sem->read_count);

/* 等待rcu_sync同步 */
rcu_sync_drain(&sem->rss);

/* 等待写者完成 */
wait_event(sem->wait_writer, !atomic_read(&sem->block));

/* 写者完成后, 重新获取读锁 */
this_cpu_inc(*sem->read_count);
smp_mb();
}
```

四、写者路径

写者需要等待所有读者释放锁.核心机制是先用rcu_sync等待一个GP(Grace Period),然后设置block标志阻止新读者进入,最后等待per-CPU计数器归零.

```c
static inline void percpu_down_write(struct percpu_rw_semaphore *sem)
{
/* 1. 等待一个RCU GP, 确保所有正在运行的读者完成 */
rcu_sync_enter(&sem->rss);

/* 2. 设置block标志: 新读者走慢路径 */
atomic_set(&sem->block, 1);

/* 3. 内存屏障: 确保block设置在读者counter检查之前可见 */
smp_mb();

/*
* 4. 等待所有读者释放读锁.
* 遍历所有CPU, 求和read_count.
* 当所有CPU的read_count均为0时, 写者获得锁.
*/
wait_event(sem->wait_writer,
atomic_read(&sem->readers_block) == 0);

/* 锁依赖跟踪 */
rwsem_acquire(&sem->dep_map, 0, 0, _RET_IP_);
}

static inline void percpu_up_write(struct percpu_rw_semaphore *sem)
{
rwsem_release(&sem->dep_map, _RET_IP_);

/* 1. 清除block标志, 允许读者快路径 */
atomic_set(&sem->block, 0);

/* 2. 唤醒所有等待的读者(包括在__percpu_down_read中等待的) */
wake_up_all(&sem->wait_writer);

/* 3. 退出RCU同步 */
rcu_sync_exit(&sem->rss);
}
```

五、rcu_sync的详细机制

rcu_sync是percpu_rw_semaphore实现的核心,用于高效地等待所有读者退出临界区.

```c
struct rcu_sync {
int gp_state; /* GP状态 */
int gp_count; /* GP计数 */
wait_queue_head_t gp_wait; /* 等待队列 */

struct rcu_head cb_head; /* RCU回调 */
int sync_state;
struct rcu_sync_type *type;
};
```

rcu_sync_enter的执行过程:

```c
void rcu_sync_enter(struct rcu_sync *rsp)
{
int gp_state;

/* 增加gp_count, 标记需要同步 */
gp_state = atomic_inc_return(&rsp->gp_state);

if (gp_state == 1) {
/* 第一个进入者, 需要调度GP */
WRITE_ONCE(rsp->gp_count, 1);
/*
* 调用synchronize_rcu()或call_rcu()
* 等待一个GP后, 所有在GP前的读者确保已退出
*/
rcu_sync_call(rsp);
}

/* 等待GP完成 */
wait_event(rsp->gp_wait, READ_ONCE(rsp->gp_state) == gp_state);
}
```

rcu_sync中使用的synchronize_rcu()确保:当函数返回时,所有在调用前进入RCU读端临界区的读者都已经退出.这是percpu_rw_semaphore能保证写者与读者互斥的关键.

六、percpu_rw_semaphore的工作原理动画化

```
时间轴:

写者: rcu_sync_enter() -> 等待GP -> set block=1 -> 等待read_count==0 -> 持有写锁
读者1: [RCU_READ] 释放 [新读操作快路径]
读者2: [尝试读检测到block,走慢路径]

关键点:
- GP(宽限期)确保在rcu_sync_enter之前的读者完成后,block标志才被设置
- 设置block标志后,所有试图进入的读者都走__percpu_down_read
- 此时写者等待read_count清零即可
```

七、与普通rw_semaphore的性能对比

```c
/* 普通信号量的读者路径: 原子操作 + 可能的自旋 */
void __sched down_read(struct rw_semaphore *sem)
{
/* 原子递减, 缓存行bouncing */
if (unlikely(atomic_long_sub_return_acquire(RWSEM_READER_BIAS,
&sem->count) < 0))
rwsem_down_read_failed(sem); /* 慢路径 */
}

/* percpu_rw_semaphore的读者路径: per-CPU操作, 零原子 */
this_cpu_inc(*sem->read_count);
smp_mb();

/*
* 性能差异:
* 128线程并发读, 普通rw_semaphore: ~2000ns/op (大量缓存行bouncing)
* 128线程并发读, percpu_rw_semaphore: ~50ns/op (per-CPU操作)
*/
```

八、使用场景限制

percpu_rw_semaphore的读者可以睡眠(不禁止抢占),因此不能用在中断上下文或持有spin_lock的代码路径中.同时,由于读者阻塞可能延迟写者,对于写延迟敏感的场景需要权衡.

```c
/* 典型使用者: sysfs属性读取 */
static struct percpu_rw_semaphore sysfs_sem;

ssize_t sysfs_kf_read(struct kernfs_open_file *of, char *buf,
size_t count, loff_t *pos)
{
ssize_t ret;

percpu_down_read(&sysfs_sem);
ret = of->kn->attr.ops->read(of, buf, count, pos);
percpu_up_read(&sysfs_sem);

return ret;
}

ssize_t sysfs_kf_write(struct kernfs_open_file *of, const char *buf,
size_t count, loff_t *pos)
{
ssize_t ret;

percpu_down_write(&sysfs_sem);
ret = of->kn->attr.ops->write(of, buf, count, pos);
percpu_up_write(&sysfs_sem);

return ret;
}
```

percpu_rw_semaphore通过per-CPU计数器和rcu_sync机制,实现了读者零原子操作的高效读写锁.其核心是:读者只操作本地CPU缓存,写者通过RCU GP同步和block标志确保互斥.这一设计在读者远多于写者的场景下,性能远超传统的基于原子操作的读写信号量.

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询