百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

彻底学会使用epoll(一)——ET模式实现分析

liebian365 2024-10-27 13:13 21 浏览 0 评论

1. ET模式实现分析

1.1 ET和LT的实现区别

首先给出下面一张图,这张图是从我之前的一篇博文——epoll实现分析中摘取并细化的。这张图对理解ET模式已经epoll的工作过程只管重要,当然我自己总结出来后也感觉有的小成就,在这里与大家分享。

注:上图的poll不要理解成和select相似那个poll,这是通过epoll_ctl调用的。

相关视频推荐

面试中正经“八股文”网络原理tcp/udp,网络编程epoll/reactor

epoll_wait中的events数组设置多大合适?

C/C++Linux服务器开发/后台架构师【零声教育】-学习视频教程-腾讯课堂

下面简要分析一下epoll的工作过程:

(1) epoll_wait调用ep_poll,当rdlist为空(无就绪fd)时挂起当前进程,知道rdlist不空时进程才被唤醒。

(2) 文件fd状态改变(buffer由不可读变为可读或由不可写变为可写),导致相应fd上的回调函数ep_poll_callback()被调用。

(3) ep_poll_callback将相应fd对应epitem加入rdlist,导致rdlist不空,进程被唤醒,epoll_wait得以继续执行。

(4) ep_events_transfer函数将rdlist中的epitem拷贝到txlist中,并将rdlist清空。

(5) ep_send_events函数(很关键),它扫描txlist中的每个epitem,调用其关联fd对用的poll方法(图中蓝线)。此时对poll的调用仅仅是取得fd上较新的events(防止之前events被更新),之后将取得的events和相应的fd发送到用户空间(封装在struct epoll_event,从epoll_wait返回)。之后如果这个epitem对应的fd是LT模式监听且取得的events是用户所关心的,则将其重新加入回rdlist(图中蓝线),否则(ET模式)不在加入rdlist。

具体代码:

/* 扫描整个txlist链表... */for (eventcnt = 0, uevent = esed->events;     !list_empty(head) && eventcnt < esed->maxevents;) {/* 取出第一个成员 */epi = list_first_entry(head, struct epitem, rdllink);/* 然后从链表里面移除 */list_del_init(&epi->rdllink);/* 读取events,  * 注意events我们ep_poll_callback()里面已经取过一次了, 为啥还要再取? * 1. 我们当然希望能拿到此刻的最新数据, events是会变的~ * 2. 不是所有的poll实现, 都通过等待队列传递了events, 有可能某些驱动压根没传 * 必须主动去读取. */revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) &epi->event.events;
if (revents) {/* 将当前的事件和用户传入的数据都copy给用户空间, * 就是epoll_wait()后应用程序能读到的那一堆数据. */if (__put_user(revents, &uevent->events) ||    __put_user(epi->event.data, &uevent->data)) {/* 如果copy过程中发生错误, 会中断链表的扫描, * 并把当前发生错误的epitem重新插入到ready list. * 剩下的没处理的epitem也不会丢弃, 在ep_scan_ready_list() * 中它们也会被重新插入到ready list */list_add(&epi->rdllink, head);return eventcnt ? eventcnt : -EFAULT;}eventcnt++;uevent++;if (epi->event.events & EPOLLONESHOT)epi->event.events &= EP_PRIVATE_BITS;else if (!(epi->event.events & EPOLLET)) {/* * If this file has been added with Level * Trigger mode, we need to insert back inside * the ready list, so that the next call to * epoll_wait() will check again the events * availability. At this point, noone can insert * into ep->rdllist besides us. The epoll_ctl() * callers are locked out by * ep_scan_ready_list() holding "mtx" and the * poll callback will queue them in ep->ovflist. *//* 嘿嘿, EPOLLET和非ET的区别就在这一步之差呀~ * 如果是ET, epitem是不会再进入到readly list, * 除非fd再次发生了状态改变, ep_poll_callback被调用. * 如果是非ET, 不管你还有没有有效的事件或者数据, * 都会被重新插入到ready list, 再下一次epoll_wait * 时, 会立即返回, 并通知给用户空间. 当然如果这个 * 被监听的fds确实没事件也没数据了, epoll_wait会返回一个0, * 空转一次. */list_add_tail(&epi->rdllink, &ep->rdllist);}}}

说明:

l epoll_wait返回的条件是rdlist不空,而使rdlist不空的途径有两个,分别对应图中的红线和蓝线。

l ET和LT模式下的epitem都可以通过红线方式加入rdlist从而唤醒epoll_wait,但LT模式下的epitem还可以通过蓝线方式重新加入rdlist唤醒epoll_wait。所以ET模式下,fd就绪(通过红线加入rdlist)只会被通知一次,而LT模式下只要满足相应读写条件就返回就绪(通过蓝线加入rdlist)。

l ET事件发生仅通知一次的原因是只被添加到rdlist中一次,而LT可以有多次添加的机会。

1.2 两种加入rdlist途径的不同

下面我们来分析一下图中两种将epitem加入rdlist方式(也就是红线和蓝线)的区别。

l 红线:fd状态改变是才会触发。那么什么情况会导致fd状态的改变呢?

对于读取操作:

(1) 当buffer由不可读状态变为可读的时候,即由空变为不空的时候。

(2) 当有新数据到达时,即buffer中的待读内容变多的时候。

对于写操作:

(1) 当buffer由不可写变为可写的时候,即由满状态变为不满状态的时候。

(2) 当有旧数据被发送走时,即buffer中待写的内容变少得时候。

l 蓝线:fd的events中有相应的时间(位置1)即会触发。那么什么情况下会改变events的相应位呢?

对于读操作:

(1) buffer中有数据可读的时候,即buffer不空的时候fd的events的可读为就置1。

对于写操作:

(1) buffer中有空间可写的时候,即buffer不满的时候fd的events的可写位就置1。

说明:红线是时间驱动被动触发,蓝线是函数查询主动触发。

【文章福利】:小编整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!~点击加入(832218493需要自取)

ET和LT的触发方式

分析了ET和LT的实现方式,那么分析他们的触发方式就容易多了。我们通过实现分析知道LT模式下epoll_wait被唤醒可以通过两种方式(图中红线和蓝线),而ET模式只能通过一种方式(图中红线)。所以ET模式下能被唤醒的情况,LT模式下一定也能被唤醒。我们先来讨论特殊情况(ET模式),再来讨论一般情况(LT模式)。

2.1 ET

根据上一节对两种加入rdlist途径的分析,可以得出ET模式下被唤醒(返回就绪)的条件为:

l 对于读取操作:

(1) 当buffer由不可读状态变为可读的时候,即由空变为不空的时候。

(2) 当有新数据到达时,即buffer中的待读内容变多的时候。

另外补充一点:

(3) 当buffer中有数据可读(即buffer不空)且用户对相应fd进行epoll_mod IN事件时(具体见下节内容)。

对于情况(1)(2)分别对应图1(a),图1(b)。

l 对于写操作:

(1) 当buffer由不可写变为可写的时候,即由满状态变为不满状态的时候。

(2) 当有旧数据被发送走时,即buffer中待写的内容变少得时候。

另外补充一点:

(3) 当buffer中有可写空间(即buffer不满)且用户对相应fd进行epoll_mod OUT事件时(具体见下节内容)。

对于情况(1)(2)分别对应图2(a),图2(b)。


图1 ET读触发的两种情况



图2 LT写触发的两种情况


2.2 LT

LT模式下进程被唤醒(描述符就绪)的条件就简单多了,它包含ET模式的所有条件,也就是上述列出的六中读写被唤醒的条件都是用于LT模式。此外,还有更普通的情况LT可以被唤醒,而ET则不理会,这也是我们需要注意的情况。

l 对于读操作

当buffer中有数据,且数据被读出一部分后buffer还不空的时候,即buffer中的内容减少的时候,LT模式返回读就绪。如下图所示。



l 对于写操作

当buffer不满,又写了一部分数据后扔然不满的的时候,即由于写操作的速度大于发送速度造成buffer中的内容增多的时候,LT模式会返回就绪。如下图所示。



注:poll和select都是LT模式。

ET的读操作实例分析

首先看程序一,这个程序想要实现的功能是当用户从控制台有任何输入操作时,输出”hello world!”

程序一

#include <unistd.h>

#include <iostream>

#include <sys/epoll.h>

using namespace std;

int main(void)

{

    int epfd,nfds;

    struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要处理的事件

    epfd=epoll_create(1);//只需要监听一个描述符——标准输入

    ev.data.fd=STDIN_FILENO;

    ev.events=EPOLLIN|EPOLLET;//监听读状态同时设置ET模式

    epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev);//注册epoll事件

    for(;;)

   {

     nfds=epoll_wait(epfd,events,5,-1);

     for(int i=0;i<nfds;i++)

     {

 if(events[i].data.fd==STDIN_FILENO)

           cout<<"hello world!"<<endl;

     }

   }

}

运行结果:



程序一中对标准输入的监听使用ET模式,结果实现了我们想要的功能。那么实际原理是如何呢,我们将过程分析一下:

(1) 当用户输入一组字符,这组字符被送入buffer,字符停留在buffer中,又因为buffer由空变为不空,所以ET返回读就绪,输出”hello world!”。

(2) 之后程序再次执行epoll_wait,此时虽然buffer中有内容可读,但是根据我们上节的分析,ET并不返回就绪,导致epoll_wait阻塞。(底层原因是ET下就绪fd的epitem只被放入rdlist一次)。

(3) 用户再次输入一组字符,导致buffer中的内容增多,根据我们上节的分析这将导致fd状态的改变,是对应的epitem再次加入rdlist,从而使epoll_wait返回读就绪,再次输出“hello world!”。

我们在看看LT的情况如何,将程序一以下修改:

ev.events=EPOLLIN;//默认使用LT模式

运行结果:




结果正如我们所料,程序出现死循环,因为用户输入任意数据后,数据被送入buffer且没有被读出,所以LT模式下每次epoll_wait都认为buffer可读返回读就绪。导致每次都会输出”hello world!”。下面在看程序二。

l 程序二

#include <unistd.h>
#include <iostream>
#include <sys/epoll.h>
using namespace std;
int main(void)
{
    int epfd,nfds;
    char buf[256];
    struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要处理的事件
    epfd=epoll_create(1);//只需要监听一个描述符——标准输入
    ev.data.fd=STDIN_FILENO;
    ev.events=EPOLLIN;//使用默认LT模式
    epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev);//注册epoll事件
    for(;;)
   {
     nfds=epoll_wait(epfd,events,5,-1);
     for(int i=0;i<nfds;i++)
     {
       if(events[i].data.fd==STDIN_FILENO)
       {
          read(STDIN_FILENO,buf,sizeof(buf));//将缓冲中的内容读出
          cout<<"hello world!"<<endl;
       }
    }
  }
}

运行结果:



程序二依然使用LT模式,但是每次epoll_wait返回读就绪的时候我们都将buffer(缓冲)中的内容read出来,所以导致buffer再次清空,下次调用epoll_wait就会阻塞。所以能够实现我们所想要的功能——当用户从控制台有任何输入操作时,输出”hello world!”。我们再来看看程序三。

程序三

int main(void)

{

    int epfd,nfds;

    struct epoll_event ev,events[5];//ev用于注册事件,数组用于返回要处理的事件

    epfd=epoll_create(1);//只需要监听一个描述符——标准输入

    ev.data.fd=STDIN_FILENO;

    ev.events=EPOLLIN|EPOLLET;//使用默认LT模式

    epoll_ctl(epfd,EPOLL_CTL_ADD,STDIN_FILENO,&ev);//注册epoll事件

    for(;;)

   {

     nfds=epoll_wait(epfd,events,5,-1);

     for(int i=0;i<nfds;i++)

     {

       if(events[i].data.fd==STDIN_FILENO)

        {

          cout<<"hello world!"<<endl;

          ev.data.fd=STDIN_FILENO;

          ev.events=EPOLLIN|EPOLLET;//使用默认LT模式

          epoll_ctl(epfd,EPOLL_CTL_MOD,STDIN_FILENO,&ev);//重新MOD事件(ADD无效)

        }

     }

   }

}

程序三依然使用ET,但是每次读就绪后都主动的再次MOD IN事件,我们发现程序再次出现死循环,也就是每次返回读就绪。这就验证了上一节讨论ET读就绪的第三种情况。但是注意,如果我们将MOD改为ADD,将不会产生任何影响。别忘了每次ADD一个描述符都会在epitem组成的红黑树中添加一个项,我们之前已经ADD过一次,再次ADD将阻止添加,所以在次调用ADD IN事件不会有任何影响。

相关推荐

Linux-常用操作命令介绍(linux常用的命令大全)

1.帮助命令帮助命令1.1help命令语法格式:命令--help作用:查看某个命令的帮助信息示例#ls--help#netstat--help1.2man命令语法格式:man命令...

推荐:一个小而美的Java工具类库(java工具软件)

前言是的,你没看错,没看错,它就是hutool!相信很多做java开发的朋友应该都已经认识并使用过它了,今天带大家再重温一下它都有哪些功能,并以示例来看看hutool是如何简便实现JWT认...

【SpringBoot后端开发】第三部分 Linux操作系统常用命令(3)

创作不易,请帮忙转发、点赞和评论!四、Linux常用命令对于Linux系统来说,中央处理器、内存、磁盘驱动器、键盘、鼠标、用户等都是文件,而Linux系统管理的命令是它正常运行的核心,与之DOS命令类...

linux常用命令在线查询工具(linux常用命令在线查询工具有哪些)

linuxvi编辑器常用命令linux查看iplinuxfind-name查找文件名linuxshelllinux查看端口占用linux删除文件命令linuxcp命令复制文件到另一个...

使用免费绿色工具chfs,将文件夹共享成网盘

需求:业务需求方有个需要将apk包上传到服务器中,通过chfs可以将服务器目录共享出来,可以可以登录后台自行上传apk文件包。本文就教大家三个知识点1.centos7下使用chfs,共享目录。2.使用...

Mysql和Hive之间通过Sqoop进行数据同步

文章回顾理论大数据框架原理简介大数据发展历程及技术选型实践搭建大数据运行环境之一搭建大数据运行环境之二本地MAC环境配置CPU数和内存大小查看CPU数sysctlmachdep.cpu#核数为...

真实案例记录Linux被植入rootkit导致服务器带宽跑满的解决过程

一、关于linux下的rootkitrootkit是Linux平台下最常见的一种木马后门工具,它主要通过替换系统文件来达到攻击和和隐蔽的目的,这种木马比普通木马后门更加危险和隐蔽,普通的检测工...

python周期任务调度工具Schedule使用详解

如果你想周期性地执行某个Python脚本,最出名的选择应该是Crontab脚本,但是Crontab具有以下缺点:不方便执行秒级任务。当需要执行的定时任务有上百个的时候,Crontab的管...

Linux 系统日常巡检脚本(shell巡检脚本)

Linux系统日常巡检脚本,巡检内容包含了,磁盘,内存cpu进程文件更改用户登录等一系列的操作直接用就行了。报告以邮件发送到邮箱在log下生成巡检报告。#!/bin/bash#@Au...

Schedule—简单实用的 Python 周期任务调度工具

如果你想周期性地执行某个Python脚本,最出名的选择应该是Crontab脚本,但是Crontab具有以下缺点:1.不方便执行秒级任务。2.当需要执行的定时任务有上百个的时候,Cronta...

celery定时与异步任务详解(定时任务异步执行)

celery简介Celery是一个简单、灵活且可靠的,处理大量消息的分布式系统,专注于实时处理的异步任务队列,同时也支持任务调度。Celery的架构由三部分组成,消息中间件(messagebroke...

开源免费的定时任务管理系统:Gocron

Gocron:精准调度未来,你的全能定时任务管理工具!-精选真开源,释放新价值。概览Gocron是github上一个开源免费的定时任务管理系统。它使用Go语言开发,是一个轻量级定时任务集中调度和管理...

PHP Laravel定时任务Schedule(laravel定时任务原理)

前提:本文方法是利用Linux的crontab定时任务来协助实现Laravel调度(Mac也一样)。一、首先添加Crontab定时任务,这里只做简单介绍:用命令crontab-e添加如下内容**...

Linux的常用命令就是记不住,怎么办?于是推出了这套教程

1.帮助命令1.1help命令#语法格式:命令--help#作用:查看某个命令的帮助信息#示例:#ls--help查看ls命令的帮助信息#netst...

如何定期执行 Python 脚本:5 种常见方法

定期执行任务是自动化工作流程中的重要环节,无论是数据抓取、文件备份,还是定期报告生成,定时运行脚本都可以极大提高效率。本文将介绍五种方法,通过这些方法,你可以轻松设置定期执行Python脚本的任务...

取消回复欢迎 发表评论: