以往服务器等待客户端连接需要阻塞accept(),等待客户端发数据需要阻塞,如果同时有多个客户端发起请求,那么就gg,
当然可以通过多线程,多进程来解决,但是开销太大。于是就有了IO多路转接,
其核心思想时服务器通过内核作为媒介来监听客户端的连接请求,内核阻塞去监听客户端状态,一旦其状态变化,内核就通知
服务器去处理
select()
函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
1 | //@parm1 :nfds 表示所监听的文件描述符最大值+1 |
配套操作
1 | void FD_CLR(int fd, fd_set *set); // |
再看文件描述集
三个文件描述符参数是传入传出参数
举个例子,readfds 监听了{fd1,fd2,fd3,fd4} 对应位图[1111]
select返回2,表明有两个文件描述符准备好了,这个时候监听集合可能就变成了[1001] ,因此我们需要轮循才能知道是哪个文件描述符状态发生了改变。
select 详细过程:
当用户 process 调用 select 的时候,select 会将需要监控的 readfds 集合拷贝到内核空间(假设监控的仅仅是 socket 可读),
内核遍历自己监控的 socket sk,挨个调用 sk 的 poll 逻辑以便检查该 sk 是否有可读事件,遍历完所有的 sk 后,如果没有任何一个 sk 可读,那 select 会调用 schedule_timeout 进入 schedule 循环,使得 process 进入睡眠。如果在 timeout 时间内某个 sk 上有数据可读了,或者等待 timeout 了,则调用 select 的 process 会被唤醒,
接下来 select 就是遍历监控的 sk 集合,挨个收集可读事件并返回给用户。
select()缺点
- 监听的文件描述符上限1024
- 需要轮循才能知道是哪个文件描述符状态发生了改变,当客户端连接数过多,但是准备就绪的又比较少时,轮循代价比较大
- 由于监听集合是传入传出参数,因此在调用
select()
之前需要先保存之前状态 - fds集合需要从用户空间拷贝到内核空间的问题,我们希望不需要拷贝
select处理完整代码
1 | int main(int argc, char const *argv[]) |
poll
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
poll机制和select类似,通过轮询管理的文件描述符,根据其状态来进行处理。
- 但是poll()没有最大文件描述符数量上限。
- fds描述方式和select()不同,使用pollfd()基于链表实现,而fd_set()基于数组
1
2
3
4
5struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
poll与select对比
功能实现上
- select 的描述符类型使用数组实现,FD_SETSIZE 大小默认为 1024,因此默认只能监听少于 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 没有描述符数量的限制;
- select 会修改描述符,而 poll 不会(请求事件和返回事件分开)
- poll 提供了更多的事件类型(POLLIN,POLLOUT,POLLERR,POLLHUP)
速度
select 和 poll 速度都比较慢,每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
可移植性
几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。
epoll
使用场景:连接文件描述符多,但是监听的少,或者说处于就绪态的文件描述符少,使用epoll
1 | int epoll_create(int size); //返回指向内核的红黑树根 |
epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次(处理红黑树节点时候),并且进程不需要通过轮询来获得事件完成的描述符。
为什么不在需要轮循获得准备就绪的fd?
因为epoll_wait() 返回的struct epoll_event * events
已经包含了就绪的文件描述符,并且该结构体含有fd,以及事件信息
常见的处理方式
1 | struct epoll_event activeEvs[100]; |
LT&&ET
Level Triggered (LT) 水平触发
socket接收缓冲区不为空 有数据可读 读事件一直触发
Edge Triggered (ET) 边沿触发
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
其实就是模拟电路中的上升沿以及水平概念,LT只要有数据就会读,ET只有当缓冲区状态发生改变才会触发。
ET很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
三种IO复用应用场景
select
- 实时性要求高;select 的 timeout 参数精度为微秒,而 poll 和 epoll 为毫秒,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制
- 可移植性好
poll
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
epoll
适用于:
- 只运行在linux
- 大量的连接,但是只有少数同时处于就绪态,并且这些连接为长连接
不适用:
- 监控的描述符状态变化多,而且都是非常短暂的,因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。