I/O多路复用的一点理解
写这个是因为一个关于Redis为什么这么快的答案中,其中一个原因,就是Redis使用了I/O多路复用模型。于是我回想起一个我觉得很魔幻的经历,就是在2年前换工作时,连续在三场面试中被问到了I/O多路复用,select、poll、epoll的问题。关于为什么一群写业务的工程师都痴迷于这个问题依然是个谜,不过很显然,除非你能打死他们,要不就只能选择加入。
首先先把这个词拆开看,I/O是指网络I/O;多路,是指多个TCP链接或者channel,当发生多个网络请求时,I/O多路就产生了。而复用,是指复用一个或少量线程,比如单进程的Redis,所有的请求都需要复用一个进程。所以这个词串起来理解就是:很多个网络I/O复用一个或少量的线程来处理这些连接。它还有个别称,叫“事件驱动”,就和字面意思一样,就是需要动的时候有人叫你,不叫你的时候你就躺着就行。
目前支持多路复用的系统调用有select、poll、epoll。
- select
监视多个文件句柄的状态变化,程序会阻塞在select处等待,直到有文件描述符就绪或超时。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
可以看出select可以监听fd_set *readfds, fd_set *writefds, fd_set *exceptfds三种描述符,
缺点:每次调用select,都需要把待监控的fd集合从用户态拷贝到内核态,当fd很大时,开销很大。
每次调用select,都需要轮询一遍所有的fd,查看就绪状态。
select支持的最大文件描述符(fd)数量有限,默认是1024。
- poll
和select没有很大区别,改进成了基于链表的,所以没有最大fd数量限制。
int poll(struct pollfd *fds, nfds_t nfds, int timeout) struct pollfd { short events; short revents; };
pollfd结构包括了events(要监听的事件)和revents(实际发生的事件)。而且也需要在函数返回后遍历pollfd来获取就绪的描述符。
- epoll
针对上面两位的共同缺点做了改进,
select 和 poll 监听文件描述符list,进行一个线性的查找 O(n);
epoll: 使用了内核文件级别的回调机制O(1);
于是epoll模型在处理大量fd的时候,就和之前的模型有了数量级上的进步。
看看这张图,epoll本身几乎是不受并发数的影响的。
/proc/sys/fs/epoll/max_user_watches这个文件表示用户能注册到epoll实例总最大fd的数量,我随便找了台服务器看了眼,790999,貌似够大了。所以在Linux平台,需要并发的场景下,epoll应该已经代替了select和poll。
❓那么问题来了,既然这么好用的东西出现了,select和poll还用在哪里呢?
- 管理少量连接(比如fd数 < 10),poll/select 是比较轻量级的,不需要去创建一个epoll的fd。而且,select实现起来应该比epoll简单不少。
- 编写跨平台代码,保证代码任意平台都能用,那么 cygwin 下只有 poll 给你,win32下对应 select。