如何使用linux基础设施timerfd和eventfd
前言
最近拜读陈硕所著的《Linux多线程服务端编程》, 收获颇多。作者用epoll多路复用+非阻塞IO实现了Reactor服务端框架muduo。其中使用了linux的高级特性timerfd
做定时器, eventfd
做事件通知, 统一用epoll做多路复用。为了后续muduo的学习, 本文仅仅探索这两个基础设施的用法。
timerfd接口介绍:
timerfd_create(2)
把时间变成了一个文件描述符, 该”文件”在定时器超时的那一刻变得可读, 这样就能很方便地融入多路复用中。用统一的方式来处理IO事件和超时事件, 这也正是Reactor模式的长处。(这段文字来自网络)
1 | int timerfd_create(int clockid, int flags); |
clockid注解(五选一):
- CLOCK_REALTIME: 系统实时时间,随系统实时时间改变而改变,即从UTC1970-1-1 0:0:0开始计时,中间时刻如果系统时间被用户改成其他,则对应的时间相应改变
- CLOCK_MONOTONIC: 从系统启动这一刻起开始计时,不受系统时间被用户改变的影响。这种时钟对网络应用更合适, 因此muduo使用的是这种时钟。
- CLOCK_BOOTTIME(since Linux 3.15): 与CLOCK_MONOTONIC类似, 但是将系统暂停(suspend)的时间也算入进去了, 因为我们在讨论服务器程序(不会suspend), 不考虑这个时钟。
- CLOCK_REALTIME_ALARM(since Linux 3.11): 也与系统suspend有关, 因此不讨论。
- CLOCK_BOOTTIME_ALARM(since Linux 3.11): 也与系统suspend有关, 不讨论。
flags注解(按位选择是否需要):
- TFD_NONBLOCK: 读写不阻塞。muduo中使用了它, 因为我们不希望任何IO操作阻塞。计时器没有值时
read
返回EAGAIN
。 - TFD_CLOEXEC: exec时子进程关闭此fd。
timerfd_setime
用于启停定时器:
1 | int timerfd_settime(int fd, int flags, const struct itimerspec *new_value,struct itimerspec *old_value); |
参数注解:
- fd:
timerfd_create
得到的文件描述符。 - new_value: 新定时器的配置,
it_interval
代表周期时间,it_value
代表超时时间。可以选择性的禁用某个功能, 比如我只需要周期时间, 那么就把it_value
的两个值都设置为0即可。 - old_value: 返回之前定时器的配置, 如果之前的定时器未进行任何配置, 那么
it_intervak
和it_value
的成员都是0(也就表示禁用)。
flags参数注解(按需取位):
- TFD_TIMER_ABSTIME: 将
it_value
设置为绝对时间, 也就是说如果我们需要5s后timer响应, 就需要这么做:
1 | clock_gettime(CLOCK_REALTIME, &now); |
我认为这个位是鸡肋, 因为clock_gettime
是系统调用, 开销不小。
- TFD_TIMER_CANCEL_ON_SET: 如果设置了这个位, 当
timer
里面已经有值时, 此后又重新设置了这个timer, 下次读取时返回ECANCELED
错误码。muduo里没有使用到这个配置。
timerfd_gettime
获得timer
此时的配置。个人感觉没应用场景。
1 | int timerfd_gettime(int fd, struct itimerspec *curr_value); |
如果此时有定时器过期被写入timer, read
定时器将返回8字节的无符号整数类型(uint64_t), 指示有多少个过期事件已经被写入到定时器。
timerfd实验:
如果阻塞, 堆积多个计时器read是否会一次读完? 实验程序代码:
1 |
|
可见, read
将消费所有累积的定时器事件, 这样可以避免定时器interval太短而造成busy loop
, 这点对网络服务器很友好。
eventfd
1 | int eventfd(unsigned int initval, int flags); |
flags参数注解(按位取需):
- EFD_CLOEXEC: 不提了
- EFD_NONBLOCK: epoll+多路复用常常指定非阻塞的模式
- EFD_SEMAPHORE: 信号量模式, 用于实现信号量, 之后讨论
在非EFD_SEMAPHORE
模式下, 表现和timer_fd
很相似。内核维护一个uint64
变量, 代表事件的个数。 read
此fd用8byte的无符号整数接收数据, 代表获得事件的数目。write
会增加计数。
read
和write
都应该读写的是uint64
的变量, count
应该填8。
在信号量模式下read
最多获取一个事件, 这可以用来实现跨进程信号量:
1 |
|
更多相关设施?
- signalfd: 信号探查
- userfaultfd: 用户态的page-fault探查