码迷,mamicode.com
首页 > 其他好文 > 详细

详细说说select poll epoll

时间:2016-04-26 21:39:01      阅读:189      评论:0      收藏:0      [点我收藏+]

标签:

(以下内容来自网络和自己的总结,再次感谢网络中的大神们提供的见解)


在探索select poll  epoll之前我们首先要知道什么叫多路复用:

下来探索一下为什么会用到多路复用:

首先我们看看一个客户端请求服务器的完整过程。首先,请求过来,要建立连接,然后再接收数据,接收数据后,再发送数据。

具体到系统底层,就是读写事件,而当读写事件没有准备好时,必然不可操作,如果不用非阻塞的方式来调用,那就得阻塞调用了,事件没有准备好,那就只能等了,等事件准备好了,你再继续吧。阻塞调用会进入内核等待,cpu就会让出去给其他进程使用了,你可能会说那么加进程数呀,当读写事件十分多的时候会创建很多的进程,此时进程的上下文切换会占用过多的cpu资源。有人会说那么用线程,其实线程的上下文切换也会占用过多资源,而且还会引入线程之间同步和互斥的问题,因为线程之间看到的是同一块内存资源。


所以我么就会思考能不能用一个进程来查看很多的IO时间,比如每一个人都在钓鱼每一个鱼上钩都比做是一个事件发生的话,那么一百个事件发生你可以让一百个人在那里一人拿一个鱼竿进行钓鱼,你自己负责进行鱼的收集。此时如果没有鱼上钩,那一百个人就在那阻塞等待,你自己为了收鱼也在空闲着。这里的你自己可以比作CPU,一般个人可以比作多个进程,此时如果不是所有鱼都上钩,你就十分空闲 其他人也在那拿着鱼竿空闲等待着,如果同时有多个鱼上钩了,多个人会像你汇报,此时汇报的顺序问题就是形成混乱。此时我们可以进行一下改进,比如专门找一个人拿着许多鱼竿,当一个鱼竿上的鱼上钩以后再拉起鱼竿,这样节约了人力,还解决了问题。


下来讲一个真实的故事吧:

假设你是一个机场的空管, 你需要管理到你机场的所有的航线, 包括进港,出港, 有些航班需要放到停机坪等待,有些航班需要去登机口接乘客。

你会怎么做?
最简单的做法,就是你去招一大批空管员,然后每人盯一架飞机, 从进港,接客,排位,出港,航线监控,直至交接给下一个空港,全程监控。
那么问题就来了:
    很快你就发现空管塔里面聚集起来一大票的空管员,交通稍微繁忙一点,新的空管员就已经挤不进来了。
    空管员之间需要协调,屋子里面就1, 2个人的时候还好,几十号人以后 ,基本上就成菜市场了。
    空管员经常需要更新一些公用的东西,比如起飞显示屏,比如下一个小时后的出港排期,最后你会很惊奇的发现,每个人的时间最后都花在了抢这些资源上。 (线程之间资源共享)

现实上我们的空管同时管几十架飞机稀松平常的事情, 他们怎么做的呢?
他们用这个东西

技术分享
这个东西叫flight progress strip. 每一个块代表一个航班,不同的槽代表不同的状态,然后一个空管员可以管理一组这样的块(一组航班),而他的工作,就是在航班信息有新的更新的时候,把对应的块放到不同的槽子里面。


这个东西现在还没有淘汰哦,只是变成电子的了而已。。
是不是觉得一下子效率高了很多,一个空管塔里可以调度的航线可以是前一种方法的几倍到几十倍。
如果你把每一个航线当成一个Sock(I/O 流), 空管当成你的服务端Sock管理代码的话.
第一种方法就是最传统的多进程并发模型 (每进来一个新的I/O流会分配一个新的进程管理。)
第二种方法就是I/O多路复用 (单个线程,通过记录跟踪每个I/O流(sock)的状态,来同时管理多个I/O流 。)


其实“I/O多路复用”这个坑爹翻译可能是这个概念在中文里面如此难理解的原因。所谓的I/O多路复用在英文中其实叫 I/O multiplexing. 如果你搜索multiplexing啥意思,基本上都会出这个图:

技术分享

于是大部分人都直接联想到"一根网线,多个sock复用" 这个概念,包括上面的几个回答, 其实不管你用多进程还是I/O多路复用, 网线都只有一根好伐。多个Sock复用一根网线这个功能是在内核+驱动层实现的。

重要的事情再说一遍: I/O multiplexing 这里面的 multiplexing 指的其实是在单个线程通过记录跟踪每一个Sock(I/O流)的状态(对应空管塔里面的Fight progress strip槽)来同时管理多个I/O流. 发明它的原因,是尽量多的提高服务器的吞吐能力。

在同一个进程里面, 通过拨开关的方式,来同时传输多个I/O流, (学过EE的人现在可以站出来义正严辞说这个叫“时分复用”了)。


讲了这么多故事下来到我们重点的select  poll  epoll 了

select, poll, epoll 都是I/O多路复用的具体的实现,之所以有这三个鬼存在,其实是他们出现是有先后顺序的。

I/O多路复用这个概念被提出来以后, select是第一个实现 (1983 左右在BSD里面实现的)。

select 被实现以后,很快就暴露出了很多问题。
select 会修改传入的参数数组,这个对于一个需要调用很多次的函数,是非常不友好的。
select 如果任何一个sock(I/O stream)出现了数据,select 仅仅会返回,但是并不会告诉你是那个sock上有数据,于是你只能自己一个一个的找,10几个sock可能还好,要是几万的sock每次都找一遍(其实select支持不了几万个),这个无 谓的开销就颇有海天盛筵的豪气了。
select 只能监视1024个链接, 这个跟草榴没啥关系哦,linux 定义在头文件中的,参见FD_SETSIZE。
select 不是线程安全的,如果你把一个sock加入到select, 然后突然另外一个线程发现,尼玛,这个sock不用,要收回。对不起,这个select 不支持的,如果你丧心病狂的竟然关掉这个sock, select的标准行为是。。呃。。不可预测的, 这个可是写在文档中的哦.
“If a file descriptor being monitored by select() is closed in another thread, the result is unspecified”
霸不霸气


于是14年以后(1997年)一帮人又实现了poll, poll 修复了select的很多问题,比如
poll 去掉了1024个链接的限制,于是要多少链接呢, 主人你开心就好。
poll 从设计上来说,不再修改传入数组,不过这个要看你的平台了,所以行走江湖,还是小心为妙。
其实拖14年那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select很长段时间已经满足需求。

但是poll仍然不是线程安全的, 这就意味着,不管服务器有多强悍,你也只能在一个线程里面处理一组I/O流。你当然可以那多进程来配合了,不过然后你就有了多进程的各种问题。

于是5年以后, 在2002, 大神 Davide Libenzi 实现了epoll.

epoll 可以说是I/O 多路复用最新的一个实现,epoll 修复了poll 和select绝大部分问题, 比如:
epoll 现在是线程安全的。
epoll 现在不仅告诉你sock组里面数据,还会告诉你具体哪个sock有数据,你不用自己去找了。

可是epoll 有个致命的缺点。只有linux支持。(这个缺点还是缺点吗,主人,你开心就好)  比如BSD上面对应的实现是kqueue。


 PS: 上面所有这些比较分析,都建立在大并发下面,如果你的并发数太少,用哪个,其实都没有区别。

   于Linux网络接口来说,你只有一个网卡,却需要同时处理N个链接,这里就需要Multiplexing;

上升到软件层面,也就是题主关心的几个系统调用,就是IO(网络socket包括在内)需要one to many的时候提供一种multiplexing机制。

简单的说下自己的理解
io分为磁盘io和网络io,这里说的是网络io。我们知道计算机之间传输数据是流传输。一台计算机网络io只会有一个。

这里说单进程
在最基本的c/s demo中,send/recv就是在一条io通道收发数据,这就是基本的网络io,但是这种操作是不能“填满”io的,也就是说大部分io资源你没有用,仅仅有一个io操作,当然你可以开多进程或多线程,代价可想而知

此时出现了io多路复用,自己的话翻译一下,复用网络io从而有多个io操作能在网络io中执行。
linux下网络io使用socket套接字来通信,普通io模型只能监听一个socket,而io多路复用可同时监听多个socket

io多路复用避免阻塞在io上(其实select poll epoll 都是阻塞的只是可以设置超时),原本为多进程或多线程来接收多个连接的消息变为单进程或单线程保存多个socket的状态后轮询处理(epoll进行了改进,后面会说)


实现io多路复用需要函数来支持,就是linux下的select/poll,epoll以及win下 iocp 和 BSD 的 kqueue


select:
下面是select的函数接口:
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
 
select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。内部使用三个位图实现

调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。

当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一 个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但 是这样也会造成效率的降低。



poll:
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
不同与select使用三个位图来表示三个fdset的方式,poll使用一个 pollfd的指针实现。
struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */

};
pollfd结构包含了要监视的event和发生的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。内部使用的是一个结构体的链表实现的

和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。


epoll:
epoll的接口如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);             
typedef union epoll_data {                 
    void *ptr;                 
    int fd;                 
    __uint32_t u32;                 
    __uint64_t u64;             
} epoll_data_t;              

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

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int epoll:
epoll的接口如下:
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);             
typedef union epoll_data {                 
    void *ptr;                 
    int fd;                 
    __uint32_t u32;                 
    __uint64_t u64;             
} epoll_data_t;              

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

int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

主要是epoll_create,epoll_ctl和epoll_wait三个函数。epoll_create函数创建epoll文件描述符,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。返回是epoll描述符。-1表示创建失败。epoll_ctl 控制对指定描述符fd执行op操作,event是与fd关联的监听事件。op操作有三种:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。epoll_wait 等待epfd上的io事件,最多返回maxevents个事件。


主要是epoll_create,epoll_ctl和epoll_wait三个函数。

epoll_create函数创建epoll文件描述符,(linux2.6之后)参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。返回是epoll描述符。-1表示创建失败。

epoll_ctl 控制对指定描述符fd执行op操作,event是与fd关联的监听事件。op操作有三种:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。

epoll_wait 等待epfd上的io事件,最多返回maxevents个事件,timeout超时时间。


epoll为什么高效(相比select)

1、  仅从上面的调用方式就可以看出epoll比select/poll的一个优势:select/poll每次调用都要传递所要监控的所有fd给select/poll系统调用(这意味着每次调用都要将fd列表从用户态拷贝到内核态,当fd数目很多时,这会造成低效)。而每次调用epoll_wait时(作用相当于调用select/poll),不需要再传递fd列表给内核,因为已经在epoll_ctl中将需要监控的fd告诉了内核(epoll_ctl不需要每次都拷贝所有的fd,只需要进行增量式操作)。

所以,在调用epoll_create之后,内核已经在内核态开始准备数据结构存放要监控的fd了。每次epoll_ctl只是对这个数据结构进行简单的维护。

 2、 此外,内核使用了slab机制,为epoll提供了快速的数据结构:
在内核里,一切皆文件。所以,epoll向内核注册了一个文件系统,用于存储上述的被监控的fd。当你调用epoll_create时,就会在这个虚拟的epoll文件系统里创建一个file结点。

当然这个file不是普通文件,它只服务于epoll。epoll在被内核初始化时(操作系统启动),同时会开辟出epoll自己的内核高速cache区,用于安置每一个我们想监控的fd,这些fd会以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说,就是物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。

3、 epoll的第三个优势在于:当我们调用epoll_ctl往里塞入百万个fd时,epoll_wait仍然可以飞快的返回,并有效的将发生事件的fd给我们用户。

这是由于我们在调用epoll_create时,内核除了帮我们在epoll文件系统里建了个file结点,在内核cache里建了个红黑树用于存储以后epoll_ctl传来的fd外,还会再建立一个list链表,用于存储准备就绪的事件,当epoll_wait调用时,仅仅观察这个list链表里有没有数据即可。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。

所以,epoll_wait非常高效。而且,通常情况下即使我们要监控百万计的fd,大多一次也只返回很少量的准备就绪fd而已,所以,epoll_wait仅需要从内核态copy少量的fd到用户态而已。

那么,这个准备就绪list链表是怎么维护的呢?当我们执行epoll_ctl时,除了把fd放到epoll文件系统里file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个fd的中断到了,就把它放到准备就绪list链表里。所以,当一个fd(例如socket)上有数据到了,内核在把设备(例如网卡)上的数据copy到内核中后就来把fd(socket)插入到准备就绪list链表里了。

 

如此,一颗红黑树,一张准备就绪fd链表,少量的内核cache,就帮我们解决了大并发下的fd(socket)处理问题。

1.执行epoll_create时,创建了红黑树和就绪list链表。

2.执行epoll_ctl时,如果增加fd(socket),则检查在红黑树中是否存在,存在立即返回,不存在则添加到红黑树上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪list链表中插入数据。
3.执行epoll_wait时立刻返回准备就绪链表里的数据即可。


EPOLL事件有两种模型:
Edge Triggered (ET)
Level Triggered (LT)

LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。



详细说说select poll epoll

标签:

原文地址:http://blog.csdn.net/kai8wei/article/details/51233494

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!