目录:
概述
什么是多路I/O转接技术?
多路IO转接的字面意思:原本使用socket套接字编程时,是服务器(应用程序)一直在阻塞等待客户端的连接,这样服务器端(应用程序)的压力太大。于是服务器请来了助手,即select、poll、epoll等,这几个函数借助内核来替服务器监视有无客户端的连接请求,当有客户端的连接请求时,再经select、poll、epoll等助手转接给服务器端处理,这样可以有效减轻服务器的压力。
1.select()
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout)
功 能:监听多个文件描述符的状态变化
参 数:
nfds:
监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
readfds:
监控有读数据到达文件描述符集合,传入传出参数 fd_set是一个结构体,可以理解为文件描述符的集合,理解为位图
writefds:
监控写数据到达文件描述符集合,传入传出参数
exceptfds:
监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
timeout:
定时阻塞监控时间,3种情况
1.NULL,永远等下去,即阻塞监听
2.设置timeval,等待固定时间
3.设置timeval里时间均为0,检查描述字后立即返回,轮询,即非阻塞监听
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
返回值:
>0:所有监听集合中(即读、写、异常3个集合),满足对应事件的总数
0:没有满足监听条件的文件描述符
-1:失败,并设置errno
值得一提的是 fd_set *readfds,该参数是传入传出参数,传入的是原始表,传出的是修改表,那么修改了什么?当有客户端建立连接时,相应的标志位会置1,
如上图所示,左边为原始表,一般默认为1024个标志位,所有的标志位都为0,当经过select函数传参回时,某些标志位会发生变化,一个客户端对应一个标志位,当有客户端进行连接时,相应标志位的值由0变成1.
文件描述符集类型: fd_set rdset;
○ void FD_ZERO(fd_set *set);
- 全部清空
○ void FD_CLR(int fd, fd_set *set);
- 从集合中删除某一项
○ void FD_SET(int fd, fd_set *set);
- 将某个文件描述符添加到集合
○ int FD_ISSET(int fd, fd_set *set);
使用select函的优缺点:
- 优点:
○ 跨平台 - 缺点:
○每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
○同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大,越到后面的fd,遍历的越多,速度就越慢。
○select支持的文件描述符数量太小了,默认是1024,每次调用select都需要将进程加入到所有监视socket的等待队列,每次唤醒都需要从每个队列中移除。这里涉及 了两次遍历,而且每次都要将整个fds列表传递给内核,有一定的开销。正是因为遍历操作开销大,出于效率的考量,才会规定select的最大监视数量,所以默认只能监视1024个socket。
2.poll()
poll结构体:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 等待的事件 */
short revents; /* 实际发生的事件 */
};
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds,int timeout);
fds:
存放需要被检测状态的套接字描述符,与select不同(select在调用之后会清空这个数组),
每当调用这个数组,系统不会清空这个数组,而是存放revents状态变化描述符变量,这样才做起来很方便。
nfds:
数组的最大长度, 数组中最后一个使用的元素下标+1
内核会轮询检测fd数组的每个文件描述符
timeout:
poll函数调用阻塞时间,单位是毫秒(ms)
-1: 永久阻塞
=0: 调用完成立即返回
>0: 等待的时长毫秒
返回值: IO发送变化的文件描述符的个数
3.epoll()
这里有一篇好文章,详细解释了epoll相关理论,可以参考下 epoll本质,这里将不在详细介绍相关理论。
相应代码地址https://github.com/qingyiz/client-server/tree/master/04_%E5%A4%9A%E8%B7%AFIO%E8%BD%AC%E6%8E%A5/epoll_mode
epoll相关API
1.epoll_create()
创建一个epoll句柄(可以理解为树),参数size用来告诉内核监听的文件描述符的个数,跟内存大小有关,可以自己设置。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。本代码中最后一句 Close(efd);
#include <sys/epoll.h>
int epoll_create(int size) size:监听数目
2.epoll_ctl()
控制某个epoll监控的文件描述符上的事件:注册、修改、删除。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epfd: 为epoll_creat的句柄
op: 表示动作,用3个宏来表示:
EPOLL_CTL_ADD (注册新的fd到epfd),
EPOLL_CTL_MOD (修改已经注册的fd的监听事件),
EPOLL_CTL_DEL (从epfd删除一个fd);
event: 告诉内核需要监听的事件
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
EPOLLIN : 表示对应的文件描述符可以读(包括对端SOCKET正常关闭)
EPOLLOUT: 表示对应的文件描述符可以写
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)
EPOLLERR: 表示对应的文件描述符发生错误
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)而言的
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3.epoll_wait()
等待所监控文件描述符上有事件的产生,类似于select()调用。
该函数返回需要处理的事件数目,如返回0表示已超时。
返回的事件集合在events数组中,数组中实际存放的成员个数是函数的返回值。返回0表示已经超时。
该函数用于轮询I/O事件的发生;
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
events: 用来存内核得到事件的集合,
maxevents: 告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,
timeout: 是超时时间
-1: 阻塞
0: 立即返回,非阻塞
>0: 指定毫秒
返回值: 成功返回有多少文件描述符就绪,时间到时返回0,出错返回-1
4 epoll进阶
事件模型
EPOLL事件有两种模型:
Level Triggered (LT) 水平触发
Edge Triggered (ET) 边缘触发
1.Level Triggered (LT) 水平触发:
- Level Triggered (LT) 水平触发只要有数据都会触发.,也就是epoll_wait就会返回
- 返回的次数与发送数据的次数没有关系
- 这是epoll默认的工作模式
epoll的工作模式默认为水平触发模式(LT),为了更直观的体现,我们先复制一份epoll.c改名为lt_epoll.c,只需要修改一些代码就可以有直观的体现
第一处就是把buf数组 缓冲区的内存改成5,这样每次只能接收五个字符
第二处就是在epoll_wait 函数下增加一个printf,这样可以直观的看到eopll_wait的调用次数
第三处主要是把printf(),改成write(),因为printf()函数没有/0时,可能会出现乱码的情况,而有/0时,会等待缓冲区满时再刷新,才输出到屏幕上,这样 可能现象不明显。
接下来 就是测试了,gcc lt_epoll.c wrap.c -o lt_epoll
当有几个客户端连接就会调用几次epoll_wait(),当客户端发送一段很长的数据时,你会发现epoll_wait触发了很多次,但我只发了一次数据,这既是水平触发模式,但是要知道的是,epoll_wait 调用次数越多, 系统的开销越大,所以这并不是一个明智的选择。
2.Edge Triggered (ET) 边缘触发:
Edge Triggered (ET) 边缘触发只有数据到来才触发,不管缓存区中是否还有数据。
边缘触发模式可以细分为两种模式,一个是边缘阻塞触发模式,一个是边缘非阻塞触发模式
默认为阻塞属性
1. 边缘阻塞触发模式
当客户端给server发数据时,发一次数据server 的 epoll_wait返回一次,不在乎数据是否读完,
当然也可以读完,使用一个 while(recv()){};但是这会出现一个很严重的问题,
因为fd文件描述符默认为阻塞属性,当读完一次数据时,recv阻塞,而且我们只有一个进程,此时就会一直阻塞在while(recv()){};该循环中,无法回到外层的循环中,也就是无法调用epoll_wait函数,问题出现了,那就是可以要解决问题,解决该问题就是阻塞问题,设置属性为非阻塞就行,这就出现了边缘非阻塞触发模式。
2. 边缘非阻塞触发模式
该模式是效率最高的
那要怎么设置该模式?
我们只需要修改fd的属性就可以了,这里会涉及一个函数fcntl();由于该函数功能较多,本节只介绍本节使用的功能
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
/* 设置文件cfd为非阻塞模式 */
int flag = fcntl(cfd, F_GETFL);
flag |= O_NONBLOCK;
fcntl(cfd, F_SETFL, flag);
cfd:文件描述符
F_SETFL:设置文件描述符标志
F_GETFD:读取文件描述符标志
O_NONBLOCK:非阻塞模式
效果图: