一篇学会阿里面试问的 Select、Poll、Epoll 模型

开发 前端
epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于1024,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

这一篇要说的select、poll、epoll这三个的区别,大家对于IO多路复用都了解吧,这个问题也是面试官最最爱问的问题之一了。

操作系统在处理IO的时候,主要客源分为两个阶段:

  • 等待数据传递到IO设备。
  • IO设备将数据复制到用户空间user space。

也就可以将上述过程简化理解为:

等待数据到kernel内核区域。

kernel内核区域将数据复制到用户区域user space,用户区域可以理解为JVM区域,即进程或者线程的缓冲区。

BIO

这是属于最简单的同步阻塞IO模型

当应用层有数据过来的时候,会调用recvfrom方法,但是这个时候应用层的数据还没有复制到kernel内核区,也就是上面说的第一个过程,这个过程需要时间,所以recvfrom会阻塞。

当内核kernel中的数据准备好了之后,recvfrom方法并不会返回,而是会发起一个系统调用将kernel中的数据复制到JVM的进程中的缓冲区中,也就是用户空间user space。

当这个复制也完成之后,也就是上面说的两个阶段全部完成之后,recvfrom才会返回并且解除程序的阻塞。

图片

默认情况下,所有的文件操作都是阻塞的,在进程调用期间的recvfrom,直到数据包达到并且被复制到JVM进程缓冲区或者发生错误的时候才会返回,在此期间一直阻塞。

在阻塞期间,CPU不能执行IO操作,但是CPU还是可以去做别的事情的,阻塞了,但没有完全阻塞。

我们再看操作系统处理IO的两个步骤

  • 等待数据到kernel内核区域
  • kernel内核区域将数据复制到用户区域user space

阻塞IO模型,其实就是把这两个阶段合并在一起,一起阻塞。

NIO

非阻塞其实就是把第一个过程的阻塞变成非阻塞,也就是recvfrom函数会不断的去询问kernel内核区域中的数据是否准备好。

如果数据没有准备好,就返回EWOULDBLOCK错误,不断的去进行轮询检查,知道发现kernel内核中的数据准备好了,就会返回。

第二个阶段属于系统调用,是必须阻塞的,就是将数据从kernel内核区域拷贝到用户区域,也就是进程缓冲区中。

图片

Linux系统将所有设备都当作文件来处理,而Linux用文件描述符fd来标识每个文件对象。

问题

阻塞模型在没有收到数据的时候就会阻塞卡主,如果一个线程需要接受到多个客户端的socket的fd连接,这样会导致在处理这种情况效率比较低。

必须处理完前面的所有fd,才可以处理后面的fd,即使后面的fd可能比前面的fd提前准备好了,但是也得等,这样会导致客户端的严重延迟。

于是为了处理多个请求,有的小伙伴可能会想到用多个线程来改善,可以引入线程池改善这个情况,并且通过线程池的最大数量来控制这个限度,但是这并没有从根本上解决问题。

应用:适用于针对大量的io请求的情况,对于服务器必须在同时处理来自客户端的大量的io操作的时候,就非常适合。

图片

Select工作流程

单个进程可以同时处理多个网络连接的IO请求,就是调用select之后,整个程序就阻塞了。

此时需要把所有的fd集合都从JVM用户空间拷贝到kernel内核空间,这个开销很大。

然后去轮询检查所有的select负责的fd,当找到一个client中的数据准备好了之后,select就会返回,此时将数据从kernel复制到进程的缓冲区中。

图片

缺点:

  • 每次都需要把fd集合从用户态拷贝到内核态,消耗资源。
  • 每次调用需要进行轮询所有传递进来的fd,也比较消耗资源。

你想啊,要是有十万个连接,而活跃的只有几个,那每次都要遍历这十万个,岂不是很糟糕。

  • 支持的fd数量有限,根据fd_size的定义,它的大小为32个整数大小(32位机器为32*32,所有共有1024bits可以记录fd),每个fd一个bit,所以最大只能同时处理1024个fd。

每一次呼叫select都需要先从 user space把 FD_SET复制到 kernel(约线性时间成本)。

为什么 select 不能像epoll一样,只做一次复制就好呢?

每一次呼叫 select()前,FD_SET都可能更动,而 epoll 提供了共享记忆存储结构,所以不需要这里的kernel内核区域和用户区域之间的全量数据复制。

Poll

poll的原理与select非常相似,但是呢,有两点的不同之处。

一个是存储fd的集合不同,select是有限的,而poll的方式存储是链式的,没有最大连接数的限制。

另一点就是水平触发,也就是通知程序fd就绪后,这次没有被处理,那么下次poll的时候会再次通知同个fd已经就绪。

Epoll

epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?

在此之前,我们先看一下epoll和select和poll的调用接口上的不同,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait。

epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

对于每次都需要把数据从用户区全量复制到kernel内核区这个缺点,epoll的解决方案在epoll_ctl函数中。

每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。

对于第二个每次都需要轮询所有fd这个缺点

epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。

epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。

把主动权交给了每一个连接,当设备就绪的时候,调用回调函数才会加入到这个就绪的集合。

对于第三个数量的缺点

epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于1024,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

图片

责任编辑:武晓燕 来源: 左耳君
相关推荐

2020-11-04 07:49:04

Select

2022-02-22 08:55:29

SelectPoll/ Epoll

2021-05-31 06:50:47

SelectPoll系统

2022-01-02 08:43:46

Python

2022-02-07 11:01:23

ZooKeeper

2023-11-06 07:27:38

模型NLP领域

2021-10-26 10:40:26

代理模式虚拟

2021-12-04 22:05:02

Linux

2022-05-17 08:02:55

GoTryLock模式

2021-08-01 07:19:16

语言OpenrestyNginx

2022-06-30 22:53:18

数据结构算法

2021-07-02 09:45:29

MySQL InnoDB数据

2021-07-05 22:11:38

MySQL体系架构

2023-01-03 08:31:54

Spring读取器配置

2021-05-11 08:54:59

建造者模式设计

2021-07-06 08:59:18

抽象工厂模式

2022-08-26 09:29:01

Kubernetes策略Master

2023-11-28 08:29:31

Rust内存布局

2022-08-23 08:00:59

磁盘性能网络

2021-07-16 22:43:10

Go并发Golang
点赞
收藏

51CTO技术栈公众号