一、基础概念
1、select
使用Select就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况,如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率较高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况–读写或是异常。
函数原型:
参数解释:
(1)参数nfds是需要监视的最大的文件描述符值+1;
(2)rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合及异常文件描述符的集合;
(3)参数timeout为结构timeval,用来设置select()的等待时间
返回值:
(1) 负值:select错误
(2)正值:某些文件可读写或出错
(3)0:等待超时,没有可读写或错误的文件
timeout的取值:
(1) NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
(2)0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
(3)特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回
2、fd_set 结构
我们在select函数的参数中可以发现,第二、三、四个参数都是一个关于fd_set的结构体指针。
fd_set结构体:
fd_set 其实就是一个整形数组,更严格地说,是一个“位图”,使用位图中对应的位来表示要监视的文件描述符。
系统中也提供了一些操作fd_set的函数接口:
3、select的执行过程
select函数的调用过程如下图:
在调用select函数之前需要做一些准备工作:
(1)设置文件描述符
select可以同时监视多个文件描述符(套接字)。
此时需要先将文件描述符集中到一起。集中时也要按照监视项(接收,传输,异常)进行区分,即按照上述3种监视项分成三类。
使用fd_set数组变量执行此项操作,该数组是存有0和1的位数组。
为1表示用户告诉操作系统要监视该文件描述符,为0则不需要关心
(2)设置监视范围及超时
在调用select函数之前,我们还需要确定两件事情:
“文件描述符的监视范围是?”
文件描述符的监视范围与第一个参数有关,实际上,select函数要求通过第一个参数传递监视对象文件描述符的数量。因此,需要得到注册在fd_set变量中的文件描述符数。但每次新建文件描述符时,其值都会增1,故只需将最大的文件描述符值加1再传递到select函数即可。(加1是因为文件描述符的值从0开始)
“如何设定select函数的超时时间?”
超时时间与 最后一个参数有关,本来select函数只有在监视文件描述符发生变化时才返回,未发生变化会进入阻塞状态。指定超时时间就是为了防止这种情况发生。
不想设置超时最后一个参数只需要传递NULL。
(3)查看调用结果
如果select返回值大于0,说明文件描述符发生了变化。
关于文件描述符变化:
文件描述符变化是指监视的文件描述符中发生了相应的监视事件。
例如通过select的第二个参数传递的集合中存在需要读取数据的描述符时,就意味着文件描述符发生变化。
select 函数调用结束会告诉用户哪些文件描述符上的哪些事件就绪了
三、select的优缺点
1、优点
(1)select函数可以处理多个socket描述符。
(2)select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
(3)select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024(软上限),可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低,也就是说1024个描述符的集合数量大小刚刚好。
2、缺点
(1)每次调用select, 都需要手动设置fd集合, 从接口使用角度来说也非常不便.
(2) 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(3)同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
(4)select支持的文件描述符数量太小
select优点:
目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点
select缺点:
(1)每次调用 select(),都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大,同时每次调用 select() 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大。
(2)单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低
(3)select函数在每次调用之前都要对参数进行重新设定,这样做比较麻烦,而且会降低性能
二、poll函数的基本概念
1、什么是poll?
select() 和 poll() 系统调用的本质一样,poll() 的机制与 select() 类似,与 select() 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理。
参数解释:
(1)fds:指向一个结构体数组的第0个元素的指针,每个数组元素都是一个struct pollfd结构,用于指定测试某个给定的fd的条件
(2)nfds:表示fds结构体数组的长度
(3)timeout:表示poll函数的超时时间,单位是毫秒
函数功能:
监视并等待多个文件描述符的属性变化
函数返回值:
(1)返回值小于0,表示出错
(2)返回值等于0,表示poll函数等待超时
(3)返回值大于0,表示poll由于监听的文件描述符就绪返回,并且返回结果就是就绪的文件描述符的个数。
四、poll函数的优缺点
通过poll函数的结构以及小测试程序的编写,我们不难发现poll函数的一些特点:
1、优点
(1)poll() 不要求开发者计算最大文件描述符加一的大小。
(2)poll() 在应付大数目的文件描述符的时候速度更快,相比于select。
(3)它没有最大连接数的限制,原因是它是基于链表来存储的。
(4)在调用函数时,只需要对参数进行一次设置就好了
2、缺点
(1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
(2)与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符,这样会使性能下降
(3)同时连接的大量客户端在一时刻可能只有很少的就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降
2、epoll的相关系统调用
(1)epoll_create;
函数原型:
**参数说明:**size大小不是后备存储的最大大小,而是对内核如何标注内部结构的提示。但是在linux2.6.8以后,size参数是被忽略的,因为对于监控文件描述符的组织是红黑树,这个时候size实际上已经没有意义了
函数功能:该函数用来创建一个epoll句柄。
返回值:返回一个文件描述符
(2)epoll_ctl函数
函数原型:
函数功能:
epoll的时间注册函数
参数说明:
它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型.
(1)epfd是epoll_create()的返回值(epoll的句柄)
(2)第二个参数表示动作,用三个宏来表示
(3) 第三个参数是需要监听的fd
(4)第四个参数是告诉内核需要监听什么事.
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;
struct epoll_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 */
} __attribute__ ((__packed__));
events可以是以下几个宏的集合:
事件 描述
EPOLLIN 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT 表示对应的文件描述符可以写
EPOLLPRI 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR 表示对应的文件描述符发生错误
EPOLLHUP 表示对应的文件描述符被挂断
EPOLLET 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered) 来说的
EPOLLONESHOT 只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
(3)epoll_wait()函数
函数原型:
成员变量说明:
(1)fd:每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。
(2)events:表示要告诉操作系统需要监测fd的事件(输入、输出、错误),每一个事件有多个取值
(3)revents:revents 域是文件描述符的操作结果事件,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回。
events&revents的取值如下:
3、总的理解epoll函数
在epoll函数句柄的创建过程中,epoll函数主要做了以下三件事情:
创建红黑树:红黑树结点内容,保存了用户想要告诉操作系统要监控的哪些文件描述符上的哪些事件。
创建就绪队列:在事件就绪后,操作系统将对应的文件描述符上的事件的结点放在就绪队列中,由用户检查就绪队列,来判断是否有事件就绪。
建立驱动到内核的回调机制:回调机制不需要操作系统一直在等,在事件就绪时,驱动会告诉操作系统,有事件就绪了,操作系统就会处理眼前的事件。这个回调机制在内核中称为epollcallback,它将发生的事件添加到rdlist
一张图让我们了解epoll的工作原理:
4、epoll函数的调用过程
epoll函数调用过程主要分为以下三步:
(1)调用epoll_create函数创建一个epoll句柄
(2)调用epoll_ctl函数将要监控的文件描述符进行注册。告诉操作系统用户关心的哪些文件描述符上的哪些事件
(3)调用epoll_wait函数返回给用户哪些文件描述符上的哪些事件就绪了
二、epoll的工作方式
epoll有两种工作方式:水平触发&边沿触发
1、水平触发(LT)—默认触发方式
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
2、边沿触发(ET)
当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
四、epoll优点总结
(1)文件描述符数目无上限: 通过epoll_ctl()来注册一个文件描述符, 内核中使用红⿊黑树的数据结构来管 理所有需要监控的文件描述符.
(2)基于事件的就绪通知方式: 一旦被监听的某个文件描述符就绪, 内核会采用类似于callback的回调机制, 迅速激活这个文件描述符. 这样随着文件描述符数量的增加, 也不会影响判定就绪的性能;
(3) 维护就绪队列: 当文件描述符就绪, 就会被放到内核中的一个就绪队列中. 这样调用epoll_wait获取 就绪文件描述符的时候, 只要取队列中的元素即可, 操作的时间复杂度是O(1);
!