框架篇:见识一下linux高性能网络IO+Reactor模型

cscw 2021-06-23 22:14:04
java 程序员 linux reactor


前言

网络I/O,可以理解为网络上的数据流。通常我们会基于socket与远端建立一条TCP或者UDP通道,然后进行读写。单个socket时,使用一个线程即可高效处理;然而如果是10K个socket连接,或者更多,我们如何做到高性能处理?

  • 基本概念介绍
  • 网络I/O的读写过程
  • linux下的五种网络I/O模型
  • 多路复用I/O深入理解一波
  • Reactor模型
  • Proacotr模型

关注公众号,一起交流 :潜行前行

github地址,感谢star

基本概念介绍

  • 进程(线程)切换

    * 所有系统都有调度进程的能力,它可以挂起一个当前正在运行的进程,并恢复之前挂起的进程
  • 进程(线程)的阻塞

    * 运行中的进程,有时会等待其他事件的执行完成,比如等待锁,请求I/O的读写;进程在等待过程会被系统自动执行阻塞,此时进程不占用CPU
  • 文件描述符

    * 在Linux,文件描述符是一个用于表述指向文件引用的抽象化概念,它是一个非负整数。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符
  • linux信号处理

    * Linux进程运行中可以接受来自系统或者进程的信号值,然后根据信号值去运行相应捕捉函数;信号相当于是硬件中断的软件模拟
    

在零拷贝机制篇章已介绍过 用户空间和内核空间缓冲区,这里就省略了

网络IO的读写过程

  • 当在用户空间发起对socket套接字的读操作时,会导致上下文切换,用户进程阻塞(R1)等待网络数据流到来,从网卡复制到内核;(R2)然后从内核缓冲区向用户进程缓冲区复制。此时进程切换恢复,处理拿到的数据
  • 这里我们给socket读操作的第一阶段起个别名R1,第二阶段称为R2
  • 当在用户空间发起对socket的send操作时,导致上下文切换,用户进程阻塞等待(1)数据从用户进程缓冲区复制到内核缓冲区。数据copy完成,此时进程切换恢复

linux五种网络IO模型

阻塞式I/O (blocking IO)

ssize_t recvfrom(int sockfd,void *buf,size_t len,unsigned int flags, struct sockaddr *from,socket_t *fromlen);

  • 最基础的I/O模型就是阻塞I/O模型,也是最简单的模型。所有的操作都是顺序执行的
  • 阻塞IO模型中,用户空间的应用程序执行一个系统调用(recvform),会导致应用程序被阻塞,直到内核缓冲区的数据准备好,并且将数据从内核复制到用户进程。最后进程才被系统唤醒处理数据
  • 在R1、R2连续两个阶段,整个进程都被阻塞

非阻塞式I/O (nonblocking IO)

  • 非阻塞IO也是一种同步IO。它是基于轮询(polling)机制实现,在这种模型中,套接字是以非阻塞的形式打开的。就是说I/O操作不会立即完成,但是I/O操作会返回一个错误代码(EWOULDBLOCK),提示操作未完成
  • 轮询检查内核数据,如果数据未准备好,则返回EWOULDBLOCK。进程再继续发起recvfrom调用,当然你可以暂停去做其他事
  • 直到内核数据准备好,再拷贝数据到用户空间,然后进程拿到非错误码数据,接着进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态
  • 进程在R2阶段阻塞,虽然在R1阶段没有被阻塞,但是需要不断轮询

多路复用I/O (IO multiplexing)

  • 一般后端服务都会存在大量的socket连接,如果一次能查询多个套接字的读写状态,若有任意一个准备好,那就去处理它,效率会高很多。这就是“I/O多路复用”,多路是指多个socket套接字,复用是指复用同一个进程
  • linux提供了select、poll、epoll等多路复用I/O的实现方式
  • select或poll、epoll是阻塞调用
  • 与阻塞IO不同,select不会等到socket数据全部到达再处理,而是有了一部分socket数据准备好就会恢复用户进程来处理。怎么知道有一部分数据在内核准备好了呢?答案:交给了系统系统处理吧
  • 进程在R1、R2阶段也是阻塞;不过在R1阶段有个技巧,在多进程、多线程编程的环境下,我们可以只分配一个进程(线程)去阻塞调用select,其他线程不就可以解放了吗

信号驱动式I/O (SIGIO)

  • 需要提供一个信号捕捉函数,并和socket套接字关联;发起sigaction调用之后进程就能解放去处理其他事
  • 当数据在内核准备好后,进程会收到一个SIGIO信号,继而中断去运行信号捕捉函数,调用recvfrom把数据从内核读取到用户空间,再处理数据
  • 可以看出用户进程是不会阻塞在R1阶段,但R2还是会阻塞等待

异步IO (POSIX的aio_系列函数)

  • 相对同步IO,异步IO在用户进程发起异步读(aio_read)系统调用之后,无论内核缓冲区数据是否准备好,都不会阻塞当前进程;在aio_read系统调用返回后进程就可以处理其他逻辑
  • socket数据在内核就绪时,系统直接把数据从内核复制到用户空间,然后再使用信号通知用户进程
  • R1、R2两阶段时进程都是非阻塞的

多路复用IO深入理解一波

select

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • 1)使用copy_from_user从用户空间拷贝fd_set到内核空间
  • 2)注册回调函数__pollwait
  • 3)遍历所有fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据情况会调用到tcp_poll,udp_poll或者datagram_poll)
  • 4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数
  • 5)\__pollwait的主要工作就是把current(当前进程)挂到设备的等待队列中,不同的设备有不同的等待队列,对于tcp_poll来说,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不代表进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了
  • 6)poll方法返回时会返回一个描述读写操作是否就绪的mask掩码,根据这个mask掩码给fd_set赋值
  • 7)如果遍历完所有的fd,还没有返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠
  • 8) 当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。如果超过一定的超时时间(timeout指定),还是没人唤醒,则调用select的进程会重新被唤醒获得CPU,进而重新遍历fd,判断有没有就绪的fd
  • 9)把fd_set从内核空间拷贝到用户空间

select的缺点

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大
  • select支持的文件描述符数量太小了,默认是1024

epoll

int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 
  • 调用epoll_create,会在内核cache里建个红黑树用于存储以后epoll_ctl传来的socket,同时也会再建立一个rdllist双向链表用于存储准备就绪的事件。当epoll_wait调用时,仅查看这个rdllist双向链表数据即可
  • epoll_ctl在向epoll对象中添加、修改、删除事件时,是在rbr红黑树中操作的,非常快
  • 添加到epoll中的事件会与设备(如网卡)建立回调关系,设备上相应事件的发生时会调用回调方法,把事件加进rdllist双向链表中;这个回调方法在内核中叫做ep_poll_callback

epoll的两种触发模式

  • epoll有EPOLLLT和EPOLLET两种触发模式,LT是默认的模式,ET是“高速”模式(只支持no-block socket)

    * LT(水平触发)模式下,只要这个文件描述符还有数据可读,**每次epoll_wait都会触发它的读事件**
    * ET(边缘触发)模式下,检测到有I/O事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于文件描述符,如可读,则必须将该文件描述符一直读到空(或者返回EWOULDBLOCK),**否则下次的epoll_wait不会触发该事件**
    

epoll相比select的优点

  • 解决select三个缺点

    * **对于第一个缺点**:epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次(epoll_wait不需要复制)
    * **对于第二个缺点**:epoll为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(不需要遍历)
    * **对于第三个缺点**:epoll没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,一般来说这个数目和系统内存关系很大
  • epoll的高性能

    * epoll使用了红黑树来保存需要监听的文件描述符事件,epoll_ctl增删改操作快速
    * epoll不需要遍历就能获取就绪fd,直接返回就绪链表即可
    * linux2.6 之后使用了mmap技术,数据不在需要从内核复制到用户空间,零拷贝
    

关于epoll的IO模型是同步异步的疑问

  • 概念定义

    * 同步I/O操作:导致请求进程阻塞,直到I/O操作完成
    * 异步I/O操作:不导致请求进程阻塞,异步只用处理I/O操作完成后的通知,并不主动读写数据,由系统内核完成数据的读写
    * 阻塞,非阻塞:进程/线程要访问的数据是否就绪,进程/线程是否需要等待
  • 异步IO的概念是要求无阻塞I/O调用。前面有介绍到I/O操作分两阶段:R1等待数据准备好。R2从内核到进程拷贝数据。虽然epoll在2.6内核之后采用mmap机制,使得其在R2阶段不需要复制,但是它在R1还是阻塞的。因此归类到同步IO

Reactor模型

Reactor的中心思想是将所有要处理的I/O事件注册到一个中心I/O多路复用器上,同时主线程/进程阻塞在多路复用器上;一旦有I/O事件到来或是准备就绪,多路复用器返回,并将事先注册的相应I/O事件分发到对应的处理器中

相关概念介绍:

  • 事件:就是状态;比如:读就绪事件指的是我们可以从内核读取数据的状态
  • 事件分离器:一般会把事件的等待发生交给epoll、select;而事件的到来是随机,异步的,所以需要循环调用epoll,在框架里对应封装起来的模块就是事件分离器(简单理解为对epoll封装)
  • 事件处理器:事件发生后需要进程或线程去处理,这个处理者就是事件处理器,一般和事件分离器是不同的线程

    Reactor的一般流程

  • 1)应用程序在事件分离器注册读写就绪事件读写就绪事件处理器
  • 2)事件分离器等待读写就绪事件发生
  • 3)读写就绪事件发生,激活事件分离器,分离器调用读写就绪事件处理器
  • 4)事件处理器先从内核把数据读取到用户空间,然后再处理数据

单线程 + Reactor

多线程 + Reactor

多线程 + 多个Reactor

Proactor模型的一般流程

  • 1)应用程序在事件分离器注册读完成事件读完成事件处理器,并向系统发出异步读请求
  • 2)事件分离器等待读事件的完成
  • 3)在分离器等待过程中,系统利用并行的内核线程执行实际的读操作,并将数据复制进程缓冲区,最后通知事件分离器读完成到来
  • 4)事件分离器监听到读完成事件,激活读完成事件的处理器
  • 5)读完成事件处理器直接处理用户进程缓冲区中的数据

    Proactor和Reactor的区别

  • Proactor是基于异步I/O的概念,而Reactor一般则是基于多路复用I/O的概念
  • Proactor不需要把数据从内核复制到用户空间,这步由系统完成

欢迎指正文中错误

参考文章

版权声明
本文为[cscw]所创,转载请带上原文链接,感谢
https://segmentfault.com/a/1190000040229031

  1. redis cluster如何支持pipeline
  2. How does redis cluster support pipeline
  3. 上海 | 人英网络 | 招Java开发25-35K、React前端开发25-40K
  4. Shanghai | Renying network | recruit java development 25-35k, react front end development 25-40k
  5. SpringCloud+Docker+Jenkins+GitLab+Maven实现自动化构建与部署实战
  6. Spring cloud + docker + Jenkins + gitlab + Maven to realize automatic construction and deployment
  7. 性能工具之linux三剑客awk、grep、sed详解
  8. Performance tools of Linux three swordsmen awk, grep, sed
  9. 一次“不负责任”的 K8s 网络故障排查经验分享
  10. An "irresponsible" experience sharing of k8s network troubleshooting
  11. 性能工具之linux三剑客awk、grep、sed详解
  12. Performance tools of Linux three swordsmen awk, grep, sed
  13. 使用Spring Data JPA 访问 Mysql 数据库-配置项
  14. Accessing MySQL database with spring data JPA - configuration item
  15. 一次“不负责任”的 K8s 网络故障排查经验分享
  16. An "irresponsible" experience sharing of k8s network troubleshooting
  17. 注册中心ZooKeeper,Eureka,Consul,Nacos对比
  18. Linux最常用的指令大全!快看看你掌握了吗?
  19. Comparison of zookeeper, Eureka, consult and Nacos
  20. Linux most commonly used instruction encyclopedia! Let's see. Do you have it?
  21. Matrix architecture practice of Boshi fund's Internet open platform based on rocketmq
  22. 字节面试,我这样回答Spring中的循环依赖,拿下20k offer!
  23. Byte interview, I answer the circular dependence in spring like this, and get 20K offer!
  24. oracle 11g查看alert日志方法
  25. How to view alert log in Oracle 11g
  26. 手写Spring Config,最终一战,来瞅瞅撒!
  27. Handwritten spring config, the final battle, come and see!
  28. 用纯 JavaScript 撸一个 MVC 框架
  29. Build an MVC framework with pure JavaScript
  30. 使用springBoot实现服务端XML文件的前端界面读写
  31. Using springboot to read and write the front interface of server XML file
  32. 【Javascript + Vue】实现随机生成迷宫图片
  33. [Javascript + Vue] random generation of maze pictures
  34. 大数据入门:Hadoop伪分布式集群环境搭建教程
  35. Introduction to big data: Hadoop pseudo distributed cluster environment building tutorial
  36. 八股文骚套路之Java基础
  37. commons-collections反序列化利用链分析(3)
  38. Java foundation of eight part wensao routine
  39. Analysis of common collections deserialization utilization chain (3)
  40. dubbogo 社区负责人于雨说
  41. Yu Yu, head of dubbogo community, said
  42. dubbogo 社区负责人于雨说
  43. Yu Yu, head of dubbogo community, said
  44. 设计模式 选自《闻缺陷则喜》此书可免费下载
  45. The design pattern is selected from the book "you are happy when you hear defects", which can be downloaded free of charge
  46. xDAI被选为 Swarm 的侧链解决方案,将百倍降低 Swarm 网络Gas费
  47. L2 - 深入理解Arbitrum
  48. Xdai is selected as the side chain solution of swarm, which will reduce the gas cost of swarm network 100 times
  49. L2 - deep understanding of arbitrum
  50. Java全栈方向学习路线
  51. 设计模式学习04(Java实现)——单例模式
  52. Java full stack learning route
  53. Design pattern learning 04 (Java implementation) - singleton pattern
  54. Mybatis学习01:利用mybatis查询数据库
  55. Mybatis learning 01: using mybatis to query database
  56. Java程序员从零开始学Vue(01)- 前端发展史
  57. Java程序员从零开始学Vue(05)- 基础知识快速补充(html、css、js)
  58. Java programmers learn Vue from scratch
  59. Java programmers learn Vue from scratch (05) - quick supplement of basic knowledge (HTML, CSS, JS)
  60. 【Java并发编程实战14】构建自定义同步工具(Building-Custom-Synchronizers)