前提
我们创建若干 socketfd
,绑定并监听,现在我们拥有若干 socketfd
对于 IO 事件,有如下2种触发方式:
- 水平触发(在目标状态下持续触发)
- 边缘触发(首次切换变化到目标状态时触发1次)
select
用法
流程
- 对每个 socketfd 进行
accept()
得到 fd,保存到 fd 数组中,记录最大 fd 序号max_fd
。 - 循环
-
- 设置监听的
fd_set
集合(bitmap)
- 设置监听的
-
- 将
fd_set
传入select()
函数,并限制监听 fd 范围(0-max_fd)
- 将
-
-
- 内核态会拷贝一份
fd_set
,并进行监听,发生事件时,覆盖用户态的fd_set
- 内核态会拷贝一份
-
-
-
- select 会阻塞当前进程,直到返回(发生事件或超时)
-
-
- 等待
select()
返回
- 等待
-
-
- 内核置为
fd_set
- 内核置为
-
-
- 遍历 fd,检测
fd_set
中是否置位,并进行处理
- 遍历 fd,检测
函数说明
// fd_set 全部置零
int FD_ZERO(fd_set *fdset);// 清零某一位
int FD_CLR(int fd, fd_set *fdset);// 置1某一位
int FD_SET(int fd, fd_set *fd_set);// 判断是否置1
int FD_ISSET(int fd, fd_set *fdset);int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, struct timeval *timeout)
// 参数: 最大fd 监听读事件的fd集合 监听写事件的fd集合 监听异常的fd集合 超时时间
// 超时时间:NULL-无限等待, 0-立刻返回,n-等待时间。等待可以被中断信号打断
// select 返回准备好的 fd 数量,超时返回0,出错返回-1(等待时被信号打断也会返回-1)
缺点
- fd 上限:最大监听 1024 个 fd
-
- 可调整但是受限于操作系统默认编译的值
- 重新赋值:传入的
fd_set
会被内核修改,每次需要重新赋值 - 拷贝开销:
fd_set
用户态拷贝到内核态,有开销 - 遍历 fd:
select
返回后,需要遍历所有 fd 来判断真正可用的 fd
poll
用法
流程
- 对每个 socketfd 进行
accept()
得到 fd,为每个 fd 创建pollfd
结构体,存入数组 - 循环
-
- 将
pollfd[]
数组传入poll()
函数
- 将
-
-
- 从用户态拷贝到内核态
-
-
-
poll()
阻塞当前进程,直到返回(发生事件或超时)
-
-
- 等待
poll()
返回
- 等待
-
-
- 内核置位
revents
- 内核置位
-
-
- 遍历 fd,检测可用的 fd(清零
revents
),并进行处理
- 遍历 fd,检测可用的 fd(清零
函数说明
int poll ( struct pollfd * fds, unsigned int nfds, int timeout);
// 参数 poll 数组 数组长度 超时时间struct pollfd {int fd;short events; // 监听的事件(事件掩码)short revents; // 实际发生的事件,只关注 events 设置的事件
}
// POLLIN 有数据可读
// POLLRDNORM 有普通数据可读
// POLLRDBAND 有优先数据可读
// POLLPRI 紧迫数据可读// POLLOUT 写数据,不会导致阻塞
// POLLWRNORM 写普通数据,不会导致阻塞
// POLLWRBAND 写优先数据,不会导致阻塞
// POLLMSGSIGPOLL 消息可用// POLLER 指定的 fd 发生错误
// POLLHUP 指定的 fd 挂起事件
// POLLNVAL 指定的 fd 非法
poll 对比 select
- 没有 fd 上限
pollfd
可重用
缺点
- 拷贝开销:
pollfds[]
用户态拷贝到内核态,有开销 - 遍历fd:
poll()
返回后,需要遍历所有 fd 来判断真正可用的 fd
epoll
用法
流程
epoll_create()
创建epoll
对象(eventpoll),得到描述符(epfd)- 对每个 socketfd 进行
accept()
得到 fd,将 fd 保存到epoll_event
中 epoll_ctl()
添加epoll_event
到epoll
对象中- 循环
-
epoll_wait()
阻塞并等待事件发生
-
-
- 实际调用
epoll_ctl
拷贝一次,后续复用,不需要每次都拷贝
- 实际调用
-
-
- 等待
epoll_wait()
返回可用的 fd
- 等待
-
- 遍历可用的 fd,并进行处理
函数说明
// 创建一个 epoll 对象,返回该对象的描述符
// 注意要使用 close 关闭该描述符
int epoll_create(int size);// 操作控制 epoll 对象
// 主要涉及 epoll 红黑树上节点的一些操作,比如添加节点,删除节点,修改节点事件
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// op:EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD// 阻塞一段时间并等待事件发生,返回事件集合
// 也就是获取内核的事件通知。即遍历双向链表,把双向链表里的节点数据拷贝出来,拷贝完毕后就从双向链表移除
int epoll_wait(int epid, struct epoll_event *events, int maxevents, int timeout);
// epid:epoll_create 返回的 epoll 对象描述符
// events:存放就绪的事件集合,这个是传出参数
// maxevents:代表可以存放的事件个数,也就是 events 数组的大小
// timeout:阻塞等待的时间长短,以毫秒为单位,如果传入 -1 代表阻塞等待typedef union epoll_data
{void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event
{uint32_t events; // Epoll 事件epoll_data_t data; // 用户数据
};
// EPOLLIN: 监听 fd 的读事件。举例:如果客户端发送消息过来,代表服务器收到了可读事件
// EPOLLOUT: 监听 fd 的写事件。如果 fd 对应的发数据内核缓冲区不为满,只要监听了写事件,就会触发可写事件
// EPOLLRDHUP: 监听套接字关闭或半关闭事件,Linux 内核 2.6.17 后可用
// EPOLLPRI: 监听紧急数据可读事件
原理
说明
- epoll 没有“置位”操作,通过“重排”来标识可用的 fd
- fd 可用时,被移到 epoll 对象的最前面,
实现
- rbr 是一棵红黑树,支持快速查找键值对,所有需要加入监听事件的描述符都需要添加到这棵红黑树来,rbcnt 记录红黑树节点个数
- rdlist 是一个双向链表,当红黑树上监听的描述符发生对应的监听事件时,内核会将这个节点插入到该双向链表来,rdnum 是双向链表节点的个数,也就是发生了事件的节点个数
epoll 对比 poll
- 几乎无拷贝开销:
epoll
仅拷贝一次到内核态,后续复用 -
- 没有共享内存:https://www.zhihu.com/question/39792257,
epoll_wait()
会将当前的触发的事件和用户传入的数据放在双向链表中,都 copy 给用户态
- 没有共享内存:https://www.zhihu.com/question/39792257,
- 不需要遍历 fd
参考
- io多路复用:https://www.bilibili.com/video/BV1qJ411w7du
- select: https://www.cnblogs.com/alantu2018/p/8612722.html
- poll: https://www.cnblogs.com/fah936861121/articles/6656885.html
- epoll: https://zhuanlan.zhihu.com/p/406175793