假設您有兩個進程:一個服務器和一個客戶端。服務器進程從硬件接口讀取一些 I/O 并將數據傳遞給客戶端進程。這些進程可能在也可能不在單獨的處理器上運行。特別是,它們沒有共同的共享內存區域。

在這種情況下,服務器和客戶端必須通過它們之間的一些顯式管道進行通信。該通信機制可以根據系統以不同方式實現。該系統的服務器部分可以使用類似于以下偽代碼的代碼運行:
while (1) {
get_data_from_pins();
發送數據到客戶端();
}
并且客戶端部分可以使用以下代碼模式運行:
while (1) {
wait_for_then_get_data_from_server();
處理數據();
}
在這種情況下,服務器初始化通信,客戶端等待并響應通信。所以服務器是主機,客戶端是從機。這也許是意料之中的,因為整個過程是由硬件接口上的數據到達驅動的。到目前為止,這很簡單。但是,如果考慮到時序要求,事情會變得有點復雜。根據系統的需要,兩個進程之間的協議可能必須更加復雜。
阻塞行為和緩沖
假設通信管道本身只有有限的緩沖,服務器進程中對send_data_to_client () 的調用將被阻塞– 它將等到客戶端準備好。如果客戶端及時準備好并且數據可以在需要從硬件讀取下一項數據之前進行通信,這很好。
但是,如果不是這種情況并且客戶端太慢,那么來自硬件的下一個數據將會丟失。
有不同的方法可以解決阻塞問題。如果硬件具有某種流控制,則可能會推遲接口并在數據流中向上游推送延遲。但是,有時這是不可能的。
本文的其余部分著眼于沒有流量控制的情況,其中客戶端的拉動具有可變時間,而硬件的推流具有固定時間。在這種情況下,常見的解決方案是為數據使用緩沖區。
使用緩沖區,服務器進程從硬件接口讀取數據并將數據放入 FIFO。客戶端進程要求服務器從 FIFO 的另一端提供數據。緩沖區需要足夠大,以應對在客戶端最長處理時間內可以到達的數據量。

問題是如何設計兩個進程之間的通信協議,使得服務器可以在需要時從硬件中讀取數據,而客戶端可以在需要時獲取數據?
輪詢
該問題的一種解決方案是服務器在每次從硬件收集數據之間反復輪詢客戶端以確保其準備就緒。代碼看起來像這樣:
while (1) {
get_data_from_pins();
add_data_to_fifo();
while (!time_to_get_data()) {
client_ready = poll_client();
if (!fifo_empty() && client_ready) {
get_data_from_fifo();
發送數據到客戶端();
}
}
}
對應的客戶端代碼是:
while (1) {
signal_ready_to_server();
get_data_from_server();
處理數據();
服務器
可能會在硬件交互之間發送幾項數據或不發送數據,在這種情況下,緩沖區將開始填滿以待稍后清空。
基于事件的編程:選擇
用一個在固定時間內重復輪詢的循環編寫通信是編寫此類代碼的一種略顯笨拙的方式。更好的方法是使用一種編程風格,指示進程直接對系統中發生的事件做出反應。
以這種風格編碼的關鍵是使用選擇來等待事件在指定集合之外發生,然后在其中一個發生時做出反應。XC 編程語言中的 select 結構可以做到這一點,其他領域的結構(例如 Unix 中的 select 系統調用或 SystemC 中的 wait 調用)也是如此。
XC 風格的 select 語句與 C 中的 switch 語句有類似的形式:
select {
case event1:
。..
break;
案例事件2:
。..
休息;
。..。
}
該語句等待 event1、event2 等之一發生,然后執行相關案例主體中的代碼。鑒于此構造,服務器代碼可以以非輪詢方式重寫:
而(1){
選擇{
案例pins_ready():
get_data_from_pins();
add_data_to_fifo();
休息;
case !fifo_empty() && client_ready():
get_data_from_fifo();
發送數據到客戶端();
休息;
}
}
使客戶端再次成為從屬
添加緩沖區的行為使客戶端進程成為通信的主控。它標志著服務器和客戶端之間數據事務的開始。如果客戶端想要對 select 語句中的其他事件以及傳入數據做出反應,這將是一個問題。
您可以通過引入一個從服務器拉取并推送到客戶端的中間進程來使客戶端再次成為從屬:

在這種情況下,中間進程的偽代碼是:
while (1) {
signal_ready_to_server();
get_data_from_server();
發送數據到客戶端();
現在
客戶端進程可以對中間進程推送數據的事件做出反應,服務器進程可以對中間進程拉取數據的事件做出反應。
提高效率:利用通信緩沖區
中間過程的引入是低效的。有一個完整的過程,只關心鏟數據,改變其他進程的主從關系。如果過程很便宜,這不是問題,但在許多情況下它們很昂貴或有限。幸運的是,如果管道中有少量緩沖,您可以不使用中間過程。
如果有足夠的緩沖在管道本身中存儲一個字節,服務器可以發送一個通知字節,然后繼續處理。當服務器第一次將數據放入其緩沖區時,它會發送一個通知:

然后,客戶端可以對該通知到達的事件做出反應,并知道現在有可用的數據。然后它可以向服務器發出信號,表明它已準備好接收數據:

服務器可以對此做出響應(前提是它不忙于處理硬件)并將數據發送給客戶端:

此事務完成后,管道將清除通知字節。因此,如果緩沖區中有更多數據,服務器可以發送一個新的。

如果緩沖區為空,則管道保持暢通,直到服務器接收到更多數據。在任何時候,管道中只有一個通知字節,因此管道緩沖區永遠不會溢出并阻塞服務器進程。
在這種情況下,服務器的代碼如下所示:
int notify = 0; // 此變量跟蹤通知是否在
// 位于管道中
while (1) {
select {
case pins_ready():
get_data_from_pins();
add_data_to_fifo();
if (!notified) {
send_notification_to_client();
通知 = 1;
}
打破;
案例 !fifo_empty() && client_ready():
get_data_from_fifo();
發送數據到客戶端();
if (fifo_empty()) {
通知 = 0;
}
其他 {
send_notification_to_client();
通知 = 1;
}
打破;
這段代碼需要注意的重要一點是send_notification_to_client
調用
不會阻塞,因此代碼會一直運行。另一方面,對 send_data_to_client 的調用將阻塞,直到客戶端準備好。但是,在這種情況下,客戶端將準備就緒,因為它向服務器發出了準備就緒的信號。
這種情況下的客戶端代碼是:
while (1) {
select {
案例 get_notification_from_server():
signal_ready_to_server();
get_data_from_server();
處理數據();
休息;
。..
}
}
此版本的通信協議允許客戶端成為從屬服務器,服務器緩沖區位于正確的位置,而無需中間進程。
在 XC 中執行 在
使用XC 的XMOS 平臺上,服務器和客戶端進程將是 XC 線程,通信機制將是 XC 通道。
來自服務器的通知需要使用異步 outct 原語來發送控制令牌,而無需在通信中進行正常的 XC 同步握手。
此控制令牌應該是 XS1_CT_END 令牌,以確保在令牌傳遞到目標通道結束緩沖區后線程之間的任何內核間切換都是空閑的:
send_notification_to_client(chanend c) {
outct(c, XS1_CT_END);
}
客戶端可以在類似下面的代碼中選擇這個通知:
select {
。..
case inct_byref(c, tmp): // 接收通知
c 《: 0; // 發送就緒信號
c :》 len; // 接收數據長度
for (int i=0;i》len;i++ // 接收數據
c :》 data[i];
break;
}
評論