导言
在 Linux 系统开发中,定时器是一个非常常见的需求。除了传统的setitimer
、alarm
等接口,Linux 还提供了一种基于文件描述符的定时器机制 ——timerfd
。这种机制将定时器事件转化为文件描述符的可读事件,非常适合与 I/O 多路复用(如poll
、epoll
)结合使用。
一、简介
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:
Timerfd(TimerfdCallback && cb, int initSec, int peridoSec);
~Timerfd();
void start();
void stop();
private: int createTimerFd();
void handleRead();
void setTimerFd(int initSec, int peridoSec);
int _timerfd; TimerfdCallback _cb; bool _isStarted; int _initSec; int _peridoSec; };
#endif
|
类的核心成员包括:
_timerfd
:存储timerfd
创建的文件描述符
_cb
:std::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()) , _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() { int fd = timerfd_create(CLOCK_REALTIME, 0); if(fd == -1) { perror("timerfd_create error"); return -1; } return fd; }
|
timerfd_create
的第一个参数指定时钟类型(CLOCK_REALTIME
或CLOCK_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;
newValue.it_interval.tv_sec = peridoSec; newValue.it_interval.tv_nsec = 0;
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; pfd.events = POLLIN;
setTimerFd(_initSec, _peridoSec); _isStarted = true;
while(_isStarted) { int nready = poll(&pfd, 1, 5000); if(-1 == nready && errno == EINTR) { continue; } else if(-1 == nready) { perror("poll error"); break; } else if(0 == nready) { cout << ">> poll timeout!" << endl; } else { if(pfd.revents & POLLIN) { handleRead(); if(_cb) { _cb(); } } } } }
void Timerfd::stop() { _isStarted = false; }
|
start
方法是定时器的核心逻辑:
- 使用
poll
监听timerfd
的可读事件
- 当定时器到期,
timerfd
变为可读,触发POLLIN
事件
- 调用
handleRead
读取数据(timerfd
到期后会写入 8 字节的计数,必须读取才能继续触发)
- 执行用户注册的回调函数
stop
方法通过设置_isStarted
为false
,使事件循环退出,从而停止定时器。
5. 处理可读事件
1 2 3 4 5 6 7
| void Timerfd::handleRead() { uint64_t u; 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; Timerfd tfd(bind(&MyTask::process, &task), 1, 4);
thread th(bind(&Timerfd::start, &tfd));
sleep(30); tfd.stop(); th.join(); }
int main(int argc, char *argv[]) { test(); return 0; }
|
测试逻辑说明:
- 定义
MyTask
类,其中process
方法为定时任务的具体逻辑
- 创建
Timerfd
对象,绑定MyTask::process
作为回调,设置初始延迟 1 秒,每 4 秒触发一次
- 使用子线程定时器的
start
方法(因为start
会阻塞)
- 主线程 30 秒后,调用
stop
停止定时器,并等待子线程结束