I/O多路转接——select,poll,epoll

准备

通信的本质就是I/O,像我们的进程,线程,网络通信,不就是因为一端进行输入一端进行输出,简而言之就是I/O。其中I/O也就是两个步骤,分别就是等和拷贝,等即是,读端等待写端传入数据,拷贝就是将数据拷贝到内存中。
其中I/O我们在Os中有五种模型。如下图所示。
在这里插入图片描述

其中我们的多路转接效率最高,因为它等待的时间最短,通过监控多个fd,有合适的就直接进行分配I/O了。

常见的多路转接模型

select

select只负责对fd进行等待。select可以同时对多个fd进行等待,这就是他的多路转接功能。

我们先看一下它的接口。

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

nfds:表示最大的fd+1。
readfds:表示只关心读事件。
writefds:表示只关心写事件。
exceptfds:表示只关心异常事件。
timeout:设置等待时间。如果为0,表示非阻塞等待,如果为NULL,表示阻塞等待。
返回值:>0,实际就绪的fd的个数。<0,表示select出错了,=0,则select超时了。

接口中有一个fd_set作为一个数据类型,它是fd的集合。在内核中它是用位图表示的。其中用三个位图分别进行表示。

每一个fd在位图中用比特位表示位置,用比特位的内容表示当前是否关心或是否就绪当前的fd。

fd_set* readfds,fd_set* writefds,…,这三个是输入输出型参数。

如果是输入参数时,表示用户告诉内核,你需要帮我关心fd_set中的所有事件。比特位的内容表示当前是否关心当前的fd。
如果是输出参数时,表示内核告诉用户,fd_set中的那些事件是已经就绪的。比特位的内容表示当前的fd是否就绪。
对fd_set进行操作,我们需要利用函数进行操作。
void FD_CLR(int fd, fd_set *set);//set中删除fd
int FD_ISSET(int fd, fd_set *set);//判断
void FD_SET(int fd, fd_set *set);//添加
void FD_ZERO(fd_set *set);//清空

优缺点

优点:可以等待多个fd
缺点:1.等待的fd具有上限。2.输入输出参数混合,每次需要重新设定。3.内核与用户需要进行拷贝。4.监视时,Os会遍历所有的fd,至少有一个就绪进行返回,没有,就按照策略。

代码可以参考:select演示代码

poll

poll在select基础上解决了两个问题:1.等待的fd没有上限,2.输入输出参数分离。
我还是先看一下的它的接口,看看与select有什么区别?

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// pollfd 结构
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};

fds 是一个poll 函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合.
nfds 表示fds 数组的长度.
timeout 表示poll 函数的超时时间, 单位是毫秒(ms).

它与select相比,没有了nfds,rfds,wfds,用pollfd来进行对读写事件的监控。

• pollfd 结构包含了要监视的event 和发生的event,不再使用select“参数-值”传递的方式. 接口使用比select 更方便.
• poll 并没有最大数量限制(但是数量过大后性能也是会下降).
在这里插入图片描述

缺点:

poll中监听的文件描述符增多时。

• 和select一样,poll返回后,需要轮询pollfd 来获取就绪的描述符.
• 每次调用poll 都需要把大量的pollfd 结构从用户态拷贝到内核中.
• 同时连接的大量客户端在一时刻可能只有很少的处于就绪状态, 因此随着监视的描述符数量的增长, 其效率也会线性下降.

epoll

epoll是为了处理大量的句柄而做了改进的poll。而句柄就是file,fd。也是目前性能最好的多路转接。
我们继续查看它的系统调用接口。

int epoll_create(int size);
创建一个epoll 的句柄.
• 自从linux2.6.8 之后,size 参数是被忽略的.
• 用完之后, 必须调用close()关闭.
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
• 它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里
先注册要监听的事件类型.
• 第一个参数是epoll_create()的返回值(epoll 的句柄).
• 第二个参数表示动作,用下面的三个宏来表示.
	• EPOLL_CTL_ADD:注册新的fd 到epfd 中;
	• EPOLL_CTL_MOD:修改已经注册的fd 的监听事件;
	• EPOLL_CTL_DEL:从epfd 中删除一个fd;
• 第三个参数是需要监听的fd.
• 第四个参数是告诉内核需要监听什么事件

epoll_event结构如下:
在这里插入图片描述
events对应事件
在这里插入图片描述

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

收集在epoll 监控的事件中已经发送的事件。

参数events 是分配好的epoll_event 结构体数组.
epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个events 数组中,不会去帮助我们在用户态中分配内存).
maxevents 告之内核这个events 有多大,这个maxevents 的值不能大于创建epoll_create()时的size.
参数timeout 是超时时间(毫秒,0 会立即返回,-1 是永久阻塞).
如果函数调用成功,返回对应I/O 上已准备好的文件描述符数目,如返回0 表示已超时, 返回小于0 表示函数失败.

现在我们来谈谈epoll的工作原理

当某一进程调用epoll_create 方法时,Linux 内核会创建一个eventpoll 结构体,这个结构体中有两个成员与epoll 的使用方式密切相关.
struct eventpoll{

/*红黑树的根节点,这颗树中存储着所有添加到epoll 中的需要监控的事件
*/
struct rb_root rbr;
/双链表中则存放着将要通过epoll_wait 返回给用户的满足条件的事件/
struct list_head rdlist;

};

1.每一个epoll 对象都有一个独立的eventpoll 结构体,用于存放通过epoll_ctl 方法向epoll 对象中添加进来的事件.
2.这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n 为树的高度)。
3.而所有添加到epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.
4.这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist 双链表中。
5.在epoll 中,对于每一个事件,都会建立一个epitem 结构体。
6.当调用epoll_wait检查否有事件发生时,只需要检查eventpoll 对象中的rdlist 双链表中是否有epitem 元素即可。
7.如果rdist不为空,则将发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1)。(高效原因)

struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll 对象
struct epoll_event event; //期待发生的事件类型

在这里插入图片描述

优点
• 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开。
• 数据拷贝轻量: 只在合适的时候调用EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll 都是每次循环都要进行拷贝)。
• 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响。
• 没有数量限制: 文件描述符数目无上限。
缺点
•epoll 是 Linux 特有的机制,不支持跨平台
•在内核中维护一个事件表,这会占用一定的内存。
•在高并发长连接场景下表现优异,但在短连接场景下可能不如其他技术高效。
•只能监控文件描述符上的 I/O 事件,无法直接处理信号(如 SIGINT、SIGTERM)。
epoll的工作模式

epoll的工作模式有两种:LTET

LT,水平触发Level Triggered 工作模式

举个栗子,当你收快递时,你有多个快递,快递员张三,打电话叫你来取快递,而你却来一次取一个快递,这时候张三会叫你快来取剩下的快递,而你仍就是一次取一个,张三也就一直给你打电话。这种情况就是LT模式。
epoll默认是LT模式。

• 当epoll 检测到socket 上事件就绪的时候, 可以不立刻进行处理. 或者只处理一
部分.
• 如上面的例子, 由于只读了1K 数据, 缓冲区中还剩1K 数据, 在第二次调用
epoll_wait 时, epoll_wait 仍然会立刻返回并通知socket 读事件就绪.
• 直到缓冲区上所有的数据都被处理完, epoll_wait 才不会立刻返回.
• 支持阻塞读写和非阻塞读写

ET,边缘触发Edge Triggered 工作模式

举个栗子,当你收快递时,你有多个快递,快递员李四,打电话叫你来取快递,而你却来一次取一个快递,这时候李四后面就不会和你打电话叫你来取剩下的快递,之后如果你又有快递了,李四也只会和你打一次电话,这时你就需要将所有快递拿完,这种情况就是ET模式。

当epoll 检测到socket 上事件就绪时, 必须立刻处理.
如上面的例子, 虽然只读了1K 的数据, 缓冲区还剩1K 的数据, 在第二次调用epoll_wait 的时候, epoll_wait 不会再返回了.
ET模式下,文件描述符上的事件就绪后,只有一次处理机会。
ET的性能要比LT的效率高,因为epoll_wait返回的次数减少。
只支持非阻塞的读写。

select 和poll 其实也是工作在LT 模式下. epoll 既可以支持LT, 也可以支持ET.

对比LT与ET

使用ET 能够减少epoll 触发的次数. 但是代价就是强逼着我们一次响应就绪过程中就把所有的数据都处理完。
相当于一个文件描述符就绪之后, 不会反复被提示就绪, 看起来就比LT 更高效一些. 但是在LT 情况下如果也能做到每次就绪的文件描述符都立刻处理, 不让这个就绪被重复提示的话, 其实性能也是一样的。
另一方面, ET 的代码复杂程度更高了。

ET和非阻塞文件描述符

eg.我们有一个服务器和一个客户端。当客户端向服务器发送10k请求时,如果服务端读取完10k数据时,就会向客户端发送应答。
如果当服务端只读取到了1k数据时,缓冲区有9k数据,此时由于epoll 是ET 模式, 并不会认为文件描述符读就绪. epoll_wait 就不会再次返
回. 剩下的9k 数据会一直在缓冲区中. 直到下一次客户端再给服务器写数据。epoll_wait 才能返回。
这时候就有一个问题:客户端不会向服务器发送请求,服务器也没有读完数据。
为了解决这几个问题。我们就可以采用==非阻塞轮训==的方式来读缓冲区, 保证一定能把完整的请求都读出来。
如下图:

在这里插入图片描述

epoll的使用场景

epoll 的高性能, 是有一定的特定场景的. 如果场景选择的不适宜, epoll 的性能可能适得其反.
对于多连接, 且多连接中只有一部分连接比较活跃时, 比较适合使用epoll.

例如, 典型的一个需要处理上万个客户端的服务器, 例如各种互联网APP 的入口服务器,
这样的服务器就很适合epoll。

epoll代码可以参考一下:epoll演示代码

结语

这就是简单的多路转接几种模型。
有错的地方,请大佬们指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值