精华内容
下载资源
问答
  • 多路复用器

    2020-09-27 23:35:35
    多路复用器基本知识多进程/多线程连接处理模型多路复用连接处理模型工作原理selectpollepollLT模式ET模式 基本知识 多进程/多线程连接处理模型 在该模型下,一个用户连接请求会由一个内核进程处理,而一个内核进程...

    基本知识

    多进程/多线程连接处理模型

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-68Y9WyqG-1601220469495)(C:\Users\bbxyl\Desktop\nginx\image-20200927225215149.png)]
    在该模型下,一个用户连接请求会由一个内核进程处理,而一个内核进程会创建一个应用程序进程,即app进程来处理该连接请求。应用程序进程在调用IO时,采用的是BIO(阻塞IO)通讯方式,即应用程序进程在未获取到IO响应之前是处于阻塞态的。

    优点

    • 内核进程不存在对app进程的竞争,一个内核进程对应一个app进程

    缺点

    • 若请求很多,需要创建很多的app进程
    • 一个系统的进程数量是有上限的,所以该模型不能处理高并发的情况
    • app进程中使用的用户连接请求数据是复制于内核进程的,没有使用零拷贝,效率低,消耗系统资源

    多路复用连接处理模型

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-djg0iWCu-1601220469498)(C:\Users\bbxyl\Desktop\nginx\image-20200927225714285.png)]
    在该模型下,只有一个app进程来处理内核进程事务,且app进程一次只能处理一个内核进程事务。故这种模型对于内核进程来说,存在对app进程的竞争。

    在该模型下,需要通过多路复用器来获取各个内核进程的状态信息,将已经就绪的内核进程交由app进程执行

    多路复用器通过算法分析获取内核进程的状态,常见的算法有三种:selectpollepoll

    在该模型下,app进程采用的是NIO通讯方式,即该app进程不会阻塞。

    当一个IO结果返回时,app进程会暂停当前事务,将IO结果返回给对应的内核进程。然后再继续执行暂停的线程。

    工作原理

    select

    select多路复用器是采用轮询的方式,一直在轮询所有的相关内核进程,查看它们的进程状态。若已经就绪,则马上将该内核进程放入到就绪队列(该就绪队列底层由数组实现)。否则,继续查看下一个内核进程状态。在处理内核进程事务之前,app进程首先会从内核空间中将用户连接请求相关数据复制到用户空间。
    该多路复用器的缺陷有以下几点:

    • 对所有内核进程采用轮询方式效率会很低。因为对于大多数情况下,内核进程都不属于就绪状态,只有少部分才会是就绪态。所以这种轮询结果大多数都是无意义的
    • 由于就绪队列底层由数组实现,所以其所能处理的内核进程数量是有限制的,即其能够处理的最大并发连接数量是有限制的
    • 从内核空间到用户空间的复制,系统开销大。

    poll

    poll多路复用器的工作原理与select几乎相同,不同的是,由于其就绪队列由链表实现,所以,其对于要处理的内核进程数量理论上是没有限制的,即其能够处理的最大并发连接数量是没有限制的(当然,要受限于当前系统中进程可以打开的最大文件描述符数ulimit)。

    epoll

    epoll多路复用器是对select与poll的增强与改进。其不再采用轮询方式了,而是采用回调方式实现对内核进程状态的获取:一旦内核进程就绪,其就会回调epoll多路复用器,进入到多路复用器的就绪队列(由链表实现)。所以epoll多路复用模型也称为epoll事件驱动模型
    另外,应用程序所使用的数据,也不再从内核空间复制到用户空间了,而是使用mmap零拷贝机制,大大降低了系统开销。

    问:当内核进程就绪信息通知了epoll多路复用器后,多路复用器就会马上对其进行处理,将其马上存放到就绪队列吗?

    答:不是的。根据处理方式的不同,可以分为两种处理模式:LT模式ET模式

    LT模式

    LT,Level Triggered,水平触发模式即只要内核进程的就绪通知由于某种原因暂时没有被epoll处理,则该内核进程就会定时将其就绪信息通知epoll。直到epoll将其写入到就绪队列,或由于某种原因该内核进程又不再就绪而不再通知。其支持两种通讯方式:BIO与NIO。

    ET模式

    ET,Edge Triggered,边缘触发模式其仅支持NIO的通讯方式。当内核进程的就绪信息仅会通知一次epoll,无论epoll是否处理该通知。明显该方式的效率要高于LT模式,但其有可能会出现就绪通知被忽视的情况,即连接请求丢失的情况。

    展开全文
  • 多路复用器-源码

    2021-02-17 06:09:49
    多路复用器
  • 提出了一种基于绝热耦合器的双模(de)多路复用器,并进行了实验证明。 实验结果与仿真结果吻合良好。 测量了低于-36 dB的超低模式串扰以及在1500至1600 nm的宽带宽内约0.3 dB的低插入损耗。 该设计还具有制造公差,...
  • Diodes 公司 (Nasdaq:DIOD) 今日宣布推出 PI3L2500 LAN 多路复用器/解复用器,为业界首款用于在新世代企业局域网中,以2.5/5/10千兆每秒(Gbps)的以太网络讯号速度,进行端口切换的切换器。PI3L2500 双向多路复用器/...

    Diodes 公司 (Nasdaq:DIOD) 今日宣布推出 PI3L2500 LAN 多路复用器/解复用器,为业界首款用于在新世代企业局域网中,以2.5/5/10千兆每秒(Gbps)的以太网络讯号速度,进行端口切换的切换器。

    PI3L2500 双向多路复用器/解复用器设计出色,让两个以太网络 PHY 差分输出能以单一 RJ45 接头进行作业,或让一个以太网络 PHY 输出接至两个 RJ45 接头其中之一。这项功能提供设备制造商更强大的备援、弹性和效能,让推出的新设备能以最新的 LAN 速度运作,同时提供更高速度的迁移路径。在 PI3L2500 推出前,LAN 切换器运作速度最高只能达到 1Gbps的速率。

    2b150cb08ae1b93c9b9d48f00fb74fc1.png

    PI3L2500 可以将一个八通道的输入端口多路复用到两个输出端口中的一个,或将两个输入端口多复用到一输出端口。所选的端口会使用单一 SEL 输入以及电源关闭 (PD) 输入,让装置进入待机模式以节省电力。低耗电与高效能同样重要,PI3L2500 在启用模式下表现极佳,最大主动式电流仅 1.5mA (典型 1.0mA),待机电流为 0.3mA (典型),还同时为切换状态 LED 提供三个多路复用通道。

    PI3L2500 能为高速讯号绕送提供符合未来需求的解决方案,讯号带宽高达 2GHz,能承载高达 10Gbps 的资料切换。装置是利用 0.18μm 制程来达到此成效,展现极低导通电阻4Ω 和低开/关电容,分别只有 1.5pF (开) 以及 3.0pF (关)。为了维持讯号保真度,串扰和隔离都相当低,仅-35dB。高速运作也必须依赖于低传输延迟,PI3L2500 典型传输时间仅 0.25ns,藉此提供低传输延迟。

    Diodes 公司资深营销主管 Kay Annamalai 表示:「二十多年来,Diodes 公司投入讯号切换器的开发,为业界首创因应新兴技术的先锋。PI3L2500 2.5GE-10GE LAN 切换器能支持企业内部网络迁移,从 1GE 迁移至 2.5GE 以上的以太网络,范围包括笔记本电脑和扩充坞等应用,同时也支持封包传输网络中的迁移,其中升级能力与备援都十分必要。」

    PI3L2500ZHEX 目前提供 42 接脚 TQFN 封装。

    展开全文
  • 点击下方[阅读原文],查看更多在测试测量相关应用中,模拟开关和多路复用器有着非常广泛的应用,例如运放的增益调节、ADC分时采集多路传感器信号等等。虽然它的功能很简单,但是仍然有很多细节,需要大家在使用的...

       97d042402835e182d4963c2d6aeeb869.png点击下方[阅读原文],查看更多

    在测试测量相关应用中,模拟开关和多路复用器有着非常广泛的应用,例如运放的增益调节、ADC分时采集多路传感器信号等等。虽然它的功能很简单,但是仍然有很多细节,需要大家在使用的过程中注意。所以,在这里为大家介绍一下模拟开关和多路复用器的基础参数。在开始介绍基础的参数之前,我们有必要介绍一下模拟开关和多路复用器的基本单元MOSFET开关的基本结构。一. MOSFET开关的架构MOSFET开关常见的架构有3种,如图1所示。1)NFET。2)NFET和PFET。3)带有电荷泵的NFET。三种架构各有特点,详细的介绍,可以参考《TI Precision Labs - Switches and Multiplexers》培训视频和《Selecting the Right Texas Instruments Signal Switch》应用文档。本文主要基于NFET和PFET架构展开介绍和仿真,但是涉及到的概念在三种架构中都是适用的。f32c63a86a7c3c6f03353d7a9d6f14b3.png图 1 MOSFET开关结构另外,需要注意的是,此处的MOSFET结构,S和D是对称的,所以在功能上是可以互换的,也因此,开关是双向的,为了便于讨论,我们统一把S极作为输入。二.模拟开关和多路复用器直流参数介绍1. 导通电阻 On Resistance(1). 定义7be0cac02a156d99848f05865846e5c0.png图 2 On Resistance 定义(2). 特点1) 随输入信号电压而改变:当芯片的供电电压固定时,对于NMOS而言,S级的电压越高,导通电阻越来越大,对于PMOS而言,S级的电压越高,导通电阻越来越小。81e03c3b125e9cb898be0f5490dc98b7.png图 3 导通电阻随输入信号电压变化的曲线2) 导通电阻的阻值与温度有关:当VDD和VSS固定不变时,随着温度的升高,导通电阻的曲线整体向上平移。eecf709ee3702d673525901353603007.png图 4 导通电阻随温度变化的曲线3) 导通电阻的平坦度:On-resistance flatnesseb17a1afc33584ef2ba38ce1349a27b7.png图 5 On-resistance flatness在一定的输入电压范围内,导通电阻的最大值与最小值的差称为导通电阻的平坦度,这个值越大,说明导通电阻的变化幅度越大。(3). 影响在这里,我们通过一个仿真实例来观察一下导通电阻及平坦度对于系统的影响,如图6。为了更容易地观察到影响,我们选择设置R1和R2为100Ω。3c02be9cd62b4776a39d859b22c0d00c.png图 6 MUX36S08仿真电路bb2c06460da07a12bd22b04c57b0de94.png图 7 输入及输出波形从仿真的结果我们可以看出:1) 输出电压并不是我们输入电压乘以放大比例后的结果,这是因为有导通电阻的存在。d9082cce50278488172c655ac5b88bc9.png2) 输出电压随输入电压的并不是线性关系,这是因为Ron随着Vin在变化,会在输出端引入非线性误差。所以,Ron的平坦度越小,输出的非线性误差越小。2.漏电流 Leakage current(1). 定义1) Source off-leakage current: 在开关断开时,从源极流入或流出的电流称为Is(off),如图8。2) Drain off-leakage current: 在开关断开时,从漏极流入或流出的电流称为 Id(off),如图83) On-leakage current: 当开关闭合时,从漏极流入或流出的电流称为 Id(on),如图8。469f0b8053065f6c5af8bb824aac2f6c.png图 8 漏电流定义(2). 特点漏电流随温度变化剧烈。c7f342cd88947ed53cd78a878e2205b2.png图 9 漏电流随温度变化的曲线(3). 影响在很多数据采集系统中,接入MUX前的传感器有可能是高阻抗的传感器。这时,漏电流的影响就会凸显出来。例如,在图10的仿真中,输入源有1MΩ的源阻抗,我们对这个电阻进行直流参数扫描,观察它从1MΩ变化至10MΩ时,对输出电压的影响,结果可以看到,漏电流通过传感器的内阻会给输出电压带来一个直流误差。所以,在为高输出阻抗的传感器选择MUX时,要尽可能选取低漏电流的芯片。444e9bce9655f0b90365c0536feac467.png图 10 漏电流仿真电路c9831add7c860efc8156dcf7c54b9ac3.png图 11 漏电流仿真结果三. 模拟开关和多路复用器动态参数介绍1. 导通电容 On Capacitance(1). 定义CS和CD代表了开关在断开时的源极和漏极电容。当开关导通时,CON等于源极的电容和漏极的电容之和,如图12。eb330196633361d531033e06fab61564.png图 12 On Capacitance(2). 影响357d22eee04508d04690f2d235550e83.png图 13 MUX36S08 示例当MUX在不同通道之间切换时,CD也会随着通道的切换被充电或者放电。例如,当S1闭合时,CD会被充电至V1。那么此时CD上的电荷QD1:a10e4c82573d17c38a78f23ae8ce29b1.png当MUX从S1切换至S2时,CD会被充电至V2。那么此时CD上的电荷QD2:2139b57baf594a41ae5fe5aec5c1447c.png那么两次CD上的电荷差就需要V2来提供,所以这时候,MUX输出就会需要一定的时间来稳定。7f202cb104dd770f176d0766946580e7.png对于一个N-bit的ADC:55c603d942e90bb1a2896fa90a084eeb.pngK其实是代表RC电路中,电压到达目标误差以内时所需要的时间常数的数量,例如10-bit accuracy (LSB % FS= 0.0977), K= -ln (0.0977/100)=6.931。接下来用一个仿真来说明这种现象:为了更明显地观察到这种现象,在Vout 端加入一个电容C1,可以理解为增加了CD,也可以理解为负载电容和CD的并联。b1cf20c2a2acc1e6525a76c6a541c803.png图 14 On Capacitance对输出影响的仿真示例电路当 C1=50pF时,整个回路的时间常数较大,需要更长时间稳定,所以在开关导通20uS之后,输出电压仍然没有稳定到信号源的电压。d770874858dceee28ea36fb5178f3f1c.png图 15 C1=50pF 仿真结果当 C1=10pF时,整个回路的时间常数较小,需要较短时间稳定,所以在开关导通20uS之内,输出电压稳定到了信号源的电压。d7153cea176589d8dfbcea216e135cb9.png图 16 C1=10pF 仿真结果2. 注入电荷 Charge Injection(1). 定义注入电荷指的是从控制端EN耦合至输出端的电荷。(2). 影响因为在开关导通的通道上,缺乏消耗这部分电荷的通路,所以当这部分电荷流入漏极电容和输出电容上时,会在输出产生一个电压误差。5e09059f0209aea98664e9a1a7de7097.png图 17 Charge Injection过程示意图过程如下:当在EN端有一个阶跃信号时,这个阶跃电压会通过栅极和漏极之间的寄生电容CGD,耦合至输出端,输出电压的改变取决于注入电荷QINJ,CD和CL。所以,当注入的电荷越小时,在输出端引入的误差会越小。但同时,要注意到,注入电荷是一个与供电电压、输入信号都有关的一个参数。因此,当输入信号的电压在变化时,会在输出端产生一个非线性的误差。所以在选在MUX时,除了要注意charge injection的值以外,也要注意charge injection在输入范围内的平坦度。0a792341c0f47c89192c0dc50f64e1fb.png图 18 MUX36S08 charge injection 曲线TMUX6104精密模拟多路复用器使用特殊的电荷注入消除电路,可将源极-漏极电荷注入在VSS = 0 V时降至-0.35 pC,在整个信号范围内降至-0.41 pC。a14ad347cb1be775d9fd5d45281f8aaf.png图 19 TMUX6104 Charge Injection 曲线3. 带宽Bandwidth(1). 定义当开关打开时,在漏极的输出删减至源极输入衰减3dB时的频率,如图20所示。30bc371a0283eff5b9c9a58633628724.png图 20 带宽定义(2). 计算方法3b682db8cae9b01c0835453bf43c993b.png图 21 简化的MUX内部的开关模型为了简化分析,我们忽略RS和CS。根据图21中的阻容网络,我们可以写出该电路的传递函数:e0b3189b7c9753ea4b98f15c6d7c5d5f.png其中,3dB cut off frequency:d47b793aad5264981138cc5f2c6575c1.png根据这个公式,结合MUX和负载的参数,我们就可以算出来在当前条件下MUX的带宽了。4. 通道间串扰 Channel to Channel crosstalk(1). 定义fe5d08742198f57e24d61d67af3bc0cc.png图 22 通道间串扰示意图通道间串扰定义为当已知信号施加到导通通道的源极引脚时,在截止通道的源极引脚上出现的电压。2f0bb8ae9da514b8b8e6f701a9942883.png(2). 特点ebc53d274978924a6a239d126d742457.png图 23 简化的MUX内部的开关模型及通道间串扰随信号频率的变化Channel to Channel crosstalk是和频率有关的一种现象。主要是由于关断状态下寄生电容导致的。有时,也会由于布局技术不佳而引入了寄生电容,表现为串扰。CSS表示两个输入通道之间的寄生电容。这可能是传输信号的两个输入走线之间的电容,或者是多路复用器的两个输入引脚之间的电容。在较低频率的时候,从S1到OUTPUT的阻抗是RON ,因为S2是断开的,从S2到OUTPUT的阻抗非常高。随着施加到S1的输入信号的频率增加,寄生电容CSD的阻抗变得更低,并在S2引入了一部分S1的输入信号。相同的原理,寄生电容CSS随频率的增加也会将一部分输入信号直接耦合到断开的通道S2。减少杂散电容的电路板布局技术也会有助于通道间的串扰问题。5. 关断隔离 Off isolation(1). 定义关断隔离定义为当在关闭通道的源极引脚上施加已知信号时在多路复用器输出引脚上引入的电压。4af7ae72011966fd44777fdbe7bc0706.png图 24 关断隔离示意图901085abe594ae1a9dd062c0045a8728.png(2). 特点7721e7aeced5c065ac30a38dd1ea0efe.png图 25 简化的MUX内部的开关模型及关断隔离随信号频率的变化像串扰一样,关断隔离也是一种与频率相关的现象,由于模拟开关或多路复用器的OFF状态寄生电容CSD而发生。而开关在截止状态的寄生电容又取决于多个因素,例如器件封装、引出线、制造工艺以及电路板布局技术。较低的负载电阻将产生更好的OFF隔离,但由于导通电阻的存在,如果负载电阻过低,会引入失真。较大的负载电容和漏极电容也将有助于更好的OFF隔离,但会限制多路复用器的带宽。关断隔离和串扰规范都会分为相邻和不相邻通道两类。

    a8c4fa17d15359cbb477d614b4d8d4b8.png

    展开全文
  • Unix系统有五种IO模型分别是阻塞IO(blocking IO),非阻塞IO( non-blocking IO),IO多路复用(IO multiplexing),信号驱动(SIGIO/Signal IO)和异步...而多路复用器Selector,就是采用这些IO多路复用的机制获取事件。J...

    Unix系统有五种IO模型分别是阻塞IO(blocking IO),非阻塞IO( non-blocking IO),IO多路复用(IO multiplexing),信号驱动(SIGIO/Signal IO)和异步IO(Asynchronous IO)。而IO多路复用通常有select,poll,epoll,kqueue等方式。而多路复用器Selector,就是采用这些IO多路复用的机制获取事件。JDK中的NIO(new IO)包,采用的就是IO多路复用的模型。

    select,poll和epoll

    阻塞IO下,应用程序调用IO函数,如果没有数据贮备好,那么IO操作会一直阻塞下去,阻塞IO不会占用大量CPU,但在这种IO模型下,一个线程只能处理一个文件描述符的IO事件;非阻塞IO下,应用程序调用IO事件,如果数据没有准备好,会直接返回一个错误,应用程序不会阻塞,这样就可以同时处理多个文件描述符的IO事件,但是需要不间断地轮询来获取IO事件,对CPU是很大的浪费。并且阻塞IO和非阻塞IO调用一个IO函数只能获取一个IO事件。

    select,poll和epoll是最常见的三种IO多路复用的方式,它们都支持同时感知多个IO事件,它们的工作特点和区别如下:

    select可以在一定时间内监视多个文件描述符的IO事件,select函数需要传入要监视的文件描述符的句柄作为参数,并且返回所有文件描述符,应用程序需要遍历所有的循环来看每一个文件描述符是否有IO事件发生,效率较低。并且,select默认只能监视1024个文件描述符,这些文件描述符采用数组进行存储,可以修改FD_SETSIZE的值来修改文件描述符的数量限制。

    poll和select类似,poll采用链表存储监视的文件描述符,可以超过1024的限制。

    epoll可以监控的文件描述符数量是可以打开文件的数量上限。与select和poll不同,epoll获取事件不是通过轮询得到,而是通过给每个文件描述符定义回调得到,因此,在监视的文件描述符很多的情况下,epoll的效率不会有明显的下降。并且,select和poll返回给应用程序的是所有的文件描述符,而epoll返回的是就绪(有事件发生的)的文件描述符。

    JDK NIO包中的各种Selector

    JDK中的Selector是一个抽象类,创建一个Selector通常以下面代码中的方式进行:

    /**

    * 代码片段1 创建Selector

    */

    Selector selector = Selector.open();

    下面是具体的实现:

    /**

    * 代码片段2 Selector中的open方法和SelectorProvider中的provider方法

    */

    //调用SelectorProvider的openSelector创建Selector

    public static Selector open() throws IOException {

    return SelectorProvider.provider().openSelector();

    }

    //创建SelectorProvider,最终调用sun.nio.ch.DefaultSelectorProvider.create(), 这个方法在不同平台上有不同的实现

    public static SelectorProvider provider() {

    synchronized (lock) {

    if (provider != null)

    return provider;

    return AccessController.doPrivileged(

    new PrivilegedAction() {

    public SelectorProvider run() {

    if (loadProviderFromProperty())

    return provider;

    if (loadProviderAsService())

    return provider;

    provider = sun.nio.ch.DefaultSelectorProvider.create();

    return provider;

    }

    });

    }

    }

    在不同的操作系统平台下,SelectorProvider的实现也不相同,创建出来的Selector的实现也不一样。

    windows下的多路复用实现

    windows下的jdk中只有一个非抽象的SelectorProvider的实现类——WindowsSelectorProvider。显然,sun.nio.ch.DefaultSelectorProvider.create()返回的也是一个WindowsSelectorProvider对象:

    /**

    * 代码片段3 windows环境下jdk中的sun.nio.ch.DefaultSelectorProvider.create()方法

    */

    public static SelectorProvider create() {

    return new WindowsSelectorProvider();

    }

    WindowsSelectorProvider的openSelector方法会返回一个WindowsSelectorImpl对象,WindowsSelectorImpl继承了SelectorImpl这个抽象类:

    /**

    * 代码片段4

    */

    public AbstractSelector openSelector() throws IOException {

    return new WindowsSelectorImpl(this);

    }

    WindowsSelectorImpl的成员变量pollWrapper是一个PollArrayWrapper对象,PollArrayWrapper类在openjdk的源码中有这样的一段文档注释:

    /**

    * 代码片段5 windows下的PollArrayWrapper类中的注释

    */

    /**

    * Manipulates a native array of structs corresponding to (fd, events) pairs.

    *

    * typedef struct pollfd {

    *    SOCKET fd;            // 4 bytes

    *    short events;         // 2 bytes

    * } pollfd_t;

    *

    * @author Konstantin Kladko

    * @author Mike McCloskey

    */

    PollArrayWrapper是用来操作(fd,events)对相对应的结构的原生数组。这个原生数组的结构就是上面的注释中的结构体所定义的。PollArrayWrapper类中通过操作AllocatedNativeObject类型的成员变量pollArray来操作文件描述符和事件。AllocatedNativeObject类继承了NativeObject类,NativeObject类型是驻留在本地内存中的对象的代理,提供了在堆外内存中存放和取出除boolean外的基本类型数据的方法。以byte类型为例,其存取方法如下:

    /**

    * 代码片段6  NativeObject中的getByte和putByte方法

    */

    /**

    * Reads a byte starting at the given offset from base of this native

    * object.

    *

    * @param  offset

    *         The offset at which to read the byte

    *

    * @return The byte value read

    */

    final byte getByte(int offset) {

    return unsafe.getByte(offset + address);

    }

    /**

    * Writes a byte at the specified offset from this native object's

    * base address.

    *

    * @param  offset

    *         The offset at which to write the byte

    *

    * @param  value

    *         The byte value to be written

    */

    final void putByte(int offset, byte value) {

    unsafe.putByte(offset + address,  value);

    }

    PollArrayWrapper中提供了方法存储事件和文件描述符,这些方法都通过来pollArray存取int和short类型,这些方法和PollArrayWrapper构造方法如下:

    /**

    * 代码片段7  PollArrayWrapper构造方法和对文件描述符和事件的操作方法

    */

    PollArrayWrapper(int newSize) {

    int allocationSize = newSize * SIZE_POLLFD;

    pollArray = new AllocatedNativeObject(allocationSize, true);

    pollArrayAddress = pollArray.address();

    this.size = newSize;

    }

    // Access methods for fd structures

    void putDescriptor(int i, int fd) {

    pollArray.putInt(SIZE_POLLFD * i + FD_OFFSET, fd);

    }

    void putEventOps(int i, int event) {

    pollArray.putShort(SIZE_POLLFD * i + EVENT_OFFSET, (short)event);

    }

    int getEventOps(int i) {

    return pollArray.getShort(SIZE_POLLFD * i + EVENT_OFFSET);

    }

    int getDescriptor(int i) {

    return pollArray.getInt(SIZE_POLLFD * i + FD_OFFSET);

    }

    因为pollfd结构体中,fd占用4个字节,events占用2个字节分别对应int和short的长度。FD_OFFSET,EVENT_OFFSET和SIZE_POLLFD分别是final修饰的int常量0,4和8。PollArrayWrapper会用8个字节来存储一个event和fd的配对,构造一个PollArrayWrapper对象会从堆外内存分配newSize8字节的空间。获取第i个fd则获取对应的第8i个字节对应的int,获取第i个event则只要获取第8*i+4个字节对应的short。所以构造一个size大小的PollArrayWrapper对象就可以存储size个fd,event对,并且他们在内存上是连续的(每对的空间末尾有2个字节用不到),所以这也是一个数组。

    Selector中的doSelect方法是具体对文件描述符的操作,WindowsSelectorImpl中的doSelector方法如下:

    /**

    * 代码片段8  WindowsSelectorImpl内部类SubSelector的poll方法中的doSelect方法及其调用的方法具体实现

    */

    protected int doSelect(long timeout) throws IOException {

    if (channelArray == null)

    throw new ClosedSelectorException();

    this.timeout = timeout; // set selector timeout

    processDeregisterQueue();

    if (interruptTriggered) {

    resetWakeupSocket();

    return 0;

    }

    //  计算轮询所需的辅助线程数。如果需要,在这里创建线程并开始等待startLock

    adjustThreadsCount();

    // 重置finishLock

    finishLock.reset();

    //  唤醒辅助线程,等待启动锁,线程启动后会开始轮询。冗余线程将在唤醒后退出。

    startLock.startThreads();

    // 在主线程中进行轮询。主线程负责pollArray中的前MAX_SELECTABLE_FDS(默认1024)个fd,event对。

    try {

    begin();

    try {

    subSelector.poll();

    } catch (IOException e) {

    // 保存异常

    finishLock.setException(e);

    }

    //  主线程poll()调用结束。唤醒其他线程并等待他们

    if (threads.size()0)

    finishLock.waitForHelperThreads();

    } finally {

    end();

    }

    finishLock.checkForException();

    processDeregisterQueue();

    // 更新相应channel的操作。将就绪的key添加到就绪队列。

    int updated = updateSelectedKeys();

    // poll()调用完成。为下一次运行,将wakeupSocket设置为nonsigned。

    resetWakeupSocket();

    return updated;

    }

    //WindowsSelectorImpl内部类SubSelector的poll方法

    private int poll() throws IOException{ // poll for the main thread

    return poll0(pollWrapper.pollArrayAddress,

    Math.min(totalChannels, MAX_SELECTABLE_FDS),

    readFds, writeFds, exceptFds, timeout);

    }

    //WindowsSelectorImpl内部类SubSelector的poll0方法

    private native int poll0(long pollAddress, int numfds,

    int[] readFds, int[] writeFds, int[] exceptFds, long timeout);

    poll0方法的C语言源码:

    /**

    * 代码片段9  WindowsSelectorImpl内部类SubSelector的poll方法的c源码

    */

    JNIEXPORT jint JNICALL

    Java_sun_nio_ch_WindowsSelectorImpl_00024SubSelector_poll0(JNIEnv *env, jobject this,

    jlong pollAddress, jint numfds,

    jintArray returnReadFds, jintArray returnWriteFds,

    jintArray returnExceptFds, jlong timeout)

    {

    ... //省略部分代码

    /* Call select */

    if ((result = select(0 , &readfds, &writefds, &exceptfds, tv))//调用系统的select函数

    == SOCKET_ERROR) {

    /* Bad error - this should not happen frequently */

    /* Iterate over sockets and call select() on each separately */

    FD_SET errreadfds, errwritefds, errexceptfds;

    readfds.fd_count = 0;

    writefds.fd_count = 0;

    exceptfds.fd_count = 0;

    for (i = 0; i < numfds; i++) {

    /* prepare select structures for the i-th socket */

    errreadfds.fd_count = 0;

    errwritefds.fd_count = 0;

    if (fds[i].events & POLLIN) {

    errreadfds.fd_array[0] = fds[i].fd;

    errreadfds.fd_count = 1;

    }

    if (fds[i].events & (POLLOUT | POLLCONN))

    {

    errwritefds.fd_array[0] = fds[i].fd;

    errwritefds.fd_count = 1;

    }

    errexceptfds.fd_array[0] = fds[i].fd;

    errexceptfds.fd_count = 1;

    /* call select on the i-th socket */

    if (select(0, &errreadfds, &errwritefds, &errexceptfds, &zerotime)//调用系统的select函数

    == SOCKET_ERROR) {

    /* This socket causes an error. Add it to exceptfds set */

    exceptfds.fd_array[exceptfds.fd_count] = fds[i].fd;

    exceptfds.fd_count++;

    } else {

    /* This socket does not cause an error. Process result */

    if (errreadfds.fd_count == 1) {

    readfds.fd_array[readfds.fd_count] = fds[i].fd;

    readfds.fd_count++;

    }

    if (errwritefds.fd_count == 1) {

    writefds.fd_array[writefds.fd_count] = fds[i].fd;

    writefds.fd_count++;

    }

    if (errexceptfds.fd_count == 1) {

    exceptfds.fd_array[exceptfds.fd_count] = fds[i].fd;

    exceptfds.fd_count++;

    }

    }

    }

    }

    ... //省略部分代码

    }

    可见Window环境的JDK的nio是调用select系统函数来进行的。

    linux下的多路复用实现

    linux的jdk中有2个非抽象的Selector的子类——PollSelectorImpl和EPollSelectorImpl。

    PollSelectorImpl

    顾名思义,PollSelectorImpl是采用poll来进行多路复用。PollSelectorImpl继承了AbstractPollSelectorImpl。AbstractPollSelectorImpl中也维护了一个PollArrayWrapper来存储文件描述符和事件对,但linux下的PollArrayWrapper和windows下的实现并不相同。先看PollSelectorImpl的doSelect方法如下:

    /**

    * 代码片段10  PollSelectorImpl的doSelect方法

    */

    protected int doSelect(long timeout)

    throws IOException

    {

    if (channelArray == null)

    throw new ClosedSelectorException();

    processDeregisterQueue();

    try {

    begin();

    pollWrapper.poll(totalChannels, 0, timeout);

    } finally {

    end();

    }

    processDeregisterQueue();

    // 将pollfd结构中的信息复制到相应通道的ops中。将就绪的key添加到就绪队列。

    int numKeysUpdated = updateSelectedKeys();

    if (pollWrapper.getReventOps(0) != 0) {

    // Clear the wakeup pipe

    pollWrapper.putReventOps(0, 0);

    synchronized (interruptLock) {

    IOUtil.drain(fd0);

    interruptTriggered = false;

    }

    }

    return numKeysUpdated;

    }

    doSelect方法中会调用PollArrayWrapper中的poll方法,linux下的PollArrayWrapper和windows下的不太一样。PollArrayWrapper源码中的文档注释如下:

    /**

    * 代码片段11  linux下的PollArrayWrapper类中的注释

    */

    /**

    * Manipulates a native array of pollfd structs on Solaris:

    *

    * typedef struct pollfd {

    *    int fd;

    *    short events;

    *    short revents;

    * } pollfd_t;

    *

    * @author Mike McCloskey

    * @since 1.4

    */

    可以发现,与windows的相比,linux下的PollArrayWrapper操作的结构体多了一个revents(实际发生的事件)的字段,linux下的PollArrayWrapper类继承了抽象类AbstractPollArrayWrapper,AbstractPollArrayWrapper定义了对文件描述符和事件的操作方法:

    /**

    * 代码片段12  AbstractPollArrayWrapper中定义的几个final常量和对文件描述符、事件的操作方法

    */

    static final short SIZE_POLLFD   = 8;

    static final short FD_OFFSET     = 0;

    static final short EVENT_OFFSET  = 4;

    static final short REVENT_OFFSET = 6;

    protected AllocatedNativeObject pollArray;

    // Access methods for fd structures

    int getEventOps(int i) {

    int offset = SIZE_POLLFD * i + EVENT_OFFSET;

    return pollArray.getShort(offset);

    }

    int getReventOps(int i) {

    int offset = SIZE_POLLFD * i + REVENT_OFFSET;

    return pollArray.getShort(offset);

    }

    int getDescriptor(int i) {

    int offset = SIZE_POLLFD * i + FD_OFFSET;

    return pollArray.getInt(offset);

    }

    void putEventOps(int i, int event) {

    int offset = SIZE_POLLFD * i + EVENT_OFFSET;

    pollArray.putShort(offset, (short)event);

    }

    void putReventOps(int i, int revent) {

    int offset = SIZE_POLLFD * i + REVENT_OFFSET;

    pollArray.putShort(offset, (short)revent);

    }

    void putDescriptor(int i, int fd) {

    int offset = SIZE_POLLFD * i + FD_OFFSET;

    pollArray.putInt(offset, fd);

    }

    可见,linux下的PollArrayWrapper中pollArray的每8个字节的后两个字节不是空,而是存储着两个字节的revents。PollArrayWrapper的poll方法如下:

    /**

    * 代码片段13  PollArrayWrapper中poll方法

    */

    int poll(int numfds, int offset, long timeout) {

    return poll0(pollArrayAddress + (offset * SIZE_POLLFD),

    numfds, timeout);

    }

    private native int poll0(long pollAddress, int numfds, long timeout);

    poll0方法的c语言源码:

    /**

    * 代码片段14  PollArrayWrapper中poll方法的c源码

    */

    JNIEXPORT jint JNICALL

    Java_sun_nio_ch_PollArrayWrapper_poll0(JNIEnv *env, jobject this,

    jlong address, jint numfds,

    jlong timeout)

    {

    struct pollfd *a;

    int err = 0;

    a = (struct pollfd *) jlong_to_ptr(address);

    if (timeout <= 0) {           /* Indefinite or no wait */

    //如果timeout<=0,立即调用系统的poll函数

    RESTARTABLE (poll(a, numfds, timeout), err);

    } else {                     /* Bounded wait; bounded restarts */

    //如果timeout>0,会循环的调用poll函数直到到了timeout的时间

    err = ipoll(a, numfds, timeout);

    }

    if (err < 0) {

    JNU_ThrowIOExceptionWithLastError(env, "Poll failed");

    }

    return (jint)err;

    }

    static int ipoll(struct pollfd fds[], unsigned int nfds, int timeout)

    {

    jlong start, now;

    int remaining = timeout;

    struct timeval t;

    int diff;

    gettimeofday(&t, NULL);

    start = t.tv_sec * 1000 + t.tv_usec / 1000;

    for (;;) {

    //调用poll函数 remaining是剩余的timeout,其实也就调用一次,用循环应该是为了防止poll函数的进程被异常唤醒

    int res = poll(fds, nfds, remaining);

    if (res < 0 && errno == EINTR) {

    if (remaining >= 0) {

    gettimeofday(&t, NULL);

    now = t.tv_sec * 1000 + t.tv_usec / 1000;

    diff = now - start;

    remaining -= diff;

    if (diff < 0 || remaining <= 0) {

    return 0;

    }

    start = now;

    }

    } else {

    return res;

    }

    }

    }

    可见PollSelectorImpl确实是调用系统的poll函数实现多路复用的。

    EPollSelectorImpl

    EPollSelectorImpl中使用EPollArrayWrapper来操作文件描述符和事件,EPollArrayWrapper中的对EPoll事件结构体的文档注释:

    /**

    * 代码片段15  EPollArrayWrapper类中的注释

    */

    /**

    * Manipulates a native array of epoll_event structs on Linux:

    *

    * typedef union epoll_data {

    *     void *ptr;

    *     int fd;

    *     __uint32_t u32;

    *     __uint64_t u64;

    *  } epoll_data_t;

    *

    * struct epoll_event {

    *     __uint32_t events;

    *     epoll_data_t data;

    * };

    *

    * The system call to wait for I/O events is epoll_wait(2). It populates an

    * array of epoll_event structures that are passed to the call. The data

    * member of the epoll_event structure contains the same data as was set

    * when the file descriptor was registered to epoll via epoll_ctl(2). In

    * this implementation we set data.fd to be the file descriptor that we

    * register. That way, we have the file descriptor available when we

    * process the events.

    */

    等待IO时间的系统调用函数是epoll_wait(2),它填充了一个epoll_event结构体的数组,这个数组被传递给系统调用。epoll_event结构的数据成员包含的数据与通过epoll_ctl(2)将文件描述符注册到epoll时设置的数据相同。在这个实现中,我们将data.fd设置为注册的文件描述符。这样,我们在处理事件时就有了可用的文件描述符。

    很明显,EPollSelectorImpl中操作的结构体大小比PollSelectorImpl要大,这里不一一解读了。EPoll的调用和select、poll不同,需要调用三个系统函数,分别是epoll_create,epoll_ctl 和 epoll_wait,这点在JDK NIO中也得到验证。在EPollArrayWrapper创建时会调用epollCreate方法:

    /**

    * 代码片段16  EPollArrayWrapper的构造方法和构造方法中调用的epollCreate方法

    */

    EPollArrayWrapper() throws IOException {

    // creates the epoll file descriptor

    epfd = epollCreate();

    // the epoll_event array passed to epoll_wait

    int allocationSize = NUM_EPOLLEVENTS * SIZE_EPOLLEVENT;

    pollArray = new AllocatedNativeObject(allocationSize, true);

    pollArrayAddress = pollArray.address();

    // eventHigh needed when using file descriptors > 64k

    if (OPEN_MAX > MAX_UPDATE_ARRAY_SIZE)

    eventsHigh = new HashMap<>();

    }

    private native int epollCreate();

    这里的epollCreate方法也就是进行epoll_create系统调用,创建一个EPoll实例。以下是C源码:

    /**

    * 代码片段17  epollCreate方法的c源码

    */

    JNIEXPORT jint JNICALL

    Java_sun_nio_ch_EPollArrayWrapper_epollCreate(JNIEnv *env, jobject this)

    {

    /*

    * epoll_create expects a size as a hint to the kernel about how to

    * dimension internal structures. We can't predict the size in advance.

    */

    //进行epoll_create系统调用

    int epfd = epoll_create(256);

    if (epfd < 0) {

    JNU_ThrowIOExceptionWithLastError(env, "epoll_create failed");

    }

    return epfd;

    }

    EPollSelectorImpl的构造方法中创建完成一个EPollArrayWrapper实例后,会执行该实例的initInterrupt方法,这个方法中调用了epollCtl方法:

    /**

    * 代码片段18  EPollSelectorImpl的构造方法、构造方法中调用的EPollArrayWrapper中的initInterrupt方法

    * 和initInterrupt中调用的epollCtl方法

    */

    /**

    * Package private constructor called by factory method in

    * the abstract superclass Selector.

    */

    EPollSelectorImpl(SelectorProvider sp) throws IOException {

    super(sp);

    long pipeFds = IOUtil.makePipe(false);

    fd0 = (int) (pipeFds >>> 32);

    fd1 = (int) pipeFds;

    pollWrapper = new EPollArrayWrapper();

    //调用initInterrupt方法

    pollWrapper.initInterrupt方法(fd0, fd1);

    fdToKey = new HashMap<>();

    }

    void initInterrupt(int fd0, int fd1) {

    outgoingInterruptFD = fd1;

    incomingInterruptFD = fd0;

    //调用epollCtl

    epollCtl(epfd, EPOLL_CTL_ADD, fd0, EPOLLIN);

    }

    private native void epollCtl(int epfd, int opcode, int fd, int events);

    这里的epollCtl方法也就是进行epoll_ctl系统调用,往刚刚创建的EPoll实例中添加要监控的事件。以下是C源码:

    /**

    * 代码片段19  epollCtl方法的c源码

    */

    JNIEXPORT void JNICALL

    Java_sun_nio_ch_EPollArrayWrapper_epollCtl(JNIEnv *env, jobject this, jint epfd,

    jint opcode, jint fd, jint events)

    {

    struct epoll_event event;

    int res;

    event.events = events;

    event.data.fd = fd;

    //调用epoll_ctl

    RESTARTABLE(epoll_ctl(epfd, (int)opcode, (int)fd, &event), res);

    /*

    * A channel may be registered with several Selectors. When each Selector

    * is polled a EPOLL_CTL_DEL op will be inserted into its pending update

    * list to remove the file descriptor from epoll. The "last" Selector will

    * close the file descriptor which automatically unregisters it from each

    * epoll descriptor. To avoid costly synchronization between Selectors we

    * allow pending updates to be processed, ignoring errors. The errors are

    * harmless as the last update for the file descriptor is guaranteed to

    * be EPOLL_CTL_DEL.

    */

    if (res < 0 && errno != EBADF && errno != ENOENT && errno != EPERM) {

    JNU_ThrowIOExceptionWithLastError(env, "epoll_ctl failed");

    }

    }

    EPollSelectorImpl的doSelect方法会调用EPollArrayWrapper的poll方法,而在poll方法中会调用epollWait:

    /**

    * 代码片段20  EPollSelectorImpl中的doSelect方法、doSelect方法中调用的EPollArrayWrapper的poll方法

    * 和poll方法中调用的epollWait方法

    */

    protected int doSelect(long timeout) throws IOException {

    if (closed)

    throw new ClosedSelectorException();

    processDeregisterQueue();

    try {

    begin();

    pollWrapper.poll(timeout);

    } finally {

    end();

    }

    processDeregisterQueue();

    int numKeysUpdated = updateSelectedKeys();

    if (pollWrapper.interrupted()) {

    // Clear the wakeup pipe

    pollWrapper.putEventOps(pollWrapper.interruptedIndex(), 0);

    synchronized (interruptLock) {

    pollWrapper.clearInterrupted();

    IOUtil.drain(fd0);

    interruptTriggered = false;

    }

    }

    return numKeysUpdated;

    }

    int poll(long timeout) throws IOException {

    //更新注册信息,如果监视的实践发生变化,会调用epoll_ctl往Epoll实例中增加或删除事件

    updateRegistrations();

    //调用epollWait

    updated = epollWait(pollArrayAddress, NUM_EPOLLEVENTS, timeout, epfd);

    for (int i=0; i

    if (getDescriptor(i) == incomingInterruptFD) {

    interruptedIndex = i;

    interrupted = true;

    break;

    }

    }

    return updated;

    }

    private native int epollWait(long pollAddress, int numfds, long timeout,

    int epfd) throws IOException;

    这里的epollWait方法也就是进行epoll_wait系统调用,调用者进程被挂起,在等待内核I/O事件的分发。以下是C源码:

    /**

    * 代码片段21  epollWait方法的c源码

    */

    JNIEXPORT jint JNICALL

    Java_sun_nio_ch_EPollArrayWrapper_epollWait(JNIEnv *env, jobject this,

    jlong address, jint numfds,

    jlong timeout, jint epfd)

    {

    struct epoll_event *events = jlong_to_ptr(address);

    int res;

    if (timeout <= 0) {           /* Indefinite or no wait */

    //如果timeout<=0,立即调用系统的epoll_wait函数

    RESTARTABLE(epoll_wait(epfd, events, numfds, timeout), res);

    } else {                      /* Bounded wait; bounded restarts */

    //如果timeout>0,循环调用直到超时时间到了,用循环应该是为了防止异常唤醒

    res = iepoll(epfd, events, numfds, timeout);

    }

    if (res < 0) {

    JNU_ThrowIOExceptionWithLastError(env, "epoll_wait failed");

    }

    return res;

    }

    总结

    至此,本文已经对三种IO多路复用技术和在JDK中的应用进行了解读。在windows环境下,JDK NIO中只有WindowsSelectorImpl这有一个Selector的非抽象实现,采用的IO多路复用方式是select;在linux环境下PollSelectorImpl和EPollSelectorImpl两种实现,分别采用poll和epoll实现IO多路复用。本文还对这些Selector的具体实现进行了详细的解读,不足之处,敬请指正。

    展开全文
  • 转到服务器/ API微型框架,HTTP请求路由器,多路复用器多路复用器。 :open_book: 关于 贡献者: 想要贡献? 随时发送请求请求! 有问题,错误,功能提示吗? 我们正在使用github来管理它们。 :books: 文献资料...
  • 多路复用器应用选择正确的 Δ-Σ 转换器类别可以完成具有零周期时延特性的转换工作。通过周期性地对每条通道进行采样,您电路中的多路复用器可以扫描检测许多输入通道。多路复用系统只有一个从所有通道获取数据的...
  • 本视频提供了多路复用器DC性能参数的概览,旨在让您了解多路复用器的DC 性能参数,以及它们对数据采集系统的性能会产生怎样的影响。您将学习到如何通过模拟多路复用器数据表中列出的参数了解系统性能限制和错误来源...
  • 基于调制宽带转换器的新型高效多路压缩压缩多路复用器
  • 上一篇文章讲到了Unix的I/O模型,以及在java中的具体实现,其中在java中我们最为关注的就是 I/O 复用了,这篇主要总结下I/O多路复用器。概念:文件描述符fdLinux的内核将所有外部设备都可以看做一个文件来操作。那么...
  • 使用WASM的快速MP4多路复用器/多路分解器,用于现代浏览器和Node.js。 支持什么: MP4视频混合(获取已编码的H264帧并将其包装在MP4容器中) 通过WebCodecs进行MP4 / H264编码和多路复用 什么仍然是WIP: MP4...
  • 一位任职于领先的可编程逻辑控制器(PLC)制造商的年轻工程师满怀热情,正在设计一个可...当选择多路复用器时,他有三种选项:一个是德州仪器的MUX36D04和两个来自其他供应商的多路复用器(MUX2和MUX3)。除了输入漏电流...
  • 多程序传输流多路复用器的设计与实现
  • 但通过使用一个多路复用器,如图3.01所示,将会从多个通道中切换输入并且驱动一个单一的模拟数字转换器,从而极大地降低系统的成本。这个方法被用在基于采样的系统上。采样率越高,那系统模拟理想数据采集系...
  • 1 模拟开关和多路复用器有哪些新的功能?  模拟开关和多路复用器正在扩展它们的应用范围,从工业和仪器仪表设备、通信基础设施到消费类电子设备(例如音视频接收机和手机,它们都需要灵活的带宽和信号幅度)
  • 其 2:1 的输入多路复用器简化了两个视频信号的选择,而放大器的内部固定增益為 2,在驱动 75W 反向终接电缆时无需外部增益设置电阻。 LT6555 同样非常适合用做前端接收器或输出电缆驱动器,这是在 UXGA LCD投影机和...
  • 实验二 多路复用器与加法器的实现 一、实验目的 1.熟悉多路复用器、加法器的工作原理。 2.学会使用 VHDL 语言设计多路复用器、加法器。 3.掌握 generic 的使用,设计 n-1 多路复用器。 4.兼顾速度与...
  • 多路复用器GPIO

    2013-05-28 14:02:53
    多路复用器GPIO,嵌入式培训机构的内部资料,对学习嵌入式刚入门的同学帮助很大。...
  • polysh:Polysh,远程Shell多路复用器
  • 多路复用器Selector是Java NIO编程的基础,熟练地掌握Selector对于掌握NIO编程至关重要。多路复用器提供选择已经就绪的任务的能力。简单来讲,Selector会不断地轮询注册在其上的Channel,如果某个Channel上面有新的...
  • 没有足够的时间更改设计,多路复用器的选择也少之又少。在最后关头可能面临无数的变化,但我在与设计人员合作时经常遇到的一个问题是:如何在选择了微控制器后监控增加的节点数,如图1所示。在这种情况下,我们面临...
  • Photo by Scott Evans on Unsplash阿粉第一次了解到io相关知识是在网上看面经的时候,平时只会写业务代码,面对bio,nio,多路复用器这些概念简直是一头雾水。当阿粉尝试单独去学习这些名词,发现很难学懂,如果能有...
  • 描述 SN74CBT16233是一款16位1:2 FET多路... 该器件可用作两个8位至16位多路复用器或一个16位至32位多路复用器。 两个选择(SEL1和SEL2)输入控制数据流。 当TEST输入有效时,A端口连接到B1和B2端口。 SEL1,SEL2...
  • epoll IO多路复用器

    2018-09-29 11:58:39
    epoll IO多路复用器 IO多路复用存在的意义在于应用程序可以同时监测多个fd的事件,便于单线程处理多个fd,epoll是众多多路复用器的一种,类似的还有select、poll等。服务器程序通常需要具备较高处理用户并发的能力...
  • omn​​itty:Omnitty:多机SSH多路复用器

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,688
精华内容 1,075
关键字:

多路复用器