问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

select、poll和epoll这三种IO多路复用技术的执行原理

创作时间:
作者:
@小白创作中心

select、poll和epoll这三种IO多路复用技术的执行原理

引用
CSDN
1.
https://m.blog.csdn.net/qq_46130027/article/details/145522255

在高性能网络编程中,IO多路复用技术是实现高并发处理的关键。本文将深入探讨select、poll和epoll这三种常见的IO多路复用技术的执行原理,帮助读者理解它们各自的优缺点和适用场景。

select多路复用

这是select的Server端代码,首先使用socket()函数创建了一个socket服务端,然后在这里使用了一个for循环并通过accept()函数创建了5个客户端连接,同时把它们当到了一个fd数组中。
这里的数字代表的就是文件描述符(3,5,7,8,10),这些文件描述符都是随机产生的,并不是连续的。并且还在这里求出了最大的文件描述,这里是10。

我们准备好了文件描述符信息之后就可以执行select()函数了。
这个select函数的第一个参数是max+1, 第二个参数是读文件描述符的集合,第三个参数是写文件描述符的集合,第四个参数是我们需要监听的异常事件的文件描述集合,第五个参数代表超时时间。
超时时间表示的就是说在指定的时间范围内,如果还没有检测到某个文件描述符已就绪,那么这个时候也会结束阻塞,就不等待了,立即返回。比如在Redis的底层中我们可以把它默认设置成NULL, 表示永不超时,一直阻塞下去,一直等到有数据到达,也就是说事件已就绪才返回。设置为0表示的是不阻塞,设置为大于0的数则是我们具体需要等待的时间。
用bitmap来表示具体哪一个文件描述符被关注。它是使用一个for循环,然后用FD_SET()函数去设置哪个文件描述符被关注。
在设置之前,他首先要完成的一个动作,它要对所有的本次需要关注的这些本次传入的这些读文件描述符进行初始化,相当于把它们全部置零。
bitmap中关注的文件描述符会被设置为1,不关注的文件描述符会被置为0。这里前三个0、1、2是给操作系统预留的。在操作系统启动的时候就占用了这三个文件描述符。

这里select函数中传入的&read_fds就是这个bitmap。
调用select函数的时候会把bitmap传入到内核中,拷贝到内核中之后,是由内核去判断哪一个fd或者说哪一个socket文件描述符有对应的数据到达或者说需要关注的事件已就绪。
为什么要用内核去判断呢?因为内核的效率要比用户态要高,如果用用户态去判断,用户态在判断的时候他需要去询问内核,哪个文件描述符已就绪。那这样就存在内核和用户态的上下文切换。
因此select函数就选择了将bitmap一次性的拷贝到内核态,由内核去遍历哪一个fd由数据到达。
比如说现在进程a他需要关注的文件描述符3、5、7、8、10都没有数据到达,那么这个时候进程A就会让出CPU的执行权限,然后进入阻塞状态。同时会把进程A放到进程等待队列里面去。
这个max+1就限定了在循环遍历bitmap时候的范围,因为这个bitmap它最多可以有1024位,max+1就限定了遍历的最大长度,因此可以减少无谓的扫描。

内核会一直阻塞在这里,等待客户端把数据发送过来。如果这个时候客户端把数据发送到了服务端的网卡设备上,这个时候会发生软中断,然后会调用DMA拷贝技术,将数据拷贝到内核环形缓冲区。然后再根据文件描述符信息,把数据拷贝到对应的socket的数据接收队列里面去。
哪个socket文件描述符对应的数据接收队列有数据到达,那么就标志这个文件描述符已就绪,它们就会标记成已就绪的状态。这个时候select函数就会返回了。
他是把所有的文件描述符全部拷回到用户态。同时这个select函数还会告知现在有几个文件描述就绪,不过它只是告诉了它个数没有告诉它具体是哪两个文件描述符就绪。
因此客户端需要做的事就是在用户态去调用一个for循环去循环判断具体哪两个文件描述符已就绪。他这里用的是FD_ISSET()方法去判断哪两个读事件已就绪。然后会解除阻塞。

1.1 Select IO多路复用的执行原理

  1. 将当前进程的所有文件描述符,一次性的从用户态拷贝到内核态。
  2. 在内核中快速的无差别遍历每个fd,判断是否有数据达到。
  3. 将所有fd状态,从内核态拷贝到用户态,并返回已就绪fd的个数。
  4. 在用户态遍历判断具体哪个fd已就绪,然后进行相应的事件处理。

1.2 Select IO多路复用的限制和不足

  1. 文件描述符表为bitmap结构,且有长度为1024的限制(相当于它最多可以连接1024个客户端)。
  2. fdset无法做到重用,每次循环必须重新创建(因为每次都会被内核拷贝过来的数据覆盖掉)。
  3. 频繁的用户态和内核态拷贝,性能开销较大。
  4. (无论用户态还是内核态)需要对文件描述符表进行遍历,O(n)的轮询时间复杂度。

2. poll模型

poll模型是Select模型的改进版,poll模型的最大亮点是它定义了一个pollfd的结构体。
也就是说当客户端连接上来之后,FD会将生成的文件描述保存在这里。events就是指我们用户关注的事件,比如读事件、写事件。revents指的是返回的事件,它是由系统的内核填充并返回的。
正是因为poll模型采用了这种结构体数组的方式,因此它就不会有1024个文件描述符限制。因此,它相对于Select来说,它可以承受更高的并发。

2.1 poll的执行原理

通过accept()创建4个文件描述符,就是4个连接,相当于建立4个客户端连接。
同时在这里注册了我们需要关注的读事件:
然后在这里发起poll调用:
同时这个poll调用也是一个阻塞调用,poll函数参数的含义:

  1. 第一个参数fds就是要传入的文件描述符的结构体数组。
  2. 第二个参数就是这个结构体数组的最大长度。
  3. 第三个参数就是阻塞等待时间。
    poll和select是一样的,它是一次性的将一批文件描述符发送到内核态。同样,它也需要监听或关注事件对应的文件描述符,它所执行的这个进程放到进程等待队列里面去等待,然后在内核里面去循环遍历这些文件描述符,看哪些文件描述符已就绪。
    这样,当有数据到达网卡设备上的时候,同样会利用DMA拷贝技术把数据拷贝到内核环形缓冲区。然后再根据内核描述符的信息把数据拷贝到各自的数据接收队列里面去。
    然后内核在遍历这些文件描述符的时候,哪个文件描述符的数据接收队列有数据,就把他们的revents字段给他置1。
    紧接着这个poll()函数就会返回,然后会将文件描述符的结构体数组拷回到用户态,与select方法类似。在这里,用户态仍然需要去遍历判断,具体哪个文件描述符已就绪。不同的地方就在这里判断完成之后,他就直接将这些revents恢复成0。
    不同的是Select会在每次调用select()函数之前统一做一次初始化,而这个poll就省去了初始化的动作。同时这个时候,poll函数就会解除阻塞。

2.2 Poll IO多路复用的执行原理 (和Select一样)

  1. 将当前进程的所有文件描述符,一次性的从用户态拷贝到内核态。
  2. 在内核中快速的无差别遍历每个fd,判断是否有数据达到。
  3. 将所有fd状态,从内核态拷贝到用户态,并返回已就绪fd的个数。
  4. 在用户态遍历判断具体哪个fd已就绪,然后进行相应的事件处理。

2.3 Poll IO多路复用解决的问题和不足

  1. poll模型采用的pollfd结构数组,解决了Select的1024个文件描述符的限制。
  2. 但仍然存在频繁的用户态和内核态拷贝,性能开销较大。
  3. 需要对文件描述符表进行遍历,O(n)的轮询事件复杂度。

3. epoll模型

在Linux2.6版本诞生了epoll模型,就彻底解决了性能不足的问题。
首先定义了一个epoll的结构体:
其中events表示需要关注的事件,比如说读事件、写事件。epoll_data就是一个自定义的联合体,这里通过fd去保存那些需要监听的文件描述符信息。
然后在这里调用了epoll_create()方法,用它去创建一个eventpoll结构体,这个eventpoll里面有三个重要的字段:
一个是rdyList,就是已就绪的文件描述双链表,这是一个双链表。比如说当某个读事件或者写事件就绪的时候,就会把相应的文件描述符信息放到已就绪的双链表中。
rbr就是一颗红黑树,用红黑树去管理用户进程放进来的所有socket连接。
WQ是一个等待队列,就是当每个进程需要关注的事件未就绪的时候,就会把当前进程的描述符以及回调函数放到这个进程等待队列里面去。这样当软中断数据到达的时候,就会通过检查阻塞队列,找到相应的阻塞进程。然后去唤醒他,去让他执行后续的动作。
这里是注册它需要关注的事件。
然后这里调用epoll_ctl() 函数,这个epoll_ctl()函数就相当于是创建客户端和服务端之间的连接。并把这个连接给它注册到eventpoll中。
对于每个socket连接,都会在调用epoll_ctl()函数的时候,为它分配一个叫epitem这个结构体,这个结构体有四个重要字段:

  1. 第一个就是每个socket连接对应的红黑树节点。
  2. 第二个是他的文件描述符信息。
  3. 第三个是文件描述符的连接对应的eventpoll数据。
  4. 第四个是进程等待队列。
    每当我们创建一个客户端和服务端的socket连接的时候,就会把这些连接添加到一个红黑树里面。
    可以说每个红黑树节点都对应着epitem里边的一条记录。
    紧接着就是要执行epoll_wait()函数,它首先是去检查已就绪的事件,双链表中是否有就绪的事件。如果有就绪的事件的话,那epoll_wait函数会立刻返回,不阻塞。但是当里面为空的时候,它就会进行阻塞,等待有就绪的事件到达。
    比如说现在是由进程a去执行这个epoll_wait()函数,那这个时候进程a就会让出CPU进入阻塞等待的状态。同时会把进程a关联到阻塞进程队列里面去。
    接下来就是数据到达的环节了,比如说当客户端把数据发送到了socket的数据接收队列里面去,这个时候就会将这个已就绪的读事件把它们插入到已就绪的双链表里。
    注意,这是通过回调函数去调用的,这里面有个回调函数去调用的,不是去遍历调用的。
    比如说现在7和9事件已经就绪,文件描述已经就绪,然后内核就会检查现在是否有阻塞的进程存在。
    如果这个阻塞进程恰好是执行这个7和9这两个文件描述符的事件,那么就把它唤醒。然后进程a就会进入到CPU的运行队列。
    相当于把这两个文件描述符要返回给用户态,然后就可以处理相应的读事件的信息了。

3.1 性能优越的原因

epoll()为什么会比select和poll的性能都要高。

  1. 在epoll_ctl()函数中,为每个文件描述符都指定了回调函数,基于回调函数把就绪事件放到就绪队列中,因此,把时间复杂度从O(n)降到了O (1)。
    因此,epoll就不会像select一样在在这里循环的去遍历。
  2. 只需要在epoll_ctl()时传递一次文件描述符,epoll_wait()不需要再次传递文件描述符。
    在select方法,他每次都会把全量的文件描述信息拷贝到内核态。而且当这个就绪之后又要把内核态全部的文件描述符再拷回到用户态。
  3. epoll基于红黑树+双链表存储事件,没有最大连接数的限制,不存在CLOK问题。
    不像select有1024个连接数的限制。

3.2 注意

epoll没有使用MMAP零拷贝技术。

【参考资料】:腾讯面试:请描述 select、poll、epoll 这三种IO多路复用技术的执行原理

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号