Completions - “等待完成” 屏障 API¶
簡介:¶
如果您有一個或多個執行緒必須等待某些核心活動達到某個點或特定狀態,那麼 completion 可以為此問題提供一個無競爭的解決方案。從語義上講,它們有點像 pthread_barrier(),並且具有類似的用例。
Completions 是一種程式碼同步機制,它優於任何對鎖/訊號量和忙迴圈的濫用。每當您想到使用 yield() 或某些古怪的 msleep(1) 迴圈來允許其他事情繼續進行時,您可能需要考慮使用 wait_for_completion*() 呼叫和 complete() 來代替。
使用 completions 的優點是它們具有明確定義的、集中的目的,這使得很容易看到程式碼的意圖,而且它們還可以生成更高效的程式碼,因為所有執行緒都可以繼續執行,直到實際需要結果,並且等待和訊號傳送都非常高效,使用低階排程程式睡眠/喚醒工具。
Completions 構建於 Linux 排程器的 waitqueue 和 wakeup 基礎設施之上。waitqueue 上的執行緒正在等待的事件被簡化為“struct completion”中的一個簡單標誌,適當地稱為“done”。
由於 completions 與排程相關,因此可以在 kernel/sched/completion.c 中找到程式碼。
用法:¶
使用 completions 主要有三個部分
“struct completion”同步物件的初始化
透過呼叫 wait_for_completion() 的一個變體進行的等待部分,
透過呼叫 complete() 或 complete_all() 進行的訊號傳送部分。
還有一些輔助函式用於檢查 completions 的狀態。請注意,雖然初始化必須首先發生,但等待和訊號傳送部分可以按任何順序發生。也就是說,一個執行緒在另一個執行緒檢查是否必須等待 completion 之前,已經將 completion 標記為“done”是完全正常的。
要使用 completions,您需要 #include <linux/completion.h> 並建立型別為“struct completion”的靜態或動態變數,該變數只有兩個欄位
struct completion {
unsigned int done;
struct swait_queue_head wait;
};
這提供了 ->wait waitqueue,用於放置等待的任務(如果有),以及 ->done completion 標誌,用於指示它是否已完成。
Completions 應該被命名為引用正在同步的事件。一個好的例子是
wait_for_completion(&early_console_added);
complete(&early_console_added);
良好、直觀的命名(一如既往)有助於程式碼的可讀性。將 completion 命名為“complete”是沒有幫助的,除非目的非常明顯……
初始化 completions:¶
動態分配的 completion 物件最好嵌入到資料結構中,以確保該資料結構在該函式/驅動程式的生命週期內都是有效的,以防止與來自非同步 complete() 呼叫的競爭發生。
當使用 wait_for_completion() 的 _timeout() 或 _killable()/_interruptible() 變體時,應特別注意,因為它必須確保在所有相關活動(complete() 或 reinit_completion())發生之前,不會發生記憶體釋放,即使這些等待函式由於超時或訊號觸發而提前返回。
動態分配的 completion 物件的初始化透過呼叫 init_completion() 完成
init_completion(&dynamic_object->done);
在此呼叫中,我們初始化 waitqueue 並將 ->done 設定為 0,即“未完成”。
重新初始化函式 reinit_completion() 只是將 ->done 欄位重置為 0(“未完成”),而不觸及 waitqueue。此函式的呼叫者必須確保沒有並行的 racy wait_for_completion() 呼叫正在進行。
在同一個 completion 物件上兩次呼叫 init_completion() 很可能是一個 bug,因為它會將佇列重新初始化為一個空佇列,並且排隊的任務可能會“丟失” - 在這種情況下使用 reinit_completion(),但要注意其他競爭。
對於靜態宣告和初始化,可以使用宏。
對於檔案範圍內的靜態(或全域性)宣告,您可以使用 DECLARE_COMPLETION()
static DECLARE_COMPLETION(setup_done);
DECLARE_COMPLETION(setup_done);
請注意,在這種情況下,completion 在啟動時(或模組載入時)初始化為“未完成”,並且不需要 init_completion() 呼叫。
當 completion 被宣告為函式內的區域性變數時,初始化應該始終顯式使用 DECLARE_COMPLETION_ONSTACK(),不僅要使 lockdep 高興,還要清楚地表明已經考慮了有限範圍並且是有意的
DECLARE_COMPLETION_ONSTACK(setup_done)
請注意,當使用 completion 物件作為區域性變數時,您必須敏銳地意識到函式堆疊的生命週期很短:在所有活動(例如等待執行緒)都已停止並且 completion 物件完全未使用之前,該函式不得返回到呼叫上下文中。
再次強調:特別是在使用一些具有更復雜結果的等待 API 變體時,例如超時或訊號傳送 (_timeout()、_killable() 和 _interruptible()) 變體,等待可能會提前完成,而該物件可能仍被另一個執行緒使用 - 並且從 wait_on_completion*() 呼叫者函式返回將釋放函式堆疊,如果 complete() 在其他執行緒中完成,則會導致微妙的資料損壞。簡單的測試可能不會觸發這些型別的競爭。
如果不確定,請使用動態分配的 completion 物件,最好嵌入到一些其他具有無聊的長生命週期的物件中,該物件的生命週期超過任何使用 completion 物件的輔助執行緒的生命週期,或者具有鎖或其他同步機制來確保 complete() 不會在釋放的物件上呼叫。
在堆疊上使用幼稚的 DECLARE_COMPLETION() 會觸發 lockdep 警告。
等待 completions:¶
為了使執行緒等待某些併發活動完成,它會在初始化的 completion 結構上呼叫 wait_for_completion()
void wait_for_completion(struct completion *done)
一個典型的使用場景是
CPU#1 CPU#2
struct completion setup_done;
init_completion(&setup_done);
initialize_work(...,&setup_done,...);
/* run non-dependent code */ /* do setup */
wait_for_completion(&setup_done); complete(&setup_done);
這並不意味著 wait_for_completion() 和 complete() 的呼叫之間存在任何特定的順序 - 如果對 complete() 的呼叫發生在對 wait_for_completion() 的呼叫之前,那麼等待方將立即繼續,因為所有依賴關係都已滿足;如果沒有,它將阻塞直到 complete() 發出訊號。
請注意,wait_for_completion() 呼叫 spin_lock_irq()/spin_unlock_irq(),因此只有在您知道中斷已啟用時才能安全地呼叫它。從停用 IRQ 的原子上下文中呼叫它將導致難以檢測的偽造中斷啟用。
預設行為是等待沒有超時並將任務標記為不可中斷。 wait_for_completion() 及其變體僅在程序上下文中是安全的(因為它們可以睡眠),但在原子上下文、中斷上下文、停用 IRQ 或停用搶佔的情況下則不安全 - 另請參閱下面的 try_wait_for_completion(),以處理原子/中斷上下文中的 completion。
由於 wait_for_completion() 的所有變體都可能(顯然)阻塞很長時間,具體取決於它們正在等待的活動的性質,因此在大多數情況下,您可能不想在持有互斥鎖的情況下呼叫它。
可用的 wait_for_completion*() 變體:¶
以下變體都返回狀態,並且在大多數(/所有)情況下都應檢查此狀態 - 在有意不檢查狀態的情況下,您可能需要做一個註釋來解釋這一點(例如,參見 arch/arm/kernel/smp.c:__cpu_up())。
一個常見的問題是返回值型別的不乾淨分配,因此請注意將返回值分配給正確型別的變數。
還發現對返回值的特定含義的檢查非常不準確,例如構造
if (!wait_for_completion_interruptible_timeout(...))
... 對於成功完成和中斷的情況,將執行相同的程式碼路徑 - 這可能不是您想要的
int wait_for_completion_interruptible(struct completion *done)
此函式在等待時將任務標記為 TASK_INTERRUPTIBLE。如果在等待時收到訊號,它將返回 -ERESTARTSYS;否則為 0
unsigned long wait_for_completion_timeout(struct completion *done, unsigned long timeout)
該任務被標記為 TASK_UNINTERRUPTIBLE 並且最多等待“timeout”jiffies。如果發生超時,它將返回 0,否則返回剩餘時間(以 jiffies 為單位)(但至少為 1)。
超時最好使用 msecs_to_jiffies() 或 usecs_to_jiffies() 計算,以使程式碼在很大程度上與 HZ 無關。
如果返回的超時值被有意忽略,則註釋可能應該解釋原因(例如,參見 drivers/mfd/wm8350-core.c wm8350_read_auxadc())
long wait_for_completion_interruptible_timeout(struct completion *done, unsigned long timeout)
此函式以 jiffies 為單位傳遞超時,並將任務標記為 TASK_INTERRUPTIBLE。如果收到訊號,它將返回 -ERESTARTSYS;否則,如果 completion 超時,則返回 0;如果 completion 發生,則返回剩餘時間(以 jiffies 為單位)。
其他變體包括 _killable,它使用 TASK_KILLABLE 作為指定的任務狀態,如果被中斷,將返回 -ERESTARTSYS;如果實現 completion,則返回 0。還有一個 _timeout 變體
long wait_for_completion_killable(struct completion *done)
long wait_for_completion_killable_timeout(struct completion *done, unsigned long timeout)
_io 變體 wait_for_completion_io() 的行為與非 _io 變體相同,除了將等待時間計為“等待 IO”之外,這對任務在排程/IO 統計資訊中的計算方式有影響
void wait_for_completion_io(struct completion *done)
unsigned long wait_for_completion_io_timeout(struct completion *done, unsigned long timeout)
訊號傳送 completions:¶
想要發出可以繼續的條件已滿足的訊號的執行緒呼叫 complete(),以向正好一個等待者發出可以繼續的訊號
void complete(struct completion *done)
... 或者呼叫 complete_all() 以向所有當前和未來的等待者發出訊號
void complete_all(struct completion *done)
即使線上程開始等待之前發出 completion 訊號,訊號傳送也會按預期工作。這是透過等待者“消耗”(遞減)“struct completion”的 done 欄位來實現的。等待執行緒的喚醒順序與它們排隊的順序相同(FIFO 順序)。
如果多次呼叫 complete(),那麼這將允許該數量的等待者繼續 - 每次呼叫 complete() 都會簡單地遞增 done 欄位。但是,多次呼叫 complete_all() 是一個 bug。 complete() 和 complete_all() 都可以安全地在 IRQ/原子上下文中呼叫。
在任何時候,只能有一個執行緒在特定的“struct completion”上呼叫 complete() 或 complete_all() - 透過等待佇列自旋鎖進行序列化。任何此類對 complete() 或 complete_all() 的併發呼叫都可能是一個設計 bug。
從 IRQ 上下文傳送 completion 訊號很好,因為它會適當地使用 spin_lock_irqsave()/spin_unlock_irqrestore() 進行鎖定,並且它永遠不會睡眠。
try_wait_for_completion()/completion_done():¶
try_wait_for_completion() 函式不會將執行緒放入等待佇列,而是返回 false,如果需要將執行緒排隊(阻塞),否則它會消耗一個已釋出的 completion 並返回 true
bool try_wait_for_completion(struct completion *done)
最後,要檢查 completion 的狀態而不以任何方式更改它,請呼叫 completion_done(),如果沒有尚未被等待者消耗的已釋出 completions,則返回 false(意味著有等待者),否則返回 true
bool completion_done(struct completion *done)
try_wait_for_completion() 和 completion_done() 都可以安全地在 IRQ 或原子上下文中呼叫。