errseq_t 資料型別

errseq_t 是一種在一個地方記錄錯誤的方法,並允許任意數量的“訂閱者”判斷自上次取樣以來它是否發生了變化。

最初的使用場景是跟蹤檔案同步系統呼叫(fsync、fdatasync、msync 和 sync_file_range)的錯誤,但它可能也適用於其他情況。

它被實現為一個無符號 32 位值。低位用於儲存錯誤程式碼(1 到 MAX_ERRNO 之間)。高位用作計數器。這是透過原子操作而不是加鎖實現的,因此這些函式可以在任何上下文中使用。

請注意,如果頻繁記錄新錯誤,由於我們用於計數器的位數很少,存在發生衝突的風險。

為了緩解這個問題,錯誤值和計數器之間的位被用作一個標誌,以指示自記錄新值以來該值是否已被取樣。這使得我們可以在自上次記錄錯誤以來沒有人取樣過該值時,避免遞增計數器。

因此,我們最終得到一個看起來像這樣的值:

31..13

12

11..0

計數器

SF

錯誤碼

大致思路是讓“觀察者”取樣一個 errseq_t 值並將其作為正在執行的遊標。該值以後可以用於判斷自上次取樣以來是否發生了任何新錯誤,並原子地記錄檢查時的狀態。這使得我們可以在一個地方記錄錯誤,然後有許多“觀察者”可以判斷該值自上次檢查以來是否發生了變化。

一個新的 errseq_t 應該始終清零。全零的 errseq_t 值是一個特殊(但常見)的情況,表示從未發生過錯誤。因此,全零值可以作為“紀元”,如果有人想知道自首次初始化以來是否設定過錯誤。

API 用法

讓我講一個關於一名工人無人機的故事。他總體來說是個好工人,但公司有點……管理層重。今天他得向 77 個主管彙報,明天“大老闆”要從外地過來,他肯定也會考驗這個可憐的傢伙。

他們都把工作交給他——工作多到他記不清誰給了他什麼,但這並不是一個大問題。主管們只是想知道他何時完成了他們目前交給他所有工作,以及他自上次被問及以來是否犯了任何錯誤。

他可能在他們實際沒交給他做的工作上犯了錯誤,但他無法在如此詳細的層面上記錄事情,他能記住的只是他最近犯的錯誤。

這是我們的 worker_drone 表示

struct worker_drone {
        errseq_t        wd_err; /* for recording errors */
};

每天,worker_drone 都從一個空白的狀態開始

struct worker_drone wd;

wd.wd_err = (errseq_t)0;

主管們進來,獲取當天最初的讀數。他們不關心在他們監視開始之前發生的任何事情。

struct supervisor {
        errseq_t        s_wd_err; /* private "cursor" for wd_err */
        spinlock_t      s_wd_err_lock; /* protects s_wd_err */
}

struct supervisor       su;

su.s_wd_err = errseq_sample(&wd.wd_err);
spin_lock_init(&su.s_wd_err_lock);

現在他們開始把任務交給他。每隔幾分鐘,他們就要求他完成他們目前交給他所有工作。然後他們問他是否在其中犯了任何錯誤。

spin_lock(&su.su_wd_err_lock);
err = errseq_check_and_advance(&wd.wd_err, &su.s_wd_err);
spin_unlock(&su.su_wd_err_lock);

到目前為止,這只是不斷返回 0。

現在,這家公司的老闆相當吝嗇,給了他劣質裝置來完成工作。偶爾裝置出故障,他就會犯錯。他重重地嘆了口氣,然後把它記錄下來。

errseq_set(&wd.wd_err, -EIO);

……然後回去工作。主管們最終會再次輪詢,他們在下次檢查時都會得到這個錯誤。隨後的呼叫將返回 0,直到記錄了另一個錯誤,此時它會向每個主管報告一次。

請注意,主管們無法得知他犯了多少錯誤,只能知道自他們上次檢查以來是否犯了錯誤,以及記錄的最新值。

偶爾大老闆會來突擊檢查,讓工人為他做一份一次性工作。他不像主管那樣全天候監視工人,但他確實需要知道在處理他的工作時是否發生了錯誤。

他只需取樣 worker 中當前的 errseq_t,然後用它來判斷稍後是否發生了錯誤。

errseq_t since = errseq_sample(&wd.wd_err);
/* submit some work and wait for it to complete */
err = errseq_check(&wd.wd_err, since);

由於他在此之後就會丟棄“since”值,所以他不需要在這裡推進它。他也不需要任何加鎖,因為它不會被其他人使用。

序列化 errseq_t 遊標更新

請注意,errseq_t API 在 check_and_advance 操作期間不保護 errseq_t 遊標。只有規範的錯誤程式碼是原子處理的。在多個任務可能同時使用同一個 errseq_t 遊標的情況下,序列化對該遊標的更新很重要。

如果不這樣做,遊標可能會後退,從而導致同一個錯誤被報告多次。

因此,通常最好先執行 errseq_check 來檢視是否有任何變化,然後再在獲取鎖之後執行 errseq_check_and_advance。例如:

if (errseq_check(&wd.wd_err, READ_ONCE(su.s_wd_err)) {
        /* su.s_wd_err is protected by s_wd_err_lock */
        spin_lock(&su.s_wd_err_lock);
        err = errseq_check_and_advance(&wd.wd_err, &su.s_wd_err);
        spin_unlock(&su.s_wd_err_lock);
}

這在自上次檢查以來沒有發生任何變化的常見情況下避免了自旋鎖。

函式

errseq_t errseq_set(errseq_t *eseq, int err)

設定 errseq_t 以便後續報告

引數

errseq_t *eseq

應設定的 errseq_t 欄位

int err

要設定的錯誤(必須在 -1 和 -MAX_ERRNO 之間)

描述

此函式設定 eseq 中的錯誤,如果上次序列在過去的某個時間點被取樣過,則遞增序列計數器。

任何設定的錯誤都將始終覆蓋現有錯誤。

返回

前一個值,主要用於除錯目的。返回值不應在後續呼叫中用作先前取樣值,因為它將不會設定 SEEN 標誌。

errseq_t errseq_sample(errseq_t *eseq)

獲取當前 errseq_t 值。

引數

errseq_t *eseq

指向要取樣的 errseq_t 的指標。

描述

此函式允許呼叫者初始化其 errseq_t 變數。如果錯誤已被“看到”,新的呼叫者將不會看到舊錯誤。如果在 eseq 中存在一個未見的錯誤,此函式的呼叫者將在下次檢查錯誤時看到它。

上下文

任何上下文。

返回

當前的 errseq 值。

int errseq_check(errseq_t *eseq, errseq_t since)

自某個特定取樣點以來是否發生過錯誤?

引數

errseq_t *eseq

指向要檢查的 errseq_t 值的指標。

errseq_t since

先前取樣的 errseq_t,用於檢查。

描述

獲取 eseq 指向的值,並檢視它是否自給定值取樣以來發生了變化。since 值不會前進,因此無需將該值標記為已見。

返回

errseq_t 中設定的最新錯誤,如果未改變則為 0。

int errseq_check_and_advance(errseq_t *eseq, errseq_t *since)

檢查 errseq_t 並前進到當前值。

引數

errseq_t *eseq

指向正在檢查和報告的值的指標。

errseq_t *since

指向先前取樣的 errseq_t 的指標,用於檢查和前進。

描述

獲取 eseq 值,並檢視它是否與 since 指向的值匹配。如果匹配,則返回 0。

如果不匹配,則該值已更改。設定“seen”標誌,並嘗試將其交換為新的 eseq 值。然後,將該值設定為新的“since”值,並返回錯誤部分的設定值。

請注意,此處未提供對“since”值併發更新的鎖定。如果需要,呼叫者必須提供。因此,呼叫者可能希望在獲取鎖並呼叫此函式之前,進行無鎖的 errseq_check。

返回

如果已儲存錯誤,則返回負 errno;如果未發生新錯誤,則返回 0。