Linux多路转接or多路复用模型

news2024/10/5 14:18:58

目录

一、功能

二、应用场景

三、多路转接模型的实现

1.select模型

1.1操作流程

1.2相关接口

1.3示例

1.4常见使用方式

1.5优缺点

2.poll模型

2.1操作流程

2.2相关接口

2.3示例

2.4优缺点

3.epoll模型

3.1操作流程

3.2相关接口

3.3常见使用方式

3.4epoll的事件触发方式

3.5优缺点


一、功能

        针对大量描述符进行IO事件监控,让进程可以只针对就绪的描述符进行IO操作,提高IO效率,避免针对未就绪描述符操作而导致的效率低或阻塞。

引入:

问题:

        以前的tcp服务器,会为每个客户端创建一个套接字,用于与指定客户端进行通信,但是因为不清楚哪个描述符什么时候会有数据到来,因此流程固定:先获取新建连接,然后收发数据,这样就有可能导致程序阻塞。

解决方法1:

        多执行流(多线程or多进程),为每个客户端都创建一个单独的执行流负责通信,这样即使一个执行流阻塞,也不会影响其他客户端通信。

缺陷:

        多执行流方式简单粗暴,但是占用资源较多。

解决方法2:使用多路转接模型

        针对大量描述符进行IO事件监控,可以让进程知道哪个描述符有数据到来,即哪个就绪,就操作哪个。

二、应用场景

多路转接模型:是监控大量描述符,然后针对就绪描述符逐个进行处理。

(1)有大量描述符需要进行IO就绪事件监控,但是同一时间只有少量活跃的场景。

        如果有大量描述符进行监控,并且同一时间活跃数量非常多,会导致排在后面的描述符不能及时得到处理。(可搭配多执行流处理解决)

(2)针对单个描述符有收发数据的超时控制的场景。

三、多路转接模型的实现

1.select模型

1.1操作流程

(1)针对不同的IO事件(可读事件,可写事件,异常事件),定义不同的描述符集合,若需要对哪个描述符监控哪个事件,就把这个描述符添加到对应的集合中。

(2)调用监控接口,将集合拷贝到内核中,进行轮询遍历进行监控;当有描述符就绪,或监控超时都没有就绪,则监控返回。

        在内核中,先遍历一遍集合,没有描述符就绪,则将描述符添加到内核的IO事件队列,然后等待,被唤醒后,进程会再次遍历集合,判断哪个描述符是否就绪了指定的事件。

注意:监控调用返回之前,会将集合中所有没有就绪的描述符,从集合内移除(即监控调用返回后,集合中只含有就绪的描述符)。

(3)调用返回后,这时判断哪个描述符还在哪个事件的描述符集合中,就表示哪个描述符就绪了哪个事件,从而进行操作。

1.2相关接口

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

        nfds:几种事件的描述符集合中,最大的描述符的值+1;

        readfds:可读事件的描述符集合;

        writefds:可写事件的描述符集合;

        exceptfds:异常事件的描述符集合;

        timeout:struct timeval{ uint32_t tv_usec; uint32_t tv_sec};监控超时等待时间;

返回值:

        成功,返回就绪的事件个数;出错,返回-1;没有描述符就绪,返回0。

辅助接口:

(1)从set集合中移除fd描述符

        void FD_CLR(int fd, fd_set *set);

(2)判断fd描述符是否在集合set中

        int FD_ISSET(int fd, fd_set *set);

        返回值:存在,返回true;不存在,返回false。

(3)向set集合中添加fd描述符

        void FD_SET(int fd, fd_set *set);

(4)初始化清空set集合

        void FD_ZREO(fd_set *set);

1.3示例

对标准输入进行可读事件的监控,在描述符可读时从标准输入读取数据。

#include<stdio.h>
#include<unistd.h>
#include<sys/select.h>
#include<time.h>
//#include<sys/socket.h>

int main() {
  fd_set rfds;
  //1.初始化集合
  FD_ZERO(&rfds);
  while (1) {
    struct timeval tv;
    tv.tv_usec = 0;
    tv.tv_sec = 3;//设置超时时间为3s

    //2.将标准输入描述符添加到集合中
    FD_SET(0, &rfds);//0是标准输入描述符
    int max_fd = 0;
    //因为select会修改rfds集合(返回前会将未就绪的描述符剔除),所以需要在循环内每次添加

    int ret = select(max_fd + 1, &rfds, NULL, NULL, &tv);
    if (ret < 0) {
      perror("select error!\n");
      usleep(1000);
      continue;
    }
    else if (ret == 0) {//没有描述符就绪
      printf("wait timeout!\n");
      usleep(1000);
      continue;
    }
    //有描述符就绪
    for (int i = 0; i <= max_fd; ++i) {
      if (FD_ISSET(i, &rfds)) {//若i在集合中,说明描述符i就绪
        char buf[1024] = {0};
        int res = read(i, buf, 1023);
        if (res < 0) {//接收数据出错
          perror("recv error!\n");
          FD_CLR(i, &rfds);
          return -1;
        }
        printf("描述符%d就绪, 读取数据为: %s\n", i, buf);
      }
    }
  }

  return 0;
}

实现效果:

1.4常见使用方式

        封装一个Select类,实例化的每个Select对象都是一个多路转接对象,能够完成对大量描述符的监控,向外界返回就绪的描述符数组。

class Select{
    private:
        fd_set _rfds;//可读事件的描述符集合的备份,每次监控都是从该集合复制一份出来进行监控
        int _max_fd;
    public:
        Select(){//针对成员变量进行初始化}

        bool Add(const TCPsocket& sock);//将sock中的描述符fd,添加到rfds可读事件描述符集合中
        
        bool Del(const TCPsocket& sock);//将sock中的描述符fd,从rfds可读事件描述符集合中移除

        bool Wait(std::vector<TCPsocket>& array);//开始监控,返回就绪的描述符的数组
};

1.5优缺点

优点:

遵循posix标准,跨平台移植性好(在其他平台下也可以使用select实现多路转接)。

缺点:

(1)所能监控的描述符数量有上限。

        select的监控集合是一个数组,当作位图使用,因此能监控多少描述符取决于比特位数,_FD_SETSIZE=1024。

(2)select监控原理,需要进行多次轮询遍历集合,会导致监控的描述符越多,效率就越低。

(3)因为select会修改监控集合,所以每次监控都需要重新添加描述符到集合中。

(4)select返回的是就绪的描述符集合,因此需要用户遍历集合查看哪个描述符还在集合中,操作复杂。

2.poll模型

poll模型:也是针对大量描述符进行监控,但是poll的监控是为每个描述符设置了一个事件结构体。

2.1操作流程

(1)定义要监控的描述事件结构体数组,向数组中添加需要监控的描述符及时间信息。

(2)调用监控接口开始监控,将需要监控的数据拷贝到内核中进行监控(监控原理也是多次轮询遍历)。

注意:监控调用返回前,会将描述符实际就绪事件,填充到结构体的revents成员中,若没有就绪事件则置为0。

(3)监控接口调用完毕后,遍历事件结构体数组,通过每个元素的revents成员确定描述符就绪了什么事件,进行对应操作。

2.2相关接口

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

        fds:事件结构体的首地址;

        nfds:数组中有效元素的个数;

        timeout:监控超时时间,以毫秒为单位;

返回值:

        返回实际就绪的事件个数;返回0,表示监控超时;返回值小于零,表示出错。

事件结构体pollfd:

struct pollfd{

    int fd;//要监控的事件描述符

    short events;//针对这个描述符要监控的事件
    //POLLIN-可读、POLLOUT-可写

    short revents;//监控调用返回后,这个描述符实际就绪的事件
};

2.3示例

利用poll模型,监控标准输入的可读事件:

#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<poll.h>

int main() {
  struct pollfd pfds[10];
  int poll_count = 0;

  pfds[0].fd = 0;//设置要监控的描述符是标准输入
  pfds[0].events = POLLIN;//针对标准输入要监控的是可读事件
  poll_count++;

  while (1) {
    int ret = poll(pfds, 1, 3000);
    if (ret < 0) {
      perror("poll error!");
      usleep(1000);
      continue;
    }
    else if (ret == 0) {
      printf("poll timeout!\n");
      usleep(1000);
      continue;
    }

    for (int i = 0; i < poll_count; ++i) {
      if (pfds[i].revents & POLLIN) {//就绪了可读事件
        char buf[1024] = {0};
        read(pfds[i].fd, buf, 1023);
        printf("描述符%d就绪,读取数据为:%s\n", pfds[i].fd, buf);
      }
      else if (pfds[i].revents & POLLOUT) {
        //就绪的是可写事件,对应操作
      }
    }
  }

  return 0;
}

实现效果:

2.4优缺点

优点:

(1)相较于select模型操作更加简单。

(2)所能监控的描述符数量没有上限。

缺点:

(1)无法跨平台。

(2)监控原理涉及多次轮询遍历,因此效率也会随着监控的描述符数量增加而降低。

3.epoll模型

epoll模型:针对大量描述符进行事件监控(被认为是Linux2.6以后最好用的多路转接模型)。

3.1操作流程

(1)创建epoll句柄

        在内核中会创建struct eventpoll,含有两个重要成员:rbr(红黑树)和rdllist(双向链表)。

(2)向epoll添加监控

        组织对应事件结构体,添加到epoll红黑树中。

(3)开始监控

注意:

①epoll的监控默认是一个异步阻塞操作。

②epoll开始监控,实际上是告诉系统可以开始监控了,监控过程由系统完成。

③系统在进行监控的时候为每个描述符都定义了一个回调函数,回调函数的功能是:一旦描述符就绪了指定事件,就把描述符对应的事件结构体信息添加到rdllist双向链表中。

④我们的进程则只需要根据rdllist双向链表是否为空,来判断是否有描述符就绪。

3.2相关接口

(1)创建句柄

int epoll_create(int size);

        size:所能监控的最大描述符数量,但是在Linux2.6.8之后被忽略(但是必须大于0)。

返回值:

        成功,返回epoll句柄-描述符。

注意:在内核中会创建struct eventpoll结构体

(2)添加监控

int epoll_ct(int epfd, int cmd, int fd, struct epoll_event *ev);

        epfd:epoll_create返回的操作句柄;

        cmd:要进行的操作:EPOLL_CTL_ADD-添加、EPOLL_CTL_DEL-移除、EPOLL_CTL_MOD;

        fd:要操作的描述符;

        ev:要操作的描述符所对应的事件结构体;

返回值:

        成功,返回0;失败返回-1。

(3)开始监控

int epoll_wait(int epfd, struct epoll_event *evs, int maxevents, int timeout);

        epfds:epoll操作句柄;

        evs:事件结构体的数组首地址,用于存放就绪的事件信息;

        maxevents:数组的空间大小(或者说元素个数),防止就绪事件过多,数组空间不够用,因此做的获取就绪事件个数限制。

        timeout:监控的超时时间,一毫秒为单位;

返回值:

        成功,返回就绪的事件个数;出错,返回-1;超时,返回0。

注意:在调用返回后,evs数组里保存的都是就绪的描述符对应的事件结构体,返回值就是元素的个数,用户就可以直接针对事件结构体中的evs[i].data.fd进行events事件操作。

事件结构体epoll_event:

struct epoll_event{
    uint32_t events;//要监控的事件,以及监控返回后实际就绪的事件
    //EPOLLIN-可读、EPOLLOUT-可写

    epoll_data_t data;//用户数据变量
};

typedef union epoll_data{
    void *ptr;
    int fd;
}epoll_data_t;

3.3常见使用方式

        封装Epoll类,实例化的对象可以实现对大量描述符的监控,并返回就绪的描述符。

class Epoll{
    private:
        int _epfd;///epoll操作句柄
    public:
        Epoll(){}
        bool Add(TCPsocket& sock);//对sock描述符添加epoll监控
        bool Del(TCPsocket& sock);//移除对sock描述符的监控
        bool Wait(std::vector<TCPsocket> *array, int timeout = 3000);//开始监控
};

示例:

#include "socket_tcp.hpp"
#include <vector>
#include <sys/epoll.h>
#include <cstdlib>

#define EPOLL_MAX 10

class Epoll{
  private:
    int _epfd;///epoll操作句柄
  public:
    Epoll():_epfd(-1) {
      _epfd = epoll_create(1);
      if (_epfd < 0) {
        perror("epoll error!");
        exit(0);
      }
    }
    bool Add(TCPsocket& sock) {//对sock描述符添加epoll监控
      struct epoll_event ev;
      ev.events = EPOLLIN;//对可读事件进行监控
      ev.data.fd = sock.GetFd();

      int ret = epoll_ctl(_epfd, EPOLL_CTL_ADD, sock.GetFd(), &ev);
      if (ret < 0) {
        perror("epoll_ctl error!");
        return false;
      }
      return true;
    }
    bool Del(TCPsocket& sock){//移除对sock描述符的监控
      int ret = epoll_ctl(_epfd, EPOLL_CTL_DEL, sock.GetFd(), NULL);
      if (ret < 0) {
        perror("epoll_ctl error!");
        return false;
      }
      return true;
    }
    bool Wait(std::vector<TCPsocket> *array, int timeout = 3000){//开始监控
      struct epoll_event evs[EPOLL_MAX];
      int ret = epoll_wait(_epfd, evs, EPOLL_MAX, timeout);
      if (ret < 0) {
        perror("epoll_wait error!");
        return false;
      }
      else if (ret == 0) {
        printf("epoll timeout!\n");
        return false;
      }

      //返回ret个就绪的描述符对应的事件结构体,保存在evs中
      for (int i = 0; i < ret; ++i) {
        if (evs[i].events & EPOLLIN) {//只需要可读事件就绪的描述符
          TCPsocket sock;
          sock.SetFd(evs[i].data.fd);
          array -> push_back(sock);
        }
      }
      return true;
    }
};

实现效果:

3.4epoll的事件触发方式

(一)水平触发(默认的触发方式)

触发特性:

可读事件:只要接收缓冲区中有数据,就会触发事件。

可写事件:只要发送缓冲区中有剩余空间,就会触发事件。

(二)边缘触发(EPOLLET)

触发特性:

可读事件:只有新数据到来,才会触发事件,若没有新数据到来,无论缓冲区中有没有数据,都不会二次触发事件。

可写事件:发送缓冲区剩余空间大小从无到有,才会触发事件。

作用:为了让程序员一次性将请求处理完毕,减少处理次数,以此提高效率(但实际上提高并不明显)。

问题:这么才能一次性将数据处理完?

        通常都是循环读取数据,直到缓冲区中没有数据为止。

注意:但是如果循环读取,缓冲区中没有数据时,recv就会阻塞导致程序卡死,因此边缘触发方式下,通常需要将描述符设置为非阻塞,让描述符的操作都成为非阻塞操作。这样没有数据时,读取则会报错返回。

属性相关接口:

int fcntl(int fd, int cmd, int arg……);

(1)获取描述符属性

int flag = fcntl(fd, F_GETFL, 9);

(2)新增非阻塞属性

fcntl(fd, F_SETFL, flag | O_NONBLOCK);

注意:设置非阻塞属性,是在原先的基础上新增非阻塞属性。且描述符为非阻塞属性,其相关操作也就都是非阻塞操作。

边缘触发适用场景:

(1)想要尽量一次性处理所有请求,减少事件触发次数提高效率。

(2)避免水平触发模式下,因为半条数据没有进行处理,不断触发事件的情况。即缓冲区中数据不完整,想要等到新数据到来,再去取出完整数据进行处理的场景。

3.5优缺点

缺点:

跨平台移植性差。

优点

(1)所能监控的描述符数量没有上限限制。

(2)监控的原理是异步阻塞,让系统进行监控。系统为事件做了回调函数,将就绪事件添加到就绪链表,进程只需要判断就绪链表是否为空,就可以确定是否有描述符就绪,其中不需要轮询遍历操作,因此性能不会随着描述符增多而下降。

(3)直接返回就绪的事件信息,可以直接对就绪的描述符进行指定事件的操作,不需要遍历判断哪个描述符就绪。

select&poll相较于epoll的优点:

        虽然针对大量描述符的监控性能没有epoll高,但是在单个描述符的超时控制上,select和poll操作更加轻便。

注:自了解epoll惊群问题。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

这一年,熬过许多夜,也有些许收获 | 2022年终总结

弹指一挥间&#xff0c;时间如白驹过隙。光阴似箭&#xff0c;日月如梭&#xff0c;时间如闪电&#xff0c;转瞬即逝。回望来时路&#xff0c;不觉潸然泪下… 一说到年终总结&#xff0c;好像都离不开这样煽情的开场白。但不可否认的是&#xff0c;时间确实过得很快&#xff0…

操作指南|通过Snapshot进行投票

Snapshot是一个去中心化的治理平台&#xff0c;包括Moonbeam社区在内的许多Web3社区都使用此平台进行民意投票。民意投票是一种非正式的链下治理流程&#xff0c;允许社区在链上投票之前就某件事发表意见。通过Snapshot进行的民意投票&#xff0c;以及在Moonbeam治理论坛上的讨…

canopen6.0-PDO发送

1、发送PDO 2、发送PDO参数说明 发送PDO标识符:180+NODEID 传输类型 0xff为跟随定时器发送 进制时间:防止某一个报文长时间占用总线设定的时间 envent timer:单位为ms,传输类型设置0xff,定时发送时间 3、发送PDO映射参数说明 PDO映射数目:PDO传输变量数量 7100 01 10h含义…

南卡和漫步者蓝牙耳机哪个好?国产蓝牙耳机南卡和漫步者360度对比评测

现如今&#xff0c;随着蓝牙技术的发展&#xff0c;越来越多的蓝牙耳机出现在我们的日常生活当中&#xff0c;国产蓝牙耳机也不遑多让&#xff0c;在制作上也更为精湛。现在的国产蓝牙耳机品牌有很多&#xff0c;但相较于一些新兴的国产品牌&#xff0c;人们更信赖一些国货老牌…

C#---第十九课:时间DateTime相关的应用 Convert.ToDateTime ParseExact TryParseExact

文章目录1.将字符串转换为标准的DateTime格式2.DateTime数据的相关应用3.ParseExact的应用4.TryParseExact的应用1.将字符串转换为标准的DateTime格式 DateTime 格式是标准的格式&#xff0c;通过这个格式可以方便转换为其他格式、日期的增减、日期的比较等 using System.Diagn…

RK3568平台开发系列讲解(设备驱动篇)等待队列

🚀返回专栏总目录 文章目录 一、等待队列头二、等待队列项三、添加/删除队列四、等待唤醒五、等待事件沉淀、分享、成长,让自己和他人都能有所收获!😄 📢当我们进程去访问设备的时候,经常需要等待有特定事件发生以后再继续往下运行,这个时候就需要在驱动里面实现当条…

以“微”知著,用友ISV生态的力量与担当

由规模化、集约化、智能化带来的影响&#xff0c;不仅可以让养殖户获得更大的养殖密度、更规范的养殖流程、更专业的管理运营&#xff0c;而且能够带动行业发生深远的变革! 养猪也少不了数智化 常言道&#xff1a;民以食为天&#xff0c;猪粮安天下。一头猪就是一个小银行&…

获取微信小程序码传递的参数 / 微信开发者工具模拟扫描小程序码调试

本文主要介绍如何在微信开发者工具中&#xff0c;模拟微信扫描小程序码打开小程序的场景&#xff0c;进行调试。 二维码调试可以看这篇文章&#xff1a;微信开发者工具模拟扫描二维码调试 添加编译模式 添加一个咱们自定义的编译模式 输入模式名称 主要是方便后面的查找 设…

【观察】Oracle NetSuite+德勤中国税务Digital T-Suite:做中国企业“智慧税务”新助手...

伴随着新技术的快速发展&#xff0c;智慧税务成为国家技术力量驱动、治理能力升级背景下的一次全新税务实践。也正因此&#xff0c;继2015年国家税务总局在《“互联网税务”行动计划》中首次提出智慧税务建设目标后&#xff0c;2021年国家再次印发《关于进一步深化税收征管改革…

双非本科到大厂,贫困家庭到深圳买房,我的逆袭之路

20岁的你是否在拼命努力的奋斗呢&#xff1f; 人这一生&#xff0c;其实大大小小有很多的节点&#xff0c;每个年纪该干每个年纪的事情&#xff0c;一步一步的朝前走下去&#xff0c;应该是大部分人目前的现状。 最近一年来&#xff0c;这个号新增了很多的读者&#xff0c;大…

SpringSecurity(二十三)--OAuth2:使用JWT和加密签名(上)对称密钥加密

一、前言 最近阳了所以一直都在休整&#xff0c;大家一定要注意身体&#xff0c;能不阳就不阳&#xff0c;如果阳康后还是一直咳嗽&#xff0c;最好是能去医院看看&#xff0c;这绝对不是专家口中所说的新冠感冒那么简单&#xff0c;也绝对不是什么80%的无症状&#xff0c;大家…

计算机网络——路由信息协议RIP的基本工作原理

&#x1f49f;&#x1f49f;前言 ​ 友友们大家好&#xff0c;我是你们的小王同学&#x1f617;&#x1f617; 今天给大家打来的是 计算机网络——路由信息协议RIP的基本工作原理 希望能给大家带来有用的知识 觉得小王写的不错的话麻烦动动小手 点赞&#x1f44d; 收藏⭐ 评论&…

(四)devops持续集成开发——jenkins的全局工具配置之maven环境安装及配置

前言 本节内容我们主要介绍jenkins中如何集成自定义的maven环境及流水化组件maven插件的安装&#xff0c;这样我们就可以发布流水化的maven项目工程。 正文 上传并安装maven①上传maven安装包 ②解压maven安装包 unzip apache-maven-3.8.3-bin.zip ③配置maven依赖包环境变量…

Attention:何为注意力机制?

本文来自公众号“AI大道理” 人类利用有限的注意力资源从大量信息中快速筛选出高价值信息&#xff0c;这是人类在长期进化中形成的一种生存机制&#xff0c;人类视觉注意力机制极大地提高了视觉信息处理的效率与准确性。 attention从注意力模型的命名方式看&#xff0c;借鉴了…

带你认识不一样的人工智能

人工智能简称AI&#xff0c;它是研究、模拟、延伸和扩展人类智能的理论、方法、技术和应用系统的新兴技术。人工智能是计算机科学的一个领域&#xff0c;正在向机器传递智能&#xff0c;通过模拟人的某些思维过程和智能行为&#xff0c;让机器像人类一样工作、反应和决策。自人…

学习C语言笔记:初始C语言

学习内容&#xff1a; 1.运算符——&#xff1b; 2.函数——main()、printf()&#xff1b; 3.编写一个简单的C程序&#xff1b; 4.创建整型变量&#xff0c;为其赋值并在屏幕上显示其值&#xff1b; 5.换行字符&#xff1b; 6.如何在程序中写注释&#xff0c;创建包含多个函数的…

Docker网络下-自定义网络实战

通过前面两篇的学习,我们对docker网络及四大网络类型都了解了。本文,咱们就来学习docker的自定义网络。我们为什么需要自定义网络呢?是为了让各个主机分门别类,井井有条。方便关联,使得网络之间可以通过服务名进行通信。为什么在容器中,我们要通过服务名进行通信呢?那是…

分享一个门店会员管理系统模板

会员制对于很多人来说都不陌生&#xff0c;进入中国市场几十年的时间里在许多行业都得到了广泛应用。所谓会员制&#xff0c;是指通过向特定的消费群体发放会员卡&#xff0c;并由消费者缴纳会费或者充钱的形式可享受商家的价格折扣、服务等方面优惠的经营形式&#xff0c;从而…

数据库设计以及分布式事务的产生

一、数据库架构的演进 单点时代 1在早期互联网或者当前小型网站,一般数据库和APP都采用单点方式进行部署,系统简单,容易维护读写分离 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29随着互联网的发展,网站访问量越来越大,数据库最先达到瓶颈,…

jenkins环境基本配置

上一篇文章讲解了jenkins的安装&#xff0c;这一篇文章介绍jenkins安装后的基本配置 1.进入jenkins之后&#xff0c;选择右上角admin下拉框选择设置 2.修改密码&#xff0c;并重新登录 3. jenkins的汉化 3.1 Dashboard ➥ Mange Jenkins ➥ Manage Plugins插件管理 ➥ 已安装…