IO复用--select、poll、epoll、kqueue

IO复用

  • 目标:用单线程去处理多个IO请求。
  • 文件描述符:是非负整数。它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
    • 每一个进程都有一个文件描述符表。不同进程可以拥有相同文件描述符。相同文件描述符可以指向同一文件。
    • 文件描述符可以与File*相互转化。
  • web服务器项目实现:https://github.com/oniisancr/webserver

1 select

/***
 * maxfd 监视对象文件描述符数量
 * readset 将所有关注"是否存在待读取数据"的文件描述符注册到fd_set型变量,并传递其地址值。
 * writeset 将所有关注"是否可传输无阻塞数据"的文件描述符注册到fd_set型变量,并传递其地址值。
 * exceptset 将所有关注"是否发生异常"的文件描述符注册到fd_set型变量,并传递其地址值。
 * timeout 调用select函数后,为防止陷入无限阻塞的状态,传递超时(time-out)信息。
 * return 发生错误时返回-1,超时返回时返回0。因发生关注的事件返回时,返回大于0的值,该值是发生事件的文件描述符数。
 */ 
int select(int maxfd, fd_set * readset, fd_set * writeset,
    fd_set * exceptset, struct timeval * timeout);
  • 阻塞式IO复用
  • 因为select函数可以同时监视多个文件描述符,可以将多个文件描述符集中到一起统一监视
  • 步骤
    • select函数调用前的所有准备工作
      • 设置文件描述符集合,再设置对应的fd_set变量,对应位置为1. 使用FD_ZERO、FD_SET等函数。
      • 设定监视范围,取文件描述符最大值加1。设置超时。
    • 调用select函数、最后查看结果。
      • fd_set变量中仍然为1的对应位置上的文件描述符发生了变化。使用FD_ISSET函数。
  • 缺点
    • FD_SETSIZE为1024,即最多同时监视1024个文件描述符。
    • select函数后需要遍历fd_set。
    • 每次select函数需要传递监视对象的信息。
      • 因为套接字是由操作系统管理,所以每次需要将该信息传递给操作系统,开销很大。用户态-->内核态。
      • 可以通过只传递一次监视对象,监视范围和内容发生变化时,只通知变化的事项。这样就元需每次调用 select函数时都向操作系统传递监视对象信息。Linux使用epoll,windows是IOCP。
  • 适用场景:服务器端接入者少;程序要求兼容性。

2 poll

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
};

/***
* nfds 监听对象个数
* timeout 超时时间。设置为-1代表永久阻塞,直到有就绪事件的发生
*/
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
  • 和select类似,只是描述fd集合的方式不同,poll使用pollfd结构而非select的fd_set结构。
  • 步骤
    • poll函数调用前的所有准备工作
      • 设置pollfd结构体,events是触发事件。
      • 设置监视数目。设置超时时间。
    • 调用poll,查看结果
      • pollfd结构体中revents为1的,表示触发了事件。
      • 需要手动恢复revents成0
  • 缺点
    • poll函数后需要遍历pollfd结构体。
    • 每次poll函数需要传递监视对象的信息。
      • 仍然存在用户态到内核态数据的整体复制。
  • 对比select的改进
    • 不要求用户计算编号最高的fd_max+1的值
    • 对于编号大的文件描述符更有效。如一个单900的文件描述符,select需要判断900位,而poll只需要一次。
    • 没有最大文件描述符数量的限制,select文件描述符小于1024。
    • 由于采用新的数据结构,每次处理完任务后将revents置为0后,则可以继续重用原结构体。

3 epoll

Linux uses epoll

struct epoll_event {
  uint32_t events; 
  epoll_data_t data;
};
// size是对系统的建议值
int epoll_create(int size);
/**
 epfd 用于注册监视对象的 epoll例程的文件描述符。
 op 指定监视对象的添加、删除或更改等操作
 fd 需要注册的监视对象文件描述符
 event 监视对象的事件类型
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/**
 events 保存发生事件的文件描述符集合的结构体地址值
 maxevents 第二个参数中可以保存的最大事件数
 timeout 以1/1000秒为单位的等待时间,传递-1时,一直等待直到发生事件
 return 成功时返回发生事件的文件描述符数,失败时返回-1,超时返回0
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  • 需要调用的三个函数
    • epoll_create 创建保存epoll文件描述符的空间
    • epoll_ctl 向空间注册并注销文件描述符
    • epoll_wait 与select函数类似,等待文件描述符发生变化
  • 条件触发(水平触发):在条件触发方式中,只要输入缓冲有数据就会一直通知该事件。
    • 如输入缓冲中有50字节,操作系统会通知该事件。当读完30字节还剩20字节时,仍会注册该事件。
    • epoll默认以条件触发方式工作。
  • 边缘触发中输入缓冲收到数据时仅注册1次该事件。不管读没读取完。
    • 一定要采用非阻塞read、write,否则可能会引起长时间停顿
    • 读完数据后情况为:str_len=-1,errno=EAGAIN
  • 优点:
    • 仅向操作系统传递1次监视对象监视范围或内容发生变化时只通知发生变化的事项。不需要频繁拷贝。

4 kqueue

BSD and OSX use kqueue

struct kevent
{
  uintptr_t ident;  //事件标识
  short filter;     // 监听事件的类型,如EVFILT_READ,EVFILT_WRITE,EVFILT_TIMER等
  u_short flags;    //事件操作类型,如EV_ADD,EV_ENABLE,EV_DELETE等
  u_int fflags;     /* filter flag value */
  intptr_t data;    /* filter data value */
  void *udata;      //可携带的任意用户数据
};

//类似epoll_create
int kqueue(void); 
/**
 * 相当于组合了epoll_ctl及epoll_wait的功能
 * kq 注册的kqueue
 * changelist 要监督的事件列表
 * nchanges changelist监听事件的长度
 * eventlist 用于返回已经就绪的事件列表
 * nevents eventlist列表最大长度
 * timeout 超时时间  设置nullptr为一直等待
 * return events就绪的数目
 */ 
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout); 
//设定kevent参数的宏,用来初始化kevent结构体
EV_SET(&kev, ident, filter, flags, fflags, data, udata);
  • 步骤
    • 调用kqueue,创建kq
    • EV_SET 初始化kevent结构体,以及相关参数
    • kevent函数,与epoll_wait类似。eventlist中为触发的事件,返回值为其长度。
  • 默认条件触发,event设置为EV_CLEAR后为边缘触发。

5 IOCP

Windows uses IOCP

...

非阻塞套接字设置

// Linux以提供的更改或读取文件属性的
/**
 * 成功时返回cmd参数相关值,失败时返回-1
 * 若第二个参数传递F_GETFL,获得第一个参数所指的文件描述符属性
 * 若第二个参数传递F_SETFL,可以更改文件描述符属性
 */
int fcntl(int filedes, int cmd, . . . );
  • 将套接字改为非阻塞式,则需要以下两句
//获取之前设置的属性信息
int flag = fcntl(fd,F_GETFL,0);
//添加非阻塞 O_NONBLOCK标志
fcntl(fd, F_SETFL, flag|O_NONBLOCK);

reference

链接[1]包含实例代码

posted @ 2022-08-15 22:26  Oniisan_Rui  阅读(325)  评论(0)    收藏  举报