如上文所说,select/poll/epoll本质上都是同步阻塞的,但是由于实现了IO多路复用,在处理聊天室这种需要处理大量长连接但是每个连接上数据事件较少的场景时,相比最原始的为每个连接新开一个线程的服务模式要高效许多。

但是我们也经常听到一个说法:select效率低下,在工程实践中从不使用select,而是使用效率更高的epoll

本文会尝试分析一下造成这种现象的原因

SELECT

select范例

先给出select的官方文档

可以看到关键函数如下:

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

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

先举个简单的实例帮助理解

场景:使用select来监听多个fd上的read事件

1. 创建一个fd_set

2. 调用FD_ZERO清空这个fd_set

3. 多次调用FD_SET将需要监听的fd写入fd_set中

4. 调用select方法,传入fd_set

5. select函数会在有fd可读的时候返回,返回值为可读的fd的个数

6. 用户需要调用FD_ISSET来遍历之间写入fd_set中的所有fd,如果某个fd被设置,说明这个fd可读,于是可以读取数据了

select存在的问题

1. select支持的fd数量太少

默认情况下fd的数值只能小于1024,也就是说同时支持的fd总数必然小于1024(一般0,1,2号fd都是标准输入/输出/错误),这样也就直接导致如果使用select来做网络服务器的话,一个进程最多只能支持1000个左右的并发连接。

官方文档对此的描述如下:

An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.

这种现象的原因是fd_set内部维护了一个长度为FD_SETSIZE的bitmap,所以如果传入的fd的数值 >= FD_SETSIZE,会出现越界行为。

但是我也看到某些fd_set的实现有所不同,内部维护了一个长度为FD_SETSIZE的int数组,在这种实现中就能存储数值上不受限的fd了。

但是出于保险起见,还是不要存储数值上大于等于1024的fd为好。

2. select开销过高

a. 每次调用select都需要重新设置一次fd_set

b. 每次调用select,都需要将整个fd_set对象在用户空间与kernel中来回拷贝

c. 内核中需要遍历整个fd_set才能知道是否有fd准备就绪

d. select返回后,又需要遍历所有的fd才能知道具体是哪个fd真正就绪了

3. 总结

select效率低下,不适合于高连接数的应用场景。

如果是自己写的测试代码,拿来玩玩倒是不错的,毕竟逻辑简单,易于理解。

POLL

与select相比,poll主要修正了fd的数值必须小于FD_SETSIZE的问题,本质上并没有太大的区别

select中存在的几个问题poll都有,在这里我们就不详细介绍了

EPOLL

linux kernel 2.6中引入了epoll,它彻底解决了select/poll中存在的问题,是真正实用的、可以处理大连接数的IO复用工具

epoll范例

epoll的关键函数如下:

int epoll_create1(int flags);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

epoll_create1函数会创建一个epoll的fd并返回,在使用完epoll后必须将其关闭,否则会一直占用这个fd

epoll_ctl可以对传入的fd进行监听操作,各个参数含义如下:

- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么类型的事件,struct epoll_event结构如下:

struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};

//events可以是以下几个宏的集合,可以用 | 运算符组合多种事件:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

typedef union epoll_data {
  void *ptr;
  int fd;
  uint32_t u32;
  uint64_t u64;
} epoll_data_t;

epoll_wait则是在epfd上等待,直到有关联的fd就绪为止,返回值为就绪的fd的数目,参数events则包含了就绪的fd的信息

如果使用epoll来监听多个fd上的read事件,其工作流程如下所示:

1. 调用epoll_create1创建一个epoll的fd

2. 多次调用epoll_ctl,将需要监听的fd与上一步中创建的epfd关联起来,同时将epoll_ctl的event参数的data.fd域设置为需要监听的fd

3. 循环调用epoll_wait

4. epoll_wait返回且返回值大于0,说明已经有fd准备就绪,根据返回值遍历epoll_wait的参数events,获取所有准备就绪的fd

5. 从fd上读取数据

epoll的优势

epoll解决了上文提到的select中存在的所有问题

1. 对于需要监听的fd,只需要在初始化的时候调用一次epoll_ctl将fd与epfd相关联,后续就能循环调用epoll_wait监听事件了。无需像select一样,每次调用select方法的时候都要重复设置并传入待监听的fd集合。这样可以减少重复设置fd_set、以及将fd_set在用户空间与kernel之间来回拷贝带来的开销

2. epoll_wait方法返回的时候,可以直接从events参数中获取就绪的fd的信息,无需遍历整个fd集合。这样可以减少遍历fd_set带来的开销

3. 在调用epoll_create1时,会在kernel中建立一颗fd红黑树与一个就绪fd链表,后续调用epoll_ctl中放入的fd会被挂载到这棵树上,同时也会在kernel的中断处理函数中注册一个回调函数。一旦某个正在监听的fd上有数据可读,kernel在把数据拷贝到内核缓存区中之后,还会将这个fd插入到就绪fd链表中。这样kernel就不用在有fd就绪的时候遍历整个fd集合,从而减少开销。

水平触发与边缘触发

Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你。如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率。

Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你。这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符。

这两种触发的区别与epoll的实现机理有关。

在ET模式下,每次调用epoll_wait方法的时候,系统会直接将就绪链表清空,这样只有新就绪的fd才会被插入就绪链表并返回。

在LT模式下,每次调用epoll_wait方法时,系统只将就绪链表中的事件已经被处理完毕的fd(socket关联的kernel缓冲区数据已经被读取完毕)移除,如果某个fd上还有未被处理的数据,它会被保留在就绪链表中,并在epoll_wait返回时放在events参数中回送给用户。

ps. Java的nio就是用的水平触发。

总结

在高连接数,且大部分连接都不活跃的应用场景中(聊天室),epoll是实现IO多路复用的最优解。在经过调优的系统上,使用epoll可以无压力的处理百万级别的长连接。

但在连接数很少,且每个连接都处于高度活跃状态的应用场景(内网下载文件),select可能就是更好的选择了。

参考资料

select的内核实现原理

linux下epoll如何实现高效处理百万句柄的

Java IO 学习(二)select/poll/epoll的更多相关文章

  1. Linux IO模式以及select poll epoll详解

    一 背景 同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的.所以先限定一下本文的上下文. 本文讨论的背景是Linux环境下的network ...

  2. IO模型与select,poll,epoll

    五种:阻塞,非阻塞,IO复印,信号驱动,异步. select,poll,epoll select: 典型用32个32位的整数表示1024个描述符,并发的局限. poll:功能同上,但数据结构不一样(链 ...

  3. Python学习笔记整理总结【网络编程】【线程/进程/协程/IO多路模型/select/poll/epoll/selector】

    一.socket(单链接) 1.socket:应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socke ...

  4. python开发学习-day10(select/poll/epoll回顾、redis、rabbitmq-pika)

    s12-20160319-day10 *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: ...

  5. IO多路复用之select poll epoll

    参考文档: http://blog.csdn.net/tennysonsky/article/details/45745887 select(),poll(),epoll()都是I/O多路复用的机制. ...

  6. 多路IO复用模型--select, poll, epoll

    select 1.select能监听的文件描述符个数受限于FD_SETSIZE,一般为1024,单纯改变进程打开的文件描述符个数并不能改变select监听文件个数 2.解决1024以下客户端时使用se ...

  7. Java IO学习--(二)文件

    在Java应用程序中,文件是一种常用的数据源或者存储数据的媒介.所以这一小节将会对Java中文件的使用做一个简短的概述.这篇文章不会对每一个技术细节都做出解释,而是会针对文件存取的方法提供给你一些必要 ...

  8. Linux网络通信编程(套接字模型TCP\UDP与IO多路复用模型select\poll\epoll)

    Linux下测试代码: http://www.linuxhowtos.org/C_C++/socket.htm TCP模型 //TCPClient.c #include<string.h> ...

  9. 文档-linux io模式及select,poll,epoll

    文档-Linux IO模式详解 1. 概念说明 在进行解释之前,首先要说明几个概念:- 用户空间和内核空间- 进程切换- 进程的阻塞- 文件描述符- 缓存 I/O 1.1 用户空间与内核空间 现在操作 ...

  10. 多进程、协程、事件驱动及select poll epoll

    目录 -多线程使用场景 -多进程 --简单的一个多进程例子 --进程间数据的交互实现方法 ---通过Queues和Pipe可以实现进程间数据的传递,但是不能实现数据的共享 ---Queues ---P ...

随机推荐

  1. Neon Lights in Hong Kong【香港霓虹灯】

    Neon Lights in Hong Kong Neon is to Hong Kong as red phone booths are to London and fog is to San Fr ...

  2. 最小生成树:POJ1251-Jungle Roads(最小生成树的模板)

    POJ 1251 Jungle Roads >[poj原址:http://poj.org/problem?id=1251](http://poj.org/problem?id=1251) Des ...

  3. [BZOJ1597][Usaco2008 Mar]土地购买(斜率优化)

    Description 农夫John准备扩大他的农场,他正在考虑N (1 <= N <= 50,000) 块长方形的土地. 每块土地的长宽满足(1 <= 宽 <= 1,000, ...

  4. 手机注册过哪些网站37kfenxi.com,查询注册过哪些网站

    注册过哪些网站?发现这么一个网站,https://www.37kfenxi.com?_=cnblogs 可以根据手机号码查询注册过哪些网站,然后通过大数据分析出机主的性格,爱好等. 据说还可以查老板, ...

  5. HDU 3032 Nim or not Nim?(Multi_SG,打表找规律)

    Nim or not Nim? Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)T ...

  6. 如何将int转换为datetime?

    $timestamp = 1210003200; $datetime = date('Y-m-d H:i:s', $timestamp); echo "该时间戳代表的时间:", $ ...

  7. 一次失败的刷题经历:[LeetCode]292之尼姆游戏(Nim Game)

    最近闲来无事刷LeetCode,发现这道题的Accept Rate还是挺高的,尝试着做了一下,结果悲剧了,把过程写下来,希望能长点记性.该题的描述翻译成中文如下: 你正在和你的朋友玩尼姆游戏(Nim ...

  8. Python-S9——Day83-ORM项目实战

    01 上节回顾 02 后台管理布局 03 按钮权限控制的简单形式 04 修改表结构 05 重构数据结构 06 限制权限颗粒度 01 上节回顾 1.1 项目的组织架构: 1.2 项目组件的版本说明: 使 ...

  9. c语言入门-01

    当我们学c语言我们学些什么. [1]编译机制 当我们写好c的代码,生产了程序,这中间到底做了些什么? 这个就是c语言的编译过程 我们分别来解析这上面的过程. 我们写出我们第一个c程序. #includ ...

  10. 菜鸟之路——机器学习之非线性回归个人理解及python实现

    关键词: 梯度下降:就是让数据顺着梯度最大的方向,也就是函数导数最大的放下下降,使其快速的接近结果. Cost函数等公式太长,不在这打了.网上多得是. 这个非线性回归说白了就是缩小版的神经网络. py ...