网络模型与 IO 多路复用

news2025/1/23 17:27:33

  • 一、基础概念
    • 1. socket
    • 2. FD:file descriptor**
    • 3. 内核态和用户态
  • 二、 IO 多路复用
    • 1. 常见的IO模型
    • 2. 同步和异步
    • 3. 阻塞和非阻塞
  • 三、 阻塞IO
  • 四、非阻塞 IO
    • 1、针对 read 函数造成的阻塞
    • 2、针对 accept函数造成的阻塞
    • 3、 select 模型
    • 4、poll模型
    • 5、epoll模型

一、基础概念

1. socket

  socket也称作“套接字”,用于描述IP地址和端口,是一个通信链路的描述符。应用程序通常通过“套接字”向对端发出请求或者应答网络请求。

  socket是连接运行在网络上的两个程序之间的通信端点。通信的两端都有socket,它是一个通道,数据在两个socket之间进行传输。socket把复杂的TCP/IP协议族或者UDP/IP协议族隐藏在socket接口后面,对程序员来说,只要用好socket相关的函数,就可以完成数据通信。

在这里插入图片描述

2. FD:file descriptor**

​   Linux 系统中,把一切都看做是文件(一切皆文件),当进程打开现有文件或创建新文件时,内核向进程 返回一个文件描述符,文件描述符就是内核为了高效管理已被打开的文件所创建的索引,用来指向被打开的文 件,所有执行I/O操作的系统调用都会通过文件描述符。

​   一个进程能够同时打开多个文件,对应需要多个文件描述符,所以需要用一个文件描述符表对文件描述符 进行管理;通常默认大小为1024,也即能容纳1024个文件描述符;

​   在 soket 通信中,当我们调用内核函数创建 socket 后,内核返回给我们的是 socket 对应的文件描述符( fd),所以我们对 socket 的操作基本都是通过 fd 来进行。这个文件描述符 fd 可能是就绪的状态即大于 1 的整 数,也可能是未就绪的状态-1。就绪状态表示客户端或服务端发送的全部数据通过网卡进入到内核缓冲区buf, 这时调用 系统read( )就可以将数据从内核区拷贝到用户缓冲区。

  见阻塞 IO 图。

3. 内核态和用户态

​   线程是操作系统调度CPU的最小单元,在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、 堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些 线程在同时执行。线程的实现可以分为两类:用户级线程,内核线线程。(java线程就是内核级线程)

​   虚拟内存被操作系统划分成两块:内核空间和用户空间,内核空间是内核代码运行的地方,用户空间是用 户程序代码运行的地方。当进程运行在内核空间时就处于内核态,当进程运行在用户空间时就处于用户态,为 了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。说起这个概念就是因为线程上下文切换的 概念。虽然线程上下文切换比进程切换成本要低但是,线程切换也是很影响性能的。线程上下文切换就涉及用 户态到内核态的转换。

二、 IO 多路复用

  服务器端编程经常需要构造高性能的IO模型,

1. 常见的IO模型

  • 同步阻塞IO(Blocking IO):即传统的IO模型。

  • 同步非阻塞IO(Non-blocking IO):默认创建的socket都是阻塞的,非阻塞IO要求socket被设置为NONBLOCK。

  • IO多路复用(IO Multiplexing):即经典的Reactor模式(并非23种设计模式之一),有时也称为异步阻塞IO,Java中的Selector和Linux中的epoll都是这种模型。

​  Reactor模式称为反应器模式或应答者模式,是基于事件驱动的设计模式,拥有一个或多个并发输入 源,有一个服务处理器和多个请求处理器,服务处理器会同步的将输入的请求事件以多路复用的方式分发 给相应的请求处理器。 Reactor设计模式是一种为处理并发服务请求,并将请求提交到一个或多个服务处 理程序的事件设计模式。

  • 异步IO(Asynchronous IO):即经典的Proactor设计模式,也称为异步非阻塞IO。

2. 同步和异步

  同步和异步的概念描述的是用户线程与内核的交互方式,主体是线程:

​   同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作完成后才能继续执行;

​   而异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后通知用户线程,或者调用用户线程注册的回调函数。

​   同步异步的主体是用户线程。

3. 阻塞和非阻塞

   阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式,主体是IO:

​   阻塞是指IO操作需要彻底完成后才返回到用户空间;

​   而非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

​   阻塞非阻塞的主体是IO。

三、 阻塞IO

  为了方便理解,以下所有代码都是伪代码,知道其表达的意思即可。

  服务端为了处理客户端的连接和请求的数据,写了如下代码。

服务端												 客户端
listenfd = socket();   打开一个网络通信端口
bind(listenfd);        绑定
listen(listenfd);      监听
while(1) { 											 fd = socket();
  connfd = accept(listenfd);   阻塞建立连接			 connect(fd);
          											 write(fd,buf);
  int n = read(connfd, buf);  阻塞读数据               closed(fd);
  doSomeThing(buf);  利用读到的数据做些什么
  close(connfd);     关闭连接,循环等待下一个连接
}

  服务端的线程阻塞在了两个地方,一个是 accept 函数,一个是 read 函数。

  read函数包括两个阶段:
在这里插入图片描述

  这就是传统的阻塞 IO。如果这个连接的客户端一直不发数据,那么服务端线程将会一直阻塞在 read 函数上不返回,也无法接受其他客户端连接。

四、非阻塞 IO

  为了解决上面的问题,我们需要对 accept函数和 read 函数进行改造。

1、针对 read 函数造成的阻塞

​   这个 read 函数的效果是,如果没有数据到达内核缓冲区时,即第一阶段未完成,立刻返回一个错误值-1,而不是阻塞地等待。

​   操作系统提供了这样的功能,只需要在调用 read 前,将文件描述符设置为非阻塞即可。

服务端																						       
listenfd = socket();   			       打开一个网络通信端口
bind(listenfd);        				   绑定
listen(listenfd);      				   监听
while(1) { 																						
  connfd = accept(listenfd);   	       阻塞建立连接		  
  fcntl(connfd, F_SETFL, O_NONBLOCK);  将文件描述符设置为非阻塞										
  int n = read(connfd, buffer); 	   如果 fd 未就绪,调用 read 会立即返回-1,处理下																					 一个连接
  doSomeThing(buf);  										
  close(connfd);     											
}

  如果 fd 未就绪,调用 read 会立即返回-1,如果就绪,就会阻塞式的 read。

2、针对 accept函数造成的阻塞

​   有一种聪明的办法是,每次都创建一个新的进程或线程,去调用 read 函数,并做业务处理。

while(1) { 
  connfd = accept(listenfd);  // 阻塞建立连接
  pthread_create(doWork);  // 创建一个新的线程
}
void doWork() { 
  int n = read(connfd, buf);  // 阻塞读数据
  doSomeThing(buf);  // 利用读到的数据做些什么
  close(connfd);     // 关闭连接,循环等待下一个连接
}

  这样,当给一个客户端建立好连接后,就可以立刻等待新的客户端连接,而不用阻塞在原客户端的 read 请求上。不过,这不叫非阻塞 IO,只不过用了多线程的手段使得主线程没有卡在 read 函数上不往下走罢了。操作系统为我们提供的 read 函数仍然是阻塞的。

  为每个客户端创建一个线程,服务器端的线程资源很容易被耗光。

  当然还有个聪明的办法,我们可以每 accept 一个客户端连接后,将这个文件描述符(connfd)放到一个数组里。然后弄一个新的线程去不断遍历这个数组,调用每一个元素的非阻塞 read 方法。

服务端																						       
listenfd = socket();   						打开一个网络通信端口
bind(listenfd);        					    绑定
listen(listenfd);      						监听
fdlist; 

while(1) { 																						
  connfd = accept(listenfd);   			    阻塞建立连接		  
  fcntl(connfd, F_SETFL, O_NONBLOCK);    	将文件描述符设置为非阻塞
  fdlist.add(connfd);   											
}

新线程去处理
while(1) { 																						
  for(fd <-- fdlist) { 
    if(read(fd) != -1) { 
      doSomeThing();
    }
    close(fd);
    //移除此 fd
  }     											
}

  这样,我们就成功用一个线程处理了多个客户端连接。

​   但这和我们用多线程去将阻塞 IO 改造,看起来是 一样的,这种遍历方式也是我们用户层的小把戏,每次遍 历遇到 read 返回 -1 时仍然是一次浪费资源的系统调用。

  使用 while 循环不断地做系统调用,是不合理的。每次只传给 read 函数一个文件描述符,传一次调用一次。

  我们每次传给 read 函数一批文件描述符到内核,由内核层去遍历,这个问题才能真正解决。

3、 select 模型

​   select 是操作系统提供的系统调用函数,通过它,我们可以把一个文件描述符的数组发给操作系统, 让 操作系统去遍历,确定哪个文件描述符可以读写, 然后告诉我们去处理。不过,当 select 函数返回后,用 户依然需要遍历刚刚提交给操作系统的 list。只不过,操作系统会将准备就绪的文件描述符做上标识,用户 层将不会再有无意义的系统调用开销。

在这里插入图片描述

服务端																						       
listenfd = socket();   						打开一个网络通信端口
bind(listenfd);        						绑定
listen(listenfd);      						监听
fdlist;

首先一个线程不断接受客户端连接,并把 socket 文件描述符放到一个 list 里。
while(1) { 																						
  connfd = accept(listenfd);   			    阻塞建立连接		  
  fcntl(connfd, F_SETFL, O_NONBLOCK);    	将文件描述符设置为非阻塞
  fdlist.add(connfd);   											
}

while(1) { 
  // 把一堆文件描述符 list 传给 select 函数
  // 有已就绪的文件描述符就返回,nready 表示有多少个就绪的
  nready = select(list);
    // 用户层依然要遍历,只不过少了很多无效的系统调用
  for(fd <-- fdlist) { 
    if(fd != -1) { 
      // 只读已就绪的文件描述符
      read(fd, buf);
      // 总共只有 nready 个已就绪描述符,不用过多遍历
      if(--nready == 0) 
        break;
    }
  }
}

  存在问题:

  • select 调用需要传入 fd 数组,需要拷贝一份到内核,高并发场景下这样的拷贝消耗的资源是惊人的。(可优化为不复制)

  • select 在内核层仍然是通过遍历的方式检查文件描述符的就绪状态,是个同步过程,只不过无系统调用切换上下文的开销。(内核层可优化为异步事件通知)

  • select 仅仅返回可读文件描述符的个数,具体哪个可读还是要用户自己遍历。(可优化为只返回给用户就绪的文件描述符,无需用户做无效的遍历)

4、poll模型

​   它和 select 的主要区别就是,去掉了 select 只能监听 1024 个文件描述符的限制。

5、epoll模型

​   针对select 模型的三个问题进行了改进。

  • ​ 内核中保存一份文件描述符集合,无需用户每次都重新传入,只需告诉内核修改的部分即可。

  • ​ 内核不再通过轮询的方式找到就绪的文件描述符,而是通过异步 IO 事件唤醒。

  • ​ 内核仅会将有 IO 事件的文件描述符返回给用户,用户也无需遍历整个文件描述符集合。

​   具体,操作系统提供了这三个函数。

//第一步,创建一个 epoll 句柄
int epoll_create(int size);
//第二步,向内核添加、修改或删除要监控的文件描述符。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//第三步,类似发起了 select() 调用
int epoll_wait(int epfd, struct epoll_event *events, int max events, int timeout);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

参考文章https://www.dandelioncloud.cn/article/details/1615702819904651265

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/468479.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

点成分享丨液滴生成解决方案

电源及电线为了满足用户的液滴生成需求&#xff0c;点成生物联合cellix推出了点成生物液滴生成解决方案&#xff0c;该套装可以一站式轻松满足用户液滴生成实验的需求。 Part.01 点成小科普 液滴生成&#xff0c;是一种在具备某种几何形状的微流控芯片中使用微流控泵精确控制…

Drools 规则引擎(包括动态加载数据库中规则引擎)

Drools 规则引擎 文章目录 Drools 规则引擎前言一、规则引擎是什么&#xff1f;二、Drools 简介1.引入规则引擎前后程序架构&#xff1a;2.Drools API 开发步骤&#xff1a;在这里插入图片描述 三、Drools 快速入门1.使用项目文件作为规则引擎2.使用数据库存储规则引擎 四、Dro…

【shell脚本】条件语句

一、条件测试操作 1.1test命令与 [ ] 符号 测试表达试是否成立&#xff0c;若成立返回0&#xff0c;否则返回其它数值 1.1.1文件测试常用的测试操作符 符号作用-d测试是否为目录-e测试是否为目录或文件-f测试是否为文件-r测试当前用户是否有读取权限-w测试当前用户是否有写…

2023-4-27-深入理解C++指针类型间强制转换

&#x1f37f;*★,*:.☆(&#xffe3;▽&#xffe3;)/$:*.★* &#x1f37f; &#x1f4a5;&#x1f4a5;&#x1f4a5;欢迎来到&#x1f91e;汤姆&#x1f91e;的csdn博文&#x1f4a5;&#x1f4a5;&#x1f4a5; &#x1f49f;&#x1f49f;喜欢的朋友可以关注一下&#xf…

【致敬未来的攻城狮计划】— 连续打卡第十四天:FSP固件库开发延时函数(时钟详解)

系列文章目录 1.连续打卡第一天&#xff1a;提前对CPK_RA2E1是瑞萨RA系列开发板的初体验&#xff0c;了解一下 2.开发环境的选择和调试&#xff08;从零开始&#xff0c;加油&#xff09; 3.欲速则不达&#xff0c;今天是对RA2E1 基础知识的补充学习。 4.e2 studio 使用教程 5.…

数据结构_线性表、顺序表、单链表

目录 1. 线性表的定义和基本操作 1.1 线性表的定义 1.2 线性表的特点 1.3 线性表的基本操作 2. 线性表的顺序表示 2.1 顺序表的定义 2.2 顺序表上基本操作的实现 2.2.1 插入操作 2.2.2 删除操作 2.2.3 按值查找 2.3 相关练习巩固 3. 线性表的链式表示 3.1 单链表的…

Zynq-7000、FMQL45T900的GPIO控制(五)---linux应用层配置GPIO输出控制

上文中详细阐述了对应原理图MIO/EMIO的编号&#xff0c;怎么计算获取linux下gpio的编号 本文涉及C代码上传&#xff0c;下载地址 Zynq-7000、FMQL45T900的GPIO控制c语言代码资源-CSDN文库 本文详细记录一下针对获取到gpio的编号&#xff0c;进行配置输出模式&#xff0c;并进…

RobotFramework——创建项目

创建项目 实际上在Robot Framework中创建测试项目和创建单元测试项目是一致的。 robotunittest(python)Test ProjectTest ProjectTest SuitTest SuitTest CaseTest Case 1、创建测试项目 选择菜单栏 file —> New Project Name输入"TestRobot01"&#xff1b;…

从奈奎斯特采样定理推导FMCW雷达系统性能参数

本文编辑&#xff1a;调皮哥的小助理 上文从FMCW毫米波雷达系统的性能参数理解4D成像毫米波雷达的设计思路&#xff0c;谈到关于设计4D毫米波成像雷达的思路&#xff0c;其实我还忽略了一点&#xff0c;在这里补充说明一下。 在文中谈到的最小化公式中&#xff0c;分母的有效带…

vscode - 配置Prettier插件和.editorconfig文件使用介绍

文章目录 1&#xff0c;介绍2&#xff0c;Prettier插件安装3&#xff0c;全局配置&#xff08;settings.json&#xff09;4&#xff0c;局部配置&#xff08;新建.prettierrc文件&#xff09;5&#xff0c;Prettier格式化优先级&#xff08;重要&#xff09;6&#xff0c;使用p…

【教程分享】一键部署MySQL,轻松搞定Docker安装和配置!

1 下载 MySQL 我们就可以到 docker hub 来看&#xff1a; 点击后的页面&#xff1a; 直接执行docker pull mysql&#xff0c;会下载最新版本的 MySQL。 点击 tags&#xff0c;找到并下载经典的 MySQL5.7&#xff1a; [rootservice-monitoring ~]# docker pull mysql:5.7.42-o…

Speech and Language Processing-之N-gram语言模型

正如一句老话所说&#xff0c;预测是困难的&#xff0c;尤其是预测未来。但是&#xff0c;如何预测一些看起来容易得多的事情&#xff0c;比如某人接下来要说的几句话后面可能跟着哪个单词。 希望你们大多数人都能总结出一个很可能的词是in&#xff0c;或者可能是over&#x…

【hello Linux】进程间通信——命名管道

目录 1. 命令行的命名管道 2. 命名管道 1. 命名管道的创建 2. 命名管道的使用 Linux&#x1f337; 在上篇中我们说到&#xff0c;可以使用匿名管道完成父子进程间的通信&#xff0c;它是让子进程继承父进程&#xff0c;从而达到让两个进程看到同一份资源&#xff1b; 如果我们…

急急急!Kafka Topic 资源权限紧张怎么办?

我们都知道 Kafka 的 topic 资源比较“贵”&#xff0c;所以一般会给项目 topic 权限限制&#xff0c;按需申请。Milvus 会在建新表时自动申请 kafka topic 资源&#xff0c;这时候自动申请不到怎么办&#xff1f;手动配置 topic 要符合什么规范才能被 Milvus 使用&#xff1f;…

Vue 3 第十六章:组件五(内置组件-teleport)

文章目录 1. Teleport组件的基本用法2. Teleport组件的高级用法2.1. 禁用 Teleport2.2. 多个Teleport组件共享目标元素 1. Teleport组件的基本用法 <teleport>组件用于将组件的内容插入到指定的DOM元素中&#xff0c;而不是插入到组件自身的位置。例如&#xff0c;当我们…

improperIntegral反常积分

(https://img-blog.csdnimg.cn/e5973004aba8484a82839773ff58a390.png)

【刷题记录】leetcode215 数组中的第K个最大元素

题目链接&#xff1a;215. 数组中的第K个最大元素 - 力扣&#xff08;LeetCode&#xff09; 题干&#xff1a; 给定整数数组 nums 和整数 k&#xff0c;请返回数组中第 k 个最大的元素。 请注意&#xff0c;你需要找的是数组排序后的第 k 个最大的元素&#xff0c;而不是第 k …

cleanmymac到底好不好用?有必要买吗

CleanMyMac是一款Mac电脑清理和保护工具,其最新版本为CleanMyMac X4.13。本版本在性能和功能上有较大提高,在如今的电脑使用过程中&#xff0c;保持电脑干净整洁是一项重要的任务。而随着Mac电脑越来越受欢迎&#xff0c;Mac电脑清理软件也愈发流行。在众多的Mac电脑清理软件中…

2023年PMP证书的含金量有多高?对于企业来说有多大的价值?

这里我就说一下关于PMP证书的含金量问题 1、方便就业 众所周知年这几年就业形势严峻。但是在这种大环境下&#xff0c;PMP证书持有者就业形势依然乐观。在求职市场&#xff0c;职位需求量大且薪资可观。 这种局面的形成主要是因为企项目管理规模化发展是一种发展趋势。随着公…

【error】linux运行java的jar包报错,java.lang.UnsatisfiedLinkError

目录 linux运行串口相关的java.jar报错如下&#xff1a; java.lang.UnsatisfiedLinkError是Java中的一个错误类型&#xff0c;通常发生在调用本地&#xff08;native&#xff09;方法或使用JNI&#xff08;Java Native Interface&#xff09;时。 在Java中&#xff0c;本地…