relay 介面 (原 relayfs)

relay 介面提供了一種機制,核心應用程式可以透過使用者定義的“relay 通道”有效地記錄和傳輸大量資料,從核心到使用者空間。

“relay 通道”是一種核心 -> 使用者的資料 relay 機制,實現為一組按 CPU 劃分的核心緩衝區(“通道緩衝區”),每個緩衝區在使用者空間中都表示為一個常規檔案(“relay 檔案”)。核心客戶端使用高效的寫入函式寫入通道緩衝區;這些函式自動記錄到當前 CPU 的通道緩衝區中。使用者空間應用程式使用 mmap() 或 read() 從 relay 檔案中檢索資料,並在資料可用時獲取資料。relay 檔案本身是在主機檔案系統中建立的檔案,例如 debugfs,並且使用下面描述的 API 與通道緩衝區關聯。

記錄到通道緩衝區中的資料格式完全由核心客戶端決定;但是,relay 介面確實提供了允許核心客戶端對緩衝區資料施加一些結構的鉤子。relay 介面不實現任何形式的資料過濾 - 這也留給核心客戶端。目的是儘可能保持簡單。

本文件提供了 relay 介面 API 的概述。函式引數的詳細資訊與 relay 介面程式碼中的函式一起記錄 - 請參閱這些詳細資訊。

語義

每個 relay 通道都有一個緩衝區/CPU;每個緩衝區有一個或多個子緩衝區。訊息被寫入第一個子緩衝區,直到它太滿而無法包含新訊息,在這種情況下,它被寫入下一個子緩衝區(如果可用)。訊息永遠不會跨子緩衝區拆分。此時,可以通知使用者空間,以便它清空第一個子緩衝區,而核心繼續寫入下一個子緩衝區。

當通知子緩衝區已滿時,核心知道它有多少位元組是填充,即由於完整訊息無法放入子緩衝區而發生的未使用空間。使用者空間可以使用這些知識僅複製有效資料。

複製後,使用者空間可以通知核心子緩衝區已被消耗。

relay 通道可以在一種模式下執行,在這種模式下,它將覆蓋使用者空間尚未收集的資料,而不會等待它被消耗。

relay 通道本身不提供使用者空間和核心之間此類資料的通訊,從而允許核心端保持簡單,並且不會在使用者空間上強加單一介面。但是,它提供了一組示例和一個單獨的助手,如下所述。

read() 介面既刪除填充,又在內部消耗讀取的子緩衝區;因此,在使用 read(2) 來排空通道緩衝區的情況下,核心和使用者之間不需要特殊的通訊來進行基本操作。

relay 介面的主要目標之一是提供一種低開銷機制,用於將核心資料傳遞到使用者空間。雖然 read() 介面易於使用,但它不如 mmap() 方法有效;示例程式碼嘗試儘可能減少這兩種方法之間的權衡。

klog 和 relay-apps 示例程式碼

relay 介面本身已準備好使用,但為了使事情更容易,提供了一些簡單的實用函式和一組示例。

relay-apps 示例 tarball 可在 relay sourceforge 站點上找到,其中包含一組獨立的示例,每個示例都由一對 .c 檔案組成,其中包含 relay 應用程式的使用者端和核心端的樣板程式碼。當組合時,這兩組樣板程式碼提供了粘合劑,可以輕鬆地將資料流式傳輸到磁碟,而無需擔心繁瑣的內務處理任務。

“klog 除錯函式”補丁(relay-apps tarball 中的 klog.patch)為核心提供了一些高階日誌記錄函式,這些函式允許將格式化的文字或原始資料寫入通道,而不管要寫入的通道是否存在,甚至是否將 relay 介面編譯到核心中。這些函式允許您將無條件的“跟蹤”語句放在核心或核心模組中的任何位置;只有在註冊了“klog 處理程式”時,才會實際記錄資料(有關詳細資訊,請參見 klog 和 kleak 示例)。

當然,可以從頭開始使用 relay 介面,即不使用任何 relay-apps 示例程式碼或 klog,但是您必須實現使用者空間和核心之間的通訊,允許兩者傳遞緩衝區的狀態(滿、空、填充量)。read() 介面既刪除填充,又在內部消耗讀取的子緩衝區;因此,在使用 read(2) 來排空通道緩衝區的情況下,核心和使用者之間不需要特殊的通訊來進行基本操作。但是,仍然需要透過某些通道來傳達諸如緩衝區已滿之類的條件。

klog 和 relay-apps 示例可以在 http://relayfs.sourceforge.net 上的 relay-apps tarball 中找到

relay 介面使用者空間 API

relay 介面實現了基本的檔案操作,用於使用者空間訪問 relay 通道緩衝區資料。以下是可用的檔案操作以及有關其行為的一些註釋

open()

使能夠使用者開啟_現有_通道緩衝區。

mmap()

導致通道緩衝區被對映到呼叫者的記憶體空間中。請注意,您不能進行部分 mmap - 您必須對映整個檔案,即 NRBUF * SUBBUFSIZE。

read()

讀取通道緩衝區的內容。讀取的位元組由讀取器“消耗”,即它們將不再可用於後續讀取。如果通道以非覆蓋模式(預設模式)使用,則即使存在活動的核心寫入器,也可以隨時讀取它。如果通道以覆蓋模式使用並且存在活動的通道寫入器,則結果可能無法預測 - 使用者應確保在使用覆蓋模式的 read() 之前,已結束對通道的所有日誌記錄。子緩衝區填充會自動刪除,讀取器將看不到。

sendfile()

將資料從通道緩衝區傳輸到輸出檔案描述符。子緩衝區填充會自動刪除,讀取器將看不到。

poll()

支援 POLLIN/POLLRDNORM/POLLERR。當子緩衝區邊界被跨越時,會通知使用者應用程式。

close()

遞減通道緩衝區的 refcount。當 refcount 達到 0 時,即當沒有程序或核心客戶端開啟緩衝區時,通道緩衝區將被釋放。

為了使使用者應用程式能夠使用 relay 檔案,必須掛載主機檔案系統。例如

mount -t debugfs debugfs /sys/kernel/debug

注意

核心客戶端不需要掛載主機檔案系統來建立或使用通道 - 只有當用戶空間應用程式需要訪問緩衝區資料時,才需要掛載它。

relay 介面核心 API

以下是 relay 介面向核心客戶端提供的 API 的摘要

TBD(當前行 MT:/API/)

通道管理功能

relay_open(base_filename, parent, subbuf_size, n_subbufs,
           callbacks, private_data)
relay_close(chan)
relay_flush(chan)
relay_reset(chan)

通道管理通常在使用者空間啟動時呼叫

relay_subbufs_consumed(chan, cpu, subbufs_consumed)

寫入函式

relay_write(chan, data, length)
__relay_write(chan, data, length)
relay_reserve(chan, length)

回撥

subbuf_start(buf, subbuf, prev_subbuf, prev_padding)
buf_mapped(buf, filp)
buf_unmapped(buf, filp)
create_buf_file(filename, parent, mode, buf, is_global)
remove_buf_file(dentry)

輔助函式

relay_buf_full(buf)
subbuf_start_reserve(buf, length)

建立通道

relay_open() 用於建立通道及其按 CPU 劃分的通道緩衝區。每個通道緩衝區將在主機檔案系統中建立一個關聯的檔案,該檔案可以在使用者空間中進行 mmap 或讀取。這些檔案命名為 basename0...basenameN-1,其中 N 是線上 cpu 的數量,預設情況下將在檔案系統的根目錄中建立(如果 parent 引數為 NULL)。如果您希望目錄結構包含您的 relay 檔案,則應使用主機檔案系統的目錄建立函式(例如 debugfs_create_dir())建立它,並將父目錄傳遞給 relay_open()。使用者負責清理他們建立的任何目錄結構,當通道關閉時 - 再次應使用主機檔案系統的目錄刪除函式,例如 debugfs_remove()

為了建立通道,並將主機檔案系統的檔案與其通道緩衝區關聯,使用者必須為兩個回撥函式提供定義:create_buf_file() 和 remove_buf_file()。對於來自 relay_open() 的每個按 CPU 劃分的緩衝區,都會呼叫 create_buf_file() 一次,並允許使用者建立將用於表示相應通道緩衝區的檔案。回撥應返回為表示通道緩衝區而建立的檔案的 dentry。還必須定義 remove_buf_file();它負責刪除在 create_buf_file() 中建立的檔案,並在 relay_close() 期間呼叫。

以下是這些回撥的一些典型定義,在這種情況下使用 debugfs

/*
* create_buf_file() callback.  Creates relay file in debugfs.
*/
static struct dentry *create_buf_file_handler(const char *filename,
                                            struct dentry *parent,
                                            umode_t mode,
                                            struct rchan_buf *buf,
                                            int *is_global)
{
        return debugfs_create_file(filename, mode, parent, buf,
                                &relay_file_operations);
}

/*
* remove_buf_file() callback.  Removes relay file from debugfs.
*/
static int remove_buf_file_handler(struct dentry *dentry)
{
        debugfs_remove(dentry);

        return 0;
}

/*
* relay interface callbacks
*/
static struct rchan_callbacks relay_callbacks =
{
        .create_buf_file = create_buf_file_handler,
        .remove_buf_file = remove_buf_file_handler,
};

以及使用它們的 relay_open() 呼叫示例

chan = relay_open("cpu", NULL, SUBBUF_SIZE, N_SUBBUFS, &relay_callbacks, NULL);

如果 create_buf_file() 回撥失敗或未定義,則通道建立以及 relay_open() 將失敗。

每個按 CPU 劃分的緩衝區的總大小是透過將子緩衝區的數量乘以傳遞到 relay_open() 的子緩衝區大小來計算的。子緩衝區背後的想法是,它們基本上是將雙緩衝擴充套件到 N 個緩衝區,並且它們還允許應用程式輕鬆實現緩衝區邊界上的隨機訪問方案,這對於某些大容量應用程式可能很重要。子緩衝區的數量和大小完全取決於應用程式,即使對於相同的應用程式,不同的條件也會保證這些引數在不同時間使用不同的值。通常,最好在進行一些實驗後決定要使用的正確值;但是,一般來說,可以安全地假設僅擁有 1 個子緩衝區是一個壞主意 - 您肯定會覆蓋資料或丟失事件,具體取決於使用的通道模式。

create_buf_file() 實現也可以定義為允許建立單個“全域性”緩衝區,而不是預設的按 CPU 劃分的集合。這對於主要對檢視系統範圍事件的相對順序感興趣而無需費心儲存顯式時間戳以用於在後處理步驟中合併/排序按 CPU 劃分的檔案的應用程式非常有用。

要使 relay_open() 建立全域性緩衝區,create_buf_file() 實現除了建立將用於表示單個緩衝區的檔案外,還應將 is_global outparam 的值設定為非零值。在全域性緩衝區的情況下,create_buf_file() 和 remove_buf_file() 將僅被呼叫一次。正常的通道寫入函式,例如 relay_write(),仍然可以使用 - 來自任何 CPU 的寫入將透明地最終到達全域性緩衝區中 - 但由於它是全域性緩衝區,因此呼叫者應確保他們為此類緩衝區使用適當的鎖定,或者透過將寫入包裝在自旋鎖中,或者透過從 relay.h 複製寫入函式並建立一個在內部進行適當鎖定的本地版本。

傳遞到 relay_open() 的 private_data 允許客戶端將使用者定義的資料與通道關聯,並且可以透過 chan->private_data 或 buf->chan->private_data 立即使用(包括在 create_buf_file() 中)。

通道“模式”

relay 通道可以在兩種模式下使用 - “覆蓋”或“非覆蓋”。該模式完全由 subbuf_start() 回撥的實現決定,如下所述。如果未定義 subbuf_start() 回撥,則預設值為“非覆蓋”模式。如果預設模式適合您的需求,並且您計劃使用 read() 介面來檢索通道資料,則可以忽略本節的詳細資訊,因為它主要與 mmap() 實現有關。

在“覆蓋”模式(也稱為“飛行記錄器”模式)下,寫入會持續迴圈緩衝區,並且永遠不會失敗,但是會無條件地覆蓋舊資料,而不管它是否已被實際消耗。在非覆蓋模式下,如果未消耗的子緩衝區的數量等於通道中的子緩衝區總數,則寫入將失敗,即資料將丟失。應該清楚的是,如果沒有消費者或者消費者無法足夠快地消耗子緩衝區,則無論如何都會丟失資料;唯一的區別是資料是從緩衝區的開頭還是結尾丟失。

如上所述,relay 通道由一個或多個按 CPU 劃分的通道緩衝區組成,每個通道緩衝區都實現為一個迴圈緩衝區,細分為一個或多個子緩衝區。透過下面描述的寫入函式將訊息寫入通道的當前按 CPU 劃分的緩衝區的當前子緩衝區中。每當訊息無法放入當前子緩衝區時,因為沒有剩餘空間,則透過 subbuf_start() 回撥通知客戶端即將切換到新的子緩衝區。客戶端使用此回撥來 1) 初始化下一個子緩衝區(如果適用)2) 最終確定先前的子緩衝區(如果適用)3) 返回一個布林值,指示是否實際移動到下一個子緩衝區。

要實現“非覆蓋”模式,使用者空間客戶端提供一個 subbuf_start() 回撥的實現,如下所示

static int subbuf_start(struct rchan_buf *buf,
                        void *subbuf,
                        void *prev_subbuf,
                        unsigned int prev_padding)
{
        if (prev_subbuf)
                *((unsigned *)prev_subbuf) = prev_padding;

        if (relay_buf_full(buf))
                return 0;

        subbuf_start_reserve(buf, sizeof(unsigned int));

        return 1;
}

如果當前緩衝區已滿,即所有子緩衝區都保持未消耗狀態,則回撥返回 0 以指示不應立即發生緩衝區切換,即直到消費者有機會讀取當前的一組準備好的子緩衝區為止。為了使 relay_buf_full() 函式有意義,消費者負責在透過 relay_subbufs_consumed() 消耗子緩衝區時通知 relay 介面。任何後續嘗試寫入緩衝區的操作都將再次使用相同的引數呼叫 subbuf_start() 回撥;只有當消費者消耗了一個或多個準備好的子緩衝區時,relay_buf_full() 才會返回 0,在這種情況下,緩衝區切換可以繼續。

用於“覆蓋”模式的 subbuf_start() 回撥的實現將非常相似

static int subbuf_start(struct rchan_buf *buf,
                        void *subbuf,
                        void *prev_subbuf,
                        size_t prev_padding)
{
        if (prev_subbuf)
                *((unsigned *)prev_subbuf) = prev_padding;

        subbuf_start_reserve(buf, sizeof(unsigned int));

        return 1;
}

在這種情況下,relay_buf_full() 檢查毫無意義,回撥始終返回 1,從而導致無條件地發生緩衝區切換。在這種模式下,客戶端使用 relay_subbufs_consumed() 函式也是毫無意義的,因為它永遠不會被查詢。

如果客戶端未定義任何回撥,或者未定義 subbuf_start() 回撥,則使用的預設 subbuf_start() 實現實現了最簡單的“非覆蓋”模式,即它什麼也不做,只是返回 0。

可以透過從 subbuf_start() 回撥中呼叫 subbuf_start_reserve() 輔助函式,在每個子緩衝區的開頭保留標頭資訊。此保留區域可用於儲存客戶端想要的任何資訊。在上面的示例中,在每個子緩衝區中都保留了空間來儲存該子緩衝區的填充計數。這是在 subbuf_start() 實現中為先前的子緩衝區填充的;先前的子緩衝區的填充值與指向先前子緩衝區的指標一起傳遞到 subbuf_start() 回撥中,因為在填充子緩衝區之前,填充值是未知的。當通道開啟時,也會為第一個子緩衝區呼叫 subbuf_start() 回撥,以使客戶端有機會在其中保留空間。在這種情況下,傳遞到回撥中的先前子緩衝區指標將為 NULL,因此客戶端應在寫入先前子緩衝區之前檢查 prev_subbuf 指標的值。

寫入通道

核心客戶端使用 relay_write() 或 __relay_write() 將資料寫入當前 CPU 的通道緩衝區中。relay_write() 是主要的日誌記錄函式 - 它使用 local_irqsave() 來保護緩衝區,並且如果您可能從中斷上下文進行日誌記錄,則應使用它。如果您知道您永遠不會從中斷上下文進行日誌記錄,則可以使用 __relay_write(),它僅停用搶佔。這些函式不返回值,因此您無法確定它們是否失敗 - 假設您無論如何都不想在快速日誌記錄路徑中檢查返回值,並且它們始終會成功,除非緩衝區已滿並且正在使用非覆蓋模式,在這種情況下,您可以透過呼叫 relay_buf_full() 輔助函式在 subbuf_start() 回撥中檢測到失敗的寫入。

relay_reserve() 用於在通道緩衝區中保留一個槽,該槽可以在以後寫入。這通常用於需要直接寫入通道緩衝區而無需事先在臨時緩衝區中暫存資料的應用程式。由於實際寫入可能不會在槽保留後立即發生,因此使用 relay_reserve() 的應用程式可以保留實際寫入的位元組數的計數,無論是在子緩衝區本身中保留的空間中還是作為單獨的陣列。有關如何執行此操作的示例,請參見 http://relayfs.sourceforge.net 上的 relay-apps tarball 中的“reserve”示例。由於寫入受客戶端控制並且與保留分開,因此 relay_reserve() 不保護緩衝區 - 在使用 relay_reserve() 時,由客戶端提供適當的同步。

關閉通道

當客戶端完成使用通道時,它會呼叫 relay_close()。當不再有對任何通道緩衝區的引用時,通道及其關聯的緩衝區將被銷燬。relay_flush() 強制在所有通道緩衝區上進行子緩衝區切換,並且可用於在關閉通道之前最終確定和處理最後一個子緩衝區。

其他

某些應用程式可能希望保留通道並重復使用它,而不是每次使用都開啟和關閉新通道。relay_reset() 可用於此目的 - 它將通道重置為其初始狀態,而無需重新分配通道緩衝區記憶體或銷燬現有對映。但是,只有在安全的情況下,即當前未寫入通道時,才應呼叫它。

最後,有一些實用回撥可以用於不同的目的。每當從使用者空間 mmap 通道緩衝區時,都會呼叫 buf_mapped(),當它取消對映時,會呼叫 buf_unmapped()。客戶端可以使用此通知來觸發核心應用程式中的操作,例如啟用/停用對通道的日誌記錄。

資源

有關新聞、示例程式碼、郵件列表等,請參閱 relay 介面主頁

鳴謝

relay 介面的想法和規範來自以下人員參與的跟蹤討論

Michel Dagenais <michel.dagenais@polymtl.ca> Richard Moore <richardj_moore@uk.ibm.com> Bob Wisniewski <bob@watson.ibm.com> Karim Yaghmour <karim@opersys.com> Tom Zanussi <zanussi@us.ibm.com>

還要感謝 Hubertus Franke 提供了許多有用的建議和錯誤報告。