多进程 excel service ionic framework Zeptojs jquery绑定change事件 python查看数据类型 matlab注释一段 matlab对数函数 abaqus是什么软件 python数据 python迭代 python生成多个随机数 python位运算 python中的map函数 javapackage java实例 java实战 java可变参数 javahttp java定义字符串 java系统学习 java字符比较 kafka中文教程 图吧导航怎么样 机械下载 高等数学同济第七版 h370主板 海妖花粉哪里多 subscribe 怎么设置迅雷为默认下载器 git命令 pr蒙版怎么用 密码翻译 kz文件 逆战丛林魅影 adb安装 美图秀秀pc版下载 ps平面设计基础教程 cad圆形变成多边形
当前位置: 首页 > 学习教程  > 编程语言

多路复用笔记

2020/12/5 10:21:56 文章标签:

多路复用 1. BIO 的缺陷 BIO中的B 是 Blocking 的阻塞的意思作为服务端开发,使用ServerSocket 绑定端口号之后会监听该端口,等待accept事件,accept是会阻塞当前线程当我们收到accept事件的时候,程序就会拿到客户端与当前服务端连接的Socket…

多路复用

1. BIO 的缺陷

  • BIO中的B 是 Blocking 的阻塞的意思
  • 作为服务端开发,使用ServerSocket 绑定端口号之后会监听该端口,等待accept事件,accept是会阻塞当前线程
  • 当我们收到accept事件的时候,程序就会拿到客户端与当前服务端连接的Socket
  • 针对这个socket我们可以进行读写,但是呢,这个socket读写都是会阻塞当前线程的。
  • 一般我们会有使用多线程方式进行c/s交互,但是这样很难做到C10K(比如说:1W个客户端就需要和服务端用1W个线程支持,这样的话CPU肯定就爆炸了,同时线程上下文切换也会把机器负载给拉飞。)

2. NIO 解决C10K问题

2.1 Java角度

  • 站在java 角度去看,NIO包提供了一套非阻塞的接口,这样就不需要我们为每一个c/s长连接保留一个单独的处理线程了。
  • 这个阻塞BIO之所以需要给每个socket长连接指定一个线程,就是因为它阻塞
  • 现在这个NIO API具有非阻塞的特性了,就可以用1个线程去检查n个socket
  • 在 java 层面,nio 提供了一个这样的选择器selector
  • 然后我们需要把需要检查的socket注册到这个selector,然后主线程阻塞在selectorselect方法中。
  • 当选择器发现socket就绪了,某个socket就绪了。就会唤醒主线程,
  • 然后咱们可以通过selector 获取就绪状态的socket 进行相应的处理。

其实这里selector 里面是native api ,底层Jvm调用SystemCall kernel去实现的

3. select(…) 实现原理

  • 每次调用kernel 的 select函数,都会涉及到用户态/内核态的切换

    还需要传递需要检查的socket集合

    ,其实就是需要检查的

    fd
    

    (文件描述符)集合

    • 因为咱的程序都是运行在linux或者unix操作系统上,这种操作系统,一切皆文件,socket也不例外,这里传递的fd其实就是文件系统中对应socket生成的文件描述符
  • 操作系统 这个select函数被调用以后,

    • 首先会去fd集合中去检查内存中socket套接字的状态,这个时间复杂度是O(N),然后检查完一遍之后,如果有就绪状态的socket,那么就会直接返回,不会阻塞当前线程。
    • 否则的话,那个就说明当前指定fd集合对应的socket没有就绪状态,那么就需要阻塞当前调用线程了,直到有某个socket有数据之后,才唤醒线程。

select(…) 对监听socket有1024的大小限制

  • 这个是因为

    fd
    

    集合这个结构是一个

    bitmap位图的结构

    ,这个位图结构就是一个长的二进制数,类似

    0101
    

    这种

    • 这个bitmap默认长度是1024个bit,想要修改长度非常麻烦,需要重新编译操作系统内核
  • 处于某种性能考虑,

    select
    

    函数做了两件事

    • 第一件事,跑到就绪状态的socket对应的fd文件中设置一个标记mask,表示这个fd对应的socket就绪了

    • 第二件事

      ,返回

      select
      

      函数,对应的也就是唤醒java线程,站在java层面,

      他会收到一个int结果值,表示有几个socket处于就绪状态

      • 但是具体是哪个socket就绪,java是不知道的,所以接下来会是一个O(N)的系统调用,检查fd集合中每一个socket的就绪状态,其实就是检查文件系统中指定socket的文件描述符的状态,涉及到用户态和内核态的来回切换,如果bitmap再大,就非常耗费性能
      • 还有就是系统调用涉及到参数的数据拷贝,如果数据太庞大,他也不利于系统的调用速度

4. select(…) 深入问题

问题:select (…) 第一遍 O(N) 去检查未发现就绪的socket ,后续某个socket就绪后,select(…)是如何感知道的?是不断的轮询吗?

铺垫知识

  1. 操作系统调度
  • cpu同一时刻,它只能运行一个进程,操作系统做主要的任务就是系统调度,就是有n个进程,然后让这n个进程在cpu上切换进行
  • 未挂起的进程都在工作队列内,都有机会获取到cpu的执行权
  • 挂起的进程就会从这个工作队列里移除出去,反映到咱们java层面就是线程阻塞了
  • linux系统线程其实就是轻量级进程
  1. 操作系统中断
  • 比如说,咱们用键盘打字,如果cpu正执行其他程序,一直不释放,那咱这个打字就也没法打了
  • 咱们都知道,不是这样的情况,因为就是有系统中断的存在,当按下一个键以后会给主板发送一个电流信号,主板感知到以后,它就会触发这个cpu中断、
  • 所以中断 其实就是让cpu给正在执行的进程先保留程序上下文,然后避让出cpu,给中断程序绕道
  • 中断程序就会拿到cpu的执行权限,进行相应代码的执行,比如说键盘的中断程序,就会执行输出的逻辑

回到最开始的问题

  • 这个select函数,它第一遍轮询,没有发现就绪状态的socket的话,它就会把当前进程保留给需要检查的socket等待队列中

  • socket 结构 有三块核心区域,分别就是读缓存,写缓存还有这个等待队列

  • 这个 select 函数,它会把当前进程保留到每个需要检查的socket 的等待队列中,就会把当前进程从工作队列里面移除了,移除了之后其实就是挂起了当前线程,然后这个select 函数也就不会再运行了

  • 下一个阶段,假设我们客户端往当前服务器发送了数据,数据通过网线到网卡,网卡再到DMA硬件的这种方式直接将数据写到内存里面,然后整个过程,CPU是不参与的

  • 当传输完成以后,它就会触发网络数据传输完毕的中断程序,这个中断程序它会把cpu正在执行的进程给顶掉,然后

    cpu就会执行咱这个中断程序的逻辑

    • 对应的逻辑是:根据内存中的数据包,然后分析出来数据包是哪个socket的数据,
    • 同时tcp/ip它又是保证传输的时候是有端口号的,然后根据端口号就能找到对用的socket实例,找到socket实例以后,就会把数据导入到socket读缓冲里面
    • 导入完成以后,它就开始去检查socket等待队列,看是不是有等待者,如果有等待者的话,就会把等待者移动到工作队列里面去,中断程序到这一步就执行完了
    • 这样咱们的进程就又回到了工作队列,又有机会获取到cpu时间片了
  • 然后当前进程执行的select函数再次检查,就会发现这个就绪的socket了,就会给就绪的socketfd文件描述符打标记,然后select函数就执行完了,返回到java层面就涉及到内核态和用户态的转换,后面的事情就是轮询检查每一个socket的fd是否被打了标记,然后就是处理被打了标记的socket就ok了

5. poll() 和 select()区别

  • 传参不一样
    • **select 用的是bitmap ,**它表示需要检查的socket集合
    • **poll 使用的是 链表结构,**表示需要检查的socket集合(主要是为了解决socket监听长度超过1024的socket的限制)

6. epoll 的 产生背景

  • select 和 poll 的共有缺陷

    • 第一个缺陷:

      select
      

      poll
      

      函数,

      这两系统函数每次调用都需要我们提供给它所有的需要监听的socket文件描述符集合,而且主线程是死循环调用select/poll函数的,这里面涉及到用户空间数据到内核空间拷贝的过程

      • 咱们需要监听的socket集合,数据变化非常小
      • 每次就一到两个socket_fd需要更改,但是没有办法,因为select和poll函数,只是一个很单纯的函数
      • 它在kernel层面,不会保留任何的数据信息,所以说每次调用都进行了数据拷贝
    • 第二个缺陷:

      select 和 poll 函数它的返回值都是int整型值,只能代表有几个socket就绪或者有错误了,它没办法表示具体是哪个socket就绪了

      • 这就导致了程序被唤醒以后,还需要新的一轮系统调用去检查哪个socket是就绪状态的,然后再进行socket数据处理逻辑,这里走了不少弯路(同时还存在用户态和内核态的切换,这样缺陷就更严重了)

epoll 就是为了解决这两个问题

7. epoll (…) 实现原理

  • epoll 函数

    在内核空间内,它有一个对应的数据结构去存储一些数据,这个数据结构其实就是eventpoll对象

    • 这个eventpoll 可以通过一个系统函数epoll_create()函数去创建的
  • 创建完成之后,系统函数返回一个eventpoll对象的id,相当于我们在内核空间开辟了一小块空间,并且我们也知道这块空间的位置

先说下eventpoll 的数据结构:三块重要的区域

  • 一块是存放需要监听的socket_fd描述符列表
  • 另一块就是就绪列表,存放就绪状态的socket信息
  • eventpoll 还有一块空间是eventpoll等待队列,这个等待队列保存的就是调用epoll_wait的进程
  • 另外呢还提供了两个函数,一个是epoll_ctl函数,一个是epoll_wait函数
  • 其中存放的socket集合信息采用的是红黑树的数据结构,socket集合信息经常用增删改查的,这种红黑树再适合不过了,保持了时间复杂度为O(logN)

epoll_ctl()

  • 它可以根据eventpoll-id去增删改内核空间上eventpoll 对象的检查列表(socket信息)

**epoll_wait() **

  • 它主要的参数是eventpoll-id 表示此次系统调用需要检测的socket_fd集合,是eventpoll 中已经指定好的那些socket信息

  • epoll_wait 默认情况下会阻塞系统的调用线程,直到eventpoll 对象中关联的某个或者某些个socket就绪以后,epoll_wait函数才会返回

  • 返回值是

    Int
    

    类型的

    • 返回0,表示没有就绪的socket
    • 返回大于0,表示有几个就绪的socket
    • 返回-1表示异常

8. eventpoll 对象就绪列表的维护

select函数调用的流程:

  • socket对象有三块区域
    • 读缓冲区
    • 写缓冲区
    • 等待队列
  • select函数调用的时候会把当前进程从工作队列里面拿出来
  • 然后把进程引用追加到当前进程关注的每一个socket对象的等待队列中
  • 然后当socket连接的客户端发送完数据之后,数据还是通过硬件DMA的方式把数据写入到内存,然后相应的硬件向CPU发出中断信号,CPU就会让出当前进程位置去执行网络数据就绪的中断程序,
  • 这个中断程序就会把内存中的网络数据写入到对应的socket读缓冲区里面,之后把这个socket等待队列中的进程全部移动到工作队列中,再然后select函数返回

epoll函数流程非常相似

  • 当我们调用系统函数epoll_ ctl时候,比如我们新添加一个需要关注的socket,其实内核程序会把当前的eventpoll对象追加到这个socket的等待队列里头
  • 然后当socket连接的客户端发送完数据之后,数据还是通过硬件DMA的方式把数据写入到内存,然后相应的硬件向CPU发出中断信号,CPU就会让出当前进程位置去执行网络数据就绪的中断程序,
  • 这个中断程序就会把内存中的网络数据写入到对应的socket读缓冲区里面,然后它发现这个socket的等待队列里头不是进程,而是一个eventpoll对象的引用
  • 这个时候呢,他就会根据这个eventpoll对象的引用,将当前socket的引用追加到eventpoll的就绪链表的末尾eventpoll 还有一块空间是eventpoll 的等待队列,这个等待队列保存的就是调用epoll_wait的进程)
  • 然后,当中断程序把socket的引用追加到就绪列表的末尾之后,就继续检查eventpoll对象的等待队列,如果有进程,就会把进程转移到工作队列中
  • 转移完毕之后,进程就有获取到CPU执行的时间片了,然后就是调用epoll_wait 函数,他这个函数就返回到java层面了

总结:

  • eventpoll对象等待队列里面,它有调用epoll_wait(,)函数进去的进程
  • 然后再把这个进程,从这个eventpoll的等待队列里面迁移到工作队列里面

9. epoll_wait() 获取就绪的socket

epoll_wait() 返回值是Int类型的

  • 返回0,表示没有就绪的socket
  • 返回大于0,表示有几个就绪的socket
  • 返回-1表示异常

那么获取就绪的socket是怎么实现的呢?

  • epoll_wait 函数,调用的时候会传入一个epoll_event事件数组指针
  • epoll_wait 函数正常返回之前,会把就绪的socket事件信息拷贝到这个数组指针里头
  • 这样返回到上层程序,就能通过这个数组拿到就绪列表

10. epoll_wait 可不可以设置成非阻塞的

  • 默认epoll_wait 是阻塞的
  • 它有一个参数,表示阻塞时间的长度,如果这个参数设置为0,表示这个epoll_wait 是一个非阻塞调用的
  • 每次调用都会去检查就绪列表

本文链接: http://www.dtmao.cc/news_show_450208.shtml

附件下载

相关教程

    暂无相关的数据...

共有条评论 网友评论

验证码: 看不清楚?