欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

muduo库总结(二)理论分析

程序员文章站 2022-03-14 12:13:13
...

好的网络服务器设计

在这个多核时代,服务端网络编程如何选择线程模型呢? 赞同libev作者的观点:one loop per thread is usually a good model 这样多线程服务端编程的问题就转换为如何设计一个高效且易于使用的event loop,然后每个线程run一个event loop就行了(当然线程间的同步、互斥少不了,还有其它的耗时事件需要起另外的线程做)。

event loop 是 non-blocking 网络编程的核心,在现实生活中,non-blocking 几乎总是和 IO-multiplexing 一起使用,原因有两点:

  • 没有人真的会用轮询 (busy-pooling) 来检查某个 non-blocking IO 操作是否完成,这样太浪费CPU资源了。
  • IO-multiplex 一般不能和 blocking IO 用在一起,因为 blocking IO 中read()/write()/accept()/connect() 都有可能阻塞当前线程,这样线程就没办法处理其他 socket上的 IO 事件了。
    所以,当我们提到 non-blocking 的时候,实际上指的是 non-blocking + IO-multiplexing ,单用其中任何一个都没有办法很好的实现功能。

Reactor模型

The reactor design pattern is an event handling pattern for handling service requests delivered concurrently to a service handler by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to the associated request handlers.

重要组件:Event事件、Reactor反应堆、Demultiplex事件分发器、Evanthandler事件处理器
muduo库总结(二)理论分析
muduo库的Multiple Reactors模型如下:
muduo库总结(二)理论分析

epoll

select和poll的缺点

select的缺点:

  • 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024
  • 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  • select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  • select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程;

相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

以select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的句柄结构内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到100万级别的并发访问,是一个很难完成的任务。

epoll原理以及优势

epoll的实现机制与select/poll机制完全不同,它们的缺点在epoll上不复存在。

设想一下如下场景: 有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?
在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完成后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树,磁盘IO消耗低,效率很高)。把原先的select/poll调用分成以下3个部分:

  • 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)
  • 调用epoll_ctl()向epoll对象中添加这100万个连接的套接字
  • 调用epoll_wait()收集发生的事件的fd资源

如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除事件。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。
epoll_create在内核上创建的eventpoll结构如下:

struct eventpoll{
 ....
//红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件
  struct rb_root  rbr;
//双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件
  struct list_head rdlist;
};

LT模式、ET模式

LT模式:内核数据没被读完,就会一直上报数据。
ET模式:内核数据只上报一次。

muduo采用的是LT:

  • 不会丢失数据或者消息
    应用没有读取完数据,内核是会不断上报的
  • 低延迟处理
    每次读数据只需要一次系统调用;照顾了多个连接的公平性,不会因为某个连接上的数据量过大而影响其他连接处理消息
  • 跨平台处理
    像select一样可以跨平台使用
相关标签: epoll