序列計數器和順序鎖 【ChatGPT】

序列計數器和順序鎖

介紹

序列計數器是一種具有無鎖讀取器(只讀重試循環)和無寫入者飢餓的讀者-寫者一致性機制。它們用於很少寫入數據的情況(例如系統時間),其中讀者希望獲得一致的信息集,並且願意在信息發生變化時重試。

當讀取端臨界區的序列計數在開始時是偶數,並且在臨界區結束時再次讀取相同的序列計數值時,數據集是一致的。數據集中的數據必須在讀取端臨界區內部被複製出來。如果在臨界區開始和結束之間序列計數發生了變化,讀者必須重試。

寫者在其臨界區的開始和結束時遞增序列計數。在開始臨界區後,序列計數是奇數,並指示讀者正在進行更新。在寫端臨界區結束時,序列計數再次變爲偶數,這樣讀者就可以取得進展。

序列計數器寫端臨界區絕不能被讀端部分搶佔或中斷。否則,由於奇數序列計數值和被中斷的寫者,讀者將會因爲整個調度器滴答而旋轉。如果該讀者屬於實時調度類,則它可能會永遠旋轉,內核將會活鎖。

如果受保護的數據包含指針,則無法使用此機制,因爲寫者可能會使讀者正在跟蹤的指針失效。

序列計數器(seqcount_t)

這是原始計數機制,不保護多個寫者。因此,寫端臨界區必須由外部鎖進行串行化。

如果寫串行化原語未隱式禁用搶佔,則必須在進入寫端部分之前顯式禁用搶佔。如果讀取部分可以從硬中斷或軟中斷上下文中調用,則必須在進入寫部分之前分別禁用中斷或底半部。

如果希望自動處理寫者串行化和非可搶佔性的序列計數要求,請改用順序鎖(seqlock_t)。

初始化:

/* 動態 */
seqcount_t foo_seqcount;
seqcount_init(&foo_seqcount);

/* 靜態 */
static seqcount_t foo_seqcount = SEQCNT_ZERO(foo_seqcount);

/* C99 結構初始化 */
struct {
        .seq   = SEQCNT_ZERO(foo.seq),
} foo;

寫路徑:

/* 禁用搶佔的串行化上下文 */

write_seqcount_begin(&foo_seqcount);

/* ... [[寫端臨界區]] ... */

write_seqcount_end(&foo_seqcount);

讀路徑:

do {
        seq = read_seqcount_begin(&foo_seqcount);

        /* ... [[讀端臨界區]] ... */

} while (read_seqcount_retry(&foo_seqcount, seq));

帶關聯鎖的序列計數器(seqcount_LOCKNAME_t)

如序列計數器(seqcount_t)中所述,序列計數寫端臨界區必須進行串行化和非可搶佔。此序列計數器的變體在初始化時將用於寫者串行化的鎖關聯到序列計數器上,從而使lockdep能夠驗證寫端臨界區是否得到適當的串行化。

如果禁用了lockdep,則此鎖關聯是一個NOOP,既不會增加存儲空間,也不會增加運行時開銷。如果啓用了lockdep,則鎖指針將存儲在結構seqcount中,並且lockdep的“鎖已持有”斷言將被注入到寫端臨界區的開始,以驗證其是否得到適當的保護。

對於不隱式禁用搶佔的鎖類型,在寫端函數中將強制執行搶佔保護。

以下帶關聯鎖的序列計數器已定義:

  • seqcount_spinlock_t
  • seqcount_raw_spinlock_t
  • seqcount_rwlock_t
  • seqcount_mutex_t
  • seqcount_ww_mutex_t

序列計數器的讀取和寫入API可以採用普通的seqcount_t或上述任何seqcount_LOCKNAME_t變體。

初始化(用支持的鎖替換“LOCKNAME”):

/* 動態 */
seqcount_LOCKNAME_t foo_seqcount;
seqcount_LOCKNAME_init(&foo_seqcount, &lock);

/* 靜態 */
static seqcount_LOCKNAME_t foo_seqcount =
        SEQCNT_LOCKNAME_ZERO(foo_seqcount, &lock);

/* C99 結構初始化 */
struct {
        .seq   = SEQCNT_LOCKNAME_ZERO(foo.seq, &lock),
} foo;

寫路徑:與序列計數器(seqcount_t)中的相同,同時從已獲取關聯寫串行化鎖的上下文中運行。

讀路徑:與序列計數器(seqcount_t)中的相同。

鎖定序列計數器(seqcount_latch_t)

鎖定序列計數器是一種多版本併發控制機制,其中嵌入的seqcount_t計數器的偶/奇值用於在受保護數據的兩個副本之間進行切換。這使得序列計數器讀取路徑可以安全地中斷自己的寫端臨界區。

當讀端無法保護寫端部分免受讀者中斷時,請使用seqcount_latch_t。這通常是在讀端可以從NMI處理程序中調用時的情況。

有關更多信息,請查看raw_write_seqcount_latch()。

順序鎖(seqlock_t)

這包含了前面討論的序列計數器(seqcount_t)機制,以及用於寫者串行化和非可搶佔性的嵌入式自旋鎖。

如果讀端部分可以從硬中斷或軟中斷上下文中調用,請使用禁用中斷或底半部的寫端函數變體。

初始化:

/* 動態 */
seqlock_t foo_seqlock;
seqlock_init(&foo_seqlock);

/* 靜態 */
static DEFINE_SEQLOCK(foo_seqlock);

/* C99 結構初始化 */
struct {
        .seql   = __SEQLOCK_UNLOCKED(foo.seql)
} foo;

寫路徑:

write_seqlock(&foo_seqlock);

/* ... [[寫端臨界區]] ... */

write_sequnlock(&foo_seqlock);

讀路徑,分爲三類:

  1. 普通序列讀取器永遠不會阻塞寫者,但如果檢測到序列號的變化,它們必須重試,因爲寫者正在進行中。寫者不會等待序列讀取器:
    do {
            seq = read_seqbegin(&foo_seqlock);

            /* ... [[讀端臨界區]] ... */

    } while (read_seqretry(&foo_seqlock, seq));
  1. 鎖定讀取器會在寫者或另一個鎖定讀取器進行中時等待。進行中的鎖定讀取器還會阻止寫者進入其臨界區。此讀鎖是獨佔的。與rwlock_t不同,只有一個鎖定讀取器可以獲取它:
    read_seqlock_excl(&foo_seqlock);

    /* ... [[讀端臨界區]] ... */

    read_sequnlock_excl(&foo_seqlock);
  1. 根據傳遞的標記,條件無鎖讀取器(如1)或鎖定讀取器(如2)。這用於避免在寫活動急劇增加時無鎖讀取器飢餓(太多重試循環)。首先嚐試無鎖讀取(傳遞偶數標記)。如果該嘗試失敗(返回奇數序列計數,用作下一次迭代標記),則無鎖讀取將轉換爲完全鎖定讀取,不需要重試循環:
    /* 標記;偶數初始化 */
    int seq = 0;
    do {
            read_seqbegin_or_lock(&foo_seqlock, &seq);

            /* ... [[讀端臨界區]] ... */

    } while (need_seqretry(&foo_seqlock, seq));
    done_seqretry(&foo_seqlock, seq);

API 文檔

https://www.kernel.org/doc/html/v6.6/locking/seqlock.html#api-documentation

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章