为什么要用户态的定时器?

首先是为什么要做定时器,定时器的主要说的是我们的应用(业务?功能?总之有这个需求)要做一个定时的任务。其实如果不想为什么,好像是理所当然的。我写这个的时候,知乎有一个问题(Linux内核提供了定时器机制,为什么还要自己动手实现用户态的时间轮或者最小堆定时器? - 知乎 (zhihu.com))没有人回答,其实很容易想到答案,这里当作是一个回答。

游戏业务里(因为之前一直在研究游戏服务器开发的需求,反而对  Web 等的不是很熟悉)服务器里面可能需要定时触发某个事件,可能是任务解锁、人物解锁。

比如种植类游戏(比如某宝里面的农场红包,当然对于需要停留特定时间这种可以客户端实现就行了)、挂机任务;又比如吃鸡游戏里面的毒圈缩圈(对于客户端要不要也做这个属于同步逻辑的问题,之前的文章谈论过了)、更加一般的游戏(比如 RPG)里面的持续加buff、定时恢复、技能冷却等等都是与时间有关的。

而对于网络框架他本身,也是一个定时器的应用,比如对于长时间的连接来说,不应让他永久下去(比如没有用 keepalive),需要超时断开他,这个应用在 ssh、ftp 服务器里面都有应用。

理想的情况是很简单的,因为一般的 OS 都支持定时器,而且这个东西常常是可以不占用资源的,因为我们都知道从硬件开始主板上的晶振提供了时钟的底层实现,因此他当然是异步的硬件中断,比如早期的可编程中断器(PIC)+一个时钟,然后到了 OS 层面,一般 CPU 某个 core 空闲的时候,都会去运行一些 kernel 的任务,本质上应用层的异步定时器比如 sleep 然后特定时间 interrupt 唤醒不过是 kernel 上的 mux 进行对硬件的可编程定时器的多路复用(对于 linux kernel 的时间轮+红黑树的定时器(hrtimer)的演变过程下面有个参考链接)。

然后就是需要在应用层也做一个 MUX,乍一看好像是多此一举因为内核明明已经实现了一套了,但是就是因为抽象分层和隔离导致这些东西必不可少,对于 kernel 的定时器,比如 sleep 这种,应用层根本没法注册多个定时器。linux 2.6 之后支持了 timer,POSIX 早就指定了,只不过 2.6 之前没有实现而已。顺便的基于 Unix everything is file 的思想,timerfd 也一起出来了(timerfd 等于一个做 timer 的 eventfd ,当然可以类比我们用 pipe + epoll 实现不同进程的同步功能),timerfd 是可以注册很多很多个的。

这样做思想实验,对于 kernel 中的数据结构来说,不管他是用的什么实现,红黑树时间轮还是链表什么的,都不重要,关键是他的定时触发,后面我们会有参考资料明白 linux 的高分辨率定时器是怎么巧妙复杂的基于一个硬件定时器来实现的,但是必须注意的是这个异步中断到来的时候,本质上发生了什么以及 kernel 他能做什么。

我们想到,首先是硬件的时钟来了一个中断,马上 CPU 执行 trap 逻辑(跳转中断向量表),进入到 kernel 的代码以及权限控制,然后开始 demux,对比 linux 源码就是在 trap handler 里面检查寄存器,发现,oh ,是定时器中断,然后经过一顿 routing,最终分发到了某数据结构上的一个 node,于是知道要做什么,对于 kernel 来说,我们能做的无非是修改 PCB 上的状态,从而改变进程的下一个方向。到这里我们就很清楚了。当然再插入一些小插曲,对于同一个时间来说,是不是说两个进程同时唤醒会有精度损失(linux CFS 算法时间片的大概范围是是0.75ms 到 6ms左右。)?这种情况是有的,称作 overrun,当然也要处理好。而精度问题本身高精度也可以结合 CFS (对 CFS 的调度逻辑感兴趣的另外看资料,我不涉及)的优先级实现,所以 ns 级别的高精度也是可能实现的。

所以实际我们可以完全委托给 kernel 去做,在 Linux 下,这个问题是一个是 file descriptor 的数量问题(默认开 1024 这个我们都知道,其实本来要想百万并发必然出现 too many open file,ulimit 字节改大他,最大当然是到 int 的最大数量啦),然后可能是说增加了 kernel 的 overhead。我们后面就知道 kernel 的定时器实际的复杂度是后面才提到 O(1) 的,但是对于插入删除来说,红黑树的 logn 可能还是在(实际具体我也不知道,你得看最新的 linux kernel source code)。总之可能有更大的开销。

当然上面这段说的是可能,最大的一个好处是增删改查。对于 select、poll、epoll 来说,他们都是没有 mmap 的,io_uring 才引入了 mmap,意味着我们每次修改,都要 context switch 以及进行虚拟地址翻译+copying,加上现在的 meltdown+spectre 之后的页表隔离(KPTI)补丁之后(、、、、这个说过很多次了,总之开销就是大),做复用可能是比嗯 timer_create 或者 timerfd_create 的那个东西管理快的,比如你要删除吧,比如你 overrun 了吧(对于 timerfd,没有 timer_getoverrun,这个功能通过 read 实现),每次都修改 kernel 的是不现实的。而且定时器的好处就是时间上是线性的,我们一个时间点只需要保证有一个在 kernel 被老大哥看着就行了。


时间轮和多级时间轮

时间轮这个实现方案这个帖子的 gif 比较好看:

一张图理解Kafka时间轮(TimingWheel),看不懂算我输! - 知乎 (zhihu.com)

时间轮的实现基本是两种经典做法,一种是 Linux 的内核态时间轮,一种是 kafka 的应用态时间轮,通过 DelayQueue 避免稀疏空转。

Linux 里的 timer 基础就是多级时间轮,然后根据这里说的 Red-black Trees (rbtree) in Linux — The Linux Kernel documentation,高分辨率 timer 用的是红黑树(实现的优先队列?)。

时间轮应该是很不错的,首先他很多操作都是 O(1),因为直接定位循环数组索引。然后用红黑树的问题是,他不是完全的,所以没办法实现数组存储,结果就是失去了 locality 性能。为什么时间轮只能实现低精度,而堆/红黑树能实现高精度的定时器呢?因为 kernel 的这个时间轮和上面 gif 演示那个不一样(kafka),kafka 的方案是用 cascading down 的方式,即到达的第二层的时候,会把时间插回原来的小时间轮中。

然而这个过程也要花时间的(如果处理不好可能会超过延时),kernel 的做法是二层时间轮就直接按大的时间周期走了(水表齿轮),比如一层是 1ms 并且只能容纳 31 间隔,超过 31间隔就要放到二层去,二层可能是 8ms 一个 slot (也是 32 个 slot),然后每走 8 个才执行一个二层槽的,等一层 31ms 结束了 32个槽之后,刚好结束第四个二层槽,32ms 的超时将会放在第五个二层槽,这样就会引发延迟(延迟6ms)。

时间轮的复杂度插入删除都是 O1,PPT (High resolution timers and dynamic ticks design notes — The Linux Kernel documentation)说了 higher tick frequencies don't scale due to long lasting timer callbacks and increased recascading。这也是为什么低分辨率的另一个原因回调耗时。

红黑树

linux 的高分辨率计时器前面讲过是用 rbtree 做的,High resolution timers and dynamic ticks design notes — The Linux Kernel documentation,然后 ppt 是这个High resolution timers and dynamic ticks design notes — The Linux Kernel documentation。可以看到一开始最简单的 linux timer 实现就一个双向链表而已,这个迭代思路很重要,你要开发什么东西都先把东西做出来先,优化什么的之后再说!muduo 的思想也是一开始做了一个线性表的 timerqueue 再改成 map (红黑树)的。

而且红黑树做最小堆也不是一定要 logn 复杂度的,因为树的最左边总是可以被维护的,所以可以直接 O(1) 的 peek 是可实现的,但是删除涉及 rotation 操作,所以 deleteMin 的确没有办法降低(起码降低了 findMin 的开销属于)。

从硬件到 MUX

根据PPT ,内核的 hrtimer 的实现是这样的:timers inserted into a red­black tree sorted by expiration time(absolute 时间吧应该是),base code is still tick driven (softirq is called in the timer softirq context, 这个软中断其实之前做 e1000 的 xv6 网卡驱动的时候就接触过了,就是 bottom half 。实际的网卡驱动的 bottom half 是通过 softirq 启动一个内核独立线程(进程)来运行的,实际会参与进程调度的,而且优先级不低(比如网卡收发包肯定比 app 要重要))。他最后把这个hrtimer 另外直接接收硬件中断和原来的 timing wheel 独立开来两层架构。然后其实要实现高精度有一个事情必须做的,就是让最近 expire 的那个 absolute time 的 event (which 就是红黑树的最左边那个节点)时刻必须触发一个中断,这个中断会注册到 clock_event_device 里,即可编程定时器(比如8086 的8253,现在都是在北桥上),因为你不可能一秒轮询一次的,这个东西必须要硬件的支持的,由于一次只需要注册一个事件,所以简单硬件完全足够胜任了。

当然,有一些情况必须考虑的,比如程序不能动的情况,这些情况有很多,包括 debug,回调耗时(应用层的回调当然是不会耗时的,但是这里我们说的是 kernel 做的事情,比如应用层注册了一个高清事件需要 sigalarm 中断,但是内核可能在某个 critical section 是无法被 preempt 出去的,就算是软中断也要 pending )以及虚拟机(虚拟机的可编程定时器是由软件虚拟的或者直接硬件虚拟化技术的)停机等,这个时候内核应该处理过期事件,接下来的内容其实和我们讲网络编程没有什么关系了,所以就这样点到为止吧(这已经不止点到了吧,感兴趣的读者可以阅读 Linux 官方站点的资料,注意是在 2.6.16 内核版本(PPT 说的)以后就行了,对于源码分析的资料,这里有个2012 的博客我觉得分析得不错 Linux时间子系统之六:高精度定时器(HRTIMER)的原理和实现_DroidPhone的专栏-CSDN博客)!

需要注意了本节标题叫高性能,实际我们做的用户态定时器说的高性能并不是说 high resolution 的,我们的高性能是支持高并发高可用的定时器应该,高清定时器这个东西是硬件的,直接注册 usleep 或者 ualarm 而不要在应用层再搞一个 multiplexer 才行。

Nginx 的实现

nginx 用的是 rbtree,不用 heap pq 的具体原因前面说过了一个是空间不提前预知(这个有点难讲,因为如果你用连续空间就要预知大内存块,就要均摊这个 reallocation 的 overhead,但是能保证 locality,如果你不用连续空间,就要失去 locality),然后是无法高效随机删除。

然而红黑树做优先队列查找最小值是 logn,删除是 logn,heap pq 是查找 1,删除 n。这个 trade-off 怎么做的呢?而且 heap 还有 locality !这下有点难决策的,特别是删除比较少的时候。(插入都是 logn)。看到 libevent(C语言 reactor 模型异步库) 在1.4后 use a min heap instead of a red-black tree for timeouts; as a result finding the min is a O(1) operation now; from Maxim Yegorushkin. 这个得看实测性能了。另一个思想实验的,如果 timer event 本身自己身上有一个 pointer 指向他在数据结构中的位置,自然就可以在 heap 里面实现 O(logn) 左右的删除了(因为本来要 O(n) 查找,现在直接整堆而已)?

用户态的时间轮

注意一个要点,用户态的基于 sleep 的时间轮其实是可能会不高性能的吧,他在用户态频繁 spin + sleep,然后还要计算该 sleep 的时间。。。。而且要频繁查询当前时间,这个不是 syscall 的开销吗?还不如用系统提供的呢。但是他的确搞性能属于。

特别是页表隔离以后。我们现在学习旧的实现以及编写新的实现当然要考虑这个,但是最重要的还是不要臆想臆测,一定要实地 benchmark 才能知道到底好不好。性能问题不能乱猜的。所以我上面这个对时间轮的乱猜实在是大不敬

Libev 的更高性能的 4-heap

而 libev (一个提供更多功能的 reactor 异步库,并且不使用全局变量更好支持多线程)用的是 4-heap,这个东西性能比 2-heap 块,在添加新元素(从下往上浮,一直浮找第一个比他小的就行了,没有4个节点的比较,所以就是高度)的方面:binary heap:O(log2n) vs d-ary heap: O(log4n) ,log4n < log2n 。但deleteMin(把末尾元素放到堆顶往下沉,下沉的时候必须找到最小的孩子取而代之,所以必须有 4 个比较):binary heap:O(log2n) vs d-ary heap:O((d-1)logdn),当 d > 2 时,(d-1)logdn > log2n ,另外,d-ary heap比binary heap 对缓存更加友好,更多的子结点相邻在一起(其实是整体的高度下降了,倍数关系引发换 cache 少一些)。故在实际运行效率往往会更好一些。

Golang

Golang 用的是四叉堆 + 桶。

Nodejs 用的是双向链表

网络编程 高性能定时器数据结构分析 | 时间轮 红黑树定时器性能分析 | 为什么要做用户态定时器相关推荐

  1. Linux网络编程 | 高性能定时器 :时间轮、时间堆

    文章目录 时间轮 时间堆 在上一篇博客中我实现了一个基于排序链表的定时器容器,但是其存在一个缺点--随着定时器越来越多,添加定时器的效率也会越来越低. 而下面的两个高效定时器--时间轮.时间堆,会完美 ...

  2. 网络编程之一泡尿的时间,快速读懂QUIC协议

    网络编程之一泡尿的时间,快速读懂QUIC协议 TCP协议到底怎么了? QUIC协议登场 QUIC协议的目标 QUIC协议这么好,可以大规模切换为QUIC吗? QUIC协议实践 我想试试QUIC协议,可 ...

  3. 多线程环境下海量定时任务的定时器设计丨时间轮实现丨红黑树,跳表分析

    多线程环境下海量定时任务定时器设计 1. 定时器分析 2. 红黑树,最小堆,跳表实现比较分析 3. 时间轮实现 [Linux后端开发系列]多线程环境下海量定时任务的定时器设计丨时间轮实现丨红黑树,跳表 ...

  4. 网络/Network - 网络编程 - 高性能 - 单服务器高性能模式[网络模型]及性能对比 - 学习/实践

    1.应用场景 主要用于学习单服务器高性能模式及性能对比,尤其是网络模型,这个很重要,并将这些知识在工作中验证,实践,理解,掌握. 2.学习/操作 1.文档阅读 推荐 18 | 单服务器高性能模式:PP ...

  5. jQuery 一次定时器_干货 | 小论定时器玩法(时间轮询法)

    EEWORLD 电子资讯 犀利解读 技术干货 每日更新 经常来说,对于一些不复杂的单片机应用,而且对于内存和存储要求比较严格,又需要多分时去处理一些指定的任务,在无法使用RTOS的情况下,使用一个硬件 ...

  6. 心跳超时时间设置_定时器实现之时间轮算法

    前言 在看这篇文章的时候对其中超时控制一块儿有点好奇.通过时间轮来控制超时?啥是时间轮?怎么控制的?文章会先介绍常见的计时超时处理,再引入时间轮介绍及 netty 在实现时的一些细节,最后总结下实现的 ...

  7. 网络编程(wireshare抓数据包及分析、三次握手与四次挥手、数据库sqlite3及操作)笔记-day15

    前言 今天整理了网络编程的下篇,主要归纳了wireshark抓数据包及分析.TCP安全可靠原因分析(三次握手.四次挥手).数据库sqlite3及操作(shell脚本和C语言对数据库的增.删.改.查及关 ...

  8. android 网络编程--URL获取数据/图片

    首先,开始最简单的网络编程实战,URL实现网络连接,不懂的童鞋可以参考JAVA中的URL编程,其原理是一样的,在这里不再多做解释. 直接贴出实现源代码: public class DataActivi ...

  9. linux 定时器(c++)(2)时间轮

    节点类 相比较时间升序链表中的绝对时间expire tw_timer采用的是相对时间的概念也就多出 rotation ,time_slot俩属性,rotation是转的"圈数",t ...

最新文章

  1. 如何在离开页面时弹出确认对话框
  2. 使用Xcode External Build System实现Rust 项目 Capture GPU Frame 在线调试 Metal 2018.12.18
  3. html和css知识,html和 css基础知识
  4. [导入]Netron研究(二)----容器登场
  5. CSliderCtrl鼠标点击精确定位
  6. 浅谈局部敏感哈希LSH
  7. ajax请求进error怎么弹出错诶信息,在ajax请求jqgrid之后出现错误时显示错误消息...
  8. 日志分隔工具Cronolog
  9. 5-0 51单片机流水灯
  10. 2021年烷基化工艺考试题及烷基化工艺多少钱
  11. js中浏览器失焦获焦的几种结局方法
  12. BitTorrent协议DHT网络爬虫BitTorrentNetworkSpider
  13. 计算机组成原理mgk换算,计算机组成原理十套卷(本科)计算题及答案
  14. 第15周项目二—洗牌(1)
  15. Aspx.Net的Aspx页面和Aspx.cs联用
  16. 你知道有哪些用于文件同步的方法?
  17. 游戏开发者注意了,小心触犯任天堂的这些专利
  18. Etyma01 ced ceed cess
  19. MySQL关闭慢查询日志
  20. 双十一大促|20%商家拥有头部资源,剩下80%商家怎么办?

热门文章

  1. (BGV12)同态加密方案初学
  2. 产业链剖析:2016光伏市场红利何在?
  3. 第895期机器学习日报(2017-03-01)
  4. nvme固态必须uefi启动吗_离心泵启动时,出口阀门必须关闭吗?
  5. 使用MODIS Level 1B 1KM 数据反演AOD实验流程
  6. 华为若向苹果出售5G芯片 对双方其实都是笔有利的生意
  7. 【SpringCloud Alibaba】Seta安装、处理分布式事务
  8. 淘宝可伸缩高性能互联网架构HSF
  9. Linux下安装Oracle11G详细过程
  10. Regular Expressions (1) ---- What is Regular Expressions?