C++ Webserver从零开始:基础知识(四)——I/O复用

目录

前言

select系统调用

poll系统调用

epoll系统调用

epoll_create

epoll_ctl

epoll_wait

LT和ET模式

EPOLLONESHOT事件

epoll和select/poll的区别

事件集处理方式

实现原理和效率

其他区别


前言

        在第三章中我们大概地讲解了什么是I/O复用,即:I/O复用技术即使用select,poll,epoll等系统调用,让主线程能同时监听多个文件描述符,提高服务器运行的效率。

        PS: 虽然I/O复用能同时监听多个文件描述符,但是其本身是阻塞的,当主线程已经监听到多个文件描述符时,如果不采用并发技术,那么程序也只能按顺序一个一个去处理这些文件描述符。

        在本章中,我们将依次介绍select,poll,epoll系统调用,基于简单且重要的原则,本章不会详细介绍select和poll,但他们依然是面试高频考点,希望读者能自行去了解一下。本章将重点介绍epoll,同时介绍epoll中两种重要的触发模式LT水平触发模式和ET边沿触发模式,这些内容是第三章中提到的是I/O处理单元中的核心内容,需要读者理解且记忆。


select系统调用

#include
int select(int nfds,fd_set* readfds, fd_set* writefds, 
           fd_set* exceptfds,struct timeval* timeout);
  • 作用:在指定的一段时间内,轮询监听文件描述符上的可读,可写和异常等事件
  • 参数:
    • nfds:指定被监听文件描述符的总数,通常却文件描述符中的最大值+1
    • readfds:可读事件对应的文件描述符的集合
    • writefds:可写事件对应的文件描述符的集合
    • exceptfds:异常事件对应的文件描述符的集合
    • timeout:函数运行的时间
  • 返回值:
    • 成功:就绪文件描述符的总数
    • 失败:-1

        其中fd_set结构指针类型和struct timeval指针类型感兴趣可以自行搜索。

        select系统调用中的文件描述符只有在可读,可写或异常的条件下才被计入就绪文件描述符,而这些具体条件感兴趣的读者自行搜索。


poll系统调用

#include
int poll(struct pollfd* fds,nfds_t nfds,int timeout);
struct pollfd{
    int fd;/*文件描述符*/
    short events;/*注册的事件*/
    short revents;/*实际发生的事件,由内核填充*/
  • 作用:在一定时间内,轮询一定数量的描述符,检测其中是否有就绪事件
  • 参数:
    • fds:pollfd结构类型的数组
      • fd:文件描述符
      • events:fd上有哪些事件(见下图),这些事件通过按位或传入events
      • revents:内核进行修改,通知程序fd上那些事件确实发生了
    • nfds:被监听的事件集合大小
      • nfds_t定义:typedef unsigned long int nfds_t;
    • timeout:poll运行的时间,可以理解为一个倒计时,当时间到达函数返回
      • -1:永远阻塞直到某个事件发生
  • 返回值:
    • 成功:就绪文件描述符的总数
    • 失败:-1


epoll系统调用

        epoll是非常高效的I/O复用函数,它不是一个函数,而是一组函数,即

  1. epoll_create
  2. epoll_ctl
  3. epoll_wait

        epoll把文件描述符上的事件放入一个内核事件表里,而不是像select和poll那样每次调用重复传入文件描述符,但epll需要一个额外的文件描述符来标识这个内核事件表。

epoll_create

#include
int epoll_create(int size);
  • 作用:创建文件描述符来标识内核事件表
  • 参数:size不起作用,仅仅给内核一个提示,事件表应该有多大
  • 返回值:作为其他epoll函数的第一个参数

epoll_ctl

        创建好了标识内核事件表的文件描述符,即可使用epoll_ctr对内核事件表进行设置

#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
struct epoll_event
{
    _uint32_t events;
    epoll_data_t data;
}
  • 作用:对内核事件表进行操作
  • 参数:
    • epfd:标识内核事件表
    • op:操作类型
      • EPOLL_CTL_ADD:往时间表中注册fd上的事件
      • EPOLL_CTL_MOD:修改fd上的注册事件
      • EPOLL_CTL_DEL:删除fd上的注册事件
    • fd:要操作的文件描述符
    • event:指定事件(结构体内如下)
      • _uint32_t events : 事件类型(与poll的事件类型相同,但要前加“E")
      • epoll_data_t data : 存储用户数据(联合体内如下)
        • void* ptr : 指定与fd相关的用户数据
        • int fd : 事件所从属的目标文件描述符(常用)
        • uint32_t: u32:
        • uint64_t:u64:
        • (PS:联合体union即多选一使用,不能同时使用)
  • 返回值:
    • 成功:0
    • 失败:-1

        事件类型(poll类型的,epoll在poll类型的事件类型前面 + ”E";

C++ Webserver从零开始:基础知识(四)——I/O复用_第1张图片

epoll_wait

        创建并设置好内核事件表后,即可使用epoll_wait()开始等待事件

#include
int epoll_wait(int epfd, struct epoll_event* events,int maxevents,int timeout);
  • 作用:在一定时间内接收就绪事件,并加入一个事件数组中等待程序处理
  • 参数:
    • epfd:内核事件表的标识符(epoll_create创建的)
    • events:事件数组,将epfd中的就绪事件取出放进去
    • maxevents:最多监听多少个时间
    • timeout:epoll_wait等待的时间,可以理解为倒计时,时间到了返回
  • 返回值
    • 成功:接收到的事件数量
    • 失败:-1

        epoll_wait函数检测到事件后,会将内核注册表中的就绪事件放入第二个数组中,当工作线程来数组中取事件处理时,就不需要判断事件是否是就绪事件,而poll则需要先判断是否就绪。

        可以这样理解,文件描述符上的事件是一堆未处理的食物,而I/O复用函数作用是将这些食物放入一格一格的容器中。poll容器不管食物是否烹饪过,直接放进去。而epoll会有一个大厨(内核)来将烹饪过的食物放进去。在食客挑选食物食用时(工作线程逻辑处理),如果是poll容器,则需要一个格子一个格子打开判断食物是否可以食用,而epoll容器中都是可以直接食用的食物,食客直接打开食用即可。

LT和ET模式

        epoll对文件描述符的操作又两种模式,LT模式ET模式。

        LT模式(水平触发):LT是默认的工作模式,这种模式epoll相当于一个高效的poll

        ET模式(边沿触发):当往epoll内核事件表中注册一个文件描述符上的EPOLLET事件时,epoll会切换成ET模式

        LT模式:当epoll_wait检测到文件描述符上有事件发生并通知应用程序后,应用程序可以不立即处理事件,当下一次调用epoll_wait时,epoll_wait还会再次向应用程序通告此事件,直到这个事件被处理。

        ET模式:当epoll_wait检测到文件描述符上有事件发生并通知应用程序后,应用程序必须立即处理事件,后续epoll_wait不再通知

        LT模式和ET模式可以这样理解,还是上面那个例子,EPOLL容器安装了一个语音呼叫装置,当容器某一格内有食物(就绪事件)时,它就会呼叫食客来取餐。LT模式下,食客可以不来用餐,这样当主线程循环一遍后又一次调用EPOLL时,它又会再次呼叫食客来取走上次未取走的那个格子里的食物。而ET模式下,EPOLL容器一呼叫,食客就必须来取餐。

EPOLLONESHOT事件

        在使用epoll时可能遇到这样的问题,在某个线程读取完某个socket上的数据开始处理时,这个socket上又来了新的数据,这时另一个线程被唤醒处理这些数据,那么就出现了两个线程处理同一个socket的情况。为了避免这种情况,可以使用EPOLLONESHOT事件。

        一个注册了EPOLLONESHOT事件的文件描述符,操作系统最多可以触发其上的一个事件,且只能触发一次。除非用epoll_ctl函数重置该文件描述符上的EPOLLONESHOT事件。反过来,当一个线程处理完一个socket上的事件,就该立刻重置其上的EPOLLONESHOT事件,保证下次其他线程能处理这个socket。


epoll和select/poll的区别

事件集处理方式

select:使用fd_set。

        fd_set并未将文件描述符和事件绑定,所以需要三个参数来分别传入可读,可写和异常事件。且由于内核对fd_set集合为在线修改,应用程序下次调用select前需要重置三个参数

 poll:使用pollfd

        pollfd内定义文件描述符和事件,所有事件都统一处理;内核每次修改的时revent成员,而event成员不变,所以不用重置事件集参数。

epoll:

        在内核中维护事件表,用独立系统调用epoll_ctr来控制事件表,epoll_wait调用直接从内核时间表中取得注册的事件,无需反复从用户空间读入这些事件。

实现原理和效率

select/poll:

        采用轮询的方式,每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,时间复杂度O(n)

epoll:

        采用回调的方式,内核检测到就绪的文件描述符时触发回调函数,回调函数将文件描述符上的对应事件加入内核就绪队列,内核在合适的时机将就绪事件队列中的拷贝到用户空间。时间复杂度是O(1)

其他区别

工作模式:

        select/poll只有LT模式

        epoll可以使用ET模式

最大文件描述符数:

        select:有最大值,且比poll和epoll少

        poll/epoll:65353

你可能感兴趣的:(服务器,c++,c语言,linux)