关于IO多路复用

1、Linux中基本的概念

用户空间/内核空间

操作系统的核心是内核kernel,可以访问受保护的内存空间,也可以访问硬件设备的所有权限。

为了保证用户进程不能直接操作内核,操作系统将全部的虚拟地址分为两部分,一部分为内核空间,一部分为用户空间。例如32位的操作系统,将最高的1G字节供内核使用,称为内核空间;较低的3G字节供用户进程使用。

进程切换

内核必须有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。称为进程切换,进程切换是非常消耗资源的,包括保存当前进程上下文,更新PCB,把PCB移到相应的队列,选择另一个进程执行,更新内存管理的数据结构,恢复上下文。

进程阻塞

正在执行的进程由于某些事件未发生,如请求资源失败、等待某种操作完成等,由系统自动执行阻塞Block原语,使自己由运行状态变为阻塞状态。可见,线程是在运行态主动转为阻塞态的,并且阻塞态不占用CPU资源。

文件描述符

用于表述指向文件的引用的抽象化概念。在形式上是一个非负整数,实际上它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。

缓存I/O

又称为标准I/O,大多数文件系统的默认IO操作都是缓存I/O。即数据会被先拷贝到操作系统内核的缓冲区中,然后才会从缓冲区拷贝到应用程序的地址空间。

缺点是需要进行多次数据拷贝操作,带来的开销是非常大的。

2、什么是IO多路复用?

多路指的是网络连接,复用指的是同一个线程

基本的BIO、NIO模型的缺点:

BIO给每一个连接都创建一个线程,accept一个请求后,在recv或send调用阻塞时,无法accept其他请求。

NIO当服务端accept一个请求后,将连接加入到fds集合,while循环轮询集合来recv数据,没有数据就返回错误。一直轮询会很浪费CPU资源

而IO多路复用是采用单线程,通过select/poll/epoll等系统调用获取fd列表,遍历有事件的fd进行accept/recv/send,使其能支持更多的并发连接请求。

3、IO多路复用的三种实现

select

它仅仅知道有I/O事件发生了,但不知道是哪几个流,我们只能无差别的轮询所有流,找出能读的数据,或者写入数据的流对它们进行操作。所以select具有O(n)的无差别轮询复杂度

 

select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样带来的缺点是:

1)单个进程打开的fd是有限制的,通过FD_SETSIZE设置,默认1024

2)每次调用select,就要把fd集合从用户空间拷贝到内核空间,这个开销在fd很多的时候会很大

3)对socket扫描是线性扫描,采用轮询的方式,效率较低。

如果能给socket注册某个回调函数,当它们活跃时,自动完成相关操作,这就避免了轮询。这就是epoll做的

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,但它没有最大连接数的限制,原因是它是基于链表来存储的。

同样的,每次调用poll也需要将fd集合从用户空间拷贝到内核空间,这个开销很大;其次对socket也是采用线性扫描,轮询的方式,效率较低。

epoll

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。

所以实际上epoll是事件驱动的。

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体:

#include <sys/epoll.h>

// 数据结构
// 每一个epoll对象都有一个独立的eventpoll结构体
// 用于存放通过epoll_ctl方法向epoll对象中添加进来的事件
// epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可
struct eventpoll {
    /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
    struct rb_root  rbr;
    /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
    struct list_head rdlist;
};

// API
int epoll_create(int size); // 内核中间加一个 ep 对象,把所有需要监听的 socket 都放到 ep 对象中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // epoll_ctl 负责把 socket 增加、删除到内核红黑树
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);// epoll_wait 负责检测可读队列,没有可读 socket 则阻塞进程

每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_clt方法向epoll对象中添加进来的事件。这些事件都会挂载到红黑树中,如此,重复添加的事件就可以通过红黑树而高效地标识出来。

所有添加到epoll中的事件都会与设备建立回调关系,当相应的事件发生时会调用这个回调方法,会将发生的事件添加到rdlist双向链表中去

当调用epoll_wait检查是否有事件发生时,只需要检查rdlist双向链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

 

对于epoll来讲,核心就是三步:

1)epoll_create()系统调用

2)epoll_clt()系统调用,向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,-1标识失败

3)epoll_wait()系统调用。用于收集在epoll中已经发生的事件

epoll只能在Linux下工作

epoll LT于ET模式的区别

1)LT是默认的模式,称为水平触发,只要这个fd还有数据可读,每次epoll_wait()都会返回它的事件,提醒用户程序去操作

2)ET模式是边缘触发,ET模式下它只会提示一次,直到下次有数据流入之前都不会再提示了。无论fd中是否还有数据可读。所以再ET模式下,一定要把buffer全部读完。

select/poll/epoll之间的区别

本质上它们都是多路复用的机制,即监视多个描述符,一旦某个描述符就绪,就通知程序进行相应的读写。但select/poll/epoll本质上都是同步IO,因为它们都需要在读写事件就绪后,自己负责读写,也就是说读写过程是阻塞的。而异步I/O无需自己负责读写。

 

注:poll模式下的fd集合在用户态是数组,拷贝到内核态之后是链表

FD激增后带来的性能问题

select和poll因为每次都是把fd集合从用户态拷贝到内核态,并且会线性遍历,所以fd一旦激增性能会很差。

epoll模型中只有活跃的socket会callback,当活跃socket很多的话,可能会有性能问题。

PS:主要内容源自:彻底理解 IO 多路复用实现机制 - 一角钱技术,若侵权联系删除

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值