第一百八十七章:顺序锁
大乘期涉及内核源码:
一
离开藏书阁,林小源来到一座钟楼前。钟楼高耸入云,顶部悬挂着一口巨大的铜钟,钟身上刻着一个不断跳动的数字。
钟楼前站着一位年轻的钟师,手中握着钟锤。
"那个数字是什么?"林小源问道。
"序列号。"钟师答道,"我是顺序锁seqlock的守护者。每次写者修改数据时,序列号加一。读者读取数据前先记下序列号,读完后再检查——如果序列号变了,说明读取期间有写入发生,数据可能不一致,必须重试。"
"那读者岂不是可能白读?"
"可能。"钟师敲了一下铜钟,序列号跳动了一下,"但代价很小——读取不需要任何锁,不需要阻塞写者,只需要检查一个整数。如果读取期间没有写入,一次就读对了。只有写入发生时才需要重试。"
"序列号也有阴阳。"钟师指着铜钟上的数字,"写者进入临界区时加一次,序列号变奇,告诉读者更新正在发生;写者退出时再加一次,序列号变偶。读者只有在开始看到偶数,结束又看到同一个值时,才拿到一致快照。"
"读者必须在临界区里把数据拷出来?"
"对。不能拿着半截引用出去慢慢看。"
钟数初试
seqlock 写者更新进行中时,sequence counter 通常处于奇数还是偶数状态?
二
"顺序锁的设计哲学是'乐观'。"钟师继续说道,"假设读取期间不会被写入。大多数情况下这个假设是对的——jiffies每秒才更新几次,但每秒可能被读取上百万次。对这种场景,顺序锁比读写锁更高效。"
"写者不需要等读者?"
"完全不需要。"钟师强调,"这是顺序锁和读写锁最大的区别。读写锁中,写者必须等所有读者离开;顺序锁中,写者直接修改,读者自己负责检测冲突并重试。写者的开销几乎为零。"
林小源想起了RCU——RCU的写者也需要等待宽限期。顺序锁连这一步都省了。
"前提是写者不能被读者打断太久。"钟师敲了敲钟壁,声音沉下去,"sequence counter 的写侧临界区不能被会执行读侧的上下文抢占或中断。否则读者看到奇数会一直重试;如果读者是实时调度类,甚至可能让内核活锁。"
"那 seqcount_t 和 seqlock_t 有什么区别?"
"seqcount_t 只是计数器,不负责序列化多个写者,写侧必须另有锁。seqlock_t 把 seqcount 和一个 spinlock 包在一起,自动处理写者串行化和不可抢占要求。还有 seqcount_LOCKNAME_t,能把外部锁关联进去,让 lockdep 帮你验证写侧真的持锁。"
写侧之试
只有 sequence 计数、不负责序列化多个写者的原始机制叫什么?
三
"但顺序锁不适合所有场景。"钟师收起钟锤,语气变得严肃,"如果写入很频繁,读者会不断重试,效率反而比读写锁更差。顺序锁最适合的场景是:读取极其频繁,写入很少,而且写入者不能被阻塞。"
"jiffies就是典型场景?"
"没错。"钟师指了指铜钟上的数字,"jiffies是系统时钟滴答计数,每个定时器中断都会更新,但几乎每个内核路径都会读取它。用顺序锁保护jiffies,写入零开销,读取几乎零开销,偶尔重试一下,整体效率极高。"
"还有一条禁忌。"钟师把钟锤收进袖中,"seqlock/seqcount 不能保护包含指针的数据。写者可能释放或替换指针,读者正在追着旧指针走,重试也救不回来。此时要考虑 RCU 或引用计数。"
"如果写入突然变多呢?"
"普通无锁读者可能重试到发狂。seqlock_t 还有锁定读者和条件读者模式:先乐观读,失败太多时转成独占的 locking reader,避免重试风暴。"
指针禁忌
官方 seqlock 文档明确指出,它不能用于保护包含哪类对象的数据?
/*
* 顺序锁 (seqlock):
*
* 原理:
* 维护一个序列号
* 写者获取锁,序列号+1
* 读者读取序列号,读取数据,再检查序列号
* 如果序列号变化,重试
*
* 操作:
* write_seqlock() — 写者获取锁
* write_sequnlock() — 写者释放锁
* read_seqbegin() — 读者开始
* read_seqretry() — 读者检查
*
* 优势:
* 读者不阻塞写者
* 读取开销小
*
* 缺点:
* 读者可能重试
* 不适合写多场景
*
* 使用场景:
* jiffies — 系统时间
* 时间戳
* 统计计数器
*/
/* 模拟顺序锁 */
typedef struct {
unsigned sequence;
pthread_mutex_t lock;
} seqlock_t;
void seqlock_init(seqlock_t *sl) {
sl->sequence = 0;
pthread_mutex_init(&sl->lock, NULL);
}
void write_seqlock(seqlock_t *sl) {
pthread_mutex_lock(&sl->lock);
sl->sequence++;
}
void write_sequnlock(seqlock_t *sl) {
sl->sequence++;
pthread_mutex_unlock(&sl->lock);
}
unsigned read_seqbegin(seqlock_t *sl) {
return sl->sequence;
}
int read_seqretry(seqlock_t *sl, unsigned start) {
return sl->sequence != start;
}
/* 共享数据 */
seqlock_t sl;
int jiffies = 0;
void *writer_thread(void *arg) {
for (int i = 0; i < 5; i++) {
write_seqlock(&sl);
jiffies++;
printf("写者: jiffies = %d\n", jiffies);
write_sequnlock(&sl);
}
return NULL;
}
void *reader_thread(void *arg) {
for (int i = 0; i < 5; i++) {
unsigned seq;
int val;
do {
seq = read_seqbegin(&sl);
val = jiffies;
} while (read_seqretry(&sl, seq));
printf("读者: jiffies = %d\n", val);
}
return NULL;
}
printf("=== 顺序锁 — 读者不阻塞 ===\n\n");
seqlock_init(&sl);
pthread_t writer, reader;
pthread_create(&writer, NULL, writer_thread, NULL);
pthread_create(&reader, NULL, reader_thread, NULL);
pthread_join(writer, NULL);
pthread_join(reader, NULL);
printf("\n--- 顺序锁原理 ---\n");
printf("写者:\n");
printf(" write_seqlock()\n");
printf(" 序列号+1\n");
printf(" 修改数据\n");
printf(" 序列号+1\n");
printf(" write_sequnlock()\n\n");
printf("读者:\n");
printf(" seq = read_seqbegin()\n");
printf(" 读取数据\n");
printf(" if (read_seqretry(seq)) 重试\n\n");
printf("--- 优势 ---\n");
printf("1. 读者不阻塞写者\n");
printf(" 写者不需要等待读者\n\n");
printf("2. 读取开销小\n");
printf(" 只需检查序列号\n\n");
printf("--- 缺点 ---\n");
printf("1. 读者可能重试\n");
printf(" 写入频繁时效率低\n\n");
printf("2. 不适合写多场景\n");
printf(" 写入太多,读者总重试\n\n");
printf("--- 使用场景 ---\n");
printf("1. jiffies:\n");
printf(" 系统时间\n");
printf(" 读取频繁\n");
printf(" 更新固定频率\n\n");
printf("2. 时间戳:\n");
printf(" 读取频繁\n");
printf(" 偶尔更新\n\n");
printf("3. 统计计数器:\n");
printf(" 读取频繁\n");
printf(" 偶尔更新\n");
pthread_mutex_destroy(&sl.lock);#include <stdio.h>
#include <pthread.h>
/*
* 顺序锁 (seqlock):
*
* 原理:
* 维护一个序列号
* 写者获取锁,序列号+1
* 读者读取序列号,读取数据,再检查序列号
* 如果序列号变化,重试
*
* 操作:
* write_seqlock() — 写者获取锁
* write_sequnlock() — 写者释放锁
* read_seqbegin() — 读者开始
* read_seqretry() — 读者检查
*
* 优势:
* 读者不阻塞写者
* 读取开销小
*
* 缺点:
* 读者可能重试
* 不适合写多场景
*
* 使用场景:
* jiffies — 系统时间
* 时间戳
* 统计计数器
*/
/* 模拟顺序锁 */
typedef struct {
unsigned sequence;
pthread_mutex_t lock;
} seqlock_t;
void seqlock_init(seqlock_t *sl) {
sl->sequence = 0;
pthread_mutex_init(&sl->lock, NULL);
}
void write_seqlock(seqlock_t *sl) {
pthread_mutex_lock(&sl->lock);
sl->sequence++;
}
void write_sequnlock(seqlock_t *sl) {
sl->sequence++;
pthread_mutex_unlock(&sl->lock);
}
unsigned read_seqbegin(seqlock_t *sl) {
return sl->sequence;
}
int read_seqretry(seqlock_t *sl, unsigned start) {
return sl->sequence != start;
}
/* 共享数据 */
seqlock_t sl;
int jiffies = 0;
void *writer_thread(void *arg) {
for (int i = 0; i < 5; i++) {
write_seqlock(&sl);
jiffies++;
printf("写者: jiffies = %d\n", jiffies);
write_sequnlock(&sl);
}
return NULL;
}
void *reader_thread(void *arg) {
for (int i = 0; i < 5; i++) {
unsigned seq;
int val;
do {
seq = read_seqbegin(&sl);
val = jiffies;
} while (read_seqretry(&sl, seq));
printf("读者: jiffies = %d\n", val);
}
return NULL;
}
int main() {
printf("=== 顺序锁 — 读者不阻塞 ===\n\n");
seqlock_init(&sl);
pthread_t writer, reader;
pthread_create(&writer, NULL, writer_thread, NULL);
pthread_create(&reader, NULL, reader_thread, NULL);
pthread_join(writer, NULL);
pthread_join(reader, NULL);
printf("\n--- 顺序锁原理 ---\n");
printf("写者:\n");
printf(" write_seqlock()\n");
printf(" 序列号+1\n");
printf(" 修改数据\n");
printf(" 序列号+1\n");
printf(" write_sequnlock()\n\n");
printf("读者:\n");
printf(" seq = read_seqbegin()\n");
printf(" 读取数据\n");
printf(" if (read_seqretry(seq)) 重试\n\n");
printf("--- 优势 ---\n");
printf("1. 读者不阻塞写者\n");
printf(" 写者不需要等待读者\n\n");
printf("2. 读取开销小\n");
printf(" 只需检查序列号\n\n");
printf("--- 缺点 ---\n");
printf("1. 读者可能重试\n");
printf(" 写入频繁时效率低\n\n");
printf("2. 不适合写多场景\n");
printf(" 写入太多,读者总重试\n\n");
printf("--- 使用场景 ---\n");
printf("1. jiffies:\n");
printf(" 系统时间\n");
printf(" 读取频繁\n");
printf(" 更新固定频率\n\n");
printf("2. 时间戳:\n");
printf(" 读取频繁\n");
printf(" 偶尔更新\n\n");
printf("3. 统计计数器:\n");
printf(" 读取频繁\n");
printf(" 偶尔更新\n");
pthread_mutex_destroy(&sl.lock);
return 0;
}道藏笔记
内核启示
顺序锁让读者不阻塞写者。
原理:
- 维护序列号
- 写者修改时序列号+1
- 读者检查序列号,变化则重试
优势:
- 读者不阻塞写者
- 读取开销小
缺点:
- 读者可能重试
- 不适合写多场景
- 不能保护包含指针的数据
- 写侧必须串行化且不能被读侧上下文长时间打断
使用场景:
- jiffies — 系统时间
- 时间戳
- 统计计数器
顺序锁是乐观——假设读取不被中断。
顺序锁之试
顺序锁常保护频繁读取的时间值,本章举例使用的全局节拍变量是什么?