I/O 多路复用小结

news2024/11/18 22:44:26

Socket 模型

Socket 编程是一种使用 Socket 模型进行网络通信的编程技术。它是一种基于网络套接字的编程模型,用于实现不同计算机之间的数据传输。
事实上,在进行网络通信前,通信双方都要创建一个 Socket,双方的数据读写都要依赖于此。创建 Socket 时,可以指定网络层使用 IPV4 或者 IPV6,传输层使用 TCP 或者 UDP。

基于 TCP 的 Socket 编程

服务端的程序要先跑起来,监听等待客户端的连接和数据。服务端首先调用 socket() 函数创建一个网络协议为 IPV4、传输协议为 TCP 的 Socket,然后调用 bind() 函数绑定一个 IP 地址和端口号。

  • 绑定 IP 地址:一台机器可以有多个网卡,每个网卡都有对应的 IP 地址,当绑定其中一个网卡时,操作系统内核收到该网卡上的包才会发给我们;
  • 绑定端口号:当操作系统内核收到 TCP 报文时,需要通过 TCP 头部里的端口号来找到对应的应用程序,然后才能最终把数据传输给我们

服务端在绑定完 IP 地址和端口号之后,接着调用 listen() 函数进行监听(此时对应 TCP 状态图中的 listen),然后再调用 accept() 函数从内核中获取客户端的连接,如果此时没有客户端的连接,则会进入阻塞状态,直到客户端连接的到来。
同理,客户端在创建好 Socket 之后,会调用 connect() 函数发起连接,在该函数的参数中指明了服务端的 IP 地址和端口号,然后就是进行 TCP 三次握手。
在 TCP 连接过程中,服务器的内核实际上会为每个 Socket 都维护两个队列:

  • TCP 半连接队列:还未完全建立连接的队列,这个队列里的都是没有完成三次握手的连接,此时服务端处于 syn_rcvd 状态;
  • TCP 全连接队列:已经建立连接的队列,这个队列里的都是完成了三次握手的连接,此时服务端处于 established 状态

当 TCP 全连接队列不为空时,服务端调用 accept() 函数,从内核中的 TCP 全连接队列里拿出一个已经完成三次握手的连接返回给应用程序,后续的数据传输都是通过这个 Socket。
需要注意的是,此时服务端的监听 Socket 和客户端的已连接 Socket 是两个 Socket。连接建立以后,客户端和服务端就可以通过 read() 和 write() 函数相互传输读写数据了。至此, 基于 TCP 协议的 Socket 程序的调用过程就结束了。

在这里插入图片描述

C10K 问题

C10K 问题是指支持同时处理 10000 个并发连接的服务器的挑战。C 代表 Concurrent(并发),10K 代表 10000,因此 C10K 表示服务器需要同时处理大量的并发连接。
在传统的服务器设计中,由于每个连接都需要一个操作系统级别的进程或线程来处理,而进程和线程的创建和销毁的开销较大,因此服务器往往无法同时处理大量的并发连接。这导致了 C10K 问题的出现。
为了解决 C10K 问题,有几种常见的策略和技术:

  • 多进程/多线程模型:通过创建多个进程或线程来处理并发连接,但由于创建和销毁的开销较大,不适用于处理大规模并发连接;
  • 事件驱动模型:使用事件循环和非阻塞 I/O 来处理并发连接,只使用少量的线程或进程,并通过事件通知机制处理请求和响应;
  • 异步 I/O 模型:通过使用异步 I/O 操作和回调函数来处理并发连接,可以高效地处理大量的并发请求;
  • 消息队列模型:将请求和响应分发到消息队列中,由多个处理节点并行处理请求和响应,提高并发性能

我们知道 TCP 连接是由四个值组成的四元组唯一确认的,这四个值分别是「源 IP 地址」、「源端口号」、「目标 IP 地址」、「目标端口号」。
作为服务方,服务端通常会在本地固定监听一个端口,等待客户端的连接。因此服务端的本地 IP 和端口是固定的,于是对于服务端 TCP 连接的四元组而言,只有目标 IP 地址和目标端口号是会变化的,所以服务端的最大 TCP 连接数 = 客户端 IP 数 × 客户端端口数。
以 IPv4 举例,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,所以服务端单机的最大 TCP 连接数约为 2 的 48 次方。
但实际上服务端肯定承载不了那么大的连接数,其主要受两个方面的限制:

  • 文件描述符:基于 Linux 一切皆文件的理念,在内核中 Socket 实际上也是一个文件,也对应一个文件描述符。在 Linux 下,单个进程打开的文件描述符数是有限制的,默认是 1024,可以通过 ulimit 增大文件描述符的数目;
  • 系统内存:每个 TCP 连接在内核中都有对应的数据结构,这意味着每个连接都是会占用一定的内存

多进程模型

如上所述的最基本的 Socket 通信,只能用于一对一的场景,因为其使用的是同步阻塞的方式,当服务端还没处理完某个客户端的网络 I/O 时,或者读写操作发生阻塞时,其他客户端是无法与服务端连接的。
如果服务端要支持多个客户端,可以使用多进程模型,即为每一个客户端都分配一个进程来处理请求。
服务端的主进程负责监听客户端的连接,一旦与客户端连接完成,服务端调用 accept() 函数返回一个已连接的 Socket,此时通过 fork() 函数创建一个子进程,实际上就是把父进程的所有相关的东西都复制出一份(包括文件描述符、内存地址空间、程序计数器、执行的代码等)。
子进程刚复制完的时候,几乎与父进程是一模一样的。不过,可以根据返回值来区分到底是父进程还是子进程,如果返回值是 0,则表示是子进程;如果返回值是其他的整数,就是父进程。
而正因为子进程会复制父进程的文件描述符,所以就可以直接使用已连接的 Socket 和客户端进行通信。
综上所述,子进程不需要关心「监听 Socket」,只需要关心「已连接的 Socket」;而父进程则恰恰相反,它将对客户端的服务交给子进程来处理,因此父进程不需要关心「已连接的 Socket」,只需要关心「监听 Socket」。

在这里插入图片描述

需要注意的是,当子进程退出时,内核里还会保留该进程的一些信息,这部分也是会占用内存的,如果没做好回收工作,那么这部分就会变成僵尸进程,而随着僵尸进程的增多,会慢慢耗尽系统资源。
因此,父进程要在子进程退出后妥善地回收子进程的资源:

  • wait() 函数:暂停父进程的执行,直到任何一个子进程退出,并返回子进程的退出状态。通过调用 wait()函数,父进程可以获取子进程的退出状态,并完成资源的回收;
  • waitpid() 函数:提供了更灵活的选项来等待指定子进程的退出,并回收其资源。通过传递特定的子进程 ID 或使用其他选项,父进程可以指定要等待的子进程

多进程模型,在应对 100 个客户端时还是可行的,但是当客户端的数量高达一万时肯定扛不住的,因为每产生一个进程,势必会占据一定的系统资源,而且进程间的上下文切换的开销是很大的,性能会大打折扣。

多线程模型

既然进程的创建和销毁的开销很大,那么是否可以考虑使用多线程模型呢?
我们知道,一个进程里可以包含多个线程,同一个进程内的线程共享该进程的部分资源,如文件描述符列表、进程空间、代码、全局数据、堆等,这些共享资源在上下文切换时不需要进行切换,线程只需要切换自己的非共享资源即可,如私有数据、寄存器等,因此同一个进程内的线程的上下文切换的开销要比进程小得多。这也是线程比进程更轻量级的一个原因。
当服务端与客户端完成 TCP 连接后,通过 pthread_create() 函数创建线程,然后将已连接的 Socket 的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。
但是,每来一个连接,就需要创建一个线程,而且线程运行完后,操作系统还得销毁线程。虽说线程上下文切换的开销并不大,但如果是频繁地创建和销毁,那么系统开销也是不小的。
因此,我们可以使用线程池的方式来避免线程的频繁创建和销毁。所谓线程池,就是提前创建好若干个线程,这样每当有新连接建立时,将这个已连接的 Socket 放入到一个队列里,然后由线程池里的线程负责从队列中取出该 Socket 并进行处理。

在这里插入图片描述

注意,这个队列是全局的,每个线程都会操作,所以为了避免多线程竞争,线程在操作这个队列前要加锁。
多线程模型其实还是有问题的,因为每新来一个 TCP 连接,就需要为其分配一个进程或者线程,那么如果要达到 C10K,就意味着一台机器要维护 1 万个连接,相当于要维护 1 万个进程/线程,操作系统还是扛不住的。

I/O 多路复用

一个进程虽然一次只能处理一个请求,但我们可以将每次处理请求的时间缩短为 1ms,这样在 1s 内就可以支持处理大约 1k 个请求,从宏观角度看就相当于多个请求复用了一个进程,即多路复用,这种思想类似于 CPU 并发支持多个进程,所以也称为时分多路复用。
常见的 I/O 多路复用系统调用函数有 select、poll 和 epoll。这些函数会阻塞等待,直到有一个或多个文件描述符就绪(可读或可写)或超时。一旦有文件描述符就绪,进程就可以立即对其进行读写操作,而不必等待其他文件描述符。
简单来说,就是在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接所对应的请求。

  • select

select 是最古老的 I/O 多路复用函数,适用于大多数 UNIX 系统。它通过在一个或多个文件描述符上进行轮询来检查可读、可写和异常事件的状态。select 函数会阻塞等待,直到有文件描述符就绪或超时。
具体实现多路复用的方式是:将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,内核检查的方式很粗暴,即遍历文件描述符集合,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历找到可读或可写的 Socket,最后再对其处理。
所以,对于 select,需要进行 2 次「遍历」文件描述符集合(一次是在内核态里,一次是在用户态里),而且还会发生 2 次「拷贝」文件描述符集合(先从用户态传入内核态,由内核修改后,再传回用户态中)。
此外,select 有一些限制,比如监视的文件描述符数量有限。在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认为 1024,所以只能监听 0~1023 的文件描述符。

  • poll

poll 在 select 的基础上进行了改进,适用于大多数 UNIX 系统。与 select 相比,poll 使用了一个 pollfd 结构数组来描述要监视的文件描述符及其事件,减少了每次调用的参数复制开销。
poll 函数也会阻塞等待,直到有文件描述符就绪或超时。但是与 select 相比,poll 在处理大量文件描述符时性能更好。
但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此它们都需要遍历文件描述符集合来找到可读或可写的 Socket,其时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,随着并发数的增加,性能的损耗会呈指数级增长。

  • epoll

epoll 是 Linux 特有的 I/O 多路复用机制,它提供了更高效的事件驱动模型。
epoll 使用了一个事件表来存储要监视的文件描述符和事件,通过注册事件来提供更高的性能。
epoll 提供了三个接口:epoll_create 用于创建一个事件表,epoll_ctl 用于注册和取消注册事件,epoll_wait 用于等待事件发生。epoll_wait 函数是非阻塞的,它只返回已发生的事件,而不需要遍历整个事件表。这使得 epoll 非常适合处理大量并发连接。
epoll 通过两个方面,很好地解决了 select/poll 的问题:

  • epoll 在内核里使用了红黑树数据结构来存储和跟踪要监视的文件描述符(Socket),把需要监控的 Socket 通过 epoll_ctl() 函数加入到内核中的红黑树里。红黑树是一种高效的平衡二叉查找树,在增删改的平均时间复杂度上具有较好的性能(O(log n))。相比之下,select和poll每次操作时需要将整个Socket集合传入内核,而不具备像epoll红黑树那样高效地管理已注册的Socket;
  • epoll 采用事件驱动机制。在内核中,它维护了一个就绪事件列表(ready-list),当某个 Socket 有事件发生时,内核会将该 Socket 加入到就绪事件列表中。而用户调用 epoll_wait 函数时,只会返回处于就绪状态的文件描述符,而不需要像 select 和 poll 那样轮询扫描整个 Socket 集合。这种事件驱动机制极大地提高了检测的效率,避免了无意义的遍历

总的来说,epoll 通过利用红黑树数据结构和事件驱动机制,提供了高效的 I/O 多路复用。它减少了内核态与用户态之间的大量数据拷贝和内存分配,以及无效的遍历操作,大大提高了 I/O 事件检测的效率。
epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT):

  • 边缘触发方式只在 I/O 状态变化时通知应用程序,即当某个文件描述符发生状态变化时(例如可读或可写),epoll_wait 函数仅返回一次。如果应用程序未在之前处理完该文件描述符的数据,那么下次再调用 epoll_wait 时,不会再次触发通知,除非文件描述符状态再次发生变化;
  • 边缘触发通知应用程序只有在状态发生变化的瞬间才触发,因此需要及时处理文件描述符上的数据,以免错过变化;
  • 水平触发方式会持续通知应用程序,只要文件描述符处于就绪状态,epoll_wait 函数就会返回该文件描述符。如果应用程序未在之前处理完该文件描述符的数据,再次调用 epoll_wait 时,仍然会返回该文件描述符;
  • 水平触发通知应用程序只是告诉文件描述符当前处于就绪状态,并不会跳过处理,因此应用程序需要主动处理文件描述符的数据

这两种方式类似于,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信,这个方式就是边缘触发;如果快递箱发现你的快递没有被取出,它就会不停地发送短信通知你,直到你取出了快递,这个就是水平触发。
一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,而系统调用也是有一定的开销的,毕竟也存在上下文的切换。
select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。
综上所述,边缘触发和水平触发是 epoll 在通知应用程序 I/O 事件时的两种方式。边缘触发只在 I/O 状态变化瞬间触发通知,需要及时处理数据;而水平触发则持续通知处于就绪状态的文件描述符,需要应用程序主动处理数据。在使用 epoll 时,开发人员需要根据具体场景选择适合的触发方式来处理 I/O 事件。

参考资料

  • https://xiaolincoding.com/os/8_network_system/selete_poll_epoll.html#_9-2-i-o-%E5%A4%9A%E8%B7%AF%E5%A4%8D%E7%94%A8-select-poll-epoll

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

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

相关文章

【Python】执行SQL报错

可以再数据库查询界面执行的SQL,一直报错 unsupported format character Y (0x59) at index 61 SQL如下: datapd.read_sql_query(sql"""selectdate_format(create_time,%Y-%m) as mon,count(distinct order_id) as ord_cntfrom prod.o…

HTTP与HTTPS

HTTP与HTTPS介绍 超文本传输协议HTTP协议被用于在Web浏览器和网站服务器之间传递信息,HTTP协议以明文方式发送内容,不提供任何方式的数据加密,如果攻击者截取了Web浏览器和网站服务器之间的传输报文,就可以直接读懂其中的信息&…

qt源码--事件系统

qt的事件传播主要依赖于QCoreApplication、QAbstractEventDispatcher(会根据不同的平台生成各自的处理对象)、QEvent(各种事件类型)等。 首先看下QCoreApplication的实现: 2、了解QCoreApplication的构造函数 其构造函…

在最新ICP备案域名的基础上,结合其他网络营销手段,打造强大的品牌推广效果

API接口是一种软件系统之间进行交互的方式,通过API接口,可以在不同的系统之间传递数据、命令等信息。在网络营销中,API接口可以帮助我们更加高效地进行品牌推广。本文将以在最新ICP备案域名的基础上,结合其他网络营销手段&#xf…

JVM回收算法(标记-清除算法, 复制算法, 标记-整理算法)

1.标记-清除算法 最基础的算法,分为两个阶段,“标记”和“清除” 原理: - 标记阶段:collector从mutator根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中&am…

vue-router 4.0 动态路由会跳转到 404 页面的问题

引子 开发过前端单页面应用的小伙伴们,应该对前端路由都不陌生吧。 无论是用 vue 或者 react,都有官方提供的 router 方案。 但是有些场景下,处于安全性和友好性考虑,我们需要用到动态路由。 如果你不知道什么叫动态路由&…

翻遍整个牛客网,整理出了全网最全的Java面试八股文大合集,整整4000多页

大家从 Boss 直聘上或者其他招聘网站上都可以看到 Java 岗位众多,Java 岗位的招聘薪酬天差地别,人才要求也是五花八门。而很多 Java 工程师求职过程中,也是冷暖自知。很多时候技术有,但是面试的时候就是过不了! 为了帮…

4.7 x64dbg 应用层的钩子扫描

所谓的应用层钩子(Application-level hooks)是一种编程技术,它允许应用程序通过在特定事件发生时执行特定代码来自定义或扩展其行为。这些事件可以是用户交互,系统事件,或者其他应用程序内部的事件。应用层钩子是在应用…

【Zabbix 监控 Windows 系统,Java应用,SNMP】

目录 一、Zabbix 监控 Windows 系统1、下载 Windows 客户端 Zabbix agent 22、安装客户端,配置3、在服务端 Web 页面添加主机,关联模板 二、Zabbix 监控 java 应用1、客户端开启 java jmxremote 远程监控功能1、配置 java jmxremote 远程监控功能2、启动…

【ARM Coresight 系列文章 3.1 - ARM Coresight DP 对 AP 的访问 2】

文章目录 图 1-1 如上图1-1 所示,DAP上可以集成多个MEM-AP,上图是集成了3个MEM-AP,它们可能是AXI-AP, AHB-AP, APB-AP。 那么AP的类型是如何区分的呢? 不同的组件会使用不同MEM-AP接口,如Cortex-A/Coretex-R 系列的c…

ai绘画工具有哪些?这几款好用的ai绘画工具免费分享给你

上周,我去了一家现代艺术画廊,墙上挂满了令人叹为观止的绘画作品。我被其中一幅细腻而充满情感的油画所深深吸引,想要了解背后的创作过程。这时,一位热情的艺术导师走到我身边。她微笑着说:“这幅作品实际上是由ai绘画…

解决“_mkdir无法识别空格目录“问题

在C编程里,有时候需要创建一个文件夹,通常使用库函数_mkdir(const char* dirname)来新建一个文件夹,该库函数每次只能创建一个文件夹,不能级联创建。若要级联创建文件,则请用递归方式或者for循环方式调用_mkdir()。 #…

7月6日华为云盘古气象大模型登上《Nature》杂志:相比传统数值预报快10000倍

7月6日,国际顶级学术期刊《自然》(Nature)杂志正刊发表了华为云盘古大模型研发团队的最新研究成果——《三维神经网络用于精准中期全球天气预报》(《Accurate medium-range global weather forecasting with 3D neural networks》…

CrossKD 原理与代码解析

paper:CrossKD: Cross-Head Knowledge Distillation for Dense Object Detection official implementation: https://github.com/jbwang1997/CrossKD 前言 蒸馏可以分为预测蒸馏prediction mimicking和特征蒸馏feature imitation两种,201…

【LeetCode】HOT 100(26)

题单介绍: 精选 100 道力扣(LeetCode)上最热门的题目,适合初识算法与数据结构的新手和想要在短时间内高效提升的人,熟练掌握这 100 道题,你就已经具备了在代码世界通行的基本能力。 目录 题单介绍&#…

什么是 AOP?对于 Spring IoC 和 AOP 的理解?Spring AOP 和 AspectJ AOP 有什么区别?

什么是 AOP? AOP(Aspect-Oriented Programming),即 面向切面编程, 它与OOP( ObjectOriented Programming, 面向对象编程) 相辅相成,提供了与OOP 不同的抽象软件结构的视角 在 OOP 中, 我们以类(class)作为我们的基本单元 而 A…

Zynq 多个UDP客户端组网启动问题(Auto negotiation error)PS:附UDP客户端初始化代码

最近正在进行一个Zynq项目,根据设计需求,需要将上位机作为UDP服务器,而FPGA则充当UDP客户端。同时,服务器需要能够接收和控制多个UDP客户端。 开发过程中,我是基于lwip UDP Perf Client 官方模版开发的。我遇到了以下几…

重磅!高通大客户「跳单」,智能座舱SoC赛道进入「混战期」

哪家车企是高通座舱芯片的最大客户?答案不是蔚来、理想、小鹏等智能化布局领先的新势力,而是比亚迪。 高工智能汽车研究院监测数据显示,2022年中国市场(不含进出口)乘用车智能座舱前装标配高通芯片交付367.45万辆&…

DALL·E2(unCLIP)、Stable Diffusion、IS、FID要点总结

DALLE 1 DALLE 1可以看成是VQ-VAE和文本经过BPE编码得到的embedding AE(Auto Encoder) encoder decoder结构,AE在生成任务时只会模仿不会创造,所有有了后面的VAE VAE(Variational AutoEncoder) 不再学习固定的bottleneck特征…

2023-07-07-liunx环境,python调用ITK(c++版本)批量生成Drr

文章目录 一、前言二、配置过程2.1.CMake与ITK的配置2.2.改写ITK的生成drr代码2.3.编译代码2.4.python调用cpp 三、总结四、参考博客 一、前言 最近在做配准,需要用ITK来生成数据,windows版本可以通过cmake与visual studio可以跑通生成。但是想要在linu…