定时任务,是网络编程当中经常需要处理的一类任务。比如说,基于长连接的服务,为了确认连接没有中断,通常都会要求客户端每隔一段时间发送一个心跳包给服务器,服务器也会返回一个响应到客户端, 告知彼此都还活着。
为了实现定时任务,我一开始的想法是再创建一个线程,在这个线程里面通过调用 sleep 阻塞一段时间,当 sleep 返回之后, 就通过上一节中介绍的 eventfd 来唤醒 PollLoop 来执行定时任务。 按照这个逻辑应该可以写出满足需求的代码,但它引入了多线程,而我们目前还有考虑并行运算所带来的任何竞争冒险的问题。 那么有没有可能,像处理多个连接那样,通过 PollLoop 的循环框架在单个线程中完成这件事情呢?
本文将要介绍的 timerfd 以及相关的系统调用就可以担此大任。我们将对它们进行封装提供一个计时器,并编写一个简单的 demo。 在下一篇文章我们会将之应用到 echo 服务器上,让它具有超时断开连接的功能。
参考我们以前写单片机程序时的计时器,要让服务器判断超时主动断开连接的一个比较直接的想法就是,构建一个计时器, 当计时溢出的时候产生一个事件,驱使我们调用超时的回调函数,并在这个函数中通知 PollLoop 循环关闭连接。我们知道,PollLoop 的内核 poll 调用监听的是文件描述符的读写错事件。 既然有人说在 linux 系统中,万物皆可以是文件,那么如果我们能用文件描述符来表示计时器,不就可以接到 PollLoop 的循环框架下了吗。
在 Linux 系统中,有一组以 timerfd 为前缀的调用。它们分别是 timerfd_create、timerfd_settime、timerfd_gettime。 我们可以通过指令$ man timerfd_create
来查看相关文档,如右图所示。
其中,timerfd_create 用于创建一个计时器对象,并为之提供一个文件描述符,用于通知进程计时事件。它有两个参数,clockid 说明了计时器的类型。它有以下几种选择:
- CLOCK_REALTIME: 这是一种可以设置的系统级实时时钟。
- CLOCK_MONOTONIC: 这是一种不可修改的,单调递增的计时器。
- CLOCK_BOOTTIME: 与 CLOCK_MONOTONIC 类似的,这也是一个不可修改的单调递增的计时器。只是当系统休眠的时候,CLOCK_MONOTONIC 是不会计时的。 而它在这段时间中也会计时。
- CLOCK_REALTIME_ALARM: 功能上与 CLOCK_REALTIME 没有本质区别,只是当系统休眠的时候会唤醒系统。但是这要求调用者具有 CAP_WAKE_ALARM 的能力。
- CLOCK_BOOTTIME_ALARM: 功能上与 CLOCK_BOOTTIME 没有本质区别,只是当系统休眠的时候会唤醒系统。但是这要求调用者具有 CAP_WAKE_ALARM 的能力。
在构建 timerfd 的时候,我们还可以通过参数 flags,来设定计时器的一些特性。目前主要是 TFD_NONBLOCK 用于设定文件描述符工作在非阻塞的状态下。 TFD_CLOEXEC 则用于通过 fork-exec 创建新的进程并运行其它程序时自动关闭子进程中的文件描述符。它们可以通过位或运算进行组合,目前我们不考虑这些特性。
创建了计时器之后,我们需要通过调用 timerfd_settime 来启动或者停止计时。该调用有 4 个参数,如上边右侧的截图所示。其中,fd 是将要操作的计时器的文件描述符; flags 描述了计时特性,这里我们只关注 TFD_TIMER_ABSTIME,表示绝对计时。如果设置了该特性,只有当计时器达到了第三个参数 new_value 中的 it_value 字段所描述的时刻,才认为计时到期。 默认情况下,采用的都是相对计时,即相对于调用 timerfd_settimer 的时刻,经过 new_value.it_value 字段描述的时间后,认为计时到期。
`struct timespec {
time_t tv_sec;
long tv_nsec;
};
struct itimerspec {
struct timespec it_interval;
struct timespec it_value;
};`
我们对计时器有两种需求。其一,我们希望有一个闹钟在经过了一段时间,或者到达了某个特殊时刻之后,通知我们去处理一些事务。其二,我们需要周期性的工作,即每过一段时间就去完成一个特定任务。 这两个需求都体现在 timerfd_settime 的第三个参数 new_value 上了。
该参数的数据类型是struct itimerspec
,其定义如右侧代码所示。 它有两个字段,其中 it_value 表示计时器第一次到期的时刻,如果是相对计时器则经过该字段描述的时间之后,计时器第一次计时到期。若是绝对计时器,则要求计时器到达该时刻。 这就满足了我们的第一个需求。 字段 it_interval 描述的是计时周期,计时器在第一次到期之后,每个该字段描述的一段时间之后,都会产生依次计时到期时间。这满足了我们的第二个需求。
此外,timerfd_settime 还有第四个参数 old_value,用于返回当前计时器的计时设置。如果我们不关心它,可以传递一个 NULL。当然,我们也可以通过调用 timerfd_gettime 来获取计时设置。 通过这三个系统调用,我们就可以创建一个使用文件描述符来表示的计时器,设置和获取计时到期条件。下面,我们对它们进行封装,以融合到我们的 PollLoop 框架下。
`class Timer {
private:
PollEventHandlerPtr mEventHandler;
struct timespec mOriTime;
int mFd;
};`
为了方便的使用 timerfd,这里我们将它们封装到类 Timer 中。如右侧的代码片段所示,我们为之定义了三个私有的成员。
- mEventHandler是一个事件分发器, 用于通过 PollLoop 循环来监听文件描述符 mFd 所对应的计时器的到期事件,并调用相应的回调函数 OnReadEvent。 我们将在 Timer 的构造函数中完成该对象的实例化工作,用户还需要通过 ApplyHandlerOnLoop 接口将它注册到一个 PollLoop 循环上。
- mFd是一个通过调用 timerfd_create 获得的文件描述符,它对应一个计时器。当计时到期事件发生的时候,我们都可以通过调用 read(2) 来获取计时溢出的次数。 所以我们所关心的计时到期事件,实质上是文件描述符 mFd 的可读事件。
- mOriTime是一个用于绝对计时的参考时间点。
下面左侧是 Timer 的构造函数,我们首先通过调用 timerfd_create 构建计时器,并用成员 mFd 记录下它的文件名描述符。在断言描述符一定大于零之后,实例化了事件分发器, 并打开对可读事件的监听功能。最后,在注册读事件的回调函数 OnReadEvent。该回调函数的实现如下面右边的代码所示,我们通过调用 read 读取计时溢出次数,将之记录在局部变量 exp 中。 然后调用计时溢出的回调函数。后面我们会看到这个回调函数将由 TcpServer 提供,并在该回调中通过时间轮盘的形式判定网络连接是否超时。
| ``
Timer::Timer() {mFd = timerfd_create(CLOCK_REALTIME, 0);
assert(mFd > 0);
mEventHandler = PollEventHandlerPtr(new PollEventHandler(mFd));
mEventHandler->EnableRead(true);
mEventHandler->EnableWrite(false);
mEventHandler->SetReadCallBk(std::bind(&Timer::OnReadEvent, this));
}`
| ```
`void Timer::OnReadEvent() {
uint64_t exp;
ssize_t s = read(mFd, &exp, sizeof(exp));
if (mTimeOutCb)
mTimeOutCb();
}`
|
针对我们刚刚提到的计时器的两种需求,我们在 Timer 中定义了两个接口 RunAfter 和 RunEvery,来分别用于设置单次定时任务和周期定时任务。下面是单次定时任务 RunAfter 的实现片段, 它有两个输入参数。time 表示从调用该函数开始,经历一段时间之后,执行回调函数 cb 中定义的任务。
因为我们采用的是绝对计时器,所以在调用 timerfd_settime 之前,需要先获取当前的时间。在 linux 系统中有调用 clock_gettime 来完成这一任务,我们将当前时间记录在成员变量 mOriTime 中。 作为计时的参考点。
`void Timer::RunAfter(const timespec & time, EventCallBk cb) {
if (clock_gettime(CLOCK_REALTIME, &mOriTime) == -1) {
perror("clock_gettime failed!");
exit(1);
}`
接下来,根据 mOriTime 和输入的计时配置构建 new_value 对象。RunAfter 只执行一次定时任务,所以这里将字段 it_interval 中的秒和纳秒字段都置为 0。
` struct itimerspec new_value;
new_value.it_value.tv_sec = mOriTime.tv_sec + time.tv_sec;
new_value.it_value.tv_nsec = mOriTime.tv_nsec + time.tv_nsec;
new_value.it_interval.tv_sec = 0;
new_value.it_interval.tv_nsec = 0;`
最后,我们调用 timerfd_settime 设置定时。如果成功完成定时设置,就把输入的回调任务 cb 赋值给 mTimeOutCb。
` if (timerfd_settime(mFd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1) {
perror("timerfd_settime");
exit(1);
}
mTimeOutCb = std::move(cb);
}`
周期定时任务 RunEvery 的大体与 RunAfter 都是一致的,只在构建计时配置对象 new_value 的时候,略有不同。如下面的代码片段所示,RunEvery 给字段 it_interval 赋值了。
`struct itimerspec new_value;
new_value.it_value.tv_sec = mOriTime.tv_sec;
new_value.it_value.tv_nsec = mOriTime.tv_nsec;
new_value.it_interval.tv_sec = time.tv_sec;
new_value.it_interval.tv_nsec = time.tv_nsec;`
我们提供了一个周期输出日志的demo,如下面的代码片段所示。我们在 main 函数中, 先创建了 PollLoop 和 Timer 对象。然后通过 RunEvery 接口,设定计时器每隔一秒调用一次回调函数 OnTimeOut。如右侧所示,在 OnTimeOut 中,我们直接输出函数名称。 最后在 main 函数中注册 timer 的事件分发器,并开启 Loop 循环。如果编译运行一切顺利,我们是可以看到程序在终端里每隔一秒输出一个 "OnTimeOut" 的。
| ``
int main(int argc, char *argv[]) {PollLoopPtr loop = CreatePollLoop();
TimerPtr timer = TimerPtr(new Timer());
struct timespec t = { 1, 0 };
timer->RunEvery(t, std::bind(OnTimeOut, timer));
ApplyOnLoop(timer, loop);
loop->Loop(10000);
return 0;
}`
| ```
`void OnTimeOut(TimerPtr const & timer) {
std::cout << __FUNCTION__ << std::endl;
}`
|
我们可以通过 timerfd_create 构建一个计时器并获取它的文件描述符,有了文件描述符我们就可以将计时过程融合到 PollLoop 的框架下。所以为之创建了一个数据类型 Timer 来对其进行封装。 通过 timerfd_settime 可以添加定时方案,我们为 Timer 提供了 RunAfter 和 RunEvery 两个接口,分别用于在指定时间之后执行任务,或者周期性的运行。
https://gaoyichao.com/Xiaotu/?book=Linux%E4%B8%8B%E7%9A%84%E4%BA%8B%E4%BB%B6%E4%B8%8E%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B&title=timerfd%E4%B8%8E%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1 无处不在的小土 - timerfd 与定时任务 MathJax.Hub.Config({ TeX: {equationNumbers: {autoNumber: ["AMS"], useLabelIds: true}}, "HTML-CSS": {linebreaks: {automatic: true}}, SVG: {linebreaks: {automatic: true}} }); .MathJax_Preview {color: #888} #MathJax_Message {position: fixed; left: 1em; bottom: 1.5em; background-color: #E6E6E6; border: 1px solid #959595; margin: 0px; padding: 2px 8px; z-index: 102; color: black; font-size: 80%; width: auto; white-space: nowrap} #MathJax_MSIE_Frame {position: absolute; top: 0; left: 0; width: 0px; z-index: 101; border: 0px; margin: 0px; padding: 0px} .MathJax_Error {color: #CC0000; font-style: italic} #MathJax_About {position: fixed; left: 50%; width: auto; text-align: center; border: 3px outset; padding: 1em 2em; background-color: #DDDDDD; color: black; cursor: default; font-family: message-box; font-size: 120%; font-style: normal; text-indent: 0; text-transform: none; line-height: normal; letter-spacing: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; z-index: 201; border-radius: 15px; -webkit-border-radius: 15px; -moz-border-radius: 15px; -khtml-border-radius: 15px; box-shadow: 0px 10px 20px #808080; -webkit-box-shadow: 0px 10px 20px #808080; -moz-box-shadow: 0px 10px 20px #808080; -khtml-box-shadow: 0px 10px 20px #808080; filter: progid:DXImageTransform.Microsoft.dropshadow(OffX=2, OffY=2, Color='gray', Positive='true')} #MathJax_About.MathJax_MousePost {outline: none} .MathJax_Menu {position: absolute; background-color: white; color: black; width: auto; padding: 5px 0px; border: 1px solid #CCCCCC; margin: 0; cursor: default; font: menu; text-align: left; text-indent: 0; text-transform: none; line-height: normal; letter-spacing: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; z-index: 201; border-radius: 5px; -webkit-border-radius: 5px; -moz-border-radius: 5px; -khtml-border-radius: 5px; box-shadow: 0px 10px 20px #808080; -webkit-box-shadow: 0px 10px 20px #808080; -moz-box-shadow: 0px 10px 20px #808080; -khtml-box-shadow: 0px 10px 20px #808080; filter: progid:DXImageTransform.Microsoft.dropshadow(OffX=2, OffY=2, Color='gray', Positive='true')} .MathJax_MenuItem {padding: 1px 2em; background: transparent} .MathJax_MenuArrow {position: absolute; right: .5em; padding-top: .25em; color: #666666; font-size: .75em} .MathJax_MenuActive .MathJax_MenuArrow {color: white} .MathJax_MenuArrow.RTL {left: .5em; right: auto} .MathJax_MenuCheck {position: absolute; left: .7em} .MathJax_MenuCheck.RTL {right: .7em; left: auto} .MathJax_MenuRadioCheck {position: absolute; left: .7em} .MathJax_MenuRadioCheck.RTL {right: .7em; left: auto} .MathJax_MenuLabel {padding: 1px 2em 3px 1.33em; font-style: italic} .MathJax_MenuRule {border-top: 1px solid #DDDDDD; margin: 4px 3px} .MathJax_MenuDisabled {color: GrayText} .MathJax_MenuActive {background-color: #606872; color: white} .MathJax_MenuDisabled:focus, .MathJax_MenuLabel:focus {background-color: #E8E8E8} .MathJax_ContextMenu:focus {outline: none} .MathJax_ContextMenu .MathJax_MenuItem:focus {outline: none} #MathJax_AboutClose {top: .2em; right: .2em} .MathJax_Menu .MathJax_MenuClose {top: -10px; left: -10px} .MathJax_MenuClose {position: absolute; cursor: pointer; display: inline-block; border: 2px solid #AAA; border-radius: 18px; -webkit-border-radius: 18px; -moz-border-radius: 18px; -khtml-border-radius: 18px; font-family: 'Courier New',Courier; font-size: 24px; color: #F0F0F0} .MathJax_MenuClose span {display: block; background-color: #AAA; border: 1.5px solid; border-radius: 18px; -webkit-border-radius: 18px; -moz-border-radius: 18px; -khtml-border-radius: 18px; line-height: 0; padding: 8px 0 6px} .MathJax_MenuClose:hover {color: white!important; border: 2px solid #CCC!important} .MathJax_MenuClose:hover span {background-color: #CCC!important} .MathJax_MenuClose:hover:focus {outline: none} #MathJax_Zoom {position: absolute; background-color: #F0F0F0; overflow: auto; display: block; z-index: 301; padding: .5em; border: 1px solid black; margin: 0; font-weight: normal; font-style: normal; text-align: left; text-indent: 0; text-transform: none; line-height: normal; letter-spacing: normal; word-spacing: normal; word-wrap: normal; white-space: nowrap; float: none; -webkit-box-sizing: content-box; -moz-box-sizing: content-box; box-sizing: content-box; box-shadow: 5px 5px 15px #AAAAAA; -webkit-box-shadow: 5px 5px 15px #AAAAAA; -moz-box-shadow: 5px 5px 15px #AAAAAA; -khtml-box-shadow: 5px 5px 15px #AAAAAA; filter: progid:DXImageTransform.Microsoft.dropshadow(OffX=2, OffY=2, Color='gray', Positive='true')} #MathJax_ZoomOverlay {position: absolute; left: 0; top: 0; z-index: 300; display: inline-block; width: 100%; height: 100%; border: 0; padding: 0; margin: 0; background-color: white; opacity: 0; filter: alpha(opacity=0)} #MathJax_ZoomFrame {position: relative; display: inline-block; height: 0; width: 0} #MathJax_ZoomEventTrap {position: absolute; left: 0; top: 0; z-index: 302; display: inline-block; border: 0; padding: 0; margin: 0; background-color: white; opacity: 0; filter: alpha(opacity=0)} .src-service-contentScript-browser-contentScript-contentScript__toolFrame--2uksu {position: fixed; right: 0; top: 0; width: 100%; height: 100%; z-index: 2147483646; border: none;} .src-service-contentScript-browser-contentScript-contentScript__web-clipper-loading-box--fHTwh {position: fixed; right: 0; top: 0; width: 100%; height: 100%; z-index: 2147483646; border: none;} .src-service-contentScript-browser-contentScript-contentScript__web-clipper-loading-box--fHTwh .web-clipper-loading {position: fixed; right: 10px; top: 10px; box-shadow: rgba(0, 0, 0, 0.2) 0px 0px 12px 0px; background: white; width: 324px; height: 150px; display: flex; align-items: center; justify-content: center; } .src-service-contentScript-browser-contentScript-contentScript__web-clipper-loading-box--fHTwh .web-clipper-loading .line { animation: src-service-contentScript-browser-contentScript-contentScript__expand--31ZFO 1s ease-in-out infinite; border-radius: 10px; display: inline-block; transform-origin: center center; margin: 0 3px; width: 1px; height: 25px; } .src-service-contentScript-browser-contentScript-contentScript__web-clipper-loading-box--fHTwh .web-clipper-loading .line:nth-child(1) { background: #27ae60; } .src-service-contentScript-browser-contentScript-contentScript__web-clipper-loading-box--fHTwh .web-clipper-loading .line:nth-child(2) { animation-delay: 180ms; background: #f1c40f; } .src-service-contentScript-browser-contentScript-contentScript__web-clipper-loading-box--fHTwh .web-clipper-loading .line:nth-child(3) { animation-delay: 360ms; background: #e67e22; } .src-service-contentScript-browser-contentScript-contentScript__web-clipper-loading-box--fHTwh .web-clipper-loading .line:nth-child(4) { animation-delay: 540ms; background: #2980b9; } @keyframes src-service-contentScript-browser-contentScript-contentScript__expand--31ZFO {0% { transform: scale(1); } 25% { transform: scale(2); } } #index_crossLine__2WHMH {position: fixed; height: 100%; width: 100%; z-index: 2147483645;} #index_crossLine__2WHMH::before {border: none; content: ''; height: 100%; position: absolute; width: 100%; z-index: 2147483645; border-right: 1px solid red; border-bottom: 1px solid red; left: -100%; top: -100%;} #index_crossLine__2WHMH::after {border: none; content: ''; height: 100%; position: absolute; width: 100%; z-index: 2147483645; border-top: 1px solid red; border-left: 1px solid red; left: 0; top: 0;} #index_selectArea__2G3Z3 {border: 1px solid red; position: fixed; z-index: 2147483645;} .index_highlightSelector__-qiHd {background-color: #fafafa !important; outline: 3px dashed #1976d2 !important; opacity: 0.8 !important; cursor: pointer !important; transition: opacity 0.5s ease !important;}
|
| | 首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星 |
定时任务,是网络编程当中经常需要处理的一类任务。比如说,基于长连接的服务,为了确认连接没有中断,通常都会要求客户端每隔一段时间发送一个心跳包给服务器,服务器也会返回一个响应到客户端, 告知彼此都还活着。
为了实现定时任务,我一开始的想法是再创建一个线程,在这个线程里面通过调用 sleep 阻塞一段时间,当 sleep 返回之后, 就通过上一节中介绍的 eventfd 来唤醒 PollLoop 来执行定时任务。 按照这个逻辑应该可以写出满足需求的代码,但它引入了多线程,而我们目前还有考虑并行运算所带来的任何竞争冒险的问题。 那么有没有可能,像处理多个连接那样,通过 PollLoop 的循环框架在单个线程中完成这件事情呢?
本文将要介绍的 timerfd 以及相关的系统调用就可以担此大任。我们将对它们进行封装提供一个计时器,并编写一个简单的 demo。 在下一篇文章我们会将之应用到 echo 服务器上,让它具有超时断开连接的功能。
参考我们以前写单片机程序时的计时器,要让服务器判断超时主动断开连接的一个比较直接的想法就是,构建一个计时器, 当计时溢出的时候产生一个事件,驱使我们调用超时的回调函数,并在这个函数中通知 PollLoop 循环关闭连接。我们知道,PollLoop 的内核 poll 调用监听的是文件描述符的读写错事件。 既然有人说在 linux 系统中,万物皆可以是文件,那么如果我们能用文件描述符来表示计时器,不就可以接到 PollLoop 的循环框架下了吗。
在 Linux 系统中,有一组以 timerfd 为前缀的调用。它们分别是 timerfd_create、timerfd_settime、timerfd_gettime。 我们可以通过指令$ man timerfd_create
来查看相关文档,如右图所示。
其中,timerfd_create 用于创建一个计时器对象,并为之提供一个文件描述符,用于通知进程计时事件。它有两个参数,clockid 说明了计时器的类型。它有以下几种选择:
- CLOCK_REALTIME: 这是一种可以设置的系统级实时时钟。
- CLOCK_MONOTONIC: 这是一种不可修改的,单调递增的计时器。
- CLOCK_BOOTTIME: 与 CLOCK_MONOTONIC 类似的,这也是一个不可修改的单调递增的计时器。只是当系统休眠的时候,CLOCK_MONOTONIC 是不会计时的。 而它在这段时间中也会计时。
- CLOCK_REALTIME_ALARM: 功能上与 CLOCK_REALTIME 没有本质区别,只是当系统休眠的时候会唤醒系统。但是这要求调用者具有 CAP_WAKE_ALARM 的能力。
- CLOCK_BOOTTIME_ALARM: 功能上与 CLOCK_BOOTTIME 没有本质区别,只是当系统休眠的时候会唤醒系统。但是这要求调用者具有 CAP_WAKE_ALARM 的能力。
在构建 timerfd 的时候,我们还可以通过参数 flags,来设定计时器的一些特性。目前主要是 TFD_NONBLOCK 用于设定文件描述符工作在非阻塞的状态下。 TFD_CLOEXEC 则用于通过 fork-exec 创建新的进程并运行其它程序时自动关闭子进程中的文件描述符。它们可以通过位或运算进行组合,目前我们不考虑这些特性。
创建了计时器之后,我们需要通过调用 timerfd_settime 来启动或者停止计时。该调用有 4 个参数,如上边右侧的截图所示。其中,fd 是将要操作的计时器的文件描述符; flags 描述了计时特性,这里我们只关注 TFD_TIMER_ABSTIME,表示绝对计时。如果设置了该特性,只有当计时器达到了第三个参数 new_value 中的 it_value 字段所描述的时刻,才认为计时到期。 默认情况下,采用的都是相对计时,即相对于调用 timerfd_settimer 的时刻,经过 new_value.it_value 字段描述的时间后,认为计时到期。
`struct timespec {
time_t tv_sec;
long tv_nsec;
};
struct itimerspec {
struct timespec it_interval;
struct timespec it_value;
};`
我们对计时器有两种需求。其一,我们希望有一个闹钟在经过了一段时间,或者到达了某个特殊时刻之后,通知我们去处理一些事务。其二,我们需要周期性的工作,即每过一段时间就去完成一个特定任务。 这两个需求都体现在 timerfd_settime 的第三个参数 new_value 上了。
该参数的数据类型是struct itimerspec
,其定义如右侧代码所示。 它有两个字段,其中 it_value 表示计时器第一次到期的时刻,如果是相对计时器则经过该字段描述的时间之后,计时器第一次计时到期。若是绝对计时器,则要求计时器到达该时刻。 这就满足了我们的第一个需求。 字段 it_interval 描述的是计时周期,计时器在第一次到期之后,每个该字段描述的一段时间之后,都会产生依次计时到期时间。这满足了我们的第二个需求。
此外,timerfd_settime 还有第四个参数 old_value,用于返回当前计时器的计时设置。如果我们不关心它,可以传递一个 NULL。当然,我们也可以通过调用 timerfd_gettime 来获取计时设置。 通过这三个系统调用,我们就可以创建一个使用文件描述符来表示的计时器,设置和获取计时到期条件。下面,我们对它们进行封装,以融合到我们的 PollLoop 框架下。
`class Timer {
private:
PollEventHandlerPtr mEventHandler;
struct timespec mOriTime;
int mFd;
};`
为了方便的使用 timerfd,这里我们将它们封装到类 Timer 中。如右侧的代码片段所示,我们为之定义了三个私有的成员。
- mEventHandler是一个事件分发器, 用于通过 PollLoop 循环来监听文件描述符 mFd 所对应的计时器的到期事件,并调用相应的回调函数 OnReadEvent。 我们将在 Timer 的构造函数中完成该对象的实例化工作,用户还需要通过 ApplyHandlerOnLoop 接口将它注册到一个 PollLoop 循环上。
- mFd是一个通过调用 timerfd_create 获得的文件描述符,它对应一个计时器。当计时到期事件发生的时候,我们都可以通过调用 read(2) 来获取计时溢出的次数。 所以我们所关心的计时到期事件,实质上是文件描述符 mFd 的可读事件。
- mOriTime是一个用于绝对计时的参考时间点。
下面左侧是 Timer 的构造函数,我们首先通过调用 timerfd_create 构建计时器,并用成员 mFd 记录下它的文件名描述符。在断言描述符一定大于零之后,实例化了事件分发器, 并打开对可读事件的监听功能。最后,在注册读事件的回调函数 OnReadEvent。该回调函数的实现如下面右边的代码所示,我们通过调用 read 读取计时溢出次数,将之记录在局部变量 exp 中。 然后调用计时溢出的回调函数。后面我们会看到这个回调函数将由 TcpServer 提供,并在该回调中通过时间轮盘的形式判定网络连接是否超时。
| ``
Timer::Timer() {mFd = timerfd_create(CLOCK_REALTIME, 0);
assert(mFd > 0);
mEventHandler = PollEventHandlerPtr(new PollEventHandler(mFd));
mEventHandler->EnableRead(true);
mEventHandler->EnableWrite(false);
mEventHandler->SetReadCallBk(std::bind(&Timer::OnReadEvent, this));
}`
| ```
`void Timer::OnReadEvent() {
uint64_t exp;
ssize_t s = read(mFd, &exp, sizeof(exp));
if (mTimeOutCb)
mTimeOutCb();
}`
|
针对我们刚刚提到的计时器的两种需求,我们在 Timer 中定义了两个接口 RunAfter 和 RunEvery,来分别用于设置单次定时任务和周期定时任务。下面是单次定时任务 RunAfter 的实现片段, 它有两个输入参数。time 表示从调用该函数开始,经历一段时间之后,执行回调函数 cb 中定义的任务。
因为我们采用的是绝对计时器,所以在调用 timerfd_settime 之前,需要先获取当前的时间。在 linux 系统中有调用 clock_gettime 来完成这一任务,我们将当前时间记录在成员变量 mOriTime 中。 作为计时的参考点。
`void Timer::RunAfter(const timespec & time, EventCallBk cb) {
if (clock_gettime(CLOCK_REALTIME, &mOriTime) == -1) {
perror("clock_gettime failed!");
exit(1);
}`
接下来,根据 mOriTime 和输入的计时配置构建 new_value 对象。RunAfter 只执行一次定时任务,所以这里将字段 it_interval 中的秒和纳秒字段都置为 0。
` struct itimerspec new_value;
new_value.it_value.tv_sec = mOriTime.tv_sec + time.tv_sec;
new_value.it_value.tv_nsec = mOriTime.tv_nsec + time.tv_nsec;
new_value.it_interval.tv_sec = 0;
new_value.it_interval.tv_nsec = 0;`
最后,我们调用 timerfd_settime 设置定时。如果成功完成定时设置,就把输入的回调任务 cb 赋值给 mTimeOutCb。
` if (timerfd_settime(mFd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1) {
perror("timerfd_settime");
exit(1);
}
mTimeOutCb = std::move(cb);
}`
周期定时任务 RunEvery 的大体与 RunAfter 都是一致的,只在构建计时配置对象 new_value 的时候,略有不同。如下面的代码片段所示,RunEvery 给字段 it_interval 赋值了。
`struct itimerspec new_value;
new_value.it_value.tv_sec = mOriTime.tv_sec;
new_value.it_value.tv_nsec = mOriTime.tv_nsec;
new_value.it_interval.tv_sec = time.tv_sec;
new_value.it_interval.tv_nsec = time.tv_nsec;`
我们提供了一个周期输出日志的demo,如下面的代码片段所示。我们在 main 函数中, 先创建了 PollLoop 和 Timer 对象。然后通过 RunEvery 接口,设定计时器每隔一秒调用一次回调函数 OnTimeOut。如右侧所示,在 OnTimeOut 中,我们直接输出函数名称。 最后在 main 函数中注册 timer 的事件分发器,并开启 Loop 循环。如果编译运行一切顺利,我们是可以看到程序在终端里每隔一秒输出一个 "OnTimeOut" 的。
| ``
int main(int argc, char *argv[]) {
PollLoopPtr loop = CreatePollLoop();
TimerPtr timer = TimerPtr(new Timer());
struct timespec t = { 1, 0 };
timer->RunEvery(t, std::bind(OnTimeOut, timer));
ApplyOnLoop(timer, loop);
loop->Loop(10000);
return 0;
}`
| ```
`void OnTimeOut(TimerPtr const & timer) {
std::cout << __FUNCTION__ << std::endl;
}`
|
我们可以通过 timerfd_create 构建一个计时器并获取它的文件描述符,有了文件描述符我们就可以将计时过程融合到 PollLoop 的框架下。所以为之创建了一个数据类型 Timer 来对其进行封装。 通过 timerfd_settime 可以添加定时方案,我们为 Timer 提供了 RunAfter 和 RunEvery 两个接口,分别用于在指定时间之后执行任务,或者周期性的运行。
Copyright @ 高乙超. All Rights Reserved. 京 ICP 备 16033081 号 - 1 https://gaoyichao.com/Xiaotu/?book=Linux%E4%B8%8B%E7%9A%84%E4%BA%8B%E4%BB%B6%E4%B8%8E%E7%BD%91%E7%BB%9C%E7%BC%96%E7%A8%8B&title=timerfd%E4%B8%8E%E5%AE%9A%E6%97%B6%E4%BB%BB%E5%8A%A1