IO多路复用

I/O多路复用

本质上是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般就是读就绪或写就绪),能够通知到程序进行相应的读写操作。

Unix的五种IO模型有
  1. blocking IO 阻塞IO
  2. nonblocking IO 非阻塞IO
  3. IO multiplexing IO多路复用
  4. signal driven IO 信号驱动IO
  5. asynchronous IO 异步IO

前四种IO都归类为同步IO。

select、poll、epoll

IO复用的方法,本质上是同步IO,因为他们需要在读写事件就绪后自己负责读写,也就是说这个读写过程是阻塞的。

与多线程相比,IO多路复用的优势在于系统开销小,系统不必穿件进程或线程,也不必维护这些进程/线程,大大减小了系统的开销。

  • 用户空间/内核空间

    现在操作系统都是采用虚拟存储器,对于32位操作系统来说,寻址空间为4g(2^32)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为两部分:内核空间和用户空间。

  • 进程切换

    为了控制进程的执行,内核必须有能力刮起正在CPU上运行的进程,并恢复以前挂器的某个进程的执行。这种行文被成为进程切换,因此可以说任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的,并且进程切换是非常耗费资源的。

  • 进程阻塞

    正在执行的进程,由于期待的某些时间尚未发生,如请求系统资源失败,等待某种操作的完成、新数据尚未到达或无新工作等,则又系统自动执行阻塞原语block,使自己由运行态切换到阻塞状态。进程的阻塞是进程自身的一种主动行为,只有处于运行状态的进程(获取到了CPU资源)才能将其转为阻塞状态。当进程进入阻塞状态,是不占CPU资源的

  • 文件描述符

    文件描述符是计算机科学的术语,是一个用于表述指向文件的引用的抽象化概念。

    文件描述符在形式上是一个非负整数。实际上,是一个索引值,指向内核为每一个进程维护的该进程打开文件的记录表。当程序打开一个现有文件或创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,一些设计底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用于Unix、Linux这样的OS

  • 缓存I/O

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

select

select函数原型:

int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout);
  • int maxdfp1 指定待测试的文件描述字个数,值为待测试的最大描述字+1
  • fd_set : 一个集合存放的是文件描述符,即文件句柄。 readset、writeset、exceptset分别标识我们要让内核测试读、写和异常条件的文件描述符集合。如果对某个条件不感兴趣的化,可以设置为null
  • const struct timeval *timeout: 告知内核等待所指定文件描述符集合中的任意一个就绪可花多少时间。timeval结构用于指定秒和毫秒数
  • 返回值:若有就绪描述符返回其数目,超时为0,异常为-1
select 运行机制

select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组,每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时,由内核根据IO状态修改fd_set的内容,由此来通知执行了select()的进程哪一Socket或文件可读。

从流程上来看,使用select函数进行IO请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select以后最大的优势是用户可以在一个线程内同时处理多个socket的IO请求。用户可以注册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。

select机制的问题

  1. 每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
  2. 同时每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
  3. 为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为1024)
poll

poll的机制和select类似,本质上的区别不大。管理多个描述符也是采用的轮询方式,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制,可以解决select 的3问题,不能解决1、2性能问题

poll函数原型:

int poll(
        struct pollfd *fds,     // 用于存放需要检测其状态的socket描述符并且在调用poll函数后不会被清空。一个pollfd结构体表示一个被一个被监视的文件描述符
        nfds_t nfds,            // 记录数组 fds中的描述符总数
        int timeout             // 如select
);

typedef struct pollfd {
    int fd;         // 需要被检测或选择的文件描述符
    short events;   // 文件描述符fd上感兴趣的时间
    short revents;  // 文件描述符上当前实际发生的事件
}

poll改变了文件描述符集合的描述方式,采用了pollfd结构而不是select的fd_set结构,使得poll支持的文件描述符集合限制大于1024

poll函数的返回值:返回fds集合中就绪的读、写的描述符数量,0表示超市 -1表示出错

epoll

epoll出现与linux 2.6 ,基于时间驱动IO方式,相对于select来说,epoll 没有描述符个数,使用一个文件描述符管理多个描述副,将用户关系的文件描述符的时间存放到内核的一个事件表中,这样在用户空间和内核空间进行的拷贝只需要异常。

epoll函数原型:

int epoll_create(int size);                                                         // 创建一个epoll句柄,size表示内核要监听的描述符数量,调用成功会返回epoll句柄描述符,失败返回-1
int epoll_ctl(int epdf, int op, int fd, struct epoll_event *event);                 // 注册要监听的事件类型
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);   // 灯胆时间的就绪,成功时返回就绪的事件数目,调用失败时返回-1 超时为0

epoll_ctl: 注册要监听的事件类型

  • epfd 表示epoll句柄 为epoll_create创建的返回值

  • op 表示fd操作类型,🈶如下三种

    • EPOLL_CTL_ADD: 注册新的fd到epfd中
    • EPOLL_CTL_MOD: 修改已注册的fd监听事件
    • EPOLL_CTL_DEL: 从epfd中删除一个fd
  • fd是要监听的描述符

  • event 表示要监听的事件

    struct epoll_event {
    __uint23_t events;
    epoll_data_t data;
    };
    typedef union epoll_data {
    void * ptr;
    int fd;
    __unit23_t u32;
    __unit64_t u64;
    } epoll_data_t ;
    

epoll_wait 函数等待事件的就绪,成功是返回就绪事件数目,失败返回-1 超时为0

  • epfd epoll句柄
  • events 表示从内核得到的就绪事件集合
  • maxevents 告诉内核events的大小
  • timeout 等待超时时间

epoll是linux内核为处理大批量文件描述符而做了改进的poll。能显著的提高程序在大量并发连接中只有少量获取的情况下的系统CPU利用率。原因是获取事件时,无须遍历整个被监听的描述符集合,只需要遍历被内核IO事件异步唤醒而加入Ready队列中的描述符集合即可。

epoll除了功过select/poll那种IO事件的水平触发外,还提供了边缘触发,使得用户空间有可能缓存IO状态,减少epoll_wait/epoll_pwait 的调用,提高应用程序调用。

  • 水平触发LT: 默认工作模式,即当epoll_wait 检测到某描述符事件就绪并通知应用程序时,应用程序可以不立即处理该事件;下次调用epoll_wait时,会再次通知此事件
  • 边缘触发ET: 当epoll_wait 检测到某描述符事件就绪并通知应用程序时,应用程序必须立即处理该事件,如果不处理,下次调用epoll_wait时,不会再次通知此事件。直到你做了某些操作导致该描述符变成未就绪状态了,也就是说边缘触发只在状态由未就绪变为就绪时只通知一次)。

LT和ET原本应该是用于脉冲信号的,可能用它来解释更加形象。Level和Edge指的就是触发点,Level为只要处于水平,那么就一直触发,而Edge则为上升沿和下降沿的时候触发。比如:0->1 就是Edge,1->1 就是Level。

ET模式很大程度上减少了epoll事件的触发次数,因此效率比LT模式下高。

总结
selectpollepoll
遍历方式轮询轮询回调
底层实现数组链表红黑树
IO效率每次调用都会线性遍历,时间复杂度为O(n)每次调用都会线性遍历,时间复杂度为O(n)事件通知方式,当fd就绪时,系统注册的回调函数会被调用,将就绪的fd放到readyList里面,时间复杂度为O(1)
最大连接数1024(x86) 、2048(x84)无上限无上限
fd拷贝每次调用select 都会将fd集合从用户空间拷贝到内核空间每次调用pol 都会将fd集合从用户空间拷贝到内核空间调用epoll_ctl 时会拷贝进内核并保存,之后每次epoll_wait不会进行拷贝
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值