IO复用--select、poll、epoll、kqueue
IO复用
- 目标:用单线程去处理多个IO请求。
- 文件描述符:是非负整数。它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
- 每一个进程都有一个文件描述符表。不同进程可以拥有相同文件描述符。相同文件描述符可以指向同一文件。
- 文件描述符可以与File*相互转化。
- web服务器项目实现:https://github.com/oniisancr/webserver
1 select
copy
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
/***
* 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函数。
- select函数调用前的所有准备工作
- 缺点
- FD_SETSIZE为1024,即最多同时监视1024个文件描述符。
- select函数后需要遍历fd_set。
- 每次select函数需要传递监视对象的信息。
- 因为套接字是由操作系统管理,所以每次需要将该信息传递给操作系统,开销很大。用户态-->内核态。
- 可以通过只传递一次监视对象,监视范围和内容发生变化时,只通知变化的事项。这样就元需每次调用 select函数时都向操作系统传递监视对象信息。Linux使用epoll,windows是IOCP。
- 适用场景:服务器端接入者少;程序要求兼容性。
2 poll
copy
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
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函数调用前的所有准备工作
- 缺点
- poll函数后需要遍历pollfd结构体。
- 每次poll函数需要传递监视对象的信息。
- 仍然存在用户态到内核态数据的整体复制。
- 对比select的改进
- 不要求用户计算编号最高的fd_max+1的值
- 对于编号大的文件描述符更有效。如一个单900的文件描述符,select需要判断900位,而poll只需要一次。
- 没有最大文件描述符数量的限制,select文件描述符小于1024。
- 由于采用新的数据结构,每次处理完任务后将revents置为0后,则可以继续重用原结构体。
3 epoll
Linux uses epoll
copy
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
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
copy
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
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
...
非阻塞套接字设置
copy
- 1
- 2
- 3
- 4
- 5
- 6
- 7
// Linux以提供的更改或读取文件属性的
/**
* 成功时返回cmd参数相关值,失败时返回-1
* 若第二个参数传递F_GETFL,获得第一个参数所指的文件描述符属性
* 若第二个参数传递F_SETFL,可以更改文件描述符属性
*/
int fcntl(int filedes, int cmd, . . . );
- 将套接字改为非阻塞式,则需要以下两句
copy
- 1
- 2
- 3
- 4
//获取之前设置的属性信息
int flag = fcntl(fd,F_GETFL,0);
//添加非阻塞 O_NONBLOCK标志
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
reference
- [1] https://devarea.com/linux-io-multiplexing-select-vs-poll-vs-epoll
- [2] https://www.bilibili.com/video/BV1qJ411w7du
- [3]《TCP/IP网络编程》--尹圣雨
- [4] https://dev.to/frevib/a-tcp-server-with-kqueue-527
链接[1]包含实例代码
本文作者:Oniisan_Rui
本文链接:https://www.cnblogs.com/oniisan/p/linux-io-multiplexing-select-vs-poll-vs-epoll.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步