「Tech初见」对epoll的理解

news2024/11/17 9:46:52

一、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 中,那么就读出来,等待下一轮再进行处理(为什么下一轮再进行处理?)

在下一轮中进行处理,然后将结果返回给 client。这就是 epoll demo。在另一个终端中透过 nc 命令尝试连接 server 进程,

nc 127.0.0.1 1980

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

在这里插入图片描述

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

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

相关文章

cad提示由于找不到mfc140u.dll,无法继续执行代码怎么修复

在Windows操作系统中&#xff0c;mfc140u.dll是一个重要的文件&#xff0c;很多软件运行都需要它&#xff0c;它属于Microsoft Visual C库的一部分。许多基于C的开发项目都依赖于这个文件&#xff0c;如果在使用过程中出现丢失现象&#xff0c;可能导致相关软件或游戏无法正常运…

洛谷 P1064 [NOIP2006 提高组] 金明的预算方案 python解析

P1064 [NOIP2006 提高组] 金明的预算方案 时间&#xff1a;2023.11.19 题目地址&#xff1a;[NOIP2006 提高组] 金明的预算方案 题目分析 动态规划的0-1背包&#xff0c;采用动态数组。如果不了解的话&#xff0c;可以先看看这个背包DP。 这个是0-1背包的标准状态转移方程 f…

SOME/IP 协议介绍(六)接口设计的兼容性规则

接口设计的兼容性规则&#xff08;信息性&#xff09; 对于所有序列化格式而言&#xff0c;向较新的服务接口的迁移有一定的限制。使用一组兼容性规则&#xff0c;SOME / IP允许服务接口的演进。可以以非破坏性的方式进行以下添加和增强&#xff1a; • 向服务中添加新方法 …

【C++】【Opencv】cv::Canny()边缘检测函数详解和示例

Canny边缘检测是一种流行的边缘检测算法&#xff0c;由John F. Canny在1986年开发。它是一种多阶段过程&#xff0c;包括噪声滤波、计算图像强度的梯度、非最大值抑制以及双阈值检测。本文通过函数原型解读和示例对cv::Canny()函数进行详解&#xff0c;以帮助大家理解和使用。 …

Blowfish在线加密解密调试校验工具

具体请前往&#xff1a;在线Blowfish加密解密工具

米家竞品分析

一、项目描述 1. 竞品分析描述 分析市场直接竞品和潜在竞品&#xff0c;优化产品核心结构和页面布局&#xff0c;确立产品核心功能定位。了解目标用户核心需求&#xff0c;挖掘用户魅力型需求&#xff0c;以及市场现状为产品迭代做准备。 2. 产品测试环境 二、市场 1. 行业…

Django 简单入门(一)

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

【预处理详解】

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 目录 前言 1. 预定义符号 2. #define定义常量 3. #define定义宏 4. 带有副作用的宏参数 5. 宏替换的规则 6. 宏函数的对比 7. #和## 7.1 #运算符 7.2 ## 运算符 8. 命名约定 …

基于STC12C5A60S2系列1T 8051单片的模数芯片ADC0809实现模数转换应用

基于STC12C5A60S2系列1T 8051单片的模数芯片ADC0809实现模数转换应用 STC12C5A60S2系列1T 8051单片机管脚图STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式及配置STC12C5A60S2系列1T 8051单片机I/O口各种不同工作模式介绍模数芯片ADC0809介绍通过模数芯片ADC0809把电压模…

kubenetes-Service和EndpointSlice

一、Service 二、Endpoint Endpoint记录一个 Service 对应一组 Pod 的访问地址&#xff0c;一个 Service 只有一个 Endpoint资源。Endpoint资源会去监测Pod 集合&#xff0c;只要服务中的某个 Pod 发生变更&#xff0c;Endpoint 就会进行同步更新。 三、Service、Endpoint和 P…

青少年CTF-WEB-2048

题目环境&#xff1a; 针对这种游戏通关类题目&#xff0c;常见的有两种情况 一、有参数改参数的数值达到题目规定的分数即可拿到flag 二、没有参数那么flag就是被编码了&#xff0c;找编码即可 这道题并没有说题目通关即可获得flag&#xff0c;也并没有发现参数 所以这里猜测f…

域名的理解

域名的分类 见下图 这里引用的阿里云对域名的定义&#xff0c;个人理解是有两种叫法&#xff0c;一种是传统的叫法&#xff0c;也就是将sample.org.cn划分成了三级域名&#xff0c;还有一种叫法是基于用户注册的域名来说的&#xff0c;将用户注册的整体域名称作一级域名&…

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} …

高效背单词——单词APP安利

大英赛&#xff0c;CET四六级&#xff0c;以及考研英语&#xff0c;都在不远的未来再度来临&#xff0c;年复一年的考试不曾停息&#xff0c;想要取得好成绩&#xff0c;需要我们的重视并赋予相应的努力。对于应试英语&#xff0c;词汇量是不可忽略的硬性要求。相比于传统默写&…

翻译软件Mate Translate mac中文版介绍说明

Mate Translate mac可以帮你翻译超过100种语言的单词和短语&#xff0c;使用文本到语音转换&#xff0c;并浏览历史上已经完成的翻译。你还可以使用Control S在弹出窗口中快速交换语言。 Mate Translate Mac版软件介绍 Mate Translate 可以在你的所有设备之间轻松同步&#x…

EDA实验-----4*4矩阵键盘与数码管显示测试

目录 一、实验目的 二、实验仪器设备 三、实验原理 四、实验要求 五、实验步骤 六、实验报告 七、实验过程 1.矩阵键盘按键原理 2.数码管原理 3.分频器代码 4.电路图连接 5.文件烧录 一、实验目的 了解数码管的工作原理&#xff1b;掌握4*4矩阵键盘和数码管显示的编…

如何解决msvcr100.dll丢失问题?5个实用的解决方法分享

在日常计算机操作过程中&#xff0c;相信不少小伙伴都经历过这样一种困扰&#xff0c;那便是某款应用程序或者游戏无法正常启动并弹出“找不到msvcr100.dll”的提示信息。这类问题让人头疼不已&#xff0c;严重影响到了我们的工作效率和休闲娱乐。接下来&#xff0c;就让小编带…

Node.js环境配置级安装vue-cli脚手架

一、下载安装Node.js (略) 二、验证node.js并配置 1、下载安装后&#xff0c;cmd面板输入node -v查询版本、npm -v ,查看npm是否安装成功&#xff08;有版本号就行了&#xff09; 2、选择npm镜像&#xff08;npm config set registry https://registry.npm.taobao.org&…

大数据HCIE成神之路之数学(3)——概率论

概率论 1.1 概率论内容介绍1.1.1 概率论介绍1.1.2 实验介绍 1.2 概率论内容实现1.2.1 均值实现1.2.2 方差实现1.2.3 标准差实现1.2.4 协方差实现1.2.5 相关系数1.2.6 二项分布实现1.2.7 泊松分布实现1.2.8 正态分布1.2.9 指数分布1.2.10 中心极限定理的验证 1.1 概率论内容介绍…

视频合并:掌握视频嵌套合并技巧,剪辑高手的必备秘籍

在视频剪辑的过程中&#xff0c;掌握视频合并的技巧是每个剪辑高手必备的技能之一。通过合理的合并视频&#xff0c;可以增强视频的视觉效果&#xff0c;提高观看体验。 一、视频合并的准备工作 收集素材&#xff1a;在进行视频合并之前&#xff0c;首先需要收集足够的素材&a…