顺序锁

本文详细解释了Linux中的顺序锁(seqlock)机制,包括其与读/写自旋锁的区别、顺序锁的结构、使用顺序锁的条件以及顺序锁在保护数据结构时的优势和局限性。
当使用读/写自旋锁时,内核控制路径发出的执行read_lock或write_lock操作的请求具有相同的优先权:读者必须等待,直到写操作完成。同样地,写者也必须等待,直到读操作完成。

Linux 2.6中引入了顺序锁(seqlock),它与读/写自旋锁非常相似,只是它为写者赋予了较高的优先级:事实上,即使在读者正在读的时候也允许写者继续运行。这种策略的好处是写者永远不会等待读(除非另外一个写者正在写),缺点是有些时候读者不得不反复读多次相同的数据直到它获得有效的结果。

每个顺序锁都是包括两个字段的seqlock_t结构:
typedef struct {
    unsigned sequence;
    spinlock_t lock;
} seqlock_t;

一个类型为spinlock_t的lock字段和一个整型的sequence字段,第二个字段sequence是一个顺序计数器。

每个读者都必须在读数据前后两次读顺序计数器,并检查两次读到的值是否相同,如果不相同,说明新的写者已经开始写并增加了顺序计数器,因此暗示读者刚读到的数据是无效的。

通过把SEQLOCK_UNLOCKED赋给变量seqlock_t或执行seqlock_init宏,把seqlock_t变量初始化为“未上锁”,并把sequence设为0:
#define __SEQLOCK_UNLOCKED(lockname) /
         { 0, __SPIN_LOCK_UNLOCKED(lockname) }

#define SEQLOCK_UNLOCKED /
         __SEQLOCK_UNLOCKED(old_style_seqlock_init)

# define __SPIN_LOCK_UNLOCKED(lockname) /
    (spinlock_t)    {    .raw_lock = __RAW_SPIN_LOCK_UNLOCKED,    /
                SPIN_DEP_MAP_INIT(lockname) }
#define __RAW_SPIN_LOCK_UNLOCKED    { 1 }

写者通过调用write_seqlock()和write_sequnlock()获取和释放顺序锁。write_seqlock()函数获取seqlock_t数据结构中的自旋锁,然后使顺序计数器sequence加1;write_sequnlock()函数再次增加顺序计数器sequence,然后释放自旋锁。这样可以保证写者在整个写的过程中,计数器sequence的值是奇数,并且当没有写者在改变数据的时候,计数器的值是偶数。读者进程执行下面的临界区代码:
    unsigned int seq;
    do {
        seq = read_seqbegin(&seqlock);
        /* ... CRITICAL REGION ... */
    } while (read_seqretry(&seqlock, seq));

read_seqbegin()返回顺序锁的当前顺序号;如果局部变量seq的值是奇数(写者在read_seqbegin()函数被调用后,正更新数据结构),或seq的值与顺序锁的顺序计数器的当前值不匹配(当读者正执行临界区代码时,写者开始工作),read_seqretry()就返回1:
static __always_inline int read_seqretry(const seqlock_t *sl, unsigned iv)
{
    smp_rmb();
    return (iv & 1) | (sl->sequence ^ iv);
}


注意在顺序锁机制里,读者可能反复读多次相同的数据直到它获得有效的结果(read_seqretry返回0)。另外,当读者进入临界区时,不必禁用内核抢占;另一方面,由写者获取自旋锁,所以它进入临界区时自动禁用内核抢占。

并不是每一种资源都可以使用顺序锁来保护。一般来说,必须在满足下述条件时才能使用顺序锁:

1)被保护的数据结构不包括被写者修改和被读者间接引用 的指针(否则,写者可能在读者的眼皮子底下就修改指针)。
2)读者的临界区代码没有副作用(否则,多个读者的操作会与单独的读操作有不同的结果)。

此外,读者的临界区代码应该简短,而且写者应该不常获取顺序锁,否则,反复的读访问会引起严重的开销。在Linux 2.6中,使用顺序锁主要是保护一些与系统时间处理相关的数据结构。
### **顺序加锁后,解锁的正确顺序是什么?** **解锁必须严格按照加锁的逆序(后进先出,LIFO)进行**,即: **加锁顺序:A → B → C → D** **解锁顺序:D → C → B → A** --- ## **1. 为什么必须逆序解锁?** ### **(1) 避免死锁** 如果解锁顺序不一致,可能导致 **锁的释放顺序混乱**,进而影响其他线程的加锁顺序,**破坏顺序加锁的规则**,可能间接导致死锁。 #### **错误示例(解锁顺序不一致)** ```c // 线程1:加锁顺序 A → B → C pthread_mutex_lock(&A); pthread_mutex_lock(&B); pthread_mutex_lock(&C); // 错误解锁:B → C → A(未逆序) pthread_mutex_unlock(&B); // 释放 B,但 C 仍被占用 pthread_mutex_unlock(&C); pthread_mutex_unlock(&A); ``` - 如果另一个线程 **按 A → B → C 加锁**,但 A 被释放,B 仍被占用,可能导致 **死锁**。 ### **(2) 保证资源一致性** - **逆序解锁** 能确保 **临界区操作完全结束** 后释放锁,避免数据竞争。 - **乱序解锁** 可能导致 **部分临界区未完成**,其他线程提前进入,破坏数据一致性。 --- ## **2. 正确解锁示例** ### **(1) C/C++(pthread)** ```c pthread_mutex_lock(&A); // 1. 加锁 A pthread_mutex_lock(&B); // 2. 加锁 B pthread_mutex_lock(&C); // 3. 加锁 C // 临界区操作... pthread_mutex_unlock(&C); // 3. 解锁 C(最后加的锁最先释放) pthread_mutex_unlock(&B); // 2. 解锁 B pthread_mutex_unlock(&A); // 1. 解锁 A ``` ### **(2) C++ RAII(自动管理锁)** ```cpp #include <mutex> std::mutex A, B, C; { std::lock_guard<std::mutex> lockA(A); // 加锁 A std::lock_guard<std::mutex> lockB(B); // 加锁 B std::lock_guard<std::mutex> lockC(C); // 加锁 C // 临界区操作... // RAII 机制保证析构时按 C → B → A 顺序解锁 } ``` ### **(3) Go(defer 保证逆序解锁)** ```go var muA, muB, muC sync.Mutex muA.Lock() // 加锁 A defer muA.Unlock() // 最后解锁 A muB.Lock() // 加锁 B defer muB.Unlock() // 其次解锁 B muC.Lock() // 加锁 C defer muC.Unlock() // 最先解锁 C ``` --- ## **3. 特殊情况处理** ### **(1) 动态锁顺序(如哈希表不同桶锁)** - 如果锁顺序无法静态确定(如遍历哈希表时按 key 加锁),可以使用 **全局锁顺序规则**(如按内存地址排序)。 - **示例:** ```c void lock_buckets(Bucket *a, Bucket *b) { if (a < b) { // 按地址顺序加锁 pthread_mutex_lock(&a->mu); pthread_mutex_lock(&b->mu); } else { pthread_mutex_lock(&b->mu); pthread_mutex_lock(&a->mu); } } ``` ### **(2) 异常或提前返回** - 使用 **RAII(如 C++ `std::lock_guard`)** 或 **Go `defer`** 确保即使发生异常,锁也能正确释放。 --- ## **4. 最佳实践** 1. **始终遵循 “加锁顺序 = 解锁逆序”**。 2. **使用 RAII 或 `defer` 自动管理锁**,避免手动解锁遗漏。 3. **避免嵌套过多锁**(如超过 3 层),否则容易导致逻辑混乱。 4. **测试时模拟锁竞争**,检查死锁可能性。 --- ### **
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值