Linux定时器与时间轮 实现网络连接超时关闭

目录

原理理解

定时器超时触发可读事件机制

定时器

Linux定时器

Linux内核定时器API

时间轮

​编辑

使用方法

时间轮与基于事件驱动配合

回调函数与定时器

梳理定时器加入到Reactor服务器的整体逻辑

EventLoop模块如何将新连接放入时间轮管理

 新连接和定时器封装逻辑分析

连接超时的处理逻辑


原理理解

定时器超时触发可读事件机制

总框架梳理(EventLoop、epoll、Channel)

  • 总结:每一个EventLoop(Reactor)中,都利用epoll管理着一个连接Channel对象和一个定时器Channel对象。epoll如果监控到定时器对象有事件就绪(连接超时--设定的时间内连接没有响应),则交给时间轮对该定时器事件进行处理。如果连接对象的Channel响应,则交给线程池中的其他线程处理该事件。
  • 流程总结
    • 初始化
      • EventLoop初始化,同时创建一个epoll实例,用于管理所有的事件
      • 每个连接和定时器都被包装成一个Channel对象(各自包装成一个,也就是两个Channel对象),这些Channel对象被添加到epoll实例中进行监控
    • 新连接到来时
      • 新连接到来时,TcpServer创建一个新的连接Channel对象,并将其添加到EventLoop中
      • 同时设置一个定时器对象,将该定时对象放入到时间轮中进行管理时间轮又是由该Reactor中的epoll进行管理的
    • 事件监控
      • EventLoop进入事件循环,通过调用epoll_wait等待事件发生
      • 如果有事件发生,EventLoop则会获取所有被激活的Channel对象,也就是有连接发送可读事件的时候,会调用该连接预先设置的回调函数进行处理
    • 处理连接事件
      • 若连接Channel对象的文件描述符可读,则EventLoop会调用连接Channel的读事件回调函数
      • 其他事件类似
    • 处理定时事件
      • 如果定时器Channel对象的文件描述符可读,那么EventLoop会调用定时器Channel的读事件回调函数,此时就会触发时间轮去定时检查任务
      • 时间轮检查当前槽的所有任务,如果任务的延时时间已经到达(证明该连接超时了),则执行任务的回调函数,也就是在此时关闭超时连接

定时器超时后触发epoll,然后返回通知给进程进行处理

  • timefd定时器超时后,内核会将该定时器到期事件标记为文件描述符上的可读事件(定时器能够激活epoll的原因)
  • epoll检测到该事件已经就绪,通知进程定时器已经到期

定时器到期触发可读事件为什么必须读取8个字节

  • 定时器到期后,内核在该文件描述符关联的内核缓冲区,写入一个uint64_t(8字节)的整数
  • 整数表示的是自从上次读取后,定时器到期的次数
  • 同步与清空缓冲区:读取八个字节清空了内核缓冲区中保存的到期次数。这样做就可以确保下一次定时器到期的时候,可以正确记录新的到期次数。

定时器

Linux定时器

定时器与epoll多路复用机制结合

  • 触发可读事件
    • 定时器文件描述符变成可读的时候,I/O复用机制会检测到该文件描述符的可读事件,然后通知对应进程
    • 定时器按照设置的时间触发,当触发的时候,定时器变的可读,epoll_wait就检测到该事件可读,然后通知进程处理
    • 进程则通过从定时器文件描述符中读取的超时次数来进行处理对应事件
    • 需要将定时器创建的文件描述符加入到epoll实例中,然后监控其是否可读

Linux定时器的作用

  • 系统调度:操作系统使用定时器中断机制来切换任务,从而确保系统中的进程可以平等的获取CPU时间
  • 定时任务:进程可以使用定时器实现连接超时管理等
  • 网络协议超时管理
  • 资源管理

Linux定时器的实现方式

  • POSIX定时器(本文主要讨论,目的是为了实现网络连接的超时管理)
  • 内核定时器
  • 周期性定时器 

POSIX定时器功能总结

  • 精准:纳秒级别的定时精读,可以应用于对时间有严格要求的场景中
  • 异步通知:定时器到期后可以通过信号或者线程通知机制,异步通知应用程序进行处理
  • 多定时器支持:一个进程下可以创建多个定时器,可以应用在复杂的时间管理
  • 灵活:单词定时和周期性定时可以配合使用 

Linux内核定时器API

创建定时器

  • 参数
    • clockid指定定时器使用的时钟

      • CLOCK_REALTIME:系统实时时钟。
      • CLOCK_MONOTONIC:单调递增时钟,从系统启动开始计时,不受系统时间变化影响。
    • flags:指定定时器的行为,可以是以下一个或多个标志的组合:

      • TFD_NONBLOCK:非阻塞模式。
      • TFD_CLOEXEC:在执行exec()时关闭文件描述符
  • 返回值
    • 成功时,返回一个新的文件描述符(非负整数),表示创建的定时器。
    • 失败时,返回-1,并设置errno指示错误原因

 

设置指向定时器的文件描述符

  • fd:由timerfd_create返回的文件描述符,用于标识定时器。
  • flags:标志位,可以是以下值之一:
    • 0:相对时间模式,从当前时间开始计时。
    • TFD_TIMER_ABSTIME:绝对时间模式,从系统启动时的绝对时间开始计时。
  • new_value:指向itimerspec结构体的指针,指定定时器的初始到期时间和重复间隔。
  • old_value:指向itimerspec结构体的指针,用于返回定时器之前的设置(可以为NULL

 

 itimerspec结构体

  • 使用事例:每隔三秒就会检查一次可读事件
  memset(&new_value, 0, sizeof(new_value));
    new_value.it_value.tv_sec = 3;        // 5秒后第一次到期
    new_value.it_value.tv_nsec = 0;
    new_value.it_interval.tv_sec = 3;     // 之后每隔2秒到期一次
    new_value.it_interval.tv_nsec = 0;

    if (timerfd_settime(timer_fd, 0, &new_value, NULL) == -1) {
        perror("timerfd_settime");
        exit(EXIT_FAILURE);
    }

时间轮

时间轮是用来管理大量定时任务的数据结构。核心思想是将事件划分为固定长度的周期,在每个时间可读上存储对应时刻触发的定时任务。通过移动秒针来模拟时间流逝,同时触发可读上的定时任务。

时间轮实现机制分析

  • 初始化时间轮
    • 固定大小的数组来存储定时任务,每个位置表示一个时间槽
    • 数组的大小就是时间轮容量,也就是可以管理的最大时间延迟
  • 添加定时任务
    • 添加定时任务时,依据当前时间以及任务的的延迟时间计算出应该插入到那个槽
    • 每个任务槽下面又可以放置多个任务
  • 时间轮轮转
    • 时间轮每秒移动一次,也就是当前秒针向前移动一格
    • 每次秒针移动的时候,会检查当前槽中的所有任务,然后执行任务,同时将已经完成或者取消的任务从槽中移除
  • 任务执行和超时处理
    • 如果任务执行时间到达,则执行任务的回调函数
    • 如果任务被取消或者已经完成,则从槽中移除

时间轮时间复杂度与空间复杂度

  • 时间复杂度:O(1)
  • 空间复杂度:取决于任务的数量 

使用方法

时间轮管理任务逻辑分析

  • 添加定时器:新连接建立后,设置定时器,然后将其添加到时间轮中进行管理
  • 时间轮运转:时间轮每秒移动一次,然后检查当前槽中的任务,执行这些任务
  • 超时处理:如果任务超时,执行其回调函数,关闭该连接
  • 取消定时器:如果连接在超时之前完成所有操作,那么就取消定时器移除任务

时间轮与基于事件驱动配合

定时执行任务

  • 析构函数实现:将释放连接的操作,放入到析构函数中,那么当连接的时间到了之后,就可以自动销毁任务
  • 析构函数缺陷:需要单独的定义一个定时任务类,同时如果在预定的时间中间,突然要销毁的这个连接发消息了,那么还得需要更改它的销毁事件,得不偿失
  • 智能指针shared_ptr计数器实现:借助shared_ptr内部计数机制实现,只有当计数为0的时候,才会真正释放一个对象
    • 假设在销毁时间内,连接进行了通信,此时只需要向定时任务中添加任务类对象的shared_ptr
    • 所以当到达销毁时间的时候,此时该智能指针的计数是1,不会在析构中销毁

回调函数与定时器

定时任务与回调函数配合时,回调函数可以在定时器到时间后,自动执行特定代码,无需手动检查和触发。使用回调函数将定时器的逻辑与具体的任务逻辑分离。

详细参考“回调函数分析”

梳理定时器加入到Reactor服务器的整体逻辑

EventLoop模块如何将新连接放入时间轮管理

整体逻辑梳理

  • 接收新连接
    • TcpServer类主要负责监听新连接,构造函数中,该类会设置一个回调函数(新连接到达的时候使用),SetAcceptCallback()
    • 新连接到达后,TcpServer::NewConnection方法被调用,在该方法中,利用新连接类创建并且设置回调函数(ConnectedCallback)
  • 事件循环管理
    • 一个Reactor对应一个EventLoop类,该类负责管理关心的所有事件。每个连接和定时器都会被包装成一个Channel(注意是封装成两个Channel),然后这个Channel交给EventLoop监控这些事件。
    • EventLoop调用epoll系统调用,等待事件发生,在事件发生后,调用对应回调函数
  • 定时器管理
    • TimerWheel类负责管理所有定时器任务,通过内部循环管理这些定时器任务(一个定时器就对应一个连接),按照设定的时间去检查是否有任务需要执行
    • 新任务添加时,也就是新连接到来后,需要在特定的时间内取完成某项任务,此时就会调用TimerWheel::TimerAdd方法,该方法可以将任务添加到时间轮的适当位置。
  • 新连接放入时间轮逻辑梳理
    • 接收新连接后,通过TcpServerd中的回调函数TimerWheel::TimerAdd方法,将一个与新连接相关的定时任务,添加到时间轮中。
    • 新连接就爱建立后,设置一个定时器,目的是确保连接在一定时间内完成身份验证或者其他初始化操作
    • 新连接到来后,定时器和连接封装成两个Channel,然后这个连接的定时器会放入到时间轮中进行管理,如果特定时间内没有事件到来,时间轮会对其自动释放,如果有新连接到来,时间轮则会将这个定时器的位置往后放。

代码逻辑分析

  • TcpServer接收新连接后,将一个定时任务添加到时间轮中
  • EventLoop负责管理这些事件,并且定时检查事件轮中的任务是否需要执行

 新连接和定时器封装逻辑分析

新连接和定时器会各自封装成一个Channel,EventLoop监控

  • 连接事件封装:新连接会创建一个Channel对象进行封装,然后设置对应的回调函数。创建后的Channel对象交给EventLoop中进行监控
  • 定时器事件封装:定时器也是通过Channel对象进行封装,设置读事件的回调(定位定时器超时后,会自动触发可读事件,此时epoll就可以检测到),该事件封装好后也是交给EventLoop管理

总结Channel类、EventLoop类、Epoller类在服务器中的作用

  • Channel类:总体来说,该类就是事件的抽象封装,表示一个文件描述以及该事件发生的事件,其中还封装了对应事件的回调函数
  • EventLoop类:负责管理所有的Channel,同时通过epoll系统调用来等待事件发生
  • Epoller类:封装了epoll系统调用,用于监控所有事件

 

 

连接超时的处理逻辑

定时器事件和连接事件之间配合处理超时连接

  • 连接Channel对象
    • 新连接到来后,服务器创建一个新的Channel对象来管理该连接,这个Channel对象会关联到对应的文件描述符中(新连接的),然后设置读写回调函数
  • 定时器Channel对象
    • 服务器给新连接创建一个定时器,通过创建定时器的Channel对象,然后将定时器添加到时间轮中进行管理
    • 定时器的回调函数,则是一个负责在超时的时候才需要执行的任务,也就是关闭连接的任务逻辑
  • 定时器的Channel对象与连接Channel对象
    • 如果连接在定时器规定的时间内,完成了对应的操作,则服务器调用TimerWheel::TimerCancel方法取消该定时器
    • 如果定时器超时且连接没有完成所需要的操作,那么定时器的回调函数就会被触发,也就是最终会执行关闭连接的操作

  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值