一文讀懂內核順序鎖

Linux 內核有非常多的鎖機制,如:自旋鎖、讀寫鎖、信號量和 RCU 鎖等。本文介紹一種和讀寫鎖比較相似的鎖機制:順序鎖(seqlock)。

順序鎖與讀寫鎖一樣,都是針對多讀少寫且快速處理的鎖機制。而順序鎖和讀寫鎖的區別就在於:讀寫鎖的讀鎖會阻塞寫鎖,而順序鎖的讀鎖不會阻塞寫鎖。

讀鎖原理

爲了讓讀鎖不阻塞寫鎖,讀鎖並不會真正進行上鎖操作。那麼讀鎖是如何避免在讀取臨界區數據時,數據被其他進程修改了?

爲了解決這個問題,順序鎖使用了一種類似於版本號的機制:序號。序號是一個只增不減的計數器,可以從順序鎖對象的定義看出,如下代碼所示:

typedef struct {
     struct seqcount seqcount; // 序號
     spinlock_t lock;          // 自旋鎖,寫鎖上鎖時使用
} seqlock_t;

在讀取臨界區數據前,首先需要調用 read_seqbegin() 函數來獲取讀鎖,read_seqbegin() 函數的核心邏輯是讀取順序鎖的序號。代碼如下所示:

static inline unsigned read_seqbegin(const seqlock_t *sl)
{
    unsigned ret;

repeat:
    // 讀取順序鎖的序號
    ret = sl->sequence;

    // 如果序號是單數,需要重新獲取
    if (unlikely(ret & 1)) {
        ...
        goto repeat;
    }
    ...
    return ret;
}

從上面的代碼可以看出,read_seqbegin() 函數只獲取順序鎖的序號,並不會進行上鎖操作,所以讀鎖並不會阻塞寫鎖。

注意:序號是單數時需要重新獲取的原因,會在分析寫鎖實現原理時說明。

既然讀鎖並不會進行上鎖操作,如果在讀取臨界區數據時,數據被修改了怎麼辦呢?答案就是:在退出臨界區時,比較一下當前順序鎖的序號跟之前讀取的序號是否一致。如果一致表示數據沒有被修改,否則說明數據已經被修改。如果數據被修改了,那麼需要重新讀取臨界區的數據。

比較序號是否一致可以使用 read_seqretry() 函數,所以讀鎖的正確用法如下代碼所示:

do {
    // 獲取順序鎖序號
    unsigned seq = read_seqbegin(&seqlock);
    // 讀取臨界區數據
    ...
} while (read_seqretry(&seqlock, seq)); // 對比序號是否一致?

read_seqretry() 函數的實現非常簡單,如下所示:

static inline unsigned 
read_seqretry(const seqlock_t *sl, unsigned start)
{
    ...
    return sl->sequence != start;
}

從上面代碼可以看出,read_seqretry() 函數只是簡單比較當前序號與之前讀取到的序號是否一致。

寫鎖原理

從上面的分析可知,讀鎖是通過對比前後序號是否一致來判斷數據是否被修改的。那麼序號在什麼時候被修改呢?答案就是:獲取寫鎖時。

獲取寫鎖是通過 write_seqlock() 函數來實現的,其實現也比較簡單,代碼如下所示:

static inline void write_seqlock(seqlock_t *sl)
{
    spin_lock(&sl->lock);

    sl->sequence++;
    ...
}

write_seqlock() 函數首先會獲取自旋鎖(所以寫鎖與寫鎖之間是互斥的),然後對序號進行加一操作。所以,在修改臨界區數據前,寫鎖先會增加序號的值,這樣就會導致讀鎖前後兩次獲取的序號不一致。我們可以用下圖來說明這種情況:

seqlock 原理

可以看出,當在讀臨界區前後獲取的序號值不一致時,就表示數據已經被修改,這時就需要重新讀取被修改後的數據。

寫鎖解鎖也很簡單,代碼如下:

static inline void write_sequnlock(seqlock_t *sl)
{
  ...
 s->sequence++;
 spin_unlock(&sl->lock);
}

解鎖也需要對序號進行加一操作,然後釋放自旋鎖。

由於 write_seqlock() 函數與 write_sequnlock() 函數都會對序號進行加一操作,所以解鎖後,序號的值必定爲雙數。

我們在分析讀鎖時看到,如果序號是單數時會重新獲取序號,直到序號爲雙數爲止。這是因爲序號單數時,表示正在更新數據。此時讀取臨界區的值是沒有意義的,所以需要等到更新完畢再讀取。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/yd4Ov0VXqGXMqNrj9uCWTA