一文读懂 Linux 网络 IO 模型

news2024/10/5 21:21:48

文章目录

  • 1.从一个问题说起
  • 2.多进程模型
  • 3.多线程模型
  • 4.I/O 多路复用
  • 5.select、poll、epoll 的区别?
    • 5.1 select
    • 5.2 poll
    • 5.3 epoll
    • 5.4 两种事件触发模式
  • 参考文献

1.从一个问题说起

互联网发展历史上,曾经有一个著名的问题:C10K 问题。

C 是 Client 单词首字母缩写,10K 指 1 万,C10K 指单机同时处理 1 万个并发连接问题。

C10K 问题最早是在 1999 年由 Dan Kegel 提出并发布于其个人站点( http://www.kegel.com/c10k.html )。

C10K 问题是一个优化网络套接字以同时处理大量客户端连接的问题。注意这里的并发连接和每秒请求数不同,虽然它们是相似的: 每秒处理许多请求需要很高的吞吐量(快速处理它们),但是更大的并发连接数需要高效的连接调度。

为了解决该问题,研究方向首先是网络 IO 模型的优化,具体的思路就是通过单个进程或线程服务于多个客户端请求,通过异步编程和事件触发机制替换轮询,IO 采用非阻塞的方式,减少不必要的性能损耗,等等。

现在已经解决了 C10K 的问题。 epoll、kqueue、iocp 就是 IO 模型优化的一些最佳实践,这几种技术实现分别对应于不同的系统平台。

epoll 主要用于 Linux 系统。使用一个文件描述符管理多个文件描述符的 I/O 事件。提供水平触发(Level Triggered)和边缘触发(Edge Triggered)两种模式。

kqueue 主要用于 BSD 系统,如 FreeBSD、OpenBSD 和 macOS。使用一个内核事件队列管理多个文件描述符的 I/O 事件。支持水平触发和边缘触发,也支持过滤器(Filter)概念,可以监控多种类型的事件。

iocp (Input/Output Completion Port) 主要用于 Windows 系统。使用 I/O 完成端口来实现异步 I/O 多路复用。异步 I/O 模型,采用回调方式,支持 OVERLAPPED 结构体。

2.多进程模型

如果服务器要支持多个客户端连接,其中比较传统的方式,就是使用多进程模型,也就是为每个客户端分配一个进程来处理请求。

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

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

正因为子进程会复制父进程的文件描述符,于是就可以直接使用「已连接 Socket 」和客户端通信了,可以发现,子进程不需要关心「监听 Socket」,只需要关心「已连接 Socket」;父进程则相反,将客户服务交给子进程来处理,因此父进程不需要关心「已连接 Socket」,只需要关心「监听 Socket」。

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

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

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

这种用多个进程来应付多个客户端的方式,在应对 100 个客户端还是可行的,但是当客户端数量高达一万时,肯定扛不住的,因为每产生一个进程,必会占据一定的系统资源,而且进程间上下文切换的“包袱”是很重的,性能会大打折扣。

进程的上下文切换涉及到保存和恢复大量的状态信息,不仅包含了虚拟内存、栈、全局变量等用户空间的资源,还包括了内核堆栈、寄存器等内核空间的资源。

3.多线程模型

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

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

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

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

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

在这里插入图片描述

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

上面基于进程或者线程模型的,其实还是有问题的。新到来一个 TCP 连接,就需要分配一个进程或者线程,那么如果要达到 C10K,意味着要一台机器维护 1 万个连接,相当于要维护 1 万个进程/线程,操作系统就算死扛也是扛不住的。

4.I/O 多路复用

既然为每个请求分配一个进程/线程的方式不合适,那有没有可能只使用一个进程来维护多个 Socket 呢?

答案是有的,那就是 I/O 多路复用技术。

I/O 多路复用是通过一种机制(通常是系统调用)同时监视多个文件描述符(sockets、文件、设备等)的技术。它允许单个进程能够管理多个 I/O 操作,而不必为每个 I/O 操作创建一个单独的线程或进程。

I/O 多路复用的 “多路” 指的是多个 I/O 操作(通常是多个文件描述符),复用指使用一个进程来处理多个 I/O 操作。

在这里插入图片描述
Linux 下有三种提供 I/O 多路复用的 API,分别是: select、poll、epoll。

5.select、poll、epoll 的区别?

select、poll 和 epoll 都是 Linux 实现 IO 多路复用提供的系统调用,都能实现 C10K 吗?接下来,我们分别说说它们。

5.1 select

IO 多路复用这个概念被提出来以后, select 是第一个实现 (1983 左右在 BSD 里面实现)。

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

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

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

select 被实现以后,很快暴露出了很多问题。

  • select 只能监视 1024 个连接,Linux 定义在头文件中的,参见 FD_SETSIZE。
  • 2 次「遍历」文件描述符集合,性能低下。
  • 2 次「拷贝」文件描述符集合,性能低下。

5.2 poll

因为 select 的诸多问题,14 年后(1997年)一帮人又实现了 poll,修复了 select 的很多问题。

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

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

5.3 epoll

于是 5 年以后(2002 年), 大神 Davide Libenzi 实现了 epoll。

epoll 可以说是 IO 多路复用最新的一个实现,epoll 修复了 poll 和 select 绝大部分问题,比如:

(1)高效的数据结构。
epoll 在内核里使用红黑树跟踪进程所有待检测的文件描述符,把需要监控的 socket 通过 epoll_ctl() 函数加入内核中的红黑树里,红黑树是个高效的数据结构,增删查一般时间复杂度是 O(logn),通过对这棵黑红树进行操作,这样就不需要像 select/poll 每次操作时都传入整个 socket 集合,只需要传入一个待检测的 socket,减少了内核和用户空间大量的数据拷贝和内存分配。

(2)使用事件驱动机制。
内核里维护了一个链表来记录就绪事件,当某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait() 函数时,只会返回有事件发生的 socket,不需要像 select/poll 那样轮询扫描整个 socket 集合,大大提高了检测效率。

也就是 epoll 现在不仅告诉你 Socket 组里面数据,还会告诉你具体哪个 Socket 有数据,不用自己去找了。

从下图你可以看到 epoll 相关的接口作用:

在这里插入图片描述

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

5.4 两种事件触发模式

epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT)。

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

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

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

这就是两者的区别,水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。

如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。

如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据。如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为 EAGAIN 或 EWOULDBLOCK。

一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也有开销,毕竟也存在上下文的切换。

select/poll 只有水平触发模式,epoll 默认是水平触发,但可以根据应用场景设置为边缘触发模式。


参考文献

C10K 问题
程序员怎么会不知道C10K 问题呢?- Medium
I/O 多路复用:select/poll/epoll - 小林 coding
io-multiplexing(多路复用) - Colingo碎碎念

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

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

相关文章

5-4计算一串字符的空格数字字符其他

#include<stdio.h> int main(){char c;int space0;//空格int letters0;//英文字母int numbers0;//数字int others0;//其他字符printf("请输入一行字符&#xff1a;");while((cgetchar())!\n)//获取字符的内容&#xff0c;到\n停止{if(c>a&&c<z|…

AR眼镜方案—单目光波导AR智能眼镜

光波导技术是一项具有前沿意义的技术&#xff0c;它能够将光线反射180度&#xff0c;使得眼镜框架内置的MicroLED屏幕的图像通过多次反射与扩散后准确地传递到人眼中。采用MicroLED显示技术的AR智能眼镜不仅体积显著缩小&#xff0c;屏幕只有0.68英寸大小&#xff0c;并且还能够…

【PostgreSQL】解决PostgreSQL时区(TimeZone)问题

问题描述 最近在使用PostgreSQL中&#xff0c;对行记录进行设置创建时间&#xff08;created_time&#xff09;时&#xff0c;出现了设置了now()时间而数据库中写入的数据是不一致的数据。 eg&#xff1a; insert into dept ( created_at, updated_at) VALUES (now(),now())…

Spring框架学习 -- 核心思想

目录 (1) Spring是什么? (2) 什么是IOC容器? (3) 从传统开发认识spring (4) 这种传统开发的缺陷 (5)解决传统开发中的缺陷 (6) 对比总结规律 (7) 理解IOC 创作不易多多支持 (1) Spring是什么? 我们常说的Spring的全称是: Spring Framework(Spring框架), 它是一个开源…

【经验之谈·高频PCB电路设计常见的66个问题】

文章目录 1、如何选择PCB 板材&#xff1f;2、如何避免高频干扰&#xff1f;3、在高速设计中&#xff0c;如何解决信号的完整性问题&#xff1f;4、差分布线方式是如何实现的&#xff1f;5、对于只有一个输出端的时钟信号线&#xff0c;如何实现差分布线&#xff1f;6、接收端差…

微信小程序记住密码,让登录解放双手

密码是用户最重要的数据&#xff0c;也是系统最需要保护的数据&#xff0c;我们在登录的时候需要用账号密码请求登录接口&#xff0c;如果用户勾选记住密码&#xff0c;那么下一次登录时&#xff0c;我们需要将账号密码回填到输入框&#xff0c;用户可以直接登录系统。我们分别…

win11,安装python,pip,和opencv

1,安装python 在应用商店&#xff0c;输入python&#xff0c;下载安装 2&#xff0c;安装pip 在cmd中&#xff0c;输入pip install SomePackage&#xff0c;安装某一个版本的pip 3,安装opencv 在cmd中&#xff0c;输入 pip3 install opencv-contrib-python -i https://pyp…

css 实现鼠标上移添加下划线

效果图 实现代码 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-wi…

Linux网络驱动

Linux 网络设备驱动结构 网络协议接口层向网络层协议提供统一的数据包收发接口&#xff0c;这一层使得上层协议独立于具体的设备网络设备接口层向协议接口层提供统一的用于描述具体网络设备属性和操作的结构体net_device&#xff0c;该结构体是设备驱动功能层中各函数的容器设备…

力扣C++学习笔记——C++ 给vector去重

要使用std::set对std::vector进行去重操作&#xff0c;您可以将向量中的元素插入到集合中&#xff0c;因为std::set会自动去除重复元素。然后&#xff0c;您可以将集合中的元素重新存回向量中。以下是一个示例代码&#xff0c;演示如何使用std::set对std::vector进行去重&#…

阵列 MEMS-IMU的解算系统

使用飞速发展的硅半导体工艺制成的微机械电子系统&#xff08;Micro Electro Mechanical System&#xff0c;MEMS&#xff09;具有体积小、成本低、重量轻、低功耗等诸多优势。MEMS-IMU&#xff08;Inertial Measurement Unit, IMU&#xff09;构成的捷联惯导系统可以应用到无人…

关闭bitlocker加密

windows11的笔记本电脑买回来发现分驱都处于bitlocker状态&#xff0c;上网上搜索都是说进入控制面板的安全项进行关闭&#xff0c;包括去搜索栏搜索“管理 BitLocker”&#xff0c;对搜索出来的项打开&#xff0c;经过试验&#xff0c;它们进入的是同一个位置&#xff0c;只有…

高防CDN如何预防攻击?

现在网络攻击事件越来越多&#xff0c;而且愈发凶猛&#xff0c;为了保障互联网业务能稳定正常的运行&#xff0c;市场上出现了很多高防产品&#xff0c;例如高防服务器、高防IP、高防CDN等等。其中究竟高防CDN怎么防攻击&#xff0c;能防哪些攻击&#xff1f;高防CDN如何实现防…

HarmonyOS基础组件之Button三种类型的使用

简介 HarmonyOS在明年将正式不再兼容Android原生功能&#xff0c;这意味着对于客户端的小伙伴不得不开始学习HarmonyOS开发语言。本篇文章主要介绍鸿蒙中的Button使用。 HarmonyOS中的Button相较于Android原生来说&#xff0c;功能比较丰富&#xff0c;扩展性高&#xff0c;减…

线程(线程基本概念、java实现多线程、使用多线程、线程的生命周期、线程同步、线程死锁)

&#xff08;一&#xff09;线程基本概念 一、 程序, 进程, 线程的概念 程序: 使用某种语言编写一组指令(代码)的集合,静态的 进程: 运行的程序,表示程序一次完整的执行, 当程序运行完成, 进程也就结束了 个人电脑: CPU 单个, 双核, CPU的时间分片, 抢占式 每个独立执行的程…

【20年扬大真题】编写对数组求逆的递归算法

【20年扬大真题】 编写对数组求逆的递归算法 void swap(int* a, int* b) {int tmp *b;*b *a;*a tmp; } void Ni(int arr[],int left,int right) {if (left > right) {return;}swap(&arr[left], &arr[right]);Ni(arr, left 1, right - 1); } int main() {int ar…

威视佰科荣获:2023年“AI天马”高成长性企业

11月14日下午&#xff0c;2023年度“AI天马”认定评审会顺利召开。落实《深圳经济人工智能产业促进条例》&#xff0c;加快推进智能机器人、智能传感器、智能网联汽车、智能终端、脑科学和类脑智能等人工智能相关产业发展&#xff0c;加速AI产业化和产业AI化进程&#xff0c;持…

对象分配规则

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一份大厂面试资料《史上最全大厂面试题》&#xff0c;Springboot、微服务、算法、数据结构、Zookeeper、Mybatis、Dubbo、linux、Kafka、Elasticsearch、数据库等等 …

Python Opencv实践 - 二维码和条形码识别

使用pyzbar模块来识别二维码和条形码。ZBar是一个开源软件&#xff0c;用来从图像中读取条形码&#xff0c;支持多种编码,比如EAN-13/UPC-A、UPC-E、EAN-8、代码128、代码39、交错2/5以及二维码。 pyzbar是python封装ZBar的模块&#xff0c;我们用它来做条形码和二维码的识别。…

算法设计与分析复习--回溯法(二)

文章目录 上一篇0-1背包问题图着色问题n皇后问题下一篇 上一篇 算法设计与分析复习–回溯&#xff08;一&#xff09; 0-1背包问题 问题描述&#xff1a;给定n中物品和一个背包。物品 i i i 的重量是 w i w_i wi​ &#xff0c;其价格为 v i v_i vi​ , 背包容量为 c c …