假设您有两个进程:一个服务器和一个客户端。服务器进程从硬件接口读取一些 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;
}
评论
查看更多