I/O 多路复用:select/poll/epoll

news2025/7/18 6:01:41

url:9.2 I/O 多路复用:select/poll/epoll | 小林coding        (仅供自己学习使用)

为什么要使用I/O多路复用技术

TCP Socket 调用流程是最简单、最基本的,它基本只能一对一通信,因为使用的是同步阻塞的方式,当服务端在还没处理完一个客户端的网络 I/O 时,或者 读写操作发生阻塞时,其他客户端是无法与服务端连接的。可如果我们服务器只能服务一个客户,那这样就太浪费资源了,于是我们要改进这个网络 I/O 模型,以支持更多的客户端。

 TCP 连接是由四元组唯一确认的,这个四元组就是:本机IP, 本机端口, 对端IP, 对端端口,服务器作为服务方,通常会在本地固定监听一个端口,等待客户端的连接。因此服务器的本地 IP 和端口是固定的,于是对于服务端 TCP 连接的四元组只有对端 IP 和端口是会变化的,所以最大 TCP 连接数 = 客户端 IP 数×客户端端口数

对于 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数约为 2 的 48 次方

这个理论值相当“丰满”,但是服务器肯定承载不了那么大的连接数,主要会受两个方面的限制:

  • 文件描述符,Socket 实际上是一个文件,也就会对应一个文件描述符。在 Linux 下,单个进程打开的文件描述符数是有限制的,没有经过修改的值一般都是 1024,不过我们可以通过 ulimit 增大文件描述符的数目;
  • 系统内存,每个 TCP 连接在内核中都有对应的数据结构,意味着每个连接都是会占用一定内存的;

那如果服务器的内存只有 2 GB,网卡是千兆的,能支持并发 1 万请求吗?

多进程模型

基于最原始的阻塞网络 I/O, 如果服务器要支持多个客户端,其中比较传统的方式,就是使用多进程模型,也就是为每个客户端分配一个进程来处理请求

服务器的主进程负责监听客户的连接,一旦与客户端连接完成,accept() 函数就会返回一个「已连接 Socket」,这时就通过 fork() 函数创建一个子进程,实际上就把父进程所有相关的东西都复制一份,包括文件描述符、内存地址空间、程序计数器、执行的代码等。

这两个进程刚复制完的时候,几乎一模一样。不过,会根据返回值来区分是父进程还是子进程,如果返回值是 0,则是子进程;如果返回值是其他的整数,就是父进程。

正因为子进程会复制父进程的文件描述符,于是就可以直接使用「已连接 Socket 」和客户端通信了,

可以发现,子进程不需要关心「监听 Socket」,只需要关心「已连接 Socket」;父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心「已连接 Socket」,只需要关心「监听 Socket」。

下面这张图描述了从连接请求到连接建立,父进程创建生子进程为客户服务。

另外,当「子进程」退出时,实际上内核里还会保留该进程的一些信息,也是会占用内存的,如果不做好“回收”工作,就会变成僵尸进程,随着僵尸进程越多,会慢慢耗尽我们的系统资源。

因此,父进程要“善后”好自己的孩子,怎么善后呢?那么有两种方式可以在子进程退出后回收资源,分别是调用 wait() 和 waitpid() 函数。

多线程模型

既然进程间上下文切换的“包袱”很重,那我们就搞个比较轻量级的模型来应对多用户的请求 —— 多线程模型

线程是运行在进程中的一个“逻辑流”,单进程中可以运行多个线程,同进程里的线程可以共享进程的部分资源,比如文件描述符列表、进程空间、代码、全局数据、堆、共享库等,这些共享些资源在上下文切换时不需要切换,而只需要切换线程的私有数据、寄存器等不共享的数据,因此同一个进程下的线程上下文切换的开销要比进程小得多。

当服务器与客户端 TCP 完成连接后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。

如果每来一个连接就创建一个线程,线程运行完后,还得操作系统还得销毁线程,虽说线程切换的上写文开销不大,但是如果频繁创建和销毁线程,系统开销也是不小的。

那么,我们可以使用线程池的方式来避免线程的频繁创建和销毁,所谓的线程池,就是提前创建若干个线程,这样当由新连接建立时,将这个已连接的 Socket 放入到一个队列里,然后线程池里的线程负责从队列中取出「已连接 Socket 」进行处理。

需要注意的是,这个队列是全局的,每个线程都会操作,为了避免多线程竞争,线程在操作这个队列前要加锁

I/O 多路复用(主角登场)

时分多路复用

既然为每个请求分配一个进程/线程的方式不合适,那有没有可能只使用一个进程来维护多个 Socket 呢?答案是有的,那就是 I/O 多路复用技术。

一个进程虽然任一时刻只能处理一个请求,但是处理每个请求的事件时,耗时控制在 1 毫秒以内,这样 1 秒内就可以处理上千个请求,把时间拉长来看,多个请求复用了一个进程,这就是多路复用,这种思想很类似一个 CPU 并发多个进程,所以也叫做时分多路复用。

我们熟悉的 select/poll/epoll 内核提供给用户态的多路复用系统调用,进程可以通过一个系统调用函数从内核中获取多个事件

select/poll/epoll 是如何获取网络事件的呢?在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可。

select/poll

select 实现多路复用的方式是,将已连接的 Socket 都放到一个文件描述符集合,然后调用 select 函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此 Socket 标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的 Socket,然后再对其处理。

所以,对于 select 这种方式,需要进行 2 次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生 2 次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。

        select 使用固定长度的 BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在 Linux 系统中,由内核中的 FD_SETSIZE 限制, 默认最大值为 1024,只能监听 0~1023 的文件描述符。

        poll 不再用 BitsMap 来存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了 select 的文件描述符个数限制,当然还会受到系统文件描述符限制。

·        但是 poll 和 select 并没有太大的本质区别,都是使用「线性结构」存储进程关注的 Socket 集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合,这种方式随着并发数上来,性能的损耗会呈指数级增长

epoll

先用e poll_create 创建一个 epol l对象 epfd,再通过 epoll_ctl 将需要监视的 socket 添加到epfd中,最后调用 epoll_wait 等待数据。

int s = socket(AF_INET, SOCK_STREAM, 0);
bind(s, ...);
listen(s, ...)

int epfd = epoll_create(...);
epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中

while(1) {
    int n = epoll_wait(...);
    for(接收到数据的socket){
        //处理
    }
}

epoll 通过两个方面,很好解决了 select/poll 的问题。

  •         第一点,epoll 在内核里使用红黑树来跟踪进程所有待检测的文件描述字,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn)。而 select/poll 内核里没有类似 epoll 红黑树这种保存所有待检测的 socket 的数据结构,所以 select/poll 每次操作时都传入整个 socket 集合给内核,而 epoll 因为在内核维护了红黑树,可以保存所有待检测的 socket ,所以只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配
  •         第二点, epoll 使用事件驱动的机制,内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的文件描述符的个数,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测的效率。

 epoll 的方式即使监听的 Socket 数量越多的时候,效率不会大幅度降低,能够同时监听的 Socket 的数目也非常的多了,上限就为系统定义的进程打开的最大文件描述符个数。因而,epoll 被称为解决 C10K 问题的利器

边缘触发和水平触发

这两个术语还挺抽象的,其实它们的区别还是很好理解的。

  • 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
  • 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。

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

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

相关文章

Linux作为主力机--Manjaro 22.0.4

1、对操作系统的看法 个人是做软件开发的,已经使用Manjaro作为主力机两年多了,真的是特别喜欢这个操作系统。经过两年的打磨,个人16年的惠普老电脑加上这个Manjaro 22.0.4操作系统完全可以再战五年,完全满足日常的办公使用&#…

ctf pwn基础-3

学习pwn的第三天,今天是ret2text。 目录 基础 实例讲解 实例讲解2 基础 ret2text就是ROP中最简单的,然后的意思就是我们利用栈溢出,来修改eip的值,让他输出的时候,输出我们想要执行的本身已有的代码,通常…

利用Splunk构建SOC-SOC建设漫谈及splunk的角色

零、免喷符 SOC部门小菜鸟一枚,此乃自闭学安全的笔记记录,行文潦草,随性笔记。 通过上一篇的勒索病毒案例,已经了解到Splunk的强大之处。Splunk那么死贵,他的角色是怎样的,又是怎么和安全及SOC联系起来的…

基于BP神经网络的性别识别,BP神经网络详细原理,自编码神经网络代码,神经网络案例之18

目标 背影 BP神经网络的原理 BP神经网络的定义 BP神经网络的基本结构 BP神经网络的神经元 BP神经网络的激活函数, BP神经网络的传递函数 数据 神经网络参数 基于BP神经网络 性别识别的MATLAB代码 效果图 结果分析 展望 背影 男人体内蛋白质比例大,女生…

Windows下 IDEA编译调试 hive2.3.9

Windows下 IDEA编译调试 hive2.3.9 环境 IDEA 2021.2 JDK1.8(试过用高版本的JDK17编译,不兼容编译不过) 一个Hadoop集群,涉及配置文件core-site.xml,hdfs-site.xml,yarn-site.xml,mapred-sit…

并发与多线程

目录 第一节 并发基本概念及实现,进程,线程基本概念 (1)并发,进程,线程的基本概念和综述 (1.1)并发 (1.2)可执行程序 (1.3)进程 …

物理服务器与云服务器备份相同吗?

自从云计算兴起以来,服务器备份已经从两阶段的模拟操作演变为由云服务器备份软件执行的复杂的多个过程。但是支持物理服务器和虚拟服务器之间的备份相同吗?主要区别是什么?我们接下来将详细讨论这个问题。 物理服务器与云服务器备份的区别 如果您不熟悉虚拟服务器…

qt QCustomPlot学习

QCustomPlot 是一个基于Qt的画图和数据可视化C控件。QCustomPlot 致力于提供美观的界面,高质量的2D画图、图画和图表,同时为实时数据可视化应用提供良好的解决方案。 该绘图库专注于制作美观、出版物质量高的2D绘图、图形和图表,并为实时可视…

数据库专题

请简洁描述 MySQL 中 InnoDB 支持的四种事务隔离级别名称,以及逐级之间的区别? 默认隔离级别 mysql repeatable-read oracle read-committed 脏读:不可重复读:幻读: CHAR 和 VARCHAR 的区别?…

公众号运营之竞品分析,教你拆解公众号

知己知彼,百战不殆,公众号运营亦是如此。 当运营者只关注自己账号的时候,很容易陷入某个误区中出不来。这个时候就要拓宽我们的视野,多去看看“外面的世界”,不要只局限于自己的一片小天地中。 看看同领域优秀公众号…

stm32f407探索者开发板(二十二)——通用定时器基本原理讲解

文章目录一、三种定时器的区别二、通用定时器特点2.1 功能特点描述2.2 计数器模式三、通用定时器工作过程四、附一、三种定时器的区别 STM32F40x系列总共最多有14个定时器 三种(4)STM32定时器区别 二、通用定时器特点 2.1 功能特点描述 STM3 F4的通…

PHY设备驱动

1. 概述 MAC控制器的驱动使用的是platform总线的连接方式,PHY设备驱动是基于device、driver、bus的连接方式。 其驱动涉及如下几个重要部分: 总线 - sturct mii_bus (mii stand for media independent interface) 设备 - struct phy_device 驱动 - struc…

零日漏洞发展格局及防御策略

在过去的一年半中, 在野利用的零日漏洞数量持续飙升 ,这些软件制造商尚不知晓的漏洞正在被国家行为体黑客组织和勒索软件团伙滥用。 今年上半年,Google Project Zero统计了近20个零日漏洞,其中 大部分针对微软、苹果和谷歌构建的…

【《C Primer Plus》读书笔记】第13章:文件输入/输出

【《C Primer Plus》读书笔记】第13章:文件输入/输出13.1 与文件进行通信13.1.1 文件是什么13.1.2 文本模式和二进制模式13.1.3 I/O的级别13.1.4 标准文件13.2 标准I/O13.3 一个简单的文件压缩程序13.4 文件I/O:fprintf()、fscanf()、fgets()和fputs()13…

【LVGL】学习笔记--(1)Keil中嵌入式系统移植LVGL

一 LVGL简介最近emwin用的比较烦躁,同时被LVGL酷炫的界面吸引到了,所以准备换用LVGL试试水。LVGL(轻量级和通用图形库)是一个免费和开源的图形库,它提供了创建嵌入式GUI所需的一切,具有易于使用的图形元素,美丽的视觉效…

极光笔记 | 埋点体系建设与实施方法论

PART 01 前 言随着网络技术的发展,从粗犷型到精细化运营型,再到现在的数字化运营,数据变得越来越细分和重要,不仅可以进行策略调整,还可以实现自动化的精细化运营。而数据价值的起点就是埋点,只有合理地埋点…

[计算机网络(第八版)]第一章 概述(学习笔记)

1.1 计算机网络在信息时代中的作用 21世纪是以网络为核心的信息时代,21世纪的重要重要特征:数字化、网络化与信息化。 三大类网络 电信网络:向用户提供电话、电报、传真等服务;有线电视网络:向用户传送各种电视节目&am…

zabbix4.0-使用zabbix监控别的主机-使用模板来创建图形

目录 1、 配置zabbix的yum源 2、下载zabbix-agent 3、配置zabbix-agent的配置文件 4、关闭防火墙,selinux 5、重启zabbix-agent 6、连通性测试,在zabbix-server服务器上面使用zabbix_get获取zabbix-agent服务器上的数据 7、在zabbix web端配置zab…

Windows 11 22H2 中文版、英文版 (x64、ARM64) 下载 (updated Feb 2023)

Windows 11, version 22H2,2023 年 2 月 更新 请访问原文链接:https://sysin.org/blog/windows-11/,查看最新版。原创作品,转载请保留出处。 作者主页:www.sysin.org 全新推出 Windows 11 全新 Windows 体验&#x…

考PMP的用处有哪些?备考攻略+资料分享

说到底,考PMP就是为了给工作提供便利,你考了之后会用它,将学习的东西运用到工作中,比如提高项目完成率,能升职加薪啊等等,那自然很是有用的。 不用,那就只是一张当摆设的纸,当然&am…