muduo网络库剖析——监听者EpollPoller类

muduo网络库剖析——监听者EpollPoller类

  • 前情
    • 从muduo到my_muduo
  • 概要
    • epoll原理解析
    • epoll提供的接口
    • epoll的触发模式
    • epoll实现多路复用
  • 框架与细节
    • 成员
    • 函数
    • 使用方法
  • 源码
  • 结尾

前情

从muduo到my_muduo

作为一个宏大的、功能健全的muduo库,考虑的肯定是众多情况是否可以高效满足;而作为学习者,我们需要抽取其中的精华进行简要实现,这要求我们足够了解muduo库。

做项目 = 模仿 + 修改,不要担心自己学了也不会写怎么办,重要的是积累,学到了这些方法,如果下次在遇到通用需求的时候你能够回想起之前的解决方法就够了。送上一段话!

在这里插入图片描述

概要

转自夏天匆匆2过。

epoll原理解析

从socket接收网络数据说起:
1、网络传输中,网卡会把接收到的数据写入内存,网卡向 CPU 发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
2、进程执行socket()函数创建socket,这个socket 对象包含了发送缓冲区、接收缓冲区与等待队列等成员,等待队列指向所有需要等待该 Socket 事件的进程。
3、假设上面socket进程为A,另外内核还有进程B和C,内核会分时执行运行状态的ABC进程。
4、当程序执行到 Recv 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中,A进程被阻塞,不会往下执行代码,也就不会占用CPU资源,此时内核只剩B和C进程分时执行。
5、一个socket 对应着一个端口号,而网络数据包中包含了 IP 和端口的信息,内核可以通过端口号找到对应的socket。
6、当socket 接收到数据后,操作系统将该socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。同时由于 socket 的接收缓冲区已经有了数据,Recv 可以返回接收到的数据。

epoll的设计思路:
服务服务器需要管理多个客户端连接,而Recv 只能监视单个socket,epoll 的诞生就是高效地监视多个socket。
epoll是select 和poll的增强版本,epoll的改进:
1、epoll将“维护等待队列”和“阻塞进程“分离,先用 epoll_create 创建一个epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据。
2、内核维护一个“就绪列表”Rdlist ,引用收到数据的 Socket,当进程被唤醒后,只要获取 Rdlist 的内容,就能够知道哪些 Socket 收到数据。

epoll的工作流程
1、当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(Epfd),eventpoll 对象是文件系统中的一员,有等待队列。Rdlist 是eventpoll的成员。
2、创建 Epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 Socket,内核会将 eventpoll 添加到这个 Socket 的等待队列中。当 Socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。
3、当 Socket 收到数据后,中断程序会给 eventpoll 的就绪列表Rdlist 添加这个Socket 引用。eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。
4、假设计算机正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。 内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态。因为 Rdlist 的存在,进程 A 可以知道哪些 Socket 发生了变化。

epoll数据结构
eventpoll结构体包含了 Lock、MTX、WQ(等待队列)与 Rdlist 等成员。
就绪列表Rdlist:是一种能够快速插入和删除的数据结构,Epoll 使用双向链表来实现就绪队列。
索引结构RBR:epoll使用红黑树作为索引结构来保存监听的socket列表。

在这里插入图片描述

epoll提供的接口

1、调用epoll_create建立epoll对象,创建一个eventpoll结构体,包括rbr(在内核cache里创建红黑树用于存储以后epoll_ctl传来的socket)和rdllist(用于存储准备就绪事件的向链表)。

//创建一个epoll实例(本质是红黑树),也占用个文件描述符,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
//返回值size,用来告诉内核这个监听的数目一共有多大,自从Linux 2.6.8开始,size参数被忽略,但是依然要大于0。
int epoll_create(int size);
struct eventpoll {
  ...
  /*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,
  也就是这个epoll监控的事件*/
  struct rb_root rbr;
  /*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/
  struct list_head rdllist;
  ...
};

2、调用epoll_ctl向epoll对象中添加或删除socket事件,所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。

/**
 * @brief 将监听的文件描述符添加到epoll对象中
 * @param epfd epoll_create的返回值,epoll对象
 * @param op   要执行的动作:EPOLL_CTL_ADD:注册新的fd到epfd中;
                           EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
                           EPOLL_CTL_DEL:从epfd中删除一个fd;

 * @param fd   要执行动作的fd
 * @param event告诉内核需要监听什么事件,epoll_event结构体:
 *     struct epoll_event {
            __uint32_t events; // Epoll events
            epoll_data_t data; // User data variable
        };
        events可以是以下几个宏的集合(常用的IN/OUT/ERR/ET):
            EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
            EPOLLOUT:表示对应的文件描述符可以写;
            EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
            EPOLLERR:表示对应的文件描述符发生错误;
            EPOLLHUP:表示对应的文件描述符被挂断;
            EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
            EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
        epoll_data_t联合体定义如下:(注意是联合体)
            typedef union epoll_data
            {
              void *ptr;		//可以传递任意类型数据,常用来传 回调函数
              int fd;		//可以直接传递客户端的fd
              uint32_t u32;
              uint64_t u64;
            } epoll_data_t;

 * @return 返回值:成功返回0。发生错误时返回-1并设置errno
 */
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

3、当epoll_wait调用时,观察rdllist双向链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。

/**
 * @brief           等待epoll事件从epoll实例中发生
 * @param epfd      等待的监听描述符,也就是哪个池子中的内容
 * @param events    出参,指针,指向epoll_event的数组,监听描述符中的连接描述符就绪后,将会依次将信息填入
 * @param maxevents 表示每次能处理的最大事件数,告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size
 * @param timeout   等待时间,要是有连接描述符就绪,立马返回,如果没有,timeout时间后也返回,单位是ms;(超时情况下,0会立即返回,-1将不确定,也有说法说是永久阻塞)
 * @return          成功返回为请求的I / O准备就绪的文件描述符的数目,如果在请求的超时毫秒内没有文件描述符准备就绪,则返回零。发生错误时,epoll_wait()返回-1并正确设置errno。
 */
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 

epoll的触发模式

epoll的两种触发模式:
边沿触发vs水平触发
epoll事件有两种模型,边沿触发:edge-triggered (EPOLLET), 水平触发:level-triggered (EPOLLLT)
水平触发(level-triggered),是epoll的默认模式
socket接收缓冲区不为空 有数据可读 读事件一直触发
socket发送缓冲区不满 可以继续写入数据 写事件一直触发
边沿触发(edge-triggered)
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
边沿触发仅触发一次,水平触发会一直触发。
开源库:libevent 采用水平触发, nginx 采用边沿触发。

epoll实现多路复用

使用一个进程(线程)同时监控若干个文件描述符读写情况,这种读写模式称为多路复用。
多用于TCP的服务端,用于监控客户端的连接和数据的发送。
优点:不需要频繁地创建、销毁进程,从而节约了内存资源、时间资源,也避免了进程之间的竞争、等待。
缺点:要求单个客户端的任务不能太过于耗时,否则其它客户端就会感知到卡顿。
适合并发量高、但是任务量短小的情景,例如:Web服务器。

epoll就是为实现多路复用而生,一个epoll线程可同时监听多个fd收发、tcp服务监听、异常事件监听等。

框架与细节

对于EpollPoller,主要是使用epoll家族来进行监听与对channel的控制。

成员

在这里插入图片描述
创建要用到的epoll文件描述符,以及events的监听事件列表。

函数

在这里插入图片描述
epoll_create1可以传入一个flag,这里调用EPOLL_CLOEXEC,和SOCK_CLOEXEC一样,关闭新进程的继承效果。
在这里插入图片描述
析构重写,调用close函数,关闭epoll文件描述符。

在poll函数中,主要使用了epoll_wait函数监听准备好的事件,以及调用了fillactiveChannels来准备激活的channel列表。下面是对epoll_wait函数的一段具体解释。并且给epoll_wait函数设定了timeOut时间,超过该时间就结束等待,返回相应的值。
在这里插入图片描述
对于updatechannel函数,给channel设置了三种状态,kNew,kAdded,kDeleted,分别代表未注册到Poller上,已注册到Poller上,已从Poller上删除。针对这三种状态,对相应的哈希表进行修改。在这里我对为什么muduo源码选择实现了vector的channel列表和哈希表的channel列表有一些理解。vector其实是监听到的激活的channel通道集合,哈希表则是是否这个channel还注册在Poller上面,或者是已经从Poller上消失了。那这么看可能vector的size会比哈希表的小,虽然这只是猜测,没有验证过。对于相应的事件,会调用update去更新通道。
在这里插入图片描述
removechannel其实也是对哈希表的channel通道集合进行一些处理,包括状态的转换。
在这里插入图片描述
对于update,就是更改channel对应的event。
在这里插入图片描述
fillactivechannels就是建立监听到的events列表与channel列表之间的联系,这样channel在之后的更新状态或删除都可以访问到对应的event。
在这里插入图片描述

使用方法

源码

//EpollPoller.h
#pragma once

#include <sys/epoll.h>

#include "Poller.h"
#include "EventLoop.h"
#include "string.h"
#include "Log.h"

class Channel;

class EpollPoller : public Poller {
public:
    EpollPoller(EventLoop* loop);
    ~EpollPoller() override;
    // 重写父类的函数
    Timestamp poll(int timeoutMs, ChannelList* activeChannels) override;
    void updateChannel(Channel* channel) override;
    void removeChannel(Channel* channel) override;
    
private:
    static const int kInitEventListSize = 16;
    using EventList = std::vector<epoll_event>; //自己用,为私有
    void update(int operation, Channel* channel);
    void fillActiveChannels(int numEvents, ChannelList* activeChannels) const;
    int epollfd_;
    EventList events_;
};

//EpollPoller.cc
#include "EpollPoller.h"

//实现channel与epoll_event一一映射

enum status {
    kNew, //channel 未添加到 Poller 中
    kAdded,     //channel 已添加到 Poller 中
    kDeleted,    //channel 从 Poller 中删除
};

EpollPoller::EpollPoller(EventLoop* loop) : Poller(loop), epollfd_(::epoll_create1(EPOLL_CLOEXEC)), events_(kInitEventListSize) {
    if (epollfd_ < 0) {
        LOG_FATAL("%s--%s--%d--%d : epoll_create error\n", __FILE__, __FUNCTION__, __LINE__, errno);
    }
}

EpollPoller::~EpollPoller() {
    ::close(epollfd_);
}

Timestamp EpollPoller::poll(int timeoutMs, ChannelList* activeChannels) {   //设置channel感兴趣的事件
    int numEvent = ::epoll_wait(epollfd_, &*events_.begin(), events_.size(), timeoutMs);
    Timestamp now = Timestamp::now();
    int saveErrno = errno;
    if (numEvent < 0) {
        if (saveErrno != EINTR) { //中断
            errno = saveErrno;
            LOG_FATAL("%s--%s--%d--%d : epoll_wait error\n", __FILE__, __FUNCTION__, __LINE__, errno);
        }
    }
    else if (numEvent == 0) {
        LOG_INFO("%s--%s--%d : epoll_wait timeout\n", __FILE__, __FUNCTION__, __LINE__);
    }
    else {
        LOG_INFO("%s--%s--%d : epoll_wait %d events happened\n", __FILE__, __FUNCTION__, __LINE__, numEvent);
        fillActiveChannels(numEvent, activeChannels);
        if (numEvent == events_.size()) {
            events_.resize(numEvent * 2);
        }
    }
    return now;
}

void EpollPoller::updateChannel(Channel* channel) { //通过改变channel来改变对应的epoll_event
    int status = channel->status();
    if (status == kNew || status == kDeleted) {
        if (status == kNew) {
            int fd = channel->fd();
            channels_[fd] = channel;
        }
        channel->set_status(kAdded);
        update(EPOLL_CTL_ADD, channel);
    }
    else {  //channel已注册到Poller上了
        int fd = channel->fd();
        if (channel->isNoneEvent()) {
            update(EPOLL_CTL_DEL, channel);
            channel->set_status(kDeleted);  //只是不监听了
        }
        else {
            update(EPOLL_CTL_MOD, channel);
        }
    }
}

void EpollPoller::removeChannel(Channel* channel) {
    int fd = channel->fd();
    channels_.erase(fd);
    int status = channel->status();
    if (status == kAdded) {
        update(EPOLL_CTL_DEL, channel);
    }
    channel->set_status(kNew);
}

void EpollPoller::update(int operation, Channel* channel) { //epoll_ctl,对指定的channel进行修改
    epoll_event event;
    memset(&event, 0, sizeof event);
    event.events = channel->events();
    event.data.fd = channel->fd();
    event.data.ptr = channel;
    if (::epoll_ctl(epollfd_, operation, channel->fd(), &event) == -1) {
        if (operation == EPOLL_CTL_DEL) {
            LOG_ERROR("%s--%s--%d--%d : epoll_ctl error\n", __FILE__, __FUNCTION__, __LINE__, errno);
        }
        else {
            LOG_FATAL("%s--%s--%d--%d : epoll_ctl error\n", __FILE__, __FUNCTION__, __LINE__, errno);
        }
    }
}

void EpollPoller::fillActiveChannels(int numEvents, ChannelList* activeChannels) const {
    for (int i = 0; i < numEvents; i++) {
        Channel* channel = static_cast<Channel*>(events_[i].data.ptr);
        channel->set_revents(events_[i].events);    //channel和event之间建立了连接
        activeChannels->push_back(channel);
    }
}

结尾

以上就是监听者EpollPoller类的相关介绍,以及我在进行项目重写的时候遇到的一些问题,和我自己的一些心得体会。发现写博客真的会记录好多你的成长,而且对于一个好的项目,写博客也是证明你确实有过深度思考,并且在之后面试或者工作时遇到同样的问题能够进行复盘的一种有效的手段。所以,希望uu们也可以像我一样,养成写博客的习惯,逐渐脱离菜鸡队列,向大佬前进!!!加油!!!

也希望我能够完成muduo网络库项目的深度学习与重写,并在功能上能够拓展。也希望在完成这个博客系列之后,能够引导想要学习muduo网络库源码的人,更好地探索这篇美丽繁华的土壤。致敬chenshuo大神!!!

鉴于博主只是一名平平无奇的大三学生,没什么项目经验,所以可能很多东西有所疏漏,如果有大神发现了,还劳烦您在评论区留言,我会努力尝试解决问题!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Jenkins-Pipeline

Pipeline 1 安装插件 2 新建一个 Pipline 工程 3 配置Pipeline 脚本 agent的使用可以参考这个文档 pipeline {agent anystages {stage(Build) { steps {echo Building project...}}stage(Test) { steps {echo Testing project...}}stage(Deploy) { steps {echo Deploying …

STL之vector容器的介绍与模拟实现

STL之vector容器的介绍与模拟实现 1. vector简介2. vector容器使用2.1vectord 定义2.2 vector iterator 的使用2.3 vector 空间增长问题2.4 注意事项 3. vector功能模拟实现3.1 架构搭建3.2 空间控制板块3.3 迭代器3.4 增加/删除数据3.5 运算符重载3.6构造/析构 4. 整体代码 所…

vscode mysql cmake windows 常见问题和推荐文章

1.在windows中安装mingw64和cmake&#xff08;可查一下网上的安装教程&#xff09;&#xff0c;配置环境变量 2.在vscode中用CMake构建项目的时候&#xff0c;可能会出现这样的问题:“The C compiler identification is unknownn...”,可参考这篇博客 在windows下使用Vscode用…

Java基础面试题(四)

Java基础面试题&#xff08;四&#xff09; 文章目录 Java基础面试题&#xff08;四&#xff09;Oracle JDK vs OpenJDKJava 和 C 的区别? 文章来自Java Guide 用于学习如有侵权&#xff0c;立即删除 Oracle JDK vs OpenJDK 可能在看这个问题之前很多人和我一样并没有接触和使…

基于springboot+vue的图书个性化推荐系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目背景…

1. 安装Git

01. 安装Git 最早Git是在Linux上开发的&#xff0c;很长一段时间内&#xff0c;Git也只能在Linux和Unix系统上跑。不过&#xff0c;慢慢地有人把它移植到了Windows上。现在&#xff0c;Git可以在Linux、Unix、Mac和Windows这几大平台上正常运行了。 要使用Git&#xff0c;第一…

【C++入门到精通】智能指针 auto_ptr、unique_ptr简介及C++模拟实现 [ C++入门 ]

阅读导航 引言一、std::auto_ptr1. 简介2. 使用示例3. C模拟实现 二、std::unique_ptr1. 简介2. 使用示例3. C模拟实现 温馨提示 引言 在 C 中&#xff0c;智能指针是一种非常重要的概念&#xff0c;它能够帮助我们自动管理动态分配的内存&#xff0c;避免出现内存泄漏等问题。…

靶机-Billu_b0x root 123456

查找靶机IP nmap查看开放端口22&#xff0c;80 目录扫描 查看网站&#xff0c;典型注入 phpmy 果然是登陆界面&#xff0c;不过不知道账户及密码 in.php php的配置信息&#xff0c;可以看看 add.php 上传文件目录&#xff0c;可以上传&#xff0c;不过没有回显 …

C# ObjectArx 绘制表格并设置单元格合并

第一行默认是标题&#xff0c;可设置行【RowType】进行设置类型 Document doc Application.DocumentManager.MdiActiveDocument;using (Transaction tr doc.TransactionManager.StartOpenCloseTransaction()){BlockTable bt tr.GetObject(doc.Database.BlockTableId, OpenMo…

UE4 添加按键输入事件 并在蓝图中使用按键输入节点

绑定按键 选择Edit/ProjectSettings/Engine/Input 在bindings中可以选择添加ActionMappings或则AxisMappings ActionMappings:按键事件&#xff0c;有按下和抬起两个事件&#xff0c;需要分别用两个键触发AxisMappings:输入事件&#xff0c;返回值为float&#xff0c;对于键盘…

Rust之构建命令行程序(三):重构改进模块化和错误处理

开发环境 Windows 10Rust 1.74.1 VS Code 1.85.1 项目工程 这次创建了新的工程minigrep. 重构改进模块化和错误处理 为了改进我们的程序&#xff0c;我们将修复与程序结构及其处理潜在错误的方式有关的四个问题。首先&#xff0c;我们的main函数现在执行两项任务:解析参数和…

一文了解Servlet

文章目录 1、什么是Servlet2、Servlet快速入门3、Servlet生命周期4、Servlet体系结构5、urlPatern配置6、XML编写Servlet 1、什么是Servlet Servlet是Java提供的一门动态web资源开发技术Servlet是JavaEE规范之一&#xff0c;其实就是一个接口&#xff0c;将来我们需要定义Serv…

Apache JMeter 5.6.3压力测试步骤详解

Apache JMeter 5.6.3压力测试步骤详解 压力测试简介软件测试概述性能测试性能测试指标性能指标推算web资源公式 1. 安装 Jmeter2. 创建测试任务2.1 创建线程组2.2 创建 HTTP 请求2.3 添加HTTP消息头管理器 3.添加查看结果监听器4. 执行测试5. 查看结果6. 非GUI模式测试7. 使用c…

CSS||Emmet语法

1、简介 ​ Emmet语法的前身是Zen coding,它使用缩写,来提高html/css的编写速度, Vscode内部已经集成该语法。 ​ 快速生成HTML结构语法 ​ 快速生成CSS样式语法 2、快速生成HTML结构语法 生成标签 直接输入标签名 按tab键即可 比如 div 然后tab 键&#xff0c; 就可以生成 <…

CSS 设置背景图片

文章目录 设置背景颜色设置背景图片背景图片偏移量计算原点背景图片尺寸设置背景图片位置设置背景图片重复方式设置背景范围设置背景图片是否跟随元素移动测试背景图片 本文概念部分参考&#xff1a;CSS背景background设置 设置背景颜色 background-color 设置背景颜色 设置…

UE5 独立程序的网络TCP/UDP服务器与客户端基础流程

引擎源码版&#xff0c;复制\Engine\Source\Programs\路径下的BlankProgram空项目示例。 重命名BlankProgram&#xff0c;例如CustomTcpProgram&#xff0c;并修改项目名称。 修改.Build.cs内容 修改Target.cs内容 修改Private文件夹内.h.cpp文件名并修改.cpp内容 刷新引擎 …

联想模拟器抓包

联想模拟器抓包 下载抓包工具配置代理并启动模拟器配置代理返回reqable查看抓包数据 下载抓包工具 reqable 配置代理并启动 模拟器配置代理 返回reqable查看抓包数据

定义公共样式css

index.less 文件 // 全局按钮颜色 btn_background: #005298; btn_border-color: #6fa18d;// 默认的 btn_border-color-highlight: #0598d3;// 高亮边框 btn_border-color-success: #36be7e;// 成功边框 btn_font_color: #fff;// 边框颜色 背景色 文字颜色 .btn_public(btn_bo…

外贸网站建设有哪些技巧?如何做海洋建站?

搭建外贸网站需要什么流程步骤&#xff1f;独立网站建站怎么做&#xff1f; 外贸网站的建设变得至关重要。一家成功的外贸企业需要一个强大而专业的在线存在&#xff0c;以便吸引国际客户。在这篇文章中&#xff0c;海洋建站将探讨一些关键的技巧&#xff0c;帮助您打造出色的…

Vg3225vfn压控晶体振荡器规格书

频率范围: 25mhz ~ 500mhz电源电压:3.3 V类型绝对拉力范围:50 10 6分钟。/ 25mhz至42.5 MHz50 MHz至85 MHz&#xff0c;100mhz ~ 170mhz:20 10 6最小&#xff0c;10 10 6分钟。/ 25mhz ~ 250mhz:10 10 6分钟。/ 250 MHz至500 MHz(85C最高)操作温度温度范围:-40℃~ 85℃温度…