「Tech初见」对epoll的理解

一、Motivation

通常,操作系统会为每个进程划分一个时间片的,在这个时间片内进程可以合法占有 cpu 进行一些计算任务。并当时间片结束后自动退回至就绪状态待命,等待下一次的调度

但是,有一种情况会使进程提前(时间片还未用完)进入等待状态,即是进程发生了阻塞(多半是因为 I/O 请求)。进程一旦发生了阻塞,它就要让出 cpu 给其他进程,这个让位的动作就是进程之间切换的操作,这种操作非常蠢(在开发者眼里是无用功),也很耗时。可以说是时间和 cpu 资源没用在正儿八经的计算任务上

select 和 epoll 的提出就是来解决这个愚蠢的问题,有一种设想:在分配给该进程时间片还未结束之前,如果进程的某个 socket 连接发生阻塞,先不急着逼该进程退位,而是通过某种手段去查询一下进程的其他 socket 连接是否有已就绪的。如果其他 socket 连接有活动可以处理,不如充分利用 cpu 先进行计算,在处理完成 OR 时间片到期后再让位也不迟。这样不就可以提高计算机资源的利用率了嘛

但是,在 Linux 老的版本中,有关事件触发的问题,一直是采用 select 轮询手段来解决的,所谓的轮询就是 cpu 不停地去查询任务队列是否有已经就绪的任务。这种方法在任务较少的情况下还能勉强应付,当任务数量增加至千级数量级之后,效率就会出现断崖式地降低。因为每次需要轮询上千个任务,自然非常耗时

为此,Linux 提出了新的解决方法 epoll,不再采用轮询的方法来感知新事件的发生,而是通过 epoll 结构体内部的红黑树来自动将等待的任务和就绪的任务分开,从而使 kernel 能够快速感知新事件的发生

再说直白一点,只要活儿足够多,epoll_wait 根本就不会让用户进程阻塞,用户进程会一直干活,直到属于该进程的时间片结束。这样就大大减少了进程切换次数,提高了效率

二、Solutions

S1 - epoll_create

创建一个 epoll 句柄,size 用来告诉 kernel 共能监听多少个事件,

int epoll_create(int size)

这个参数在现在的版本中没有意义,kernel 会根据实际情况自行决定的,意思就是说这个 size 只是我们规定的事件的大致数量,而不是能够处理的最大事件数

epoll 结构体中定义的等待队列 wq 存放阻塞在 epoll 对象上的用户进程,当软中断数据就绪时会前来寻找进程;epoll 对象用红黑树 rbr 来管理用户进程 accept 添加进来的所有 socket 连接,选用红黑树的原因是因为红黑树能够更好地支持海量连接的查找、插入和删除;就绪链表 rdllist 存放着一些已就绪的任务,这样一来,应用进程只需要查询 rdllist 就能判断是否有就绪任务可供处理,而不必去遍历整棵红黑树

S2 - epoll_ctl

该方法向 epoll 对象中添加、修改和删除特定的事件,返回 0 表示成功,-1 表示失败,

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event)

添加意味着对这件事感兴趣,应用进程想收来处理;删除则表示对这件事没了兴趣。其中,epfd 是 epoll 对象的 id,epoll_create() 的返回值;op 有三种操作类型,EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL;fd 是需要监听的文件描述符,通常是连接至服务端的 socket;最后一个参数 event 可以是以下几种宏的集合,

  • EPOLLIN:文件描述符可读
  • EPOLLOUT:文件描述符可写
  • EPOLLPRI:文件描述符有紧急数据可读
  • EPOLLERR:文件描述符发生错误
  • EPOLLHUP:文件描述符被挂断
  • EPOLLET:边缘触发(后面会讲到)
  • EPOLLONESHOT:只监听一次,意味着触发来事件之后就被踢出 epoll 对象中了

它是一个传入的指针,这就要求我们需要在进入函数之前分配好空间并初始化,以便 epoll_create() 可以在方法内获取内容,但 epoll_create() 并不会替我们释放 events 空间

再进一步解释,当有新的 socket 连接加入 epoll 对象时,epoll 对象会创建一个 epitem 用来关联该 socket 连接,然后将 epitem 挂到红黑树 rbr 中。之后,会设置该 epitem 的回调函数(如果该连接有数据写入,请将其存入 epoll 对象的就绪链表 rdllist 中),以及其他的回调函数

在这我只列举了 “增” 的一个例子,其他关于 “删” 和 “改” 的操作,它们的本质是一样的,都是 socket 连接有什么动作就会去调用对应的回调函数。关于能够快速实现 “增删改查” 最主要的原因是因为选用了红黑树

S3 - epoll_wait

等待处于监听范围的事件发生,

int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout)

epoll 对象会将已经发生的事件复制到数组 events 中,maxevents 是数组的长度;timeout 如果为 0,则意味着就绪链表 rdllist 若为空则立刻返回,不会等待;-1 表示阻塞,会一直陷入 epoll_wait 状态中

关于 ET 和 LT 模式,我想用简短的语言去描述,不要深究细节。ET(边缘触发)模式仅当状态发生变化时才会感知事件的发生,即使这个事件对应的缓冲区内还有未读取的数据;而 LT(水平触发)模式是只要有数据没处理就会一直通知下去

三、Result

我想透过一个简单的 demo 来介绍 epoll 的经典用法。说到用法,最常用的就是连接 socket,监听 socket 的动静并读/写数据进行处理,之后返回给 client 结果。我写了一个小写转大写的程式来说明 epoll 的用法,请看代码,

#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <ctype.h>
#include <unistd.h>

#define EPOLL_MAXSIZE 16
#define SRV_PORT_ID 1980  /* 端口号 */
#define SOCKET_QUEUE_LEN 20
#define BUFSIZE 256

struct myepoll_data {
  int fd;
  char data[BUFSIZE];
};

int main()
{
  int i,j;
  int epfd, sockfd, nfds, clntfd;
  struct sockaddr_in srvaddr, clntaddr;
  struct epoll_event ev, evs[EPOLL_MAXSIZE];
  socklen_t clntlen = sizeof(clntaddr);
  char buf[BUFSIZE];

  /* 创建epoll结构体(就绪链表、等待队列和红黑树) */
  epfd = epoll_create(EPOLL_MAXSIZE);
  if(epfd == -1) {
    printf("epoll_create err\n");
    goto over;
  }
  printf("epoll_create ok\n");

  /* 创建socket结构体 */
  sockfd = socket(AF_INET, SOCK_STREAM, 0);
  if(sockfd == -1) {
    printf("socket_create err\n");
    goto over;
  }
  printf("socket_create ok\n");
  /* 初始化socket绑定监听 */
  bzero(&srvaddr, sizeof(srvaddr));
  srvaddr.sin_family = AF_INET;
  srvaddr.sin_port = htons(SRV_PORT_ID);
  srvaddr.sin_addr.s_addr = htonl(INADDR_ANY);

  if(bind(sockfd, (struct sockaddr*)&srvaddr, sizeof(struct sockaddr)) == -1) {
    printf("socket_bind err\n");
    goto over;
  }
  printf("socket_bind ok\n");

  if(listen(sockfd, SOCKET_QUEUE_LEN) == -1) {
    printf("socket_listen err\n");
    goto over;
  }
  printf("socket_listen ok\n");

  /* 向epoll结构体中注册socket,实现监听功能 */
  ev.data.fd = sockfd;
  ev.events = EPOLLIN | EPOLLET;
  if(epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev) == -1) {
    printf("epoll_ctl_add err\n");
    goto over;
  }
  printf("epoll_ctl_add ok\n");

  /* 不停地处理外来事件 */
  while(1) {
    /* 阻塞地等待事件发生,其中0为没有就绪事件就立刻返回,-1为阻塞 */
    nfds = epoll_wait(epfd, evs, EPOLL_MAXSIZE, -1);
    /* 处理每个收上来的事件 */
    for(i=0; i<nfds; i++) {
      if(evs[i].data.fd == sockfd) {  /* 有人敲sockfd的门了(收到新的连接)*/
        clntfd = accept(sockfd, (struct sockaddr*)&clntaddr, &clntlen);
        ev.events = EPOLLIN | EPOLLET;
        ev.data.fd = clntfd;

        if(epoll_ctl(epfd, EPOLL_CTL_ADD, clntfd, &ev) == -1)
          printf("epoll_ctl_add %d err\n", clntfd);
        else
          printf("epoll_ctL_add %d clnt ok\n", clntfd);
      } else if(evs[i].events & EPOLLIN) {  /* 读取数据但先不处理 */
        clntfd = evs[i].data.fd;
        memset(buf, 0, BUFSIZE);

        if(read(clntfd, buf, BUFSIZE) == 0) { /* 客户端关闭连接 */
          if(epoll_ctl(epfd, EPOLL_CTL_DEL, clntfd, NULL) == -1) {
            printf("epoll_ctl_del %d err\n", clntfd);
          } else {
            printf("epoll_ctl_del %d ok\n", clntfd);
            close(clntfd);
          }
          continue;
        }

        /* 先接收client的请求 */
        struct myepoll_data fddata;
        fddata.fd = clntfd;
        strcpy(fddata.data, buf);
        ev.data.ptr = &fddata;
        memset(buf, 0, BUFSIZE);
        strcpy(buf, "i'm keep u's data, deal with it later, please check u can be written...\n");
        send(clntfd, buf, strlen(buf), 0);

        ev.events = EPOLLOUT | EPOLLET;
        /* 下一次epoll时再处理client的请求 */
        if(epoll_ctl(epfd, EPOLL_CTL_MOD, clntfd, &ev) == -1) 
          printf("epoll_ctl_mod clnt %d EPOLLIN -> EPOLLOUT err\n", clntfd);
        else 
          printf("epoll_ctl_mod clnt %d EPOLLIN -> EPOLLOUT ok\n", clntfd);
      } else if(evs[i].events & EPOLLOUT) { /* 对之前读取的数据予以处理并将处理结果返回给client */
        struct myepoll_data* fddata = (struct myepoll_data*)evs[i].data.ptr;
        clntfd = fddata->fd;
        char* data = fddata->data;

        memset(buf, 0, BUFSIZE);
        strcpy(buf, "i'm processing u's data, please waiting...\n");
        send(clntfd, buf, strlen(buf), 0);

        /* 将小写转为大写的业务逻辑 */
        for(j=0; j<strlen(data); j++)
          data[j] = toupper(data[j]);
        send(clntfd, data, strlen(data), 0);

        ev.events = EPOLLIN | EPOLLET;
        /* 准备接收client的下一次计算请求 */
        if(epoll_ctl(epfd, EPOLL_CTL_MOD, clntfd, &ev) == -1)
          printf("epoll_ctl_mod clnt %d EPOLLOUT -> EPOLLIN err\n", clntfd);
        else 
          printf("epoll_ctl_mod clnt %d EPOLLOUT -> EPOLLIN ok\n", clntfd);
      } else {
        printf("unknown event\n");
      }
    }
  }

over:
  return 0;
}

整个流程,我认为较为清晰,首先创建 socket,然后将 socket 添加进 epoll 对象中,这就意味着让 epoll 对象监听 socket 的一举一动。如果有数据写入 socket 中,那么就读出来,等待下一轮再进行处理(为什么下一轮再进行处理?而不是接收了请求就处理,其中的道理我暂时还没有悟透,但有人告诉我,先接收后处理的手法是 epoll 的精髓,说实话我并不认同,因为我不能说服自己要相信这种脱裤子放屁的说法)

按照流程走下去,在下一轮中进行处理(小写转大写),然后将结果返回给 client。这就是 epoll demo。在另一个终端中透过 nc 命令尝试连接 server 进程,

nc 127.0.0.1 1980

作为 client,输入小写的字符串,server 就会返回大写的结果,

在这里插入图片描述

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

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

相关文章

Web实战:基于Django与Bootstrap的在线计算器

文章目录 写在前面实验目标实验内容1. 创建项目2. 导入框架3. 配置项目前端代码后端代码 4. 运行项目 注意事项写在后面 写在前面 本期内容&#xff1a;基于Django与Bootstrap的在线计算器 实验环境&#xff1a; vscodepython(3.11.4)django(4.2.7)bootstrap(3.4.1)jquery(3…

1、cvpr2024

CVPR2024官网&#xff1a; Overleaf模板&#xff1a; 更改作者&#xff08;去掉CVPR标识&#xff09; % \usepackage{cvpr} % To produce the CAMERA-READY version \usepackage[review]{cvpr} % To produce the REVIEW version改成 \usepackage{cvpr} …

性格懦弱怎么办?如何改变懦弱的性格?

性格懦弱是一个比较常见的话题了&#xff0c;懦弱带来的苦恼和困扰&#xff0c;深深影响着我们的生活&#xff0c;人际关系&#xff0c;以及事业的发展。然后如何摆脱懦弱&#xff0c;却并非易事&#xff0c;尤其是对于成年人来说&#xff0c;这种懦弱的性格特征&#xff0c;已…

Prometheus+Grafana监控

Prometheus是一种开源监控系统&#xff0c;可用于收集指标和统计数据&#xff0c;并提供强大的查询语言&#xff0c;以便分析和可视化这些数据。它被广泛用于云原生和容器化环境中&#xff0c;可以嵌入到Kubernetes集群中&#xff0c;并与其他Kubernetes工具进行集成。 Grafan…

大模型的交互能力

摘要&#xff1a; 基础大模型显示出明显的潜力&#xff0c;可以改变AI系统的开发人员和用户体验&#xff1a;基础模型降低了原型设计和构建AI应用程序的难度阈值&#xff0c;因为它们在适应方面的样本效率&#xff0c;并提高了新用户交互的上限&#xff0c;因为它们的多模式和生…

代码随想录算法训练营|五十六天

回文子串 647. 回文子串 - 力扣&#xff08;LeetCode&#xff09; dp含义&#xff1a;表示区间内[i,j]是否有回文子串&#xff0c;有true&#xff0c;没有false。 递推公式&#xff1a;当s[i]和s[j]不相等&#xff0c;false&#xff1b;相等时&#xff0c;情况一&#xff0c;…

图书管理系统 保姆级教学 手把手教你图书管理系统设计!

天梯无捷径&#xff0c;唯有苦攀登。 一起加油&#xff0c;小伙伴们&#xff01;&#xff01; 目录 1. 实现思路: 2. 那么如何找对象呢? 3. Book类的实现 Book类总代码&#xff1a; 4. BookList类的实现 BookList类总代码&#xff1a; 5. 用户的操作 5.1 AddOperation类…

在线识别二维码工具

具体请前往&#xff1a;在线二维码识别解码工具--在线识别并解码二维码网址等内容

10、背景分离 —— 大津算法

上一节学习了通过一些传统计算机视觉算法,比如Canny算法来完成一个图片的边缘检测,从而可以区分出图像的边缘。 今天再看一个视觉中更常见的应用,那就是把图片的前景和背景的分离。 前景和背景 先看看什么是前景什么是背景。 在图像处理和计算机视觉中,"前景"…

Go——一、Go语言安装及介绍

Go 一、Windows下安装Go1、下载Go2、配置环境变量3、下载Jetbrain下的GoLang4、编写hello world5、编译和执行 二、Go语言介绍1、开发文档2、Go语言核心开发团队3、为什么要创建Go4、Go语言发展史5、Go语言特点6、Golang执行过程6.1 执行过程分析6.2 编译是什么 7、开发注意事项…

线性变换概论

线性变换 定义 设 V V V 和 W W W 都是在域 K K K上定义的向量空间&#xff0c; T : V → W T :V \rightarrow W T:V→W 对任二向量 x , y ∈ V x,y \in V x,y∈V,与任何标量 a ∈ K a \in K a∈K&#xff0c;满足&#xff1a; T ( x y ) T ( x ) T ( y ) T(xy)T(x)T(…

c语言:解决数组有关的删除,排序,合并等问题。

题目1&#xff1a;判断数组是否有序&#xff08;升序或者降序&#xff09; 思路和代码&#xff1a; #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() {int a 0;scanf("%d", &a);int arr[50];int flag1 0;//是降序int flag2 0;//是升序…

系列十一、你平时工作用过的JVM常用基本配置参数有哪些?

一、常用参数 1.1、-Xms 功能&#xff1a;初始内存大小&#xff0c;默认为物理内存的1/64&#xff0c;等价于 -XX:InitialHeapSize 1.2、-Xmx 功能&#xff1a;最大分配内存&#xff0c;默认为物理内存的1/4&#xff0c;等价于 -XX:MaxHeapSize 1.3、-Xss 功能&#xff1a;设置…

解决在pycharm中使用matplotlib画图问题

第一&#xff0c;再导入包后直接绘图出现&#xff1a; AttributeError: module backend_interagg has no attribute FigureCanvas表明版本不兼容&#xff0c;我们需要加入&#xff1a;matplotlib.use(‘TkAgg’) 导入函数就变成了&#xff1a; import matplotlib matplotlib.…

项目点使用Redis作为缓存技术-自用

在spring boot项目中&#xff0c;使用缓存技术只需在项目中导入相关缓存技术的依赖包&#xff0c;并在启动类上使用EnableCaching开启缓存支持即可。 例如&#xff0c;使用Redis作为缓存技术&#xff0c;只需要导入Spring data Redis的maven坐标即可。 描述 使用Redis缓存高频数…

趣学python编程 (三、计算机基础知识)

如果不了解些计算机的基础知识上来就编程&#xff0c;往往容易“不识庐山真面目&#xff0c;只缘身在此山中”。因此对于计算机的一些基础知识&#xff0c;在开始编程前&#xff0c;需要理解和掌握。 计算机软件系统 计算机软件是控制计算机实现用户需求的计算机操作以及管理计…

Django 简单入门(一)

一、配置虚拟环境 1、安装虚拟环境库vitualenv 与vitualenvwrapper-win 2、创建虚拟环境 myenv 3、在此环境中安装django 二、创建一个Django项目 1、使用命令来创建&#xff1a;django-admin startproject Django2023 工程名为Django2023 2、 使用PyCharm专业版创建Django项…

【C++初阶】STL详解(四)vector的模拟实现

本专栏内容为&#xff1a;C学习专栏&#xff0c;分为初阶和进阶两部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握C。 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;C &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&…

OpenCV快速入门:图像形态学操作

文章目录 前言一、图像形态学基础1.1 背景介绍1.2 像素距离1.2.1 什么是像素距离&#xff1f;1.2.2 常见的像素距离度量方法1.2.3 计算像素距离的代码实现 1.3 图像连通性1.3.1 什么是图像连通性&#xff1f;1.3.2 连通类型1.3.3 连通组件标记1.3.4 连通性在图像处理中的应用 1…