Linux 下 IO 复用和实现方式

61 阅读3分钟

1. 为什么要 IO 复用

1.1 IO 操作

在 Linux 下,IO 操作主要就是操作fd,如 socket,文件,事件 fd 等

1.2 IO 操作的进化之路

IO 操作耗时,进而需要在单独线程运行

方案 1:

每个操作单独放一个线程
优点:实现简单
缺点:如果 IO 数量众多,导致多线程数量剧增,效率反而低下

方案 2:

专门找个线程轮询这些 fd 是否可读可写,可读或者可写后再单独创建一个线程执行操作
这就要求单线程轮询,同时需要 fd 的可读可写检查操作为非阻塞

优点:线程减少
缺点:轮询线程要么一直轮询占用 CPU,要么定时轮询,可能导致事件处理延迟

方案 3:

轮询线程监听所有 fd,没有事件轮询线程休眠,有事件轮询唤醒
    

2. 平台实现方式

1 select
2 pool
3 epoll

这里不详细介绍他们的区别,只说结论 epoll:
比如我们一共有 1000 个 fd 需要监测,当有事件来临时,select 不知道是哪个 fd 来了哪个事件,
进而需要全部遍历,而 epoll 准确的知道是哪个 fd 来了什么事件

3. epoll 使用

1. int epoll_create(int size);

创建一个监控和管理句柄 fd 的池子
size: 表示池子的大小
返回值表示:epool 本身也是个句柄

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

向 epoll 池中,op(添加删除更新)感兴趣的句柄 fd, 以及对应句柄感兴趣的事件

2.1 epfd:epoll 池句柄

2.2 op:
    EPOLL_CTL_ADD:注册新的fd到epfd中;  
    EPOLL_CTL_MOD:修改已经注册的fd的监听事件;  
    EPOLL_CTL_DEL:从epfd中删除一个fd;

2.3 fd:感兴趣的文件句柄

2.4 event:对 fd 句柄感兴趣的事件

 struct epoll_event {
     __uint32_t events; //监听的事件
     epoll_data_t data; //事件的 host 信息
 };

typedef union epoll_data {
     void *ptr;
     int fd; //感兴趣句柄 fd
     __uint32_t u32;
     __uint64_t u64;
 } epoll_data_t;

events可以是以下几个宏的集合:  
    EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);  
    EPOLLOUT:表示对应的文件描述符可以写;  
    EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);  
    EPOLLERR:表示对应的文件描述符发生错误;  
    EPOLLHUP:表示对应的文件描述符被挂断;  
    EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发
                (Level Triggered)来说的。  
    EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket
                的话,需要再次把这个socket加入到EPOLL队列里
         
   

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

 等待事件的产生
 
events:
    用户提供的给内核写入事件的集合;
    跟 epoll_ctl 传入的最后一个参数类型一样,当有事件发生时,内核内核会把发生的事件复制到 events 中
    
maxevents:最大的事件值

timeout:超时时间

4. epool 原理

1. 红黑树

  每个监听的 fd 都对应一个红黑树节点,时间复杂度为 O(log n)保证效率
  每个节点为一个 epitem 数据结构

2. 就绪队列

  有事件发生的队列,数据结构为 epitem
  从这里可以知道哪个 fd 发生了什么事件