Bendi新闻
>
Linux五大网络IO模型图解

Linux五大网络IO模型图解

6月前

用户空间与内核空间

  对于一个应用程序即一个操作系统进程来说,它既有内核空间(与其他进程共享),也有用户空间(进程私有),它们都是处于虚拟地址空间中。用户进程是无法访问内核空间的,它只能访问用户空间,通过用户空间去内核空间复制数据,然后进行处理。为了避免用户应用导致冲突甚至内核崩溃,所以进程的寻址空间就划分为两部分:内核空间、用户空间。

  用户空间:只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
  内核空间:可以执行特权命令(Ring0),调用一切系统资源
  写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备; 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

  

阻塞io(同步io)

  发起请求就一直等待,直到数据返回。全程阻塞在第一阶段 用户进程尝试读取数据时,此时数据尚未到达,内核需要等待数据,此时用户进程处于阻塞状态;第二阶段 数据拷贝到内核缓冲区后 此时已就绪,然后将数据拷贝到用户进程缓冲区,拷贝的过程中用户进程也是阻塞状态。(好比你去商场试衣间,里面有人,那你就一直在门外等着)

非阻塞io(同步io)

  不管有没有数据都返回,没有就隔一段时间再来请求,如此循环。复制数据时阻塞在第一阶段 用户进程尝试读取数据时,此时数据尚未到达,就返回异常给用户进程,用户进程拿到error后,就再次重复尝试,直到数据就绪;第二阶段 数据拷贝到内核缓冲区后 此时已就绪,然后将数据拷贝到用户进程缓冲区,拷贝的过程中用户进程也是阻塞状态。(好比你要喝水,水还没烧开,你就隔段时间去看一下饮水机,直到水烧开为止,水烧开了就等在那里接水喝)

  可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态。虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致CPU空转,CPU使用率暴增。无论是阻塞IO还是非阻塞IO,用户应用在一阶段都需要调用recvfrom来获取数据,差别在于无数据时的处理方案: 

  如果调用recvfrom时,恰好没有数据,阻塞IO会使CPU阻塞,非阻塞IO使CPU空转,都不能充分发挥CPU的作用。
  如果调用recvfrom时,恰好有数据,则用户进程可以直接进入第二阶段,读取并处理数据

  

io多路复用(同步io)

  文件描述符(File Descriptor):简称FD,是一个从0 开始的无符号整数,用来关联Linux中的一个文件。在Linux中,一切皆文件,例如常规文件、视频、硬件设备等,当然也包括网络套接字(Socket)。
  IO多路复用:是利用单个线程来同时监听多个FD,并在某个FD可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。检查FD就绪时阻塞复制数据时阻塞。它的实现有三种方式   select   poll   epoll。

   

select

// 定义类型别名 __fd_mask,本质是 long inttypedef long int __fd_mask;
/* fd_set 记录要监听的fd集合,及其对应状态 */typedef struct { // fds_bits是long类型数组,长度为 1024/32 = 32 // 共1024个bit位,每个bit位代表一个fd,0代表未就绪,1代表就绪 __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS]; // ...} fd_set;
// select函数,用于监听fd_set,也就是多个fd的集合int select( int nfds, // 要监视的fd_set的最大fd + 1 fd_set *readfds, // 要监听读事件的fd集合 fd_set *writefds,// 要监听写事件的fd集合 fd_set *exceptfds, // // 要监听异常事件的fd集合 // 超时时间,null-用不超时;0-不阻塞等待;大于0-固定等待时间 struct timeval *timeout);

  可以看到 select 函数 里面有5个属性。ndfs 用来遍历fd集合的,当遍历到最大值就说明集合遍历完了;timeout 超时时间;fd_set 读/写/异常 三种集合,可以看到他是一个长度为32的数组,每个元素是32bit,每个数组1024bit 也就是每种fd的监听上限个数是1024个。

  1 建立连接时创建 fd_set ,然后用户进程调用 select 函数。2 内核进程在 timeout 前遍历 fd_set 等待数据就绪或超时,数据准备好后 就从内核空间 缓冲区 拷贝到 用户空间 缓冲区,并返回就绪的df个数。3 用户进程遍历整个 fd_set 找到就绪的 fd 。

  

poll

/
/ pollfd 中的事件类型#define POLLIN     //可读事件#define POLLOUT    //可写事件#define POLLERR    //错误事件#define POLLNVAL   //fd未打开
// pollfd结构struct pollfd { int fd; /* 要监听的fd */ short int events; /* 要监听的事件类型:读、写、异常 */ short int revents;/* 实际发生的事件类型 */};
// poll函数int poll( struct pollfd *fds, // pollfd数组,可以自定义大小 nfds_t nfds, // 数组元素个数 int timeout // 超时时间);


  poll 相比于 select 做了几点优化。首先 poll 函数的 fds 数组大小可以自定义,理论上无限大。(由于需要遍历整个数组找到就绪的 fd ,所以他不可能特别的大);其次所有的 fd 事件都封装在 pollfd 类型里面,包括 fd句柄 监听的事件 实际发生的事件。

epoll


struct eventpoll {    //...    struct rb_root  rbr; // 一颗红黑树,记录要监听的FD    struct list_head rdlist;// 一个链表,记录就绪的FD    //...};
// 1.创建一个epoll实例,内部是event poll,返回对应的句柄epfdint epoll_create(int size);
// 2.将一个FD添加到epoll的红黑树中,并设置ep_poll_callback// callback触发时,就把对应的FD加入到rdlist这个就绪列表中int epoll_ctl( int epfd, // epoll实例的句柄 int op, // 要执行的操作,包括:ADD、MOD、DEL int fd, // 要监听的FD struct epoll_event *event // 要监听的事件类型:读、写、异常等);
// 3.检查rdlist列表是否为空,不为空则返回就绪的FD的数量int epoll_wait( int epfd, // epoll实例的句柄 struct epoll_event *events, // 空event数组,用于接收就绪的FD int maxevents, // events数组的最大长度 int timeout // 超时时间,-1用不超时;0不阻塞;大于0为阻塞时间);


  1 建立连接时创建 fd_set ,然后用户进程调用 epoll_create 函数,创建 eventpoll 对象,里面通过 红黑树 记录要监听的 fd,还有一个 链表 记录就绪的 fd。2 调用 epoll_ctl 函数往红黑树中添加要监听的 fd ,并且为每一个 fd 函数添加 callback 函数,当 callback 触发时,就把相应的 fd 添加到 就绪链表中(返回具体的 fd 和 fd 数量,而不是像 select/poll 那样只返回个数量)。3 调用 epoll_wait 等待并检查链表是否为空,然后对具体的 fd 进行操作,同时将 fd 从链表中摘除。(多个进程监听同一个 eventpoll 时,调用epoll_wait 监听链表,当任意一个fd就绪时,所有的进程都会被通知到,这种就是惊群现象。)

  1 基于epoll实例中的红黑树保存要监听的FD,理论上无上限,而且增删改查效率都非常高
  2 每个FD只需要执行一次epoll_ctl添加到红黑树,以后每次epol_wait无需传递任何参数,无需重复拷贝FD到内核空间
  3 利用ep_poll_callback机制来监听FD状态,无需遍历所有FD,因此性能不会随监听的FD数量增多而下降

  

事件通知机制

  当FD有数据可读时,我们调用epoll_wait(或者select、poll)可以得到通知。但是事件通知的模式有两种:LevelTriggered:简称LT,也叫做水平触发,默认是这种。只要某个FD中有数据可读,每次调用epoll_wait都会得到通知。EdgeTriggered:简称ET,也叫做边沿触发。只有在某个FD有状态变化时,调用epoll_wait才会被通知。


LT 举个栗子:  1. 假设一个客户端socket对应的FD已经注册到了epoll实例中  2. 客户端socket发送了2kb的数据  3. 服务端调用epoll_wait,得到通知说FD就绪  4. 服务端从FD读取了1kb数据   5. 回到步骤3(再次调用epoll_wait,形成循环)

如果是 LT 模式,则事件通知频率较高,需要重复进行通知 完成对整个数据流操作,影响性能。

==============
ET 流程的话,它仅通知一次,效率高,我们只需要对FD操作一次,在第四步 读取1kb之后就不管了。所以我们需要手动修改策略,要么每次操作fd后 如果有剩余数据,则把fd放入链表,循环操作;要么一次性读取整个fd。


信号驱动io(同步io)

  事先发出一个请求,当有数据后会返回一个标识回调,这时你可以去请求数据。复制数据时阻塞。信号驱动IO是与内核建立SIGIO的信号关联并设置回调,当内核有FD就绪时,会发出SIGIO信号通知用户,期间用户应用可以执行其它业务,无需阻塞等待。用户进程收到SIGIO信号就调用recvfrom读取内核空间缓存区数据。(好比银行排号,当叫到你的时候,你就可以去处理业务了)。

  

异步io

  发出请求就返回,剩下的事情会异步自动完成,不需要做任何处理。(好比有事秘书干,自己啥也不用管)  

  用户进程调用aio_read,创建信号回调函数,然后内核等待数据就绪,用户进程无需阻塞,可以做任何事情。当内核数据就绪,就把数据从内核数据拷贝到用户空间。拷贝完成,内核递交信号触发aio_read中的回调函数,用户进程处理数据

  

总结

  

链接:https://www.cnblogs.com/wlwl/p/10291397.html

(版权归原作者所有,侵删)


微信扫码关注该文公众号作者

来源:马哥Linux运维

相关新闻

图解大模型推理优化之KV Cache图解大模型计算加速系列:Flash Attention V1,从硬件到计算逻辑一顿饭的事儿,搞懂 Linux 5 种 IO 模型CVPR 2024 | OmniParser:统一图文解析模型:文字检测识别、视觉信息抽取和表格识别KDD 2024|港大黄超团队深度解析大模型在图机器学习领域的「未知边界」图像解码器;多头混合专家网络;视觉模型美学对齐;医学视觉任务适应基准为什么要做长文本、长图文、长语音的大模型?深度解读讯飞星火V3.5春季上新巧解「数据稀缺」问题!清华开源GPD:用扩散模型生成神经网络参数|ICLR 2024日经指数大跌,彭博模型解密背后原因图解台积电斯坦福博士图解AlphaFold 3:超多细节+可视化还原ML工程师眼中的AF3图解各类大学专业,很实用!linux网络运维命令--route一步搞定:详解Linux磁盘分区扩容方法图解:多租户系统架构设计Linux 网络参数和 ifconfig图解海报|《国家药监局关于发布优化化妆品安全评估管理若干措施的公告》系列解读(一)图解海报|《国家药监局关于发布优化化妆品安全评估管理若干措施的公告》系列解读(二)一图解密:撰写高质量SCI论文的正确姿势(2024)Linux网络参数和ifconfig图解Transformer架构设计一图解榜 | Chambers钱伯斯篇图解海报 |《化妆品检查管理办法》系列解读(一)图解海报 |《化妆品检查管理办法》系列解读(二)
logo
联系我们隐私协议©2024 bendi.news
Bendi新闻
Bendi.news刊载任何文章,不代表同意其说法或描述,仅为提供更多信息,也不构成任何建议。文章信息的合法性及真实性由其作者负责,与Bendi.news及其运营公司无关。欢迎投稿,如发现稿件侵权,或作者不愿在本网发表文章,请版权拥有者通知本网处理。