详解网络服务器中的IO模型

阻塞、非阻塞、同步、异步

典型的一次IO的两个阶段是什么? 数据准备 和 数据读写

数据准备:根据系统IO操作的就绪状态

  • 阻塞 (默认):无数据可读时,函数不会返回,而是阻塞在当前线程一直等待
  • 非阻塞 无数据可读时,函数立即会 返回,不会阻塞当前线程  下面的代码
int size = recv(socket, buf, 1024,0);
// 若 size == -1 && errno = EAGAIN,表示连接正常,只是没有数据,立刻返回了 

数据读写:根据应用程序和内核的交互方式,这是I/O的同步异步,不是业务的同步异步(并发)

  • 同步:数据准备好了,由应用层自己去内核层取数据,send、recv都是同步的函数接口
  • 异步(通常是非阻塞):当我请求内核的时候,是麻烦内核当数据准备好时,将其搬到我这个buf上,然后我可以处理自己的任务,内核搬完了通知我一下,通知(回调)是异步最大的标识

陈硕大神原话:在处理 IO 的时候,阻塞和非阻塞都是同步 IO。只有使用了特殊的 API 才是异步 IO。

一个典型的网络IO接口调用,分为两个阶段,分别是“数据就绪”和“数据读写”,数据就绪阶 段分为阻塞和非阻塞,表现得结果就是,阻塞当前线程或是直接返回。 同步表示A向B请求调用一个网络IO接口时(或者调用某个业务逻辑API接口时),数据的读写都 是由请求方A自己来完成的(不管是阻塞还是非阻塞);异步表示A向B请求调用一个网络IO接口 时(或者调用某个业务逻辑API接口时),向B传入请求的事件以及事件发生时通知的方式,A就 可以处理其它逻辑了,当B监听到事件处理完成后,会用事先约定好的通知方式,通知A处理结 果。

  • 同步阻塞 int size = recv(fd, buf, 1024, 0)
  • 同步非阻塞 int size = recv(fd, buf, 1024, 0)
  • 异步阻塞
  • 异步非阻塞

Unix/Linux上的五种IO模型

阻塞 blocking

就是阻塞等待,每次只能处理一个socket,对于多个客户端请求,需要不断轮询

非阻塞 non-blocking

就是非阻塞等待,每次只能处理一个socket,对于多个客户端请求,需要不断轮询

IO复用(IO multiplexing)

  • 可以同时监视多个文件描述符,适合处理多个并发连接。

  • 比单纯的非阻塞 I/O 更加高效,不需要不断轮询。

  • select、poll、epoll它们本身不是阻塞或非阻塞的,而是可以用于实现阻塞或非阻塞的 I/O 多路复用,我们通常使用的是非阻塞IO复用

信号驱动(signal-driven)

内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进 程不断的轮询检查,减少了系统API的调用次数,提高了效率。

内核在第一个阶段是异步,在第二个阶段是同步;与非阻塞IO的区别在于它提供了消息通知机制,不需 要用户进程不断的轮询检查,减少了系统API的调用次数,提高了效率。

异步(asynchronous)

和上面的异步非阻塞一样,非常高效,需要特殊的API,如AIO

典型的异步非阻塞状态,Node.js采用的网络IO模型。

好的网络服务器设计

在这个多核时代,服务端网络编程如何选择线程模型呢? 赞同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,单用其 中任何一个都没有办法很好的实现功能。

epoll + fork不如epoll + pthread?

强大的nginx服务器采用了epoll+fork模型作为网络模块的架构设计,实现了简单好用的负载算法,使 各个fork网络进程不会忙的越忙、闲的越闲,并且通过引入一把乐观锁解决了该模型导致的服务器惊群 现象,功能十分强大。

Reactor模型

reactor反应堆模型主要由四个组件组成:事件、反应堆、事件分发器、事件处理器,首先呢各个事件和其对应的处理回调函数统一在反应堆中注册,事件分发器在IO多路复用中就相当于epoll,它利用反应堆提供的一些方法对时间进行调整(修改、增加、删除),然后epoll开启阻塞状态等待新用户的链接,或者是已链接用户的读写事件,然后事件分发器将就绪的事件返回给反应堆,通过反应堆中对应的事件处理函数,让事件处理器进行相应的处理:读写操作、并将结果发送出去。

muduo采用的是多反应堆模型构建的

muduo库的Multiple Reactors模型如下:

select和poll相对于epoll 的缺点

select的缺点:

1、单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于 select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;(在linux内核头文件中,有 这样的定义:#define __FD_SETSIZE 1024

2、内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销

3、select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件

4、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个部 分:

1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字

3)调用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模式

内核数据没被读完,就会一直上报数据。

  • 因为是 LT 工作模式,故当底层数据就绪后,上层可以暂时不处理,或者只处理一部分;
  • 因为此时服务端的接收缓冲区还有1KB的数据,因此,第二次调用 epoll_wait 时,依旧会立刻返回,代表事件就绪;
  • 直到接收缓冲区的数据被全部拷贝到上层,epoll_wait 才不会立刻返回,而是阻塞,等待事件就绪;
  • LT 工作模式即支持阻塞读写,也支持非阻塞读写。

ET模式

内核数据只上报一次。

  • 将服务套接字添加到 epoll 模型时,使用 EPOLLET 标志,代表这个服务套接字的工作模式是 ET 模式;
  • 即便此时服务端的接收缓冲区还有 1KB 的数据,但是,第二次调用 epoll_wait 时,epoll_wait 不会再返回;
  • 此时,如果后续没有数据到来,那么上面的 1KB 数据,上层就拿不到了,即数据丢失,因此,我们说,在ET模式下,事件就绪后,只有一次处理机会,在这一次处理过程中,用户必须将数据全部拷贝到上层;
  • 一般情况下,ET 性能比 LT 性能高,原因其一, epoll_wait 返回次数少了很多;
  • ET 模式的服务器,只支持非阻塞的读写。

muduo采用的是LT

  • 不会丢失数据或者消息
    • 应用没有读取完数据,内核是会不断上报的
  • 低延迟处理
    • 每次读数据只需要一次系统调用;照顾了多个连接的公平性,不会因为某个连接上的数据量 过大而影响其他连接处理消息
  • 跨平台处理
    • 像select一样可以跨平台使
  • 42
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
VGG16是一种卷积神经网络模型,由Karen Simonyan和Andrew Zisserman在2014年提出。它是基于深度卷积神经网络的经典模型之一,具有16层深度,因此得名VGG16。 VGG16的主要特点是使用了非常小的卷积核大小(3x3),以及相对较深的网络结构。它的核心思想是通过多个连续的小卷积核和池化层来增加网络的深度,以提高特征提取能力。VGG16共有13个卷积层和3个全连接层。 整个VGG16网络模型的结构非常简单而规整,每个卷积层都使用了3x3大小的卷积核,并使用ReLU激活函数进行非线性变换。卷积层之间会进行池化操作,通常使用2x2的最大池化。这种简单而规整的结构使得VGG16易于理解和实现。 VGG16的最后三个全连接层负责对提取到的特征进行分类。其前两个全连接层具有4096个输出节点,使用ReLU激活函数进行非线性变换。最后一个全连接层使用Softmax激活函数,将输出映射到类别概率上。 VGG16的训练通常使用大规模的图像数据集,如ImageNet。通过在ImageNet上进行训练,VGG16可以学习到丰富的图像特征,并在图像分类、目标检测等任务取得良好的性能。 总结来说,VGG16是一种经典的深度卷积神经网络模型,通过多个小卷积核和池化层的堆叠来增加网络深度,以提高特征提取能力。它在图像分类等计算机视觉任务表现卓越,成为了深度学习领域的重要里程碑之一。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值