select
系统由select函数来实现多路复用。
这个函数用来让我们的程序监控多个文件描述符的状态变化。
程序会停在select这里等待,直到被监视的文件描述符一个或者多个发生变化。
函数原型如下:
#include <sys/select.h>
int select(nfds, readfds, writefds, exceptfds, timeout)
nfds:需要监视的文件描述符+1
readfds:可读文件描述符集合
writefds:可写文件描述符集合
exceptfds:异常文件描述符集合
timeout:用来设置select等待的时间
timeout的取值有三种:
1.NULL,表示select没有timeout,select将一直被阻塞,直到某个文件描述符上发生事件
2.0,仅仅检测描述符集合的状态
3.特定的其他时间,在这个时间没有事件发生,select将会超时
fd_set相关:
FD_ZERO(fd_set *fdset):清空fdset与所有文件句柄的联系。
FD_SET(int fd, fd_set *fdset):建立文件句柄fd与fdset的联系。
FD_CLR(int fd, fd_set *fdset):清除文件句柄fd与fdset的联系。
FD_ISSET(int fd, fd_set *fdset):检查fdset联系的文件句柄fd是否可读写,当>0表示可读写。
select的过程:(8个比特位)
1.fd_set set; FD_ZERO(&set);置零 00000000
2.需要监视的文件描述符为6,FD_SET(fd,&set); 00100000
3.再加入1,2 00100011
4.执行select(7,&set,0,0,0)
5.若1,2有事件发生 返回00000011,没事件发生的6就会被置零
select的两个特点:
1.可监控的文件描述符取决于sizeof(fd_set)的大小,由于fd_set有固定值,所以可监控的文件描述符是上限的
2.将fd加入select的同时,需要用一个数组来进行保存select中监控的fd
一是用于select返回后,数组和fd_set进行判断
二是select每次返回后都要清空并未发生的fd,每次开始select前都要从数组中获取fd然后加入到fd_set中,并且获得fd的最大值,用于select的第一个参数。
select的缺点:
1.每次select,都需要设置fd集合
2.每次select,都需要把fd集合从用户态拷贝到内核态
3.每次select,都需要再内核遍历所有的fd
4.select支持的文件描述符太小了
epoll
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
epoll的三个函数调用:
int epoll_create
int epoll_create(int size)
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
自从Linux 2.6.8开始,size参数被忽略,但是依然要大于0。
int epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注册函数,它不同与select()是在监听事件时告诉 内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,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队列里。
int epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,类似于select()调用。参数events用来从 内核得到事件的集合,maxevents表示每次能处理的最大事件数,告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
epoll的工作原理:
1.每一个epoll对象都有一个eventpoll结构体,用来存放epoll_ctl存放的事件
2.这些事件会被挂载到红黑树当中,红黑树可以识别出重复的事件
3.所有添加到epoll中的事件都会和设备驱动程序建立回调关系,也就是说,当需要响应的事件发生时,就会调用这个回调函数
4.通过这个回调方法,就会把将发生的事件挂载到双向链表中
5.在epoll中,每一个事件,都会对应建立一个epitem结构体
6.在调用epoll_wait的时候,只需要判断epitem结构体是否在这个双向链表当中
7.当这个双向链表不为空,说明有事件就绪,将发生的事件复制到用户态,并且就绪事件的数量返回给用户
epoll的使用过程:
1.epoll_create创建句柄
2.epoll_ctl,将需要监控的文件描述符注册
3.epoll_wait等待事件就绪
epoll相比于select的优点:
1.函数变成三个,调用的过程变得方便高效,不需要每次的设置需要关注的文件描述符
2.只在某个阶段需要将文件描述符结构拷贝到内核态,这个操作不频繁
3.采用事件回调机制,不需要每次都遍历整个文件描述符集合,只需要将就绪的事件添加到就绪队列,然后再epoll_wait返回
4.文件描述符没有上限
epoll的工作方式:
1.水平触发工作方式(LT)epoll默认的工作方式
epoll检测socket事件就绪的时候,不会立刻进行处理,或者可以处理一部分
比如,有10k的数据,可以先读5k,在第二次调用epoll_wait将剩下的5k读完,这时候epoll_wait任然可以直接立刻返回并且通知socket读事件就绪
直到缓冲区没有任何数据epoll_wait才不会立刻返回
支持阻塞读写和非阻塞读写
2.边缘触发工作方式(ET)
epoll检测socket事件就绪的时候,必须立刻作出处理
比如,有10k的数据,可以先读5k,在第二次调用epoll_wait读剩下5k的时候,epoll_wait就不会再返回了
所以在ET模式下,文件描述符就绪后,只有一次处理机会
ET比LT的调用epoll_wait次数少的多,所以ET比LT更加高效
只支持非阻塞的读写