当前位置: 首页 > 科技观察

一篇学习阿里面试题中Select、Poll、Epoll模型

时间:2023-03-12 00:07:51 科技观察

的文章。这篇文章会讲一下select、poll、epoll的区别。我最喜欢的问题之一。操作系统在处理IO时,主要分为两个阶段:等待数据传输到IO设备。IO设备将数据复制到用户空间用户空间。也就是说,上面的过程可以简化理解为:等待数据到kernel内核区。kernel内核区将数据拷贝到用户区userspace,用户区可以理解为JVM区,即进程或线程的缓冲区。BIO是最简单的同步阻塞IO模型。当应用层有数据时,会调用recvfrom方法,但是此时应用层的数据还没有复制到kernel内核区,也就是上面说的第一个进程。这个过程需要时间,所以recvfrom会阻塞。当内核kernel中的数据准备好后,recvfrom方法不会返回,而是会发起系统调用将内核中的数据复制到JVM进程中的缓冲区,即用户空间userspace。当这个copy也完成,也就是上面说的两个阶段都完成之后,recvfrom就会返回,解除对程序的阻塞。默认情况下,所有文件操作都被阻止。在进程调用过程中,recvfrom直到数据包到达并复制到JVM进程缓冲区或者发生错误才会返回,这期间一直处于阻塞状态。阻塞期间,CPU不能进行IO操作,但CPU仍然可以做其他事情。它被阻止了,但没有完全被阻止。我们来看一下操作系统处理IO和等待数据发送到内核区的两个步骤。内核区将数据复制到用户区。阻塞IO模型实际上就是将这两个阶段和块结合在一起。NIO非阻塞其实就是把第一个进程的阻塞变成了非阻塞,也就是recvfrom函数会不断的询问kernel内核区的数据是否就绪。如果数据没有准备好,会返回EWOULDBLOCK错误,并继续进行轮询检查,直到发现内核中的数据准备好了,才会返回。第二阶段属于系统调用,必须阻塞,即将数据从内核区复制到用户区,即进程缓冲区。Linux系统把所有的设备都当作文件来对待,Linux使用文件描述符fd来标识每一个文件对象。问题的阻塞模型会在没有收到数据时阻塞卡主。如果一个线程需要接收来自多个客户端socket的fd连接,会导致处理这种情况的效率低下。必须先处理完前面的所有fds,才能处理后续的fds。即使后面的fd可能比前面的fd准备得更早,也要等待,这会给client带来严重的延迟。因此,为了处理多个请求,可能有小伙伴会想到使用多线程来改善这种情况。他们可以引入线程池来改善这种情况,通过线程池的最大数量来控制这个限制,但这并不能从根本上解决问题。.应用:适用于大量的io请求,当服务端必须同时处理来自客户端的大量io操作时非常适合。Select工作流程单个进程可以同时处理多个网络连接IO请求,即调用select后,整个程序被阻塞。这时候需要将所有的fd集合从JVM用户空间复制到kernel内核空间,开销很大。然后轮询检查所有负责select的fd。当发现客户端中的数据准备就绪时,select将返回。这时,数据就会从内核中拷贝到进程的缓冲区中。缺点:每次都需要将fd集合从用户态复制到内核态,比较耗资源。每次调用都需要轮询所有传入的fd,也很耗资源。你想,如果有10万个连接,只有几个是活跃的,那么每次都要遍历这10万个连接,岂不是很可怕。支持的fds数量有限。根据fd_size的定义,它的大小是32个整数(32位机器是32*32,所有1024bits都可以记录fds),而每个fd只有一位,所以只能同时处理1024个。fd。每次调用select都需要将FD_SET从用户空间复制到内核(近似线性时间成本)。为什么select不能像epoll那样只做一份呢?FD_SET在每次调用select()之前可能会被改变,而epoll提供了一个共享内存存储结构,所以这里不需要内核区和用户区之间的全量数据复制Pollpoll的原理和select很相似,但是有是两个区别。一是存储fd的集合不同,select是有限制的,而poll存储是链式的,没有最大连接数限制。还有一点是水平触发,就是通知程序fd就绪后,如果这次没有处理,那么下次轮询时会再次通知同一个fd准备就绪。由于epollepoll是对select和poll的改进,所以应该可以避免以上三个缺点。那么epoll是怎么解决的呢?在此之前,我们先看看epoll和select、poll的调用接口有什么区别。select和poll都只提供一个功能——select或poll功能。而epoll提供了三个函数,epoll_create、epoll_ctl和epoll_wait。epoll_create是创建一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait是等待事件产生。对于每次都要从用户区复制所有数据到内核区的缺点,epoll的解决方案是在epoll_ctl函数中。每次有新的事件注册到epoll句柄(在epoll_ctl中指定EPOLL_CTL_ADD),所有的fds都会被复制到内核中,而不是在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内存的机器上大约是100000。具体数量可以通过cat/proc/sys/fs/file-max查看。一般来说,这个数字与系统内存有很大关系。