select, poll和epoll的区别

转载   select, poll和epoll的区别 收藏

select()系统调用提供一个机制来实现同步多元I/O:


#
include < sys/ time . h>
# include < sys/ types. h>
# include < unistd. h>

int select ( int n,
fd_set * readfds,
fd_set * writefds,
fd_set * exceptfds,
struct timeval * timeout) ;

FD_CLR( int fd, fd_set * set ) ;
FD_ISSET( int fd, fd_set * set ) ;
FD_SET( int fd, fd_set * set ) ;
FD_ZERO( fd_set * set ) ;


调用select()将阻塞,直到指定的文件描述符准备好执行I/O,或者可选参数timeout指定的时间已经过去。
监视的文件描述符 分为三类set,每一种对应等待不同的事件。readfds中列出的文件描述符被监视是否有数据可供读取(如果读取操作完成则不会阻塞)。 writefds中列出的文件描述符则被监视是否写入操作完成而不阻塞。最后,exceptfds中列出的文件描述符则被监视是否发生异常,或者无法控制 的数据是否可用(这些状态仅仅应用于套接字)。这三类set可以是NULL,这种情况下select()不监视这一类事件。
select()成功 返回时,每组set都被修改以使它只包含准备好I/O的文件描述符。例如,假设有两个文件描述符,值分别是7和9,被放在readfds中。当 select()返回时,如果7仍然在set中,则这个文件描述符已经准备好被读取而不会阻塞。如果9已经不在set中,则读取它将可能会阻塞(我说可能 是因为数据可能正好在select返回后就可用,这种情况下,下一次调用select()将返回文件描述符准备好读取)。
第一个参数n,等于所有 set中最大的那个文件描述符的值加1。因此,select()的调用者负责检查哪个文件描述符拥有最大值,并且把这个值加1再传递给第一个参数。
timeout 参数是一个指向timeval结构体的指针,timeval定义如下:

# include < sys/ time . h>
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* 10E-6 second */
} ;


如果这个参数不是NULL,则即使没有文件描述符准备好I/O,select()也会在经过tv_sec秒和tv_usec微秒后返回。当 select()返回时,timeout参数的状态在不同的系统中是未定义的,因此每次调用select()之前必须重新初始化timeout和文件描述 符set。实际上,当前版本的Linux会自动修改timeout参数,设置它的值为剩余时间。因此,如果timeout被设置为5秒,然后在文件描述符 准备好之前经过了3秒,则这一次调用select()返回时tv_sec将变为2。
如果timeout中的两个值都设置为0,则调用 select()将立即返回,报告调用时所有未决的事件,但不等待任何随后的事件。
文件描述符set不会直接操作,一般使用几个助手宏来管理。这 允许Unix系统以自己喜欢的方式来实现文件描述符set。但大多数系统都简单地实现set为位数组。FD_ZERO移除指定set中的所有文件描述符。 每一次调用select()之前都应该先调用它。
fd_set writefds;
FD_ZERO(&writefds);

FD_SET 添加一个文件描述符到指定的set中,FD_CLR则从指定的set中移除一个文件描述符:
FD_SET(fd, &writefds); /* add 'fd' to the set */
FD_CLR(fd, &writefds); /* oops, remove 'fd' from the set */

设计良好的代码应该永远不使用FD_CLR,而且实际情况中 它也确实很少被使用。
FD_ISSET测试一个文件描述符是否指定set的一部分。如果文件描述符在set中则返回一个非0整数,不在则返回0。 FD_ISSET在调用select()返回之后使用,测试指定的文件描述符是否准备好相关动作:
if (FD_ISSET(fd, &readfds))
/* 'fd' is readable without blocking! */

因为文件描述符set是静态创建的,它们对文件描述符的最大数目强加了一个限制,能够放 进set中的最大文件描述符的值由FD_SETSIZE指定。在Linux中,这个值是1024。本章后面我们还将看到这个限制的衍生物。
返 回值和错误代码
select()成功时返回准备好I/O的文件描述符数目,包括所有三个set。如果提供了timeout,返回值可能是0;错误 时返回-1,并且设置errno为下面几个值之一:
EBADF
给某个set提供了无效文件描述符。
EINTR
等待时捕获 到信号,可以重新发起调用。
EINVAL
参数n为负数,或者指定的timeout非法。
ENOMEM
不够可用内存来完成 请求。
--------------------------------------------------------------------------------------------------------------

poll()系统调用是System V的多元I/O解决方案。它解 决了select()的几个不足,尽管select()仍然经常使用 (多数还是出于习惯,或者打着可移植的名义):

# include < sys/ poll. h>
int poll ( struct pollfd * fds, unsigned int nfds, int timeout) ;


和select()不一样,poll()没有使用低效的三个基于位 的文件描述符set,而是采用了一个单独的结构体pollfd数组,由fds指针指向这个组 。pollfd结构体定义如下:

# include < sys/ poll. h>

struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
} ;


每一个pollfd结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示poll()监视多个文件描述符。每个结构体的events域 是监视该文件描述符的事件掩码,由用户来设置这个域。revents域是文件描述符的操作结果事件掩码。内核在调用返回时设置这个域。events域中请 求的任何事件都可能在revents域中返回。合法的事件如下:
POLLIN
有数据可读。
POLLRDNORM
有普通数 据可读。
POLLRDBAND
有优先数据可读。
POLLPRI
有紧迫数据可读。
POLLOUT
写数据不 会导致阻塞。
POLLWRNORM
写普通数据不会导致阻塞。
POLLWRBAND
写优先数据不会导致阻塞。
POLLMSG
SIGPOLL 消息可用。

此外,revents域中还可能返回下列事件:
POLLER
指定的文件描述符发生错误。
POLLHUP
指 定的文件描述符挂起事件。
POLLNVAL
指定的文件描述符非法。

这些事件在events域中无意义,因为它们在合适的 时候总是会从revents中返回。使用poll()和select()不一样,你不需要显式地请求异常情况报告。
POLLIN | POLLPRI等价于select()的读事件,POLLOUT | POLLWRBAND等价于select()的写事件。POLLIN等价于POLLRDNORM | POLLRDBAND,而POLLOUT则等价于POLLWRNORM。
例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN | POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文 件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描 述符的读取和写入操作都会正常返回而不阻塞。
timeout参数指定等待的毫秒数,无论I/O是否准备好,poll都会返回。timeout指定 为负数值表示无限超时;timeout为0指示poll调用立即返回并列出准备好I/O的文件描述符,但并不等待其它的事件。这种情况下,poll()就 像它的名字那样,一旦选举出来,立即返回。
返回值和错误代码
成功时,poll()返回结构体中revents域不为0的文件描述符个数; 如果在超时前没有任何事件发生,poll()返回0;失败时,poll()返回-1,并设置errno为下列值之一:
EBADF
一个或多 个结构体中指定的文件描述符无效。
EFAULT
fds指针指向的地址超出进程的地址空间。
EINTR
请求的事件之前产生 一个信号,调用可以重新发起。
EINVAL
nfds参数超出PLIMIT_NOFILE值。
ENOMEM
可用内存不足, 无法完成请求。
--------------------------------------------------------------------------------------------------------------
以 上内容来自《OReilly.Linux.System.Programming - Talking.Directly.to.the.Kernel.and.C.Library.2007》
--------------------------------------------------------------------------------------------------------------

epoll的优点:
1.支持一个进程打开大数目的socket描述符(FD)
    select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显 然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完 美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

2.IO效率不随FD数目增加而线性下降
    传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的, 但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行 操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相 反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。

3.使用mmap加速内核与用户空间的消息传递。
    这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就 很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。

4.内核微调
    这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。 比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小 --- 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手 的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网 卡驱动架构。

### Linux I/O 复用机制:`select`、`poll` `epoll` 的区别比较 #### 一、基本概念 I/O 多路复用是一种允许单个线程同时监视多个文件描述符的技术,以便在任意一个或多个文件描述符变为可读/写时得到通知。Linux 提供了三种主要的 I/O 多路复用机制:`select`、`poll` `epoll`。 --- #### 二、具体特性对比 1. **`select`** - **特点**: - 支持的最大文件描述符数受限于系统常量(通常为 1024)。可以通过重新编译内核调整上限[^3]。 - 每次调用都需要将所有的文件描述符集合从用户空间拷贝到内核空间,并在线性扫描这些文件描述符以判断其是否准备好[^3]。 - 时间复杂度为 O(n),其中 n 表示要监控的文件描述符总数。 - **性能**: - 在少量文件描述符的情况下表现尚可,但在高并发场景下由于需要频繁地复制扫描大量文件描述符,性能下降明显[^3]。 - **典型应用场景**: - 小规模网络应用或者嵌入式设备上的简单通信程序[^3]。 2. **`poll`** - **特点**: - 解决了 `select` 中最大文件描述符数量限制的问题,理论上可以支持无限多的文件描述符。 - 类似于 `select`,仍然存在每次调用都要将所有感兴趣的文件描述符列表传送到内核的空间开销问题[^3]。 - 同样采用轮询方式检测哪些文件描述符已准备就绪,时间复杂度仍为 O(n)[^3]。 - **性能**: - 相较于 `select` 略有改进,但由于底层实现原理相似,在大规模并发情况下依然不够高效[^3]。 - **典型应用场景**: - 较大但非极高的并发环境下的服务器端编程。 3. **`epoll`** - **特点**: - 基于事件驱动模型设计,专门针对高性能需求而优化[^1]。 - 使用红黑树存储注册过的文件描述符及其关联的状态信息;利用双向链表管理活跃的事件队列[^4]。 - 只需一次性的将文件描述符加入到 epoll 实例中即可,后续无需重复传输整个集合至内核层。 - 支持两种触发模式——边缘触发(Edge Triggered, ET)与水平触发(Level Triggered, LT),提供了更大的灵活性[^1]。 - **性能**: - 即使面对海量连接也能维持较高的效率,因为它只会关注那些确实发生了变化的少数几个文件描述符而不是全部检查一遍[^1]。 - 数据结构的设计使得无论是添加还是查询操作都非常迅速,整体时间复杂度接近于 O(1)[^4]。 - **典型应用场景**: - Web 服务、数据库代理等超高并发的服务端架构。 --- #### 三、总结表格 | 属性 | Select | Poll | Epoll | |--------------|---------------------------------|--------------------------------|-------------------------------| | 文件描述符限 | 默认最多 1024 | 无固定限制 | 几乎不受限 | | 效率 | O(n) | O(n) | 接近 O(1) | | 用户态<->内核态交互频率 | 每次都需传递完整集合 | 每次也都需传递完整集合 | 初始化阶段一次性设置 | | 是否支持多种触发模式 | 否 | 否 | 是 | --- #### 四、代码示例 以下是使用 `epoll` 的一段简化版 C++ 示例代码: ```c++ #include <sys/epoll.h> #include <unistd.h> #define MAX_EVENTS 10 int main() { int epoll_fd = epoll_create1(0); if (epoll_fd == -1) { perror("epoll_create1"); return 1; } struct epoll_event event; event.events = EPOLLIN; event.data.fd = STDIN_FILENO; if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, STDIN_FILENO, &event) == -1) { perror("epoll_ctl: add"); close(epoll_fd); return 1; } struct epoll_event events[MAX_EVENTS]; while (true) { int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_pwait"); break; } for(int i = 0;i < nfds; ++i){ printf("Event on fd %d\n",events[i].data.fd ); } } close(epoll_fd); } ``` --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值