I/O复用

465 阅读5分钟

1. 使用情景

  1. 处理多个socket
  2. 同时处理数据输入和网络连接
  3. 同时处理监听socket和连接socket
  4. 同时处理TCP和UDP请求
  5. 监听多个端口, 处理多种服务

2. select 系统调用

用途: 监听用户感兴趣的文件描述符上的可读, 可写, 异常事件

#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
  1. nfds指定被监听的文件描述符总数, 因为文件描述符从0开始, 它通常被设置为最大的文件描述符值加1
  2. readfds, writefds, exceptfds分别为可读, 可写, 异常事件对应的文件描述符集合, 调用select时传入, 返回时, 内核会修改集合
  3. fd_set结构体能容纳的文件描述符数量由FD_SETSIZE指定, 限制了select能同时处理文件描述符的数量
#include <sys/select.h>
FD_ZERO(fd_set* fdset); /* 清除fdset所有位 */
FD_SET(int fd, fd_set* fdset);  /* 设置fdset的第fd位 */
FD_CLR(int fd, fd_set* fdset);  /* 清除fdset的第fd位 */
int FD_ISSET(int fd, fd_set* fdset);    /* fdset的第fd位是否被设置 */
  1. timeout参数设置超时时间, 如果成员都为0, select会立刻返回, 如果timeout传入NULL, 则阻塞直到有文件描述符就绪
struct timeval
{
    long tv_set;    /* 秒 */
    long tv_usec;   /* 微妙 */
};
  1. 返回就绪的文件描述符总数

文件描述符可读就绪条件:

  1. socket内核接收缓冲区字节可以无阻塞的读, 并且读操作返回的字节数大于0
  2. 对方关闭连接, 此时socket读操作返回0
  3. 监听socket上有新的连接
  4. socket有未处理的错误, 此时用getsockopt读取和清除错误

文件描述符可写就绪条件:

  1. socket内核发送缓冲区可用字节可以无阻塞的写该socket, 并且写操作返回的字节数大于0
  2. 写操作被关闭, 对关闭的socket执行写操作会触发SIGPIPE信号
  3. socket使用非阻塞connet成功或者失败
  4. socket上有未处理错误, 同上4

3. poll 系统调用

用途: 和select类似, 轮询一定数量的文件描述符, 以测试是否有就绪者

#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
  1. fds是结构体数组, 指定要监听的文件描述符以及感兴趣的事件
struct pollfd
{
    int fd; /* 文件描述符 */
    short events; /* 注册的事件 */
    short revents; /* 实际发生的事件, 内核填充 */
};
  1. events, revents由一系列事件按位或, 事件类型如下:
POLLIN: 数据(普通和优先)可读
POLLPRI: 高优先级数据可读  
POLLOUT: 数据(普通和优先)可写
POLLRDHUP: 对方关闭TCP连接或者关闭写端  
POLLERR: 错误  
POLLHUP: 挂起  
POLLNVAL: 文件描述符没有打开
  1. 应该根据recv调用的返回值区分socket接收到的是有效数据还是对方关闭连接的请求
  2. nfds指定fds数组的大小 5.timeout单位为ms, 为-1时调用将阻塞直到有事件发生,为0则立刻返回
  3. 返回就绪的文件描述符总数

4. epoll系统调用

由一组函数完成任务, 把用户关心的文件描述符上的事件放在内核里的一个事件表中, 无需每次调用重传. epoll需要一个额外的文件描述符标识内核中的这个事件表

#include <sys/epoll.h>
int epoll_create(int size);

size并不起作用, 只是提示内核事件表需要多大, 返回内核事件表的文件描述符

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

fd是要操作的文件描述符, op是操作类型:

EPOLL_CTL_ADD: 往事件表中注册fd上的事件  
EPOLL_CTL_MOD: 修改fd上注册的事件  
EPOLL_CTL_DEL: 删除fd上注册的事件  

event 指定事件

struct epoll_event
{
    __uint32_t events; /* epoll事件 */
    epoll_data_t data; /* 用户数据 */
};
typedef union epoll_data
{
    void* ptr;
    int fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

epoll事件类型的宏是在poll对应的宏前加E ptr可用来指向用户自定义类数据, 并包含fd以此将文件描述符和用户数据关联起来
epoll_ctl 成功返回0 失败返回-1 并设置errno

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

成功返回就绪的文件描述符个数, 失败返回-1并设置errno
timeout 和poll中的一样, maxevents指定最多监听的事件数,必须大于0
就绪事件从内核事件表复制到events指向的数组中

5. LT 和 ET 模式

LT: Level Trigger 水平触发, 对于读操作, 读缓冲区内容不为空, 每次调用都会返回该文件描述符读就绪; 对于写操作, 只要写缓冲区不满, 每次调用都会返回该文件描述符写就绪. 应用程序可以不立即处理该事件.

ET: Edge Trigger 边缘触发, 当有就绪事件时应用程序必须立刻处理该事件, 后续的调用将不再触发该文件描述符的就绪事件. 此模式降低了事件被触发的次数.

ET模式的注册的文件描述符应该是非阻塞的, 否则, 读写操作会阻塞.

6. select poll epoll区别

事件集合:
select: 通过3个参数分别传入可读,可写,异常事件对应的文件描述符, 每次调用都要重置这三个参数.
poll: 通过pollfd.events传入文件描述符和感兴趣的事件, 内核修改pollfd.revents反馈就绪的事件.
epoll: 内核通过事件表管理文件描述符及其事件, 每次调用无需重复传入, 通过events参数反馈就绪的事件

索引就绪文件的时间复杂度:
select: O(n)
poll: O(n)
epoll: O(1)

最大支持的文件描述符数:
select: 一般有最大值限制
poll: 65535 epoll: 65535

工作模式:
select: LT
poll: LT
epoll: LT和ET

内核实现和工作效率:
select: 轮询的方式检测就绪事件, 时间复杂度O(n)
poll: 同上
epoll: 回调的方式检测就绪事件, O(1)