1.阻塞I/O模型
老李去火车站买票,排队三天买到一张退票。
耗费:在车站吃喝拉撒睡 3天,其他事一件没干。
2.非阻塞I/O模型
老李去火车站买票,隔12小时去火车站问有没有退票,三天后买到一张票。
耗费:往返车站6次,路上6小时,其他时间做了好多事。
3.I/O复用模型
老李去火车站买票,委托黄牛,然后每隔6小时电话黄牛询问,黄牛三天内买到票,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,打电话17次
2.epoll
老李去火车站买票,委托黄牛,黄牛买到后即通知老李去领,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,黄牛手续费100元,无需打电话
4.信号驱动I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李,然后老李去火车站交钱领票。
耗费:往返车站2次,路上2小时,免黄牛费100元,无需打电话
5.异步I/O模型
老李去火车站买票,给售票员留下电话,有票后,售票员电话通知老李并快递送票上门。
耗费:往返车站1次,路上1小时,免黄牛费100元,无需打电话
1同2的区别是:自己轮询
2同3的区别是:委托黄牛
3同4的区别是:电话代替黄牛
4同5的区别是:电话通知是自取还是送票上门
redis IO 多路复用是5种I/O模型中的第3种
对于 Linux 而言:
一切都是文件
然而为了区分不同类型的事物,我们有了:
- 普通文件
- 目录文件
- 链接文件
- 设备文件
其中文件描述符(file descriptor)是内核为了高效管理已被打开的文件所创建的索引,其值是一个非负整数(通常是小整数),用于指代被打开的文件,所有执行 I/O 操作的系统调用都通过文件描述符。
如果直接这么讲可能有些难以理解,对于 Linux 有一些使用的用户来说,会有类似如下的写法:
g++ lots_of_errors 2>&1 | head
其中2>&1
中的 2 就是表示的「标准错误」,1 就是「标准输出」,中间的 & 表示后面跟的数字是文件描述符而不是一个文件(不然所有的「标准错误」就都重定向到了一个名为 1 的文件中了)。
有了上面的知识,我们就可以开始来探索 select,poll 和 epoll 分别是什么了~
多路复用
如文初的说明表示,这三者都是 I/O 多路复用机制,且简要介绍了多路复用的定义,那么如何更加直观地了解多路复用呢?这里有张图:
对于网页服务器 Nginx 来说,会有很多连接进来, epoll 会把他们都监视起来,然后像拨开关一样,谁有数据就拨向谁,然后调用相应的代码处理。
一般来说以下场合需要使用 I/O 多路复用:
- 当客户处理多个描述字时(一般是交互式输入和网络套接口)
- 如果一个服务器既要处理 TCP,又要处理 UDP,一般要使用 I/O 复用
- 如果一个 TCP 服务器既要处理监听套接口,又要处理已连接套接口
select (1983)
对应的头文件和函数原型为:
#include <sys/select.h>
#include <sys/time.h>
int select (int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
/* Returns: positive count of ready descriptors, 0 on timeout, –1 on error */
I/O 多路复用这个概念被提出来以后, select 是第一个实现,一个 select 的调用过程图如下所示:
其缺点为:
- 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时也很大
- select 支持的文件描述符数量只有 1024,非常小
如果系统支持的文件描述符数量不够,在 Linux 上一般就会表现为:
Too many open files (24)
此时就需要通过类似:ulimit -n 2048的方式来临时提升。
poll (1997)
对应的头文件和函数原型为:
#include <sys/poll.h>
int poll (struct pollfd *fdarray, unsigned long nfds, int timeout);
/* Returns: count of ready descriptors, 0 on timeout, –1 on error */
poll 和 select 原理一样,不过相比较 select 而言,poll 可以支持大于 1024 个文件描述符。
epoll (2002)
对应的头文件和函数原型为:
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
相比较 select 和 poll,epoll 的最大特点是:
- epoll 现在是线程安全的,而 select 和 poll 不是。
- epoll 内部使用了 mmap 共享了用户和内核的部分空间,避免了数据的来回拷贝。
- epoll 基于事件驱动,epoll_ctl 注册事件并注册 callback 回调函数,epoll_wait 只返回发生的事件避免了像 select 和 poll 对事件的整个轮寻操作。
什么是回调?一个简单的例子:
- 四六级考试成绩快要出来的那段时间,小张每隔一段时间就去尝试查一下成绩,这个被称为轮训。
- 小张并不在意疯狂刷新页面的事情,等到四六级成绩出来之后他的手机会自动收到考试院推送的一个小时:「叮,你的六级没过」,这样就是回调。
另一个方便理解的对比如下:
- 对于 select / poll 模型来说,可以理解为让酒店代理订票,然后每隔几个小时就问一下买到没有,酒店在第二天订到了票,交钱给酒店拿到票,这样会需要额外的打电话时间和精力。
- 对于 epoll 来说则是委托酒店帮忙订票,但是并不反复去问,酒店在第二天买到了票,酒店打电话通知来领票,交钱给酒店拿到票。