简要概述网络I/O与并发

以下内容可能存在错误,由博主自主查阅资料所得,仅作参考。

  计算机的基本组成其实很简单,处理器,存储器加上输入输出设备就构成了计算机。大至超级计算机,小到手机发等都是一样的模型组成。计算的本质就是从输入设备读取数据处理后输出。可以理解计算机做的事就是IO和计算。

  在网络发明之前,计算机从存储设备中读取数据,进程通过内存通道进行通信。互联网诞生之后,越来越多计算机通过互联网连接,将数据传输到世界各地。计算机之间可以通信,本质上也是计算机进程之间互相通信。为了方便不同终端进行通信,网络协议栈抽象出socket层,通过对socket文件描述符的操作来进行网络IO。当然,不同的应用场景,衍生出不同的网络模型。

本文描述为发起IO进程,也可以描述为发起IO的线程。

一次网络响应

  互联网应用中,多数架构是CS模式,即client发出请求,server接收请求,处理之后返回响应。这样的一次交互,伴随着client和server的IO操作。对于常见的爬虫,client将尽可能提升其并发发送请求IO能力,对于后端server也需要尽可能提升其并发处理多个client请求的能力。

  例如有多个用户,发送了一个请求,请求服务器上的一个文件。假设服务器监听的端口是8000。client请求的文件是hello.txt。当server开启服务后,就会监听来自8000端口的请求,client把请求发送给server,server再从自己的磁盘上读取hello.txt,然后返回给客户端。这样一次简单的交互,涉及到网络IO和磁盘文件的IO。大致流程如下:

![效果图

上图只表述server处理响应的过程:

  1. server的进程发起Read系统调用,内核随即从硬件Disk读取数据到内核缓冲区(kernel buf)。
  2. 内核再把缓冲区的数据copy到应用程序进程的缓冲区(app buffer),应用程序server进程就可以对数据进行修改。
  3. 应用程序server进程将数据通过系统调用Send发送到socket缓冲区,每个socket文件描述符都在内核维护一个发送/接收缓冲区。
  4. 最后再把socket发送缓冲区的数据copy到NIC网卡中,通过协议栈发送到对端的网卡中。
  5. 对端的网卡接收数据的过程中,client会发起一个Recv的系统调用,然后内核会从网卡中读取数据,然后copy到应用程序的缓冲区。

整个过程中,数据在三个主要层次流动,即硬件、内核、应用程序。在流动的过程中,从一个层流向另外一个层即为IO操作。

DMA Direct Memory Access,直接内存访问方式,即现在的计算机硬件设备可以独立地直接读写系统内存,而不是CPU完全介入处理。也就是数据从DISK或者NIC中copy到内核buf,不需要计算机CPU的参与,而是通过设备上的芯片(CPU)参与。即对于内核来说,这样的数据读取过程中,CPU可以做其它事情,大大提高了CPU的利用率。

一次I/O过程

  通过上面的数据流动,可以看到IO的基本方式,那么什么是IO呢?通常现代的程序软件都运行在内存里,内存又分为用户态和内核态,后者隶属于操作系统。所谓的IO,就是将硬件(磁盘、网卡)的数据读取到程序的内存中。

  因为应用程序很少可以直接和硬件交互,因为操作系统作为两者的桥梁。通常情况下,操作系统在对接两端(应用程序与硬件)时,自身有一个内核buf,用于数据的copy中转。

应用的读IO操作,即将网卡的数据copy到应用程序的buf,中途会经过内核的buf。

  1. 应用程序进程发起read系统调用。
  2. 内核接受到应用进程的请求,如果内核buf有数据,则把数据copy到应用buf中,调用结束。
  3. 如果内核buf中没有数据,会向IO模块发送请求,IO模块和硬件交互。
  4. 当NIC接收到协议栈的数据后,NIC会通过DMA技术将数据copy到内核buf中。
  5. 内核将内核buf 的数据copy到应用进程的buf中,调用结束。

一般网络IO分为两个阶段,等待数据阶段和拷贝数据阶段。前者是数据通过协议栈发送到网卡,网卡再通过DMA copy到内核buf。后者是进程阻塞,要么是同步调用,要么是异步调用。

I/O基本模型

《Unix网络编程》中提到了5种基本的网络I/O模型,主要分为同步和异步I/O:

  1. 阻塞I/O(blocking)
  2. 非阻塞I/O(nonblocking)
  3. 多路复用I/O(multiplexing)
  4. 信号驱动I/O(SIGIO)
  5. 异步I/O(asynchronous)

最好用的是第一种,代码逻辑简单,符合人的正常思考方式。现实中常用的是第三种,第二种不太好用,第四种也很少,第五种不太成熟。下面针对具体的方式逐一讲解。

阻塞I/O(blocking)

  前面已经介绍,IO过程分为两个阶段,数据拷贝过程和等待数据准备。这里涉及两个对象,其一是发起IO操作的进程(线程),其二是内核对象。所谓阻塞是指进程在两个阶段都阻塞,即线程挂起,不能做别的事情。

红色的虚线表示IO函数调用过程。加粗的红线(CPU copy)过程表示数据从内核buf拷贝到应用buf的过程,在该过程种应用进程阻塞。

  应用进程发起Recv操作,这是一个系统调用,然后内核会看内核buf是否有数据,如果没有数据,那么应用进程会被挂起,直到内核CPU从硬件或者网络中读到数据之后,内核再把数据从内核buf拷贝(copy)到进程buf中,然后唤醒发起调用的进程,并且Recv操作将会返回数据。接下来应用进程就可以对进程buf的数据进行处理。

一个简单的C++单线程同步阻塞server(Windows平台下):

#include <iostream>
//网络库
#include <WinSock2.h>
//引入ws2_32.lib
#pragma comment(lib,"ws2_32.lib")
using std::cout;
using std::endl;
using std::cin;int main()
{//在Windows平台下需要额外的加载socket库WSADATA wsaData;if (WSAStartup(MAKEWORD(2, 2), &wsaData) == SOCKET_ERROR){cout << "create WSAStartup Error " << GetLastError() << "\n";return 0;}//1.创建一个socket套接字(socket)SOCKET serverSock = socket(AF_INET, SOCK_STREAM, 0);if (INVALID_SOCKET == serverSock){cout << "create socket Error " << GetLastError() << "\n";return 0;}//2.IP端口和socket相关联(bind)SOCKADDR_IN serverAddr;serverAddr.sin_family = AF_INET;serverAddr.sin_port = htons(7890);//主机字节序转换为网络字节序serverAddr.sin_addr.s_addr = inet_addr("127.0.0.1");//将点分十进制字符串转换为网络地址(u_long)if (SOCKET_ERROR == bind(serverSock, (sockaddr *)&serverAddr, sizeof(SOCKADDR_IN))){cout << "bind Error " << GetLastError() << "\n";return 0;}cout << "bind Success\n";//3.监听端口(listen)if (INVALID_SOCKET == listen(serverSock, 5)){cout << "listen Error\n";return 0;}while (true){//4.等地用户连接(accept)printf("Hold User\n");SOCKET clientSock;SOCKADDR_IN clientAddr;int clientAddrLen = sizeof(SOCKADDR_IN);clientSock = accept(serverSock, (sockaddr *)&clientAddr, &clientAddrLen);if (INVALID_SOCKET == clientSock){cout << "accept Error " << GetLastError() << "\n";return 0;}printf("accept Success SOCKET[%d] ip[%s] port[%d]\n", clientSock, inet_ntoa(clientAddr.sin_addr), ntohs(clientAddr.sin_port));//5.跟连接的客户端通信(recv、send)while (true){char buffer[1024]{ 0 };int recvLen = recv(clientSock, buffer, 1024, 0);if (recvLen <= 0){cout << "客户端断开连接或者发生错误\n";break;}else{cout << "接收到的数据长度为: " << recvLen << "\n";send(clientSock, buffer, sizeof(buffer), 0);}}//关闭客户端closesocket(clientSock);}//关闭服务端closesocket(serverSock);//卸载动态库WSACleanup();return 0;
}

  server的socket套接字有两种,一种是监听套接字(sock),它有一个accept方法,该方法的作用就是从已握手的队列中取出一个连接。另外一种就是连接套接字(connet),即accept方法返回的socket。

非阻塞I/O(nonblocking)

  线程在blockingIO(阻塞IO)中,发起IO之后随即被挂起,不能做其它事情。在nonblockingIO中,如果没有IO数据,那么发起的系统调用也会马上返回,会返回一个EWOULDBLOCK错误。函数返回之后,线程没有被挂起,当然可以继续做其它事情。

  正如图上所示,在真实的环境中,进程发起了非阻塞IO请求,返回了EWOULDBLOCK之后,将会继续再次发起非阻塞的IO请求,这个过程还是会使用CPU,因此也称之为轮询(polling)。当内核有数据的时候,内核将内核buf的数据copy到应用buf的过程还是需要CPU参与,这个过程对于nonblocking来说,线程仍然是阻塞的

多路复用I/O(multiplexing)

  阻塞IO会让线程挂起不能做其它事情,非阻塞IO则提供了新的思路,函数调用之后就返回,可是为了完成IO,需要不同的polling(轮询)。每次轮询都是一次系统调用。某种程度下,非阻塞IO的性能还不如阻塞IO。既然需要内核频繁操作,这时有人想出了新的模型。

  让内核代理去做轮询,然后应用进程只有数据准备了再发起IO操作不就好了吗?的确,多路复用IO就是这样的原理。由内核负责监控应用程序指定的socket文件描述符,当socket准备好数据(可读、可写、异常)的时候,通知应用进程。准备好数据是一个事件,当事件发生的时候,通知应用进程,而应用进程可以根据事件事先注册回调函数。

  进程发起select或poll或epoll调用之后,可以设置进程阻塞。当内核数据准备好的时候通知应用进程,即事件发生。应用进程注册了回调函数(这里回调函数是recv)。因此进程可以再次发起recv系统调用。后面这个过程与前面的阻塞IO和非阻塞IO一样,都是系统调用recv。只不过这里通常是一定可以读到数据,非阻塞的方式也不会返回错误。但是整个copy的过程中,进程还是处于阻塞状态。

  对于单个IO请求,这样的做法其实并没有多大优势,甚至不如阻塞IO。不过多路复用的好处在于多路,即可以同时监听多个socket描述符,当大量的文件描述符可读可写事件发生的时候,更有利于服务器的并发性能。

多路复用的I/O本质就是多路监听 + 阻塞/非阻塞IO。多路监听即select、poll、epoll这些系统调用。后半部分才是真正的IO。红色的线即是前文所叙述的阻塞IO或者非阻塞IO。

select、poll、epoll更多的时候是配合非阻塞的方式使用。如下:

  一般多路复用IO都是配合非阻塞IO使用。因为读写socket的时候,并不确定读到什么时候才能读完。在一个循环里读,如果设置为阻塞模式,那么进程将会被挂起。比较好的做法是设置成非阻塞。

多路复用IO几乎成为了主流server方式。尤其是epoll,成为了nginx、redis,tornado等软件高性能的基石。

select模型的原理

  网络通信过程在Unix系统中通常被抽象为文件的读写过程。select模型中的一个socket文件描述符通常可以看成一个由设备驱动程序管理的一个设备,驱动程序可以知道自身的数据是否可用。同时,该设备支持阻塞操作并实现了一组自身的等待队列,如读/写等待队列用户支持上层(用户层)所需的block(阻塞)和non-block(非阻塞)操作。设备的资源如果可用(可读/可写)则会通知应用进程。反之则会让进程睡眠,等待数据到来的时候,再唤醒应用进程。

  多个这样的设备的文件描述符被放在一个队列中,然后select调用的时候遍历这个队列,如果对应的文件描述符可读/可写则会返回该文件描述符(调用应用进程的回调事件)。当遍历结束之后,如果仍然没有一个可用的文件描述符,select会让用户进程睡眠,直到等待资源可用的时候再唤醒用户进程并返回对应的文件描述符(调用应用进程的回调事件),select每次遍历都是线性的。

select模型的不足

  尽管select模型使用很便利,且具有跨平台的特性。但是select模型还是存在一些问题。select模型需要遍队列中的文件描述符,并且这个队列还有最大限制(64)。随着文件描述数量的增长,用户态和内核的地址空间的复制引发的开销也会线性增长。即使监视的文件描述符长时间不活跃,select模型还是会进行线性扫描它。

  为了解决这些问题,操作系统又提供了poll方案,但是poll的模型和select大致相当,只是改变了一些限制。目前Linux最先进的方式是epoll模型。

select模型详情链接:https://blog.csdn.net/qq135595696/article/details/121549469

信号I/O(SIGIO)

  让内核在文件描述符就绪时发送SIGIO信号通知进程。这种模型为信号驱动式I/O(signal-driven I/O),和事件驱动类似,也是一种回调方式。与非阻塞方式不一样的是,发起了信号驱动的系统调用,进程没有挂起,可以做其他事情。可在实际中,代码逻辑通常还是主循环,主循环里可能还是会阻塞。因此用这类IO的软件很少。

  当信号返回可以读写的时候,因为还需要CPU将内核数据copy到应用buf。这个过程毫无疑问还是阻塞的。

异步I/O(asynchronous)

  前面一直强调,内核在copy数据从内核到应用buf的过程中,CPU需要参与,进程都会被阻塞。因此可以理解,进程和内核的步调是一致的,也就是同步。这样的IO模型称之为同步I/O。那么什么是异步I/O呢?

Unix下的异步I/O模型如下;

图中的I/O调用函数的红线只出现在第一步中。

  即无论是第一阶段数据准备还是第二阶段数据拷贝过程,发起系统调用的进程都不会被阻塞。在第二阶段的过程中,进程没有阻塞,那么可以抢占CPU,而内核copy数据的时候,也需要CPU,这就造成了应用进程和内核进行CPU竞争,并且步调不一致了。某种情况下,其性能反而不如其它IO模式,使用的人很少。

简要概述网络I/O与并发相关推荐

  1. 「翻译」SAP零售预测和补货–简要概述

    SAP零售预测和补货–简要概述 总体目标和适用范围 SAP Forecasting and Replenishment(F&R)是专门为零售商门店和配送中心的高效补货而创建的解决方案.其主要目 ...

  2. drill apache_Apache Drill 1.4性能增强的简要概述

    drill apache 今天,我们很高兴地宣布,MapR发行版中现已提供Apache Drill 1.4. 钻1.4是MAPR生产就绪和支持的版本,可以从下载这里 ,找到1.4版本说明这里 . Dr ...

  3. Apache Drill 1.4性能增强的简要概述

    今天,我们很高兴宣布Apache Drill 1.4现已在MapR发行版中可用. 钻1.4是MAPR生产就绪和支持的版本,可以从下载这里 ,找到1.4版本说明这里 . Drill 1.4以其高度灵活和 ...

  4. jsp 内置的对象的简要概述(转)

    JSP 内置对象简要概述 (1) HttpServletRequest 类的 Request 对象 作用:代表请求对象,主要用于接受客户端通过 HTTP 协议连接传输到服务器端的数据. (2) Htt ...

  5. 记住密码 的 简要概述_密码错误的简要历史

    记住密码 的 简要概述 IT强制的密码策略似乎是一个好主意-毕竟,攻击者有机会在八字符字符串(至少包含一个大写字母,一个低个字母)中的7.82亿个潜在组合中猜出您的确切密码字母,两个数字和一个符号? ...

  6. MOOS-ivp 实验十四(2)behavior简要概述

    MOOS-ivp 实验十四(2)behavior简要概述 继续上一章的内容,对行为使命进行进一步的介绍和参数配置工作. 文章目录 MOOS-ivp 实验十四(2)behavior简要概述 前言 一.简 ...

  7. HashMap的实现原理(简要概述)

    HashMap的实现原理(简要概述) 基于哈希算法实现的,它通过put存储,通过get取值. 当传入一个key时,HashMap会根据key.hashCode()计算出哈希值,然后根据这个哈希值将va ...

  8. SpringMVC简要概述

    SpringMVC简要概述 SpringMVC简要概述 SpringMVC简要概述 一.MVC 1.MVC简介 2.MVC三层架构分析 3.MVC框架使用机制 二.SpringMVC 1.Spring ...

  9. Spring 核心方法 refresh 刷新流程简要概述及相关源码扩展实现(二)

    前言 registerBeanPostProcessors initMessageSource 如何实际应用国际化处理 initApplicationEventMulticaster onRefres ...

  10. 数据治理从理论到实战系列(一)——简要概述

    数据治理从理论到实战系列 数据治理(一):简要概述 数据治理从理论到实战系列 一.为什么要进行数据治理? 二.什么是数据治理? 三.如何解决上述问题? 三.数据治理的体系 四.数据治理平台建设 跳转连 ...

最新文章

  1. c#获取对象的唯一标识_在 Java 中利用 redis 实现分布式全局唯一标识服务
  2. Python TimedRotatingFileHandler 多进程环境下的问题和解决方法
  3. Interview:算法岗位面试—11.19早上上海某银行(总行,四大行之一)信息技术岗面试记录
  4. 一款功能强大,可扩展端到端加密反向Shell的工具
  5. Linux 使用本地yum源及软件包管理
  6. HTML5 Canvas和EaselJS入门(译)
  7. 五年php面试题,找工作的你不容错过的45个PHP面试题附答案(下篇)
  8. Linux 2.6中基于Sysenter的系统调用机制
  9. UVA - 207 PGA Tour Prize Money
  10. 判断当前js运行的平台环境 取自vue源码
  11. UVa834 Continued Fractions【连分数】
  12. 2018年春季软件工程教学设计(初稿)
  13. 统计学习方法读书笔记13-改进的迭代尺度法(优化算法)
  14. linux tcp_nodelay,仔细看参数--NGINX之tcp_nodelay
  15. SVN之版本管理系统安装及svnadmin编码问题-yellowcong
  16. 苹果手机描述文件服务器地址是什么,iPhone|iOS设备描述文件扫盲是什么?有什么用?...
  17. linux主机独立显卡切换,linux双显卡怎么切换到独立显卡
  18. 隐藏APP图标并通过代码启动
  19. 数绵羊(矩阵快速幂)
  20. 报错:SyntaxError: Unexpected token u in JSON at position 0 at JSON.parse (<anonymous>)

热门文章

  1. 20200318_抓取51job招聘数据存数据库
  2. 金融评分卡项目—1.数据分析基础知识
  3. 内置函数——hasattr() 函数
  4. LeetCode学习记录(7-9)
  5. 混沌工程:Netflix系统稳定性之道
  6. 从你王者荣耀爱玩的英雄类型,我就知道你关注哪些技术领域!
  7. 系统架构与软件架构是一层含义吗
  8. Python输入输出详解
  9. 怎么隐藏li标签_抖音账号如何打标签-7天让抖音账号打上标签
  10. 人工智能python3+tensorflow人脸识别_Tensorflow+opencv2实现人脸识别