select / poll / epoll:实际差异对比

当设计具有非阻塞套接字I / O的高性能网络应用程序时,架构师需要决定使用哪种轮询方法来监视这些套接字产生的事件。有几种这样的方法,每个方法的用例不同。选择正确的方法可能对满足应用需求至关重要。
本文重点介绍了轮询方法之间的区别,并提供了使用方法的建议。

内容[ 隐藏 ]
1 Polling with select()
2 Polling with poll()
3 Polling with epoll()
4 Polling with libevent

用select轮询

旧的,值得信赖的劳动力从时代的插座仍然被称为伯克利插座。它并没有成为第一个规范,因为在那个时候没有非阻塞I / O的概念,但它确实使它在八十年代,并没有改变,因为在它的界面。
要使用select,开发人员需要初始化并填充几个具有要监视的描述符和事件的fd_set结构,然后调用select ()。典型的工作流程如下所示:
fd_set fd_in, fd_out;
struct timeval tv;

// Reset the sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );

// Monitor sock1 for input events
FD_SET( sock1, &fd_in );

// Monitor sock2 for output events
FD_SET( sock2, &fd_out );

// Find out which socket has the largest numeric value as select requires it
int largest_sock = sock1 > sock2 ? sock1 : sock2;

// Wait up to 10 seconds
tv.tv_sec = 10;
tv.tv_usec = 0;

// Call the select
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );

// Check if select actually succeed
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
if ( FD_ISSET( sock1, &fd_in ) )
// input event on sock1

if ( FD_ISSET( sock2, &fd_out ) )
    // output event on sock2

}
当选择界面被设计和开发时,没有人可能预期会有多线程应用程序服务数千个连接。因此,选择带有不少设计缺陷,这使得它不适合作为现代网络应用中的轮询机制。主要缺点包括:

选择修改传递的fd_sets,所以它们都不能被重用。即使您不需要更改任何内容,例如,如果描述符中的一个接收到数据并需要接收更多数据,则必须重新创建整个集合(argh!)或通过FD_COPY从备份副本还原。每次调用select时都必须这样做。
要了解哪些描述符引发事件,您必须手动迭代集合中的所有描述符,并在其中每个描述符调用FD_ISSET。当您有2000个描述符,只有其中一个描述符处于活动状态,而最后一个可能时,您每次等待时都会浪费CPU周期。
我只提到2000个描述符?那么选择不能支持那么多。至少在Linux上。受支持的描述符的最大数量由FD_SETSIZE常数定义,Linux常规定义为1024.虽然某些操作系统允许您在包含sys / select.h之前重新定义FD_SETSIZE来劫持此限制,但这不可移植。事实上,Linux只会忽略这个黑客,限制将保持不变。
等待时,不能修改不同线程的描述符集。假设一个线程正在执行上面的代码。现在假设你有一个客户线程,决定sock1一直在等待输入数据太长,现在是时候切断电源线了。由于这个套接字可以重新用于服务另一个付费的工作客户端,因此内务线程想关闭套接字。但是套接字位于fd_set中,该选项正在等待。
现在当这个套接字关闭时会发生什么?男人选择有答案,你不会喜欢。答案是:“如果在另一个线程中关闭由select()监视的文件描述符,那么结果是未指定的”。
如果另一个线程突然决定通过sock1发送东西,则会出现同样的问题。在选择返回之前,不可能开始监视输出事件的套接字。
选择等待的事件有限; 例如,要检测远程套接字是否关闭,您必须a)监视它进行输入,并且b)实际上尝试从套接字读取数据以检测关闭(读取将返回0)。如果你想从这个套接字中读取,那么如果你正在发送一个文件,现在不关心任何输入呢?
选择在填充描述符列表时会给您带来额外的负担,以计算最大的描述符数,并将其作为参数。
当然,操作系统开发人员在设计轮询方法时也会认识到这些缺点,并解决了大多数问题。所以你可能会问,是否有任何理由选择使用?为什么不把它存放在计算机科学博物馆的架子上?那么你可能很高兴知道是的,有两个原因,这可能对你来说非常重要,或者根本不重要。

第一个原因是可移植性。选择已经存在了很长时间,您可以确定,具有网络支持和非阻塞套接字的每个平台都将具有可执行的选择实现,同时它可能根本没有投票。不幸的是,我不是在说这里的管和ENIAC; 民意测验仅在Windows Vista及更高版本(包括Windows XP)上可用,尽管微软的压力,但截至2013年9月,34%的用户仍然使用该调查。另一个选择是仍然在这些平台上使用民意调查,并对没有它的人进行选择 ; 你是否认为合理的投资是由你决定的。

第二个原因是更加异国情调,并且与选择理论上可以处理一纳秒精度的超时相关,而poll和epoll只能处理一毫秒的精度。这不太可能是桌面或服务器系统的问题,哪个时钟甚至不能以这样的精度运行,但是在实时嵌入式平台上可能需要与某些硬件组件进行交互。例如降低控制棒关闭核反应堆 - 在这种情况下,请使用选择确保我们都保持安全!

上述情况可能是唯一的情况,您将不得不使用选择,不能使用任何其他。然而,如果您正在编写一个永远不会处理多个套接字(如200)的应用程序,则使用poll和select之间的区别不会基于性能,更多的是基于个人偏好或其他因素。

使用poll轮询

轮询是一种较新的轮询方法,可能是在有人实际尝试编写高性能网络服务器之后立即创建的。它设计得更好,不会受到选择的大多数问题的困扰。在绝大多数情况下,您将选择投票和epoll / libevent。

要使用poll,开发人员需要初始化struct pollfd的成员结构与描述符和事件进行监控,并调用投票()。典型的工作流程如下所示:

// The structure for two events
struct pollfd fds[2];

// Monitor sock1 for input
fds[0].fd = sock1;
fds[0].events = POLLIN;

// Monitor sock2 for output
fds[1].fd = sock2;
fds[1].events = POLLOUT;

// Wait 10 seconds
int ret = poll( &fds, 2, 10000 );
//检查投票是否真的成功
if(ret == -1)
//报告错误并中止
else if(ret == 0)
// 时间到; 没有检测到事件
其他
{
//如果我们检测到事件,将其清零,以便我们可以重用该结构
if(pfd [0] .revents&POLLIN)
pfd [0] .revents = 0;
//在sock1上输入事件

if(pfd [1] .revents&POLLOUT)
    pfd [1] .revents = 0;
    //在sock2上输出事件

}
调查主要是创建来解决悬而未决的问题选择了,所以在它具有以下优点:

轮询可以监视的描述符数量没有限制,因此1024的限制不适用于此。
它不会修改struct pollfd数据中传递的数据。因此,只要将生成事件的描述符的revents成员设置为零,就可以在poll()调用之间重新使用它。的IEEE规范规定:“ 在每种pollfd结构,轮询()应清除revents成员中,不同之处在于,其中应用程序要求对的状态的报告通过设置以上列出的事件,轮询(的比特中的一个)将设置的对应位如果所请求的条件为真 “。然而,在我的经验中,至少有一个平台没有遵循这个建议,而在Linux上的人2轮询并不能保证(3p民意调查)。
它允许比选择更细粒度地控制事件。例如,它可以检测远程对等体关闭,而不监视读取事件。
还有一些缺点,在上面提到的选择部分的末尾。值得注意的是,在Vista以上的Microsoft Windows中不存在轮询 ; 在Vista和以上它被称为WSAPoll虽然原型是一样的,它可以被定义为简单的:

if defined (WIN32)

static inline int poll( struct pollfd *pfd, int nfds, int timeout) { return WSAPoll ( pfd, nfds, timeout ); }

endif

而且,如上所述,轮询超时具有1ms精度,这在大多数情况下也不太可能被关注。不过,投票还有一些问题需要牢记:

像选择一样,仍然无法找出哪些描述符没有触发事件触发,而不需要遍历整个列表并检查回复。更糟糕的是,同样的情况也发生在内核空间中,因为内核必须遍历文件描述符列表来查找哪些套接字被监视,并再次遍历整个列表来设置事件。
像选择一样,不可能动态修改set或关闭正在轮询的套接字(见上文)。
但请记住,这些问题可能被认为对大多数客户端网络应用程序来说都不重要 - 唯一的例外就是像P2P这样的客户端软件可能需要处理数千个开放的连接。即使对于某些服务器应用程序,这些问题也许并不重要 因此,调查应该是您的默认选择了选择,除非你有上面提到的具体原因。更多的是,如果以下是真实的,投票应该是你的首选方法,即使是epoll也是如此:

您需要支持的不仅仅是Linux,并且不想使用epoll包装器,如libevent(仅限于epoll是Linux);
您的应用程序需要一次监控少于1000个插槽(您不太可能看到使用epoll的任何好处);
您的应用程序需要一次监控1000个以上的套接字,但是连接非常短暂(这是一个特殊情况,但是很可能在这种情况下,您不太可能看到使用epoll的任何好处,因为事件加速等待将浪费在添加这些新的描述符到集合中 - 见下文)
您的应用程序不是在其他线程等待它们时更改事件的方式(即,您不是使用kqueue或IO完成端口移植应用程序)。

使用epoll

epoll是Linux中最新,最大,最新的轮询方法(仅Linux)。那么它实际上是在2002年添加到内核,所以它不是那么新。它与poll和select的不同之处在于,它将关于当前监视的描述符和关联事件的信息保存在内核中,并导出API以添加/删除/修改这些信息。

要使用epoll,需要更多的准备。开发人员需要:

通过调用epoll_create创建epoll描述符;
使用所需事件和上下文数据指针初始化struct epoll结构。上下文可以是任何东西,epoll将这个值直接传递给返回的事件结构。我们存储一个指向我们的Connection类的指针。
调用epoll_ctl ( … EPOLL_CTL_ADD)将描述符添加到监视集中
请呼叫epoll_wait()等待我们保留存储空间的20个事件。与以前的方法不同,此调用接收到一个空结构,并仅在触发事件时填满它。例如,如果有200个描述符,其中有5个事件有待处理的事件,则epoll_wait将返回5,只有前端的五个成员将被初始化。如果50个描述符有待处理的事件,则前20个将被复制,30个将被排队,它们不会丢失。
通过返回的物品迭代。这将是一个短暂的迭代,因为返回的唯一事件是被触发的事件。
典型的工作流程如下所示:

//创建epoll描述符。每个应用程序只需要一个,并用于监视所有插槽。
//函数参数被忽略(它不在之前,但现在是),所以把你最喜欢的数字放在这里
int pollingfd = epoll_create(0xCAFE);

if(pollingfd <0)
//报告错误

//初始化epoll结构,以备将来添加更多成员
struct epoll_event ev = {0};

//将连接类实例与事件相关联。你可以联系任何东西
//你想要的,epoll不使用这个信息。我们存储一个连接类指针pConnection1
ev.data.ptr = pConnection1;

//监视输入,并且不要在事件之后自动重新描述描述符
ev.events = EPOLLIN | EPOLLONESHOT;
//将描述符添加到监视列表中。即使另一个线程是可以做到的
//在epoll_wait中等待 - 描述符将被正确添加
if(epoll_ctl(epollfd,EPOLL_CTL_ADD,pConnection1-> getSocket(),&ev)!= 0)
//报告错误

//等待最多20个事件(假设我们之前添加了可能有200个套接字,可能会发生)
struct epoll_event pevents [20];

//等待10秒钟
int ready = epoll_wait(pollingfd,pevents,20,10000);
//检查epoll是否真的成功
if(ret == -1)
//报告错误并中止
else if(ret == 0)
// 时间到; 没有检测到事件
其他
{
//检查是否检测到任何事件
for(int i = 0; i

使用libevent

libebent是一个库,它将本文(或其他一些)中列出的轮询方法包含在统一的API中。它的主要优点是它允许您编写代码一次,并在许多操作系统上编译并运行它,而无需更改代码。重要的是要了解libevent它只是建立在现有轮询方法之上的包装器,因此它继承了这些轮询方法具有的问题。它不会在Linux上选择支持超过1024个套接字,或者允许epoll在没有系统调用/上下文切换的情况下修改轮询事件。因此,了解每种方法的利弊还是很重要的。

不必从显着不同的方法提供对功能的访问,libevent有一个相当复杂的API,比poll甚至epoll更难使用。如果需要支持FreeBSD(epoll和kqueue),那么使用libevent比写入两个单独的后端要容易得多。因此,这是一个可行的选择,应该考虑:

您的应用程序要求表明您必须使用epoll,并且仅使用 投票功能是不够的(如果投票满足您的需求,则非常不可能,libevent将为您提供任何好处)
您需要支持其他操作系统而不是Linux,或者可能期望将来会出现这种需求。再次,这取决于您的应用程序的其他功能 - 如果它绑定到许多其他Linux特定的事情,你不会实现任何东西,通过使用libevent而不是epoll

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值