3.4 事件多路分发解决方案
第四种方法利用通过 select和 poll系统调用而可用的事件多路分发功能。这些机制突破了上述其它几种解决方案的限制。Select 和 poll 都允许网络应用程序为发生在多个 I/O 描述符上的不同类型的事件而等待的时间有长有短,既无须要求轮询,也不要求多进程或多线程调用。这一部分概述 select和 poll 系统调用,草拟使用这两个调用的日志服务器守护进程的样例实现,并且用 Reactor(反应堆)的面向对象类库的优势对照现有事件多路分发服务的局限性。
3.4.1 Select和 poll 系统调用
下面的段落 select调用(在图10中示出)和 poll调用(在图11中示出)的相似之处和不同点。这些调用支持基于 I/O 和基于定时器的事件多路分发。Select和 poll 的语法和语义在 [8] 中有极为详细的讲解。
抛开它们不同的 APIs,select 和 poll 共享许多共有的特性。例如,它们都在一组 I/O 描述符上等待不同的输入、输出以及例外事件的发生,并且返回一个整型值指示有多少个事件发生。另外,这两个系统调用允许应用程序指定一个指示等待事件发生的最大时间的超时间隔。三个基本的超时间隔包括(1)“永久”等待,(即,直到I/O事件的发生或者信号中断系统调用),(2)等待一定的时间单元(既可以以秒/微秒(select),也可以用毫秒(poll)来衡量),和(3)执行“轮询”(即,检查所有的描述符,并且立即带着结果返回)。
Select 和 poll 之间也有几个不同之处。例如,select 使用三个描述符组(descriptor set)(一个用于读,一个用于写,另一个用于例外),其当作一个位掩码(bit-mask)来实现,以减少所使用的空间量。位掩码中的每一位对应一个可能被允许检查特定I/O事件的描述符。另一方面,poll 函数多少更通用,并且接口少了一些绕弯。Poll API 一个 pollfd 结构的数组,一个此数组中结构数目的计数,以及一个超时值。数组中每一个 pollfd 结构包含(1)检查 I/O 事件的描述符(-1表示这一条结构应该被忽略),(2)感兴趣的事件(一个或多个)(例如,输入和输出情况下的各个属性),以及(3)在描述符(如,输入、输出、挂起,以及错误)上实际发生的事件(一个或多个),这些事件根据从 poll 系统调用的返回来决定是否被允许。注意,在 System V Release 4 之前的版本中,poll 只可用于像终端和网络接口这样的流(STREAM)设备。特别是,它并不可用于像 原始 UNIX 文件和目录这样的任意 I/O 描述符。Select 和 SVR4 poll 系统调用可作用于所有类型的 I/O 描述符之上。
3.4.2 基于 select 的日志服务器示例
图 13 例示了一个使用 BSD select 系统调用来执行服务器日志守护进程的代码片断。这个服务器实现使用两个描述符组:(1)read_handles(其了解与活动的客户端连接相关的 I/O 描述符)和(2)temp_handles(其是一个通过“值传递”(value/result)给 select 系统调用的 read_handles 描述符组的副本)。最初,在 read_handles 描述符组中只有那个被打开的位对应那个“监听”来自客户日志守护进程的新的入连接请求。
在初始化完成之后,主循环使用 temp_handles 作为其唯一的描述符组参数(因为服务器既无意处理“写”事件,也无意处理“例外”事件)调用 select。因为最终的参数是 NULL struct timeval * NULL 指针,select 调用阻塞,直到一个或者多个客户端发送日志记录或者请求新的连接(注意:如果发生中断,select 必须手工重新启动)。当 select 返回时,temp_handles 变量会被修改以指示哪一个描述符有挂起的日志记录数据,或者新的客户端连接请求。首先通过迭代整个 temp_handles 组来检查当前已经准备好读(注意: select 语义保证 recv 将不会阻塞在这个读操作上)的描述符来处理日志记录。Recv 函数在客户端关闭连接的时候返回 0。这将知会主服务器循环清除在 read_handles 组中代表那个连接的这个特定位。
在所有挂起的日志记录都已经被处理完毕之后,服务器检查新的连接请求是否已经到达监听的 I/O 描述符。如果一个或者多个请求已经到达,那么它们将被接受,并且在 read_handles 描述符组中的对应位也会被打开。这一部分代码例示了 select 的“轮询”特性。例如,如果在 struct timeval 参数的两个域都被置 0,select 将检查打开的描述符,并且如果有挂起的连接请求,那么就立即返回以通知应用程序。注意,服务器如何使用 width 变量来了解最大的 I/O 描述符值。这个值限制 select 在每一个调用上必须监督的描述符数目。
int
main( void )
{
// Create a server end-point.
ACE_SOCK_Acceptor acceptor( ( ACE_INET_Addr )LOGGER_PORT );
ACE_SOCK_Stream new_stream;
ACE_HANDLE s_handle = acceptor.get_handle();
ACE_HANDLE maxhandlepl = s_handle + 1;
fd_set temp_handles;
fd_set read_handles;
FD_ZERO( &temp_handles );
FD_ZERO( &read_handles );
// Loop forever performing logging server processing.
for (;;)
{
temp_handles = read_handles; // structure assigment.
// Wait for client I/O events.
ACE_OS::select( maxhandlepl, &temp_handles, 0, 0 );
// Handle pending logging records first (s_handle + 1
// is guaranteed to be lowest client descriptior).
for ( ACE_HANDLE handle = s_handle + 1; handle < maxhandlepl; handle++ )
{
if ( FD_ISSET( handle, &temp_handles ) )
{
ssize_t n = handle_logging_record( handle );
// Guranteed not to block in this case!
if ( n == -1 )
ACE_DEBUG( ( LM_DEBUG, "logging failed" ) );
else if ( n == 0 )
{
// Handle client connection shutdown.
FD_CLR( handle, &read_handles );
ACE_OS::close( handle );
if ( handle + 1 == maxhandlepl )
{
// Skip past unused descriptors.
while ( !FD_ISSET( --heandle, &read_handles ) )
continue;
maxhandlepl = handle + 1;
}
}
}
if ( FD_ISSET( s_handle, &temp_handles ) )
{
// Handle all pending connection requests
// (note use of "polling" feature).
while ( ACE_OS::select( s_handle + 1, &temp_handles, 0, 0, ACE_Time_Value::zero ) > 0 )
if ( acceptor.accept( new_stream ) == -1 )
ACE_DEBUG( ( LM_DEBUG, "accept" ) );
else
{
handle = new_stream.get_handle();
FD_SET( handle, &read_handles );
if ( handle >= maxhandlepl )
maxhandlepl = handle + 1;
}
}
} // for (;;)
}
图13:使用 select API 的事件多路分发服务器
// Maximum per-process open I/O descriptos.
const int MAX_HANDLES = 200;
int
main( void )
{
// Create a server end-point.
ACE_SOCK_Acceptor acceptor( ( ACE_INET_Addr )LOGGER_PORT );
struct pollfd poll_array[MAX_HANDLES];
ACE_HANDLE s_handle = acceptor.get_handle();
poll_array[0].fd = s_handle;
poll_array[0].events = POLLIN;
for ( int nhandles = 1;;)
{
// Wait for client I/O events.
ACE_OS::poll( poll_array, nhandles );
// Handles pending logging messages first
// (poll_array[ i = 1].fd is guaranteed to be
// lowest client descriptor).
for ( int i = 1; i < nhandles; i++ )
{
if ( ACE_BIT_ENABLED( poll_array[i].revents, POLLIN )
{
char buf[BUFSIZ];
sszie_t n_logging_record( poll_array[i].fd );
// Guaranteed not to block in this case!
if ( n == -1 )
ACE_DEBUG( ( LM_DEBUG, "read failed" ) );
else if ( n == 0 )
{
// Handle client connection shutdown.
ACE_OS::close( poll_array[i].fd );
poll_array[i].fd = poll_array[--nhandles].fd;
}
}
}
if ( ACE_BIT_ENABLED( poll_array[0].revents, POLLIN ) )
{
// Handle all pending connection requests
// (note use of "polling" feature).
while ( ACE_OS::poll( poll_array, 1, ACE_Time_Value::zero ) > 0 )
if ( acceptor.accept( new_stream, &client ) == -1 )
ACE_DEUBG( ( LM_DEBUG, "accept" ) );
else
{
poll_array[nhandles].fd = POLLIN;
poll_array[nhandles++].fd = new_stream.get_handle();
}
}
}
}
图14:使用 poll API 的事件多路分发服务器
3.4.3 基于 poll 的日志服务器示例
图14 在使用 select 的位置采用 System V UNIX poll 系统调用,重新实现服务器日志守护进程的主要处理循环。注意,这两个服务器的总体结构上几乎是一致的。不过,还是要进行许多细微的修改以适应 poll 接口。例如,不像 select (其针对读、写和例外有单独的位掩码),poll 使用一个单独的 pollfd 结构的数组。通常,poll API 比 select 更加通用,允许应用程序等待范围更广的事件(如,“优先级-分类”的 I/O 事件和信号)。不过,这两个例子中的总体复杂度和源代码的行数几乎相同。
3.4.4 现有事件多路分发服务的局限
事件多路分发服务解决几个以上所展示的几种途径的局限性。例如,基于事件多路分发的服务器日志守护进程既不要求“忙等”,也不要求单独的进程创建。不过,仍然有许多与直接使用 select 或 poll 相关的许多问题。这一部分说明一些剩余的问题,并解释 Reactor 是如何进行设计以解决这些问题。
复杂并且易错的接口: 用于 select 和 poll 的接口非常通用,在单个的系统调用入口点组合了几个像“限时等待”以及多个 I/O 事件通知等服务。这个通用性降低了正确学习和使用 I/O 多路分发工具的复杂度。另一方面,Reactor 提供了一个更少秘密的 API,这个 API 由多个成员函数组成,其中的每一个执行单个定义好的活动。另外,
|