概念简介:
tick最小单位为1毫秒
每个调度进程有自己的时间轮–(soon轮 later轮)
soon轮 早轮
早轮的每个槽宽度为1个tick,一共有2的14次方个槽。存储大约16秒内需要被触发的定时器
later轮 晚轮
晚轮的每个槽宽度为2的13次方个tick,一共有2的14次方个槽。存储大约37个小时内需要被触发的定时器
超过晚轮的定时器,怎么处理?
超过晚轮的定时器,暂且称为高级定时器; 没有超过晚轮的定时器,暂且称为普通定时器。高级定时器信息存于ErtsSchedulerData 的timer_service指向的结构中, 所有的高级定时器会计算一个最近的到期时间,构造成一个定时器插入到时间轮中,这个构造的定时器暂且称为高级总管定时器
整个时间轮流程文字叙述:
1:插入小于等于当前时间的定时器,定时器将被安插到时间轮的AtOnce插槽,erts_bump_timers被触发时立刻触发定时器
2:插入小于大于2的14次方毫秒的定时器,定时器将被安插到早轮的插槽中,早轮插槽一共2的14次方个,每个插槽存储1毫秒内触发的定时器,早轮插槽中的定时器以双向链表的数据结构组织,插入时间复杂度为O(1);
3:插入大于2的14次方毫秒,小于2的(13+14)次方毫秒的定时器,定时器将被安插到晚轮的插槽中,晚轮插槽一共2的14次方个,每个插槽存储2的13次方毫秒内触发的定时器。晚轮插槽会提前触发,触发时会将插槽内的定时器安插到早轮中;
4:大于2的27次方毫秒的定时器(称为高级定时器),将以红黑色的形式被保存在ErtsSchedulerData中,然后计算出最近的触发时间,以该触发时间设置一个定时器(为方便后文区分,这个定时器暂且称为高级总管定时器),插入到晚轮中。
以下为实现流程分析:
结构简介:
调度器结构 ErtsSchedulerData_ 其中三个字段与时间轮有关:
ErtsTimerWheel *timer_wheel; 指向时间轮的指针
ErtsNextTimeoutRef next_tmo_ref; 时间轮结构中next_timeout_time的引用
ErtsHLTimerService *timer_service; 高级定时器服务结构 存储处理高级定时器需要的相关信息
高级定时器服务结构 ErtsHLTimerService
ErtsHLTCncldTmrQ canceled_queue;
ErtsHLTimer *time_tree; 存储所有的高级定时器
ErtsBifTimer *btm_tree; 存储了普通定时器和高级总管定时器
ErtsHLTimer *next_timeout; 所有高级定时器中,最近的高级定时器
ErtsYieldingTimeoutState yield; 中断标记,类似于reduction机制,可先忽略
ErtsTWheelTimer service_timer; 存储高级总管定时器
时间轮结构 ErtsTimerWheel
ErtsTWheelTimer *slots[1 立刻触发插槽
+ ERTS_TW_SOON_WHEEL_SIZE 早轮 ERTS_TW_SOON_WHEEL_SIZE 一般为2的14次方
+ ERTS_TW_LATER_WHEEL_SIZE]; 晚轮 ERTS_TW_LATER_WHEEL_SIZE 一般为2的14次方
ErtsTWheelTimer **w; w指向slots[1]的地址
Sint scnt[ERTS_TW_SCNT_SIZE]; 效率优化字段,记录每512个插槽内定时器的数量
Sint bump_scnt[ERTS_TW_SCNT_SIZE];
ErtsMonotonicTime pos; 时间轮处理到的时间
Uint nto; 定时器个数
struct {
Uint nto; 需要立刻触发的定时器个数
} at_once;
struct {
ErtsMonotonicTime min_tpos; 早轮中最近触发定时器位置
Uint nto; 早轮中定时器个数
} soon;
struct {
ErtsMonotonicTime min_tpos; 晚轮中最近触发定时器位置
int min_tpos_slot; 晚轮中最近触发插槽
ErtsMonotonicTime pos; 晚轮处理到的位置 按照晚轮的宽度进行增长
Uint nto; 晚轮中定时器个数
} later;
int yield_slot; 中断插槽----时间轮处理有类似reduction的机制,处理了一定数量的定时器后会触发中断
int yield_slots_left;
ErtsTWheelTimer sentinel; 哨兵,用于遍历时间轮
int true_next_timeout_time;
ErtsMonotonicTime next_timeout_pos; 下次触发时间轮位置
ErtsMonotonicTime next_timeout_time; 下次触发时间轮时间
时间轮定时器结构 ErtsTWheelTimer
ErtsMonotonicTime timeout_pos; 定时器到期时间
struct ErtsTWheelTimer* next; 下个定时器
struct ErtsTWheelTimer* prev; 前一个定时器
void (*timeout)(void*); 定时器到期触发函数 高级定时器触发函数为hlt_service_timeout; 早晚轮触函数为tw_bif_timer_timeout
void* arg; 定时器到期触发函数的参数
int slot;
Bif定时器结构 ErtsBifTimer,描述通过bif创建的定时器 eg:send_after
union {
ErtsTmrHead head; 高级定时器和普通定时器通用的一些属性
ErtsHLTimer hlt; 高级定时器
ErtsTWTimer twt; 普通定时器
} type;
struct {
erts_atomic32_t state;
Uint32 refn[ERTS_REF_NUMBERS]; 定时器ref eg:send_after的返回值
ErtsBifTimerTree proc_tree;
ErtsBifTimerTree tree;
Eterm message; 定时器信息 eg:send_after的第三个参数
ErlHeapFragment *bp; 定时器信息存储的位置
} btm;
ErtsTmrHead 该结构不是重点
Uint32 roflgs; 定时器的一些标记,eg:标记send_after的第二个参数是pid还是regname
erts_atomic32_t refc; 该结构的引用计数,eg:send_after的第二个参数是pid时,该值为2,因为pid的进程结构bif_timers字段也指向了当前结构
普通定时器ErtsTWTimer
ErtsTmrHead head; /*ErtsTWTimer的首位必须存放ErtsTmrHead字段*/ 由于ErtsTWTimer存放在ErtsBifTimer的type字段里,union格式的需要
union {
ErtsTWheelTimer tw_tmr;
ErtsThrPrgrLaterOp cleanup;
} u;
高级定时器ErtsHLTimer
ErtsTmrHead head; /* NEED to be first! */ 原因同上
ErtsMonotonicTime timeout; 超时时间
union {
ErtsThrPrgrLaterOp cleanup;
ErtsHLTimerTimeTree tree; 同一超时时间超时的高级定时器,以链表的形式存于此结构中
} time;
函数实现:
create_tw_timer函数插入非高级定时器核心逻辑:
static ErtsTimer *
create_tw_timer(ErtsSchedulerData *esdp,...)
{
tmr = (ErtsTWTimer *) erts_alloc(ERTS_ALC_T_BIF_TIMER, sizeof(ErtsBifTimer)); 注:tmr指针类型是ErtsTWTimer,但是是按照ErtsBifTimer的大小申请的空间
timeout_func = tw_bif_timer_timeout; 定时器到期时触发函数
refc += init_btm_specifics(esdp,(ErtsBifTimer *) tmr,msg,refn); 主要逻辑将ErtsBifTimer->btm.message和refn(具体含义见上文)赋值到tmr中
将定时器插入时间轮的核心逻辑
erts_twheel_set_timer(esdp->timer_wheel, 时间轮结构
&tmr->u.tw_tmr, ErtsTWheelTimer定时器结构 将此结构插入时间轮中
timeout_func, 定时器到期时触发函数
tmr, 定时器到期时触发函数的参数 即到期时会触发 tw_bif_timer_timeout(tmr)
timeout_pos); 定时器到期位置
return (ErtsTimer *) tmr;
}
create_hl_timer函数插入高级定时器核心逻辑:
static ErtsTimer *
create_hl_timer(ErtsSchedulerData *esdp, ...)
{
tmr = (ErtsHLTimer *) erts_alloc(ERTS_ALC_T_BIF_TIMER, sizeof(ErtsBifTimer)); 给tmr申请空间
refc += init_btm_specifics(esdp,(ErtsBifTimer *) tmr,msg,refn); 同上
if (!srv->next_timeout || tmr->timeout < srv->next_timeout->timeout) { 若高级总管定时器为空 或者 当前高级定时器出发时间小于高级总管定时器的触发时间
if (srv->next_timeout)
erts_twheel_cancel_timer(esdp->timer_wheel,&srv->service_timer); 则取消旧的高级总管定时器
erts_twheel_set_timer(esdp->timer_wheel, 向时间轮中插入新的高级总管定时器
&srv->service_timer, 将高级总管定时器结构插入时间轮
hlt_service_timeout, 高级总管定时器到期时触发的函数
(void *) esdp, 高级总管定时器到期时触发函数的参数 即到期时会触发 hlt_service_timeout(esdp)
tmr->timeout);
srv->next_timeout = tmr;
}
st_tmr = time_rbt_lookup_insert(&srv->time_tree, tmr); 搜索同一时间触发的高级定时器,若没有则插入到按触发时间排序的红黑树中
if (st_tmr)
same_time_list_insert(&st_tmr->time.tree.same_time, tmr); 若有则插入到同一时间超时的双向链表中
return (ErtsTimer *) tmr;
}
erts_twheel_set_timer 定时器插入时间轮的核心逻辑
void
erts_twheel_set_timer(ErtsTimerWheel *tiw,...)
{
int slot;
p->timeout = timeout; 定时器到期触发函数赋值
p->arg = arg; 定时器到期触发函数的参数赋值
tiw->nto++; 时间轮中定时器数量加一
if (timeout_pos <= tiw->pos) { 超时位置小于当前时间轮的位置,则将定时器插于ERTS_TW_SLOT_AT_ONCE插槽
p->timeout_pos = timeout_pos = tiw->pos;
slot = ERTS_TW_SLOT_AT_ONCE;
}
else if (timeout_pos < tiw->pos + ERTS_TW_SOON_WHEEL_SIZE) { 超时位置属于早轮的范围内
p->timeout_pos = timeout_pos;
slot = soon_slot(timeout_pos); 计算定时器所属早轮插槽 计算方法为 timeout_pos 对2的14次方取模 (注: A&((2<<14)-1)) =:= A%(2<<14))
if (tiw->soon.min_tpos > timeout_pos) 更新早轮的最近触发位置
tiw->soon.min_tpos = timeout_pos;
}
else {
p->timeout_pos = timeout_pos;
slot = later_slot(timeout_pos); 计算定时器所属晚轮插槽 计算方法为 timeout_pos 右移13位 后 对2的14次方取模 再 加上 2的14次方
timeout_pos &= ERTS_TW_LATER_WHEEL_POS_MASK;
timeout_pos -= ERTS_TW_LATER_WHEEL_SLOT_SIZE; 设置时间轮下次到期位置为 当前位置减2的13次方。 提前触发后会把处于晚轮中的定时器插入早轮中
}
insert_timer_into_slot(tiw, slot, p); 定时器插入时间轮指定的插槽
if (timeout_pos <= tiw->next_timeout_pos) { 更新下次到期位置
tiw->true_next_timeout_time = 1;
if (timeout_pos < tiw->next_timeout_pos) {
tiw->next_timeout_pos = timeout_pos;
tiw->next_timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(timeout_pos);
}
}
}
时间轮的触发函数erts_bump_timers核心逻辑(略过yield逻辑, 略过使用哨兵遍历时间轮逻辑):
每次触发前,会进行判断,只有当前时间大于等于时间轮的next_timeout_time时,才会触发erts_bump_timers逻辑
void
erts_bump_timers(ErtsTimerWheel *tiw, ErtsMonotonicTime curr_time)
{
int slot, yield_count, slots, scnt_ix;
ErtsMonotonicTime bump_to;
Sint *scnt, *bump_scnt;
scnt = &tiw->scnt[0];
bump_scnt = &tiw->bump_scnt[0];
bump_to = ERTS_MONOTONIC_TO_CLKTCKS(curr_time);
tiw->next_timeout_pos = bump_to;
tiw->next_timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(bump_to);
while (1) {
ErtsTWheelTimer *p;
if (tiw->nto == 0) {
empty_wheel:
重新设定各种pos;
return;
}
p = tiw->w[ERTS_TW_SLOT_AT_ONCE];
if (p) {遍历at_once插槽里的定时器,执行 timeout_timer(p)}
if (tiw->pos >= bump_to) {break;}
if (tiw->nto == 0)
goto empty_wheel;
sys_memcpy((void *) bump_scnt, (void *) scnt, sizeof(Sint) * ERTS_TW_SCNT_SIZE);
if (tiw->soon.min_tpos > tiw->pos) {
该代码块为更新tiw->pos,使其跳过一些空槽
}
ErtsMonotonicTime tmp_slots = bump_to - tiw->pos;
if (tmp_slots < ERTS_TW_SOON_WHEEL_SIZE)
slots = (int) tmp_slots; 早轮需要遍历的槽数计算
else
slots = ERTS_TW_SOON_WHEEL_SIZE;
slot = soon_slot(tiw->pos+1); 根据当前位置计算早轮遍历的初始槽位
tiw->pos = bump_to; 更新时间轮的位置为当前时间位置
tiw->next_timeout_pos = bump_to;
tiw->next_timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(bump_to);
scnt_ix = scnt_get_ix(slot); 计算slot插槽在效率优化数组scnt的索引
while (slots > 0) {
p = tiw->w[slot];
if (p) {
遍历at_once插槽里的定时器,执行 timeout_timer(p)
注:遍历时会拿定时器的timeout_pos和需要执行到的dump_to进行对比,若timeout_pos更大,则表示该定时器为高级总管定时器,需要重新插回时间轮
}
scnt_soon_wheel_next(&slot, &slots, NULL, &scnt_ix, bump_scnt); 利用scnt快速寻找下一个不为空的插槽
scnt数组中,统计了时间轮每512个插槽里定时器的总数
}
if (ERTS_TW_BUMP_LATER_WHEEL(tiw)) { 判断是否需要执行晚轮 当前位置加上 2的13次方 大于等于 晚轮的当前位置
restart_yielded_later_slot:
if (bump_later_wheel(tiw, &yield_count)) 晚轮的触发函数 ----和当前函数大同小异,异在于会将晚轮插槽中的非高级总管定时器插入早轮中
return;
}
}
(void) find_next_timeout(NULL, tiw); 更新下次触发时间 同样有利用scnt加快效率
}