Relay 是一種從 Linux 內核到用戶空間的高效數據傳輸技術。通過用戶定義的 relay 通道,內核空間的程序能夠高效、可靠、便捷地將數據傳輸到用戶空間。Relay 特別適用於內核空間有大量數據需要傳輸到用戶空間的情形,目前已經廣泛應用在內核調試工具如 SystemTap中。本文介紹了 Relay 的歷史和原理,並且用一個簡單的實例介紹了 Relay 的具體用法。
Relay 要解決的問題
對於任何在內核工作的程序而言,如何把大量的調試信息從內核空間傳輸到用戶空間都是一個大麻煩,對於運行中的內核更是如此。特別是對於哪些用於調試內核性能的工具,更是如此。
對於這種大量數據需要在內核中緩存並傳輸到用戶空間需求,很多傳統的方法都已到達了極限,例如內核程序員很熟悉的 printk() 調用。此外,如果不同的內核子系統都開發自己的緩存和傳輸代碼,造成很大的代碼冗余,而且也帶來維護上的困難。
這些,都要求開發一套能夠高效可靠地將數據從內核空間轉發到用戶空間的系統,而且這個系統應該獨立於各個調試子系統。這樣就誕生了 RelayFS。
Relay的發展歷史
Relay 的前身是 RelayFS,即作為 Linux 的一個新型文件系統。2003年3月,RelayFS的第一個版本的代碼被開發出來,在7月14日,第一個針對2.6內核的版本也開始提供下載。經過廣泛的試用和改進,直到2005年9月,RelayFS才被加入mainline內核(2.6.14)。同時,RelayFS也被移植到2.4內核中。在 2006年2月,從2.6.17開始,RelayFS不再作為單獨的文件系統存在,而是成為內核的一部分。它的源碼也從fs/目錄下轉移到 kernel/relay.c中,名稱中也從RelayFS改成了Relay。
RelayFS目前已經被越來越多的內核工具使用,包括內核調試工具SystemTap、LTT,以及一些特殊的文件系統例如DebugFS。
Relay的基本原理
總的說來,Relay提供了一種機制,使得內核空間的程序能夠通過用戶定義的relay通道(channel)將大量數據高效的傳輸到用戶空間。
一個relay通道由一組和CPU一一對應的內核緩沖區組成。這些緩沖區又被稱為relay緩沖區(buffer),其中的每一個在用戶空間都用一個常規文件來表示,這被叫做relay文件(file)。內核空間的用戶可以利用relay提供的API接口來寫入數據,這些數據會被自動的寫入當前的 CPU id對應的那個relay緩沖區;同時,這些緩沖區從用戶空間看來,是一組普通文件,可以直接使用read()進行讀取,也可以使用mmap()進行映射。Relay並不關心數據的格式和內容,這些完全依賴於使用relay的用戶程序。Relay的目的是提供一個足夠簡單的接口,從而使得基本操作盡可能的高效。
Relay將數據的讀和寫分離,使得突發性大量數據寫入的時候,不需要受限於用戶空間相對較慢的讀取速度,從而大大提高了效率。Relay作為寫入和讀取的橋梁,也就是將內核用戶寫入的數據緩存並轉發給用戶空間的程序。這種轉發機制也正是Relay這個名稱的由來。
這裡的relay通道由四個relay緩沖區(kbuf0到kbuf3)組成,分別對應於系統中的cpu0到cpu1。每個CPU上的代碼調用relay_write()的時候將數據寫入自己對應的relay緩沖區內。每個relay緩沖區稱一個relay文件,即/cpu0到 /cpu3。當文件系統被mount到/mnt/以後,這個relay文件就被映射成映射到用戶空間的地址空間。一旦數據可用,用戶程序就可以把它的數據讀出來寫入到硬盤上的文件中,即cpu0.out到cpu3.out。
Relay的主要API
前面提到的 relay_write() 就是 relay API 之一。除此以外,Relay 還提供了更多的 API來支持用戶程序完整的使用 relay。這些 API,主要按照面向用戶空間和面向內核空間分為兩大類,下面我們來分別進行介紹。
面向用戶空間的 API
這些 Relay 編程接口向用戶空間程序提供了訪問 relay 通道緩沖區數據的基本操作的入口,包括:
●open() - 允許用戶打開一個已經存在的通道緩沖區。
●mmap() - 使通道緩沖區被映射到位於用戶空間的調用者的地址空間。要特別注意的是,我們不能僅對局部區域進行映射。也就是說,必須映射整個緩沖區文件,其大小是 CPU的個數和單個 CPU 緩沖區大小的乘積。
●read() - 讀取通道緩沖區的內容。這些數據一旦被讀出,就意味著他們被用戶空間的程序消費掉了,也就不能被之後的讀操作看到。
●sendfile() - 將數據從通道緩沖區傳輸到一個輸出文件描述符。其中可能的填充字符會被自動去掉,不會被用戶看到。
●poll() - 支持 POLLIN/POLLRDNORM/POLLERR 信號。每次子緩沖區的邊界被越過時,等待著的用戶空間程序會得到通知。
●close() - 將通道緩沖區的引用數減1。當引用數減為0時,表明沒有進程或者內核用戶需要打開它,從而這個通道緩沖區被釋放。
面向內核空間的 API
這些API接口向位於內核空間的用戶提供了管理relay通道、數據寫入等功能。下面介紹其中主要的部分,完整的API接口列表請參見這裡。
●relay_open() - 創建一個relay通道,包括創建每個CPU對應的relay緩沖區。
●relay_close() - 關閉一個relay通道,包括釋放所有的relay緩沖區,在此之前會調用relay_switch()來處理這些relay緩沖區以保證已讀取但是未滿的數據不會丟失
●relay_write() - 將數據寫入到當前CPU對應的relay緩沖區內。由於它使用了local_irqsave()保護,因此也可以在中斷上下文中使用。
●relay_reserve() - 在relay通道中保留一塊連續的區域來留給未來的寫入操作。這通常用於那些希望直接寫入到relay緩沖區的用戶。考慮到性能或者其它因素,這些用戶不希望先把數據寫到一個臨時緩沖區中,然後再通過relay_write()進行寫入。
Relay的例子
我們用一個最簡單的例子來介紹怎麼使用Relay。這個例子由兩部分組成:一部分是位於內核空間將數據寫入relay文件的程序,使用時需要作為一個內核模塊被加載;另一部分是位於用戶空間從relay文件中讀取數據的程序,使用時作為普通用戶態程序運行。
內核空間的程序主要操作是:
加載模塊時,打開一個relay通道,並且往打開的relay通道中寫入消息;
卸載模塊時,關閉relay通道。
程序內容:
/*
* hello-mod.c
* a kernel-space client example of relayfs filesystem
*/
#include <linux/module.h>
#include <linux/relayfs_fs.h>
static struct rchan *hello_rchan;
int init_module(void)
{
const char *msg="Hello worldn";
hello_rchan = relay_open("cpu", NULL, 8192, 2, NULL);
if(!hello_rchan){
printk("relay_open() failed.n");
return -ENOMEM;
}
relay_write(hello_rchan, msg, strlen(msg));
return 0;
}
void cleanup_module(void)
{
if(hello_rchan) {
relay_close(hello_rchan);
hello_rchan = NULL;
}
return;
}
MODULE_LICENSE ("GPL");
MODULE_DESCRIPTION ("Simple example of Relay");
用戶空間的函數主要操作是:
●如果relayfs文件系統還沒有被mount,則將其mount到目錄/mnt/relay上;
●遍歷每一個CPU對應的緩沖文件;
●打開文件;
●讀取所有文件內容;
●關閉文件;
●最後,umount掉relay文件系統。
程序內容:
/*
* audience.c
* a user-space client example of relayfs filesystem
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mount.h>
#include <fcntl.h>
#include <sched.h>
#include <errno.h>
#include <stdio.h>
#define MAX_BUFLEN 256
const char filename_base[]="/mnt/relay/cpu";
// implement your own get_cputotal() before compilation
static int get_cputotal(void);
int main(void)
{
char filename[128]={0};
char buf[MAX_BUFLEN];
int fd, c, i, bytesread, cputotal = 0;
if(mount("relayfs", "/mnt/relay", "relayfs", 0, NULL)
&& (errno != EBUSY)) {
printf("mount() failed: %sn", strerror(errno));
return 1;
}
cputotal = get_cputotal();
if(cputotal <= 0) {
printf("invalid cputotal value: %dn", cputotal);
return 1;
}
for(i=0; i<cputotal; i++) {
// open per-cpu file
sprintf(filename, "%s%d", filename_base, i);
fd = open(filename, O_RDONLY);
if (fd < 0) {
printf("fopen() failed: %sn", strerror(errno));
return 1;
}
// read per-cpu file
bytesread = read(fd, buf, MAX_BUFLEN);
while(bytesread > 0) {
buf[bytesread] = ' ';
puts(buf);
bytesread = read(fd, buf, MAX_BUFLEN);
};
// close per-cpu file
if(fd > 0) {
close(fd);
fd = 0;
}
}
if(umount("/mnt/relay") && (errno != EINVAL)) {
printf("umount() failed: %sn", strerror(errno));
return 1;
}
return 0;
}
上面這個例子給出了使用relay的一個最簡單的情形,並沒有實際用處,但是形象描述了從用戶空間和內核空間兩個方面使用relay的基本流