导言

在 Linux 系统开发中,定时器是一个非常常见的需求。除了传统的setitimeralarm等接口,Linux 还提供了一种基于文件描述符的定时器机制 ——timerfd。这种机制将定时器事件转化为文件描述符的可读事件,非常适合与 I/O 多路复用(如pollepoll)结合使用。

一、简介

timerfd是 Linux 内核 2.6.25 版本后引入的接口,它将定时器功能抽象为一个文件描述符:当定时器到期时,该文件描述符会变为可读状态,我们可以通过read操作获取到期次数,从而处理定时事件。

相比传统定时器,timerfd的优势在于:

  • 可以无缝集成到 I/O 多路复用模型中,无需单独的信号处理逻辑
  • 支持绝对时间和相对时间,支持周期性触发
  • 线程安全,可在多线程环境中安全使用

二、定时器类设计(Timerfd.h

我们首先设计一个Timerfd类,封装timerfd的创建、设置、启动、停止等操作,核心思路是通过回调函数处理定时事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#ifndef _TIMERFD_H
#define _TIMERFD_H

#include <functional>
using std::function;
using std::bind;

// 定义回调函数类型
using TimerfdCallback = function<void()>;

class Timerfd {
public:
/**
* 构造函数
* @param cb:定时到期的回调函数
* @param initSec:初始延迟时间(秒)
* @param peridoSec:周期时间(秒,0表示只触发一次)
*/
Timerfd(TimerfdCallback && cb, int initSec, int peridoSec);

~Timerfd();

// 启动定时器
void start();

// 停止定时器
void stop();

private:
// 创建timerfd
int createTimerFd();

// 处理读事件(必须读取,否则timerfd不会再次触发)
void handleRead();

// 设置定时器参数
void setTimerFd(int initSec, int peridoSec);

int _timerfd; // timerfd文件描述符
TimerfdCallback _cb; // 定时回调函数
bool _isStarted; // 定时器是否启动的标志
int _initSec; // 初始延迟时间
int _peridoSec; // 周期时间
};

#endif //_TIMERFD_H

类的核心成员包括:

  • _timerfd:存储timerfd创建的文件描述符
  • _cbstd::function类型的回调函数,定时器到期时执行
  • _isStarted:控制定时器事件循环的开关
  • 初始化和周期时间参数

三、定时器类实现(Timerfd.cc

接下来实现Timerfd类的具体方法,重点关注timerfd的创建、参数设置和事件监听逻辑。

1. 构造与析构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "Timerfd.h"
#include <unistd.h>
#include <stdio.h>
#include <poll.h>
#include <sys/timerfd.h>
#include <errno.h>
#include <iostream>
using std::cout;
using std::endl;

Timerfd::Timerfd(TimerfdCallback && cb, int initSec, int peridoSec)
: _timerfd(createTimerFd()) // 初始化列表创建timerfd
, _cb(std::move(cb)) // 移动语义接收回调函数
, _initSec(initSec)
, _peridoSec(peridoSec)
, _isStarted(false) // 初始化为未启动
{
}

Timerfd::~Timerfd() {
close(_timerfd); // 关闭文件描述符
}

构造函数通过初始化列表完成成员初始化,其中createTimerFd负责实际创建timerfd

2. 创建 timerfd

1
2
3
4
5
6
7
8
9
10
int Timerfd::createTimerFd() {
// CLOCK_REALTIME:系统实时时间,会受NTP调整影响
// TFD_NONBLOCK(可选):非阻塞模式,这里未使用
int fd = timerfd_create(CLOCK_REALTIME, 0);
if(fd == -1) {
perror("timerfd_create error");
return -1;
}
return fd;
}

timerfd_create的第一个参数指定时钟类型(CLOCK_REALTIMECLOCK_MONOTONIC),第二个参数可设置非阻塞或关闭时自动清理等标志。

3. 设置定时器参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void Timerfd::setTimerFd(int initSec, int peridoSec) {
struct itimerspec newValue;
// 初始到期时间
newValue.it_value.tv_sec = initSec;
newValue.it_value.tv_nsec = 0;

// 周期时间(0表示只触发一次)
newValue.it_interval.tv_sec = peridoSec;
newValue.it_interval.tv_nsec = 0;

// 设置定时器(第二个参数为0表示相对时间,TFD_TIMER_ABSTIME表示绝对时间)
int ret = timerfd_settime(_timerfd, 0, &newValue, nullptr);
if(ret < 0) {
perror("timerfd_settime error");
return;
}
}

timerfd_settime用于设置定时器的初始触发时间(it_value)和周期触发时间(it_interval),当it_value为 0 时定时器不工作,it_interval为 0 时只触发一次。

4. 启动与停止定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void Timerfd::start() {
struct pollfd pfd;
pfd.fd = _timerfd; // 监听timerfd
pfd.events = POLLIN; // 关注可读事件

setTimerFd(_initSec, _peridoSec); // 启动时设置定时器
_isStarted = true;

while(_isStarted) {
// 超时时间5秒(可根据需求调整)
int nready = poll(&pfd, 1, 5000);
if(-1 == nready && errno == EINTR) {
// 被信号中断,继续循环
continue;
} else if(-1 == nready) {
// poll出错
perror("poll error");
break;
} else if(0 == nready) {
// 超时,可做一些心跳操作
cout << ">> poll timeout!" << endl;
} else {
// 检查是否是timerfd可读
if(pfd.revents & POLLIN) {
handleRead(); // 必须读取数据,否则不会再次触发
if(_cb) {
_cb(); // 执行回调函数
}
}
}
}
}

void Timerfd::stop() {
_isStarted = false; // 退出事件循环
}

start方法是定时器的核心逻辑:

  • 使用poll监听timerfd的可读事件
  • 当定时器到期,timerfd变为可读,触发POLLIN事件
  • 调用handleRead读取数据(timerfd到期后会写入 8 字节的计数,必须读取才能继续触发)
  • 执行用户注册的回调函数

stop方法通过设置_isStartedfalse,使事件循环退出,从而停止定时器。

5. 处理可读事件

1
2
3
4
5
6
7
void Timerfd::handleRead() {
uint64_t u; // 存储到期次数(每次到期为1,周期触发会累计)
ssize_t s = read(_timerfd, &u, sizeof(uint64_t));
if (s != sizeof(uint64_t)) {
perror("read timerfd error");
}
}

timerfd到期后,内核会向其写入一个 8 字节的无符号整数,表示从上次读取后定时器到期的次数。必须读取该数据,否则timerfd会一直处于可读状态,导致持续触发事件。

四、使用示例(Test.cc

下面通过一个测试示例,展示如何使用Timerfd类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "Timerfd.h"
#include <unistd.h>
#include <iostream>
#include <functional>
#include <thread>

using std::cout;
using std::endl;
using std::bind;
using std::thread;

// 自定义任务类
class MyTask {
public:
void process() {
cout << ">> MyTask is running" << endl;
}
};

void test() {
MyTask task;
// 创建定时器:初始延迟1秒,周期4秒,回调绑定MyTask::process
Timerfd tfd(bind(&MyTask::process, &task), 1, 4);

// 启动子线程定时器(避免阻塞主线程)
thread th(bind(&Timerfd::start, &tfd));

// 主线程休眠30秒后停止定时器
sleep(30);
tfd.stop();
th.join(); // 等待子线程结束
}

int main(int argc, char *argv[]) {
test();
return 0;
}

测试逻辑说明:

  1. 定义MyTask类,其中process方法为定时任务的具体逻辑
  2. 创建Timerfd对象,绑定MyTask::process作为回调,设置初始延迟 1 秒,每 4 秒触发一次
  3. 使用子线程定时器的start方法(因为start会阻塞)
  4. 主线程 30 秒后,调用stop停止定时器,并等待子线程结束