如何使用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
2
3
4
5
6
7
8
9
10
11
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value,struct itimerspec *old_value);

struct timespec {
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds */
};

struct itimerspec {
struct timespec it_interval; /* Interval for periodic timer */
struct timespec it_value; /* Initial expiration */
};

参数注解:

  • fd: timerfd_create得到的文件描述符。
  • new_value: 新定时器的配置, it_interval代表周期时间, it_value代表超时时间。可以选择性的禁用某个功能, 比如我只需要周期时间, 那么就把it_value的两个值都设置为0即可。
  • old_value: 返回之前定时器的配置, 如果之前的定时器未进行任何配置, 那么it_intervakit_value的成员都是0(也就表示禁用)。

flags参数注解(按需取位):

  • TFD_TIMER_ABSTIME: 将it_value设置为绝对时间, 也就是说如果我们需要5s后timer响应, 就需要这么做:
1
2
3
clock_gettime(CLOCK_REALTIME, &now);
new_value.it_value.tv_sec = now.tv_sec + 5;
new_value.it_value.tv_sec = now.tv_nsec;

我认为这个位是鸡肋, 因为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
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

#include <sys/timerfd.h>
#include <unistd.h>
#include <stdio.h>


int main() {
int timer_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK);
printf("fd = %d\n", timer_fd);

struct itimerspec sp;

// 2s的周期定时器
sp.it_interval.tv_nsec = 0;
sp.it_interval.tv_sec = 2;

// 不设置一次性的定时器
sp.it_value.tv_nsec = 0;
sp.it_value.tv_sec = 1;

timerfd_settime(timer_fd, 0, &sp, NULL);

sleep(5);


int k = 0;
while(1) {
__uint64_t t;
int n = read(timer_fd, &t, sizeof(__uint64_t));
if (n < 0) {
break;
}
k++;
}

printf("%d\n", k);
}


/*
output:
fd = 3
read 返回 t = 3
read 返回 t = 3
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会增加计数。

readwrite都应该读写的是uint64的变量, count应该填8。

在信号量模式下read最多获取一个事件, 这可以用来实现跨进程信号量:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#define _GUN_SOURCE

#include <sys/eventfd.h>
#include <unistd.h>
#include <pthread.h>
#include <stdio.h>

// 用eventfd实现阻塞的信号量

struct sem {
int event_fd;
};


void sem_init(struct sem *s) {
s->event_fd = eventfd(0, EFD_CLOEXEC|EFD_SEMAPHORE);
}

void sem_wait(struct sem *s) {
uint64_t i;
read(s->event_fd, &i, 8);
}

void sem_post(struct sem *s) {
uint64_t i = 1;
write(s->event_fd, &i, 8);
}

void sem_destory(struct sem *s) {
close(s->event_fd);
}

struct sem mysem;


void* sender_thread(void *) {
while(1) {
sleep(1);
sem_post(&mysem);
}

return NULL;
}


int main() {
sem_init(&mysem);

pthread_t tid;
pthread_create(&tid, NULL, sender_thread, NULL);

while(1) {
sem_wait(&mysem);
printf("信号\n");
}

return 0;
}

// 结果: 每秒打印一次信号

更多相关设施?

  • signalfd: 信号探查
  • userfaultfd: 用户态的page-fault探查