Unix网络编程补充:eventfd和timerfd
1. eventfd
#include <sys/eventfd.h>
// 成功返回eventfd描述符, 出错返回-1
int eventfd(unsigned int initval, int flags);
$eventfd$ 调用返回一个 $eventfd$ 对象描述符,通过该描述符,进程可以在用户空间实现阻塞和唤醒机制,内核会负责唤醒阻塞在该描述符上的进程。$eventfd$ 对象包含一个无符号 $64$ 位整型计数器,由内核维护,初始化为 $initval$ 的值。$flags$ 值是以下值的或:
$flag$ | 说明 |
---|---|
$EFD_-CLOEXEC$ | 设置该标志后,描述符将在 $execve$ 调用之后自动关闭 |
$EFD_-NONBLOCK$ | 非阻塞模式 |
$EFD_-SEMAPHORE$ | 为读提供类似于信号量的语义 |
对于Linux 2.6.26
及以下的版本来说,$flags$ 参数没有作用且必须为 $0$ 。如果 $flags$ 参数非法,会返回 $EINVAL$ 错误。对于返回的描述符,可以进行以下操作:
- $read$ :调用成功会返回一个以主机字节序排列的 $64$ 位无符号整型;如果返回值小于 $64$ 位,则调用失败,返回 $EINVAL$ 错误。
- 如果没有指定 $EFD_-SEMAPHORE$ 并且 $eventfd$ 计数器值非零,$read$ 调用会返回计数器值,同时计数器值会被设置为 $0$ ;
- 如果指定了 $EFD_-SEMAPHORE$ 并且 $eventfd$ 计数器值非零,$read$ 调用会返回 $1$ ,同时计数器值会减一;
- 如果 $eventfd$ 计数器值为 $0$ ,$read$ 调用会阻塞直到计数器值非零。如果指定了 $EFD_-NONBLOCK$ ,$read$ 调用会返回 $EAGAIN$ 错误。
- $write$ :调用成功会为 $eventfd$ 计数器值加上一个 $64$ 位无符号整型的值的数量,最大为 $UINT_-MAX$ $-$ $1$ 。如果指定的值非 $64$ 位或者超过最大值,返回 $EINVAL$ 错误。
- 如果加上这个值会导致溢出,调用会阻塞直到该描述符被调用了一次 $read$ 。如果指定了 $EFD_-NONBLOCK$ ,$write$ 调用会返回 $EAGAIN$ 错误。
- $poll$ / $select$ / $epoll$ :
- 如果 $eventfd$ 计数器值大于 $0$ ,则描述符可读;
- 如果 $eventfd$ 计数器值至少可以加 $1$ ,则描述符可写;
- 如果 $eventfd$ 计数器值溢出,对于 $select$ ,该描述符状态会被设置为既可读也可写;对于 $poll$ ,返回 $POLLERR$ ;对于 $epoll$ ,返回 $EPOLLERR$ 。
- $close$ :递减该描述符的引用计数,当引用计数为零时,$eventfd$ 对象会被内核释放。
#include <sys/eventfd.h>
typedef uint64_t eventfd_t;
/* 成功返回调用传递的数据量,出错返回-1 */
int eventfd_read(int fd, eventfd_t *value);
int eventfd_write(int fd, eventfd_t value);
GNU C
库定义了以上函数,与直接对 $eventfd$ 调用 $read$ 和 $write$ 的作用相同。
#include <sys/eventfd.h>
#include <unistd.h>
#include <inttypes.h> // definition of PRIu64 & PRIx64
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h> // definition of uint64_t
#define handle_error(msg) \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
} while (0)
int main(int argc, char **argv) {
int efd;
uint64_t u;
if (argc < 2) {
fprintf(stderr, "Usage: %s <num>...\n", argv[0]);
exit(EXIT_FAILURE);
}
if ((efd = eventfd(0, 0)) == -1)
handle_error("eventfd");
switch (fork()) {
case 0:
for (int j = 1; j < argc; j++) {
printf("Child writing %s to efd\n", argv[j]);
u = strtoull(argv[j], NULL, 0);
if(write(efd, &u, sizeof(uint64_t)) != sizeof(uint64_t))
handle_error("write");
}
printf("Child completed write loop\n");
exit(EXIT_SUCCESS);
default:
sleep(2);
printf("Parent about to read\n");
if (read(efd, &u, sizeof(uint64_t)) != sizeof(uint64_t))
handle_error("read");
printf("Parent read %" PRIu64 " (%#" PRIx64 ") from efd\n", u, u);
exit(EXIT_SUCCESS);
case -1:
handle_error("fork");
}
}
2. timerfd
#include <sys/timerfd.h>
// 成功返回timer描述符,出错返回-1
int timerfd_create(int clockid, int flags);
这些系统调用用于创建和管理 $timerfd$ 计时器。$timerfd_-create$ 创建一个新的 $timer$ 对象,并返回该对象的描述符。$clockid$ 参数指定了 $timer$ 的时钟类型:
$clock$ | 说明 |
---|---|
$CLOCK_-REALTIME$ | 可变的系统范围内的实时时钟 |
$CLOCK_-MONOTONIC$ | 不可修改的单调递增时钟,从过去某个指定时间开始,并且在系统启动后不再改变 |
$CLOCK_-BOOTTIME$ | 类似于 $CLOCK_-MONOTONIC$ ,但是可以在系统休眠时继续计时 |
$CLOCK_-REALTIME_-ALARM$ | 类似于 $CLOCK_-REALTIME$ ,但是可以阻止系统休眠。进程必须拥有 $CAP_-WAKE_-ALARM$ 能力才能设置该时钟 |
$LOCK_-BOOTTIME_-ALARM$ | 类似于 $CLOCK_-BOOTTIME$ ,但是可以阻止系统休眠。进程必须拥有 $CAP_-WAKE_-ALARM$ 能力才能设置该时钟 |
$flags$ 标志可以改变 $timer$ 的行为。
$flag$ | 说明 |
---|---|
$TFD_-NONBLOCK$ | 非阻塞模式 |
$TFD_-CLOEXEC$ | 描述符会在 $execve$ 调用后自动关闭 |
如果 $clockid$ 或者 $flags$ 非法,返回 $EINVAL$ 错误。对于没有 $CAP_-WAKE_-ALARM$ 能力的进程指定 $CLOCK_-REALTIME_-ALARM$ 或者 $CLOCK_-BOOTTIME_-ALARM$ 标志会导致调用返回 $EPERM$ 错误。
#include <sys/timerfd.h>
/* 成功返回0,出错返回-1 */
int timerfd_settime(int fd, int flags,
const struct itimerspec *new_value, struct itimerspce *old_value);
int timerfd_gettime(int fd, struct itimerspce *curr_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
};
$timerfd_-settime$ 用于启动或者停止 $timer$ ,$new_-value$ 参数指定定时时间,如果 $it_-value$ 或者 $it_-interval$ 成员存在非零值,那么 $timer$ 会被启动;如果都为 $0$ ,那么 $timer$ 会停止。如果 $it_-value$ 和 $it_-interval$ 都非零,那么定时器会选择 $it_-value$ 的时间并且只会触发一次。$it_-value$ 默认为相对时间,可以通过 $flag$ 修改。$flag$ 参数可以为以下值:
- $TFD_-TIMER_-ABSTIME$ :$it_-value$ 表达绝对时间 ( 默认为相对时间 );
- $TFD_-TIMER_-CANCEL_-ON_-SET$ :如果和 $TFD_-TIMER_-ABSTIME$ 同时设置,并且时钟为 $CLOCK_-REALTIME$ 或 $CLOCK_-REALTIME_-ALARM$ ,那么在经历了非连续的更改 ( $settimeofday$ 、$clock_-settime$ 等 ) 之后,$timer$ 会被标记为可取消。当更改发生并且有其他进程正阻塞在对该 $timerfd$ 的 $read$ 调用上时,$read$ 调用将失败并返回 $CANCELED$ 错误。
如果 $old_-value$ 非空,那么原来的定时时间会被返回到 $old_-value$ 指向的地址中。
$timerfd_-gettime$ 调用会返回当前的定时时间。如果返回的时间值都为 $0$ ,表示当前定时器没有开始计时。无论当前定时器是否指定了 $TFD_-TIMER_-ABSTIME$ ,都将返回相对时间。
可以对 $timerfd$ 进行以下操作:
- $read$ :如果 $timer$ 已经启动,并且在上一次调用 $read$ 到现在这段时间已经超时了一次或多次,那么这次 $read$ 调用会以主机字节序返回一个 $64$ 位无符号整型,值为超时次数。
- 如果没有发生超时,那么 $read$ 会阻塞直到超时发生。如果指定了 $TFD_-NONBLOCK$ ,那么会返回 $EAGAIN$ 错误;
- 如果提供的缓冲区小于 $64$ 位,返回 $EINVAL$ 错误;
- 如果指定了 $CLOCK_-REALTIME$ / $CLOCK_-REALTIME_-ALARM$ 、$TFD_-TIMER_-ABSTIME$ 和 $TFD_-TIMER_-CANCEL_-ON_-SET$ ,并且在阻塞期间时间被以非连续方式修改,那么会返回 $ECANCELED$ 错误。
- $poll$ / $select$ / $epoll$ :如果发生了一次或多次超时,描述符将可读;
- $ioctl$ :可以执行 $TFD_-IOC_-SET_-TICKS$ 命令,修改该定时器的超时次数,参数为一个无符号 $64$ 位整型指针,指向新的超时次数。当调用成功时,所有在该定时器上等待的进程将被唤醒。只有内核配置了 $CONFIG_-CHECKPOINT_-RESTORE$ 选项后该命令才可用;
- $close$ :递减当前定时器描述符的引用计数,当引用计数为零时,该定时器将被内核释放。
如果 $fd$ 无效,返回 $EBADF$ 错误。如果 $fd$ 不是 $timerfd$ ,返回 $EINVAL$ 错误。如果 $new_-value$ 、$old_-value$ 或者 $curr_-value$ 无效,返回 $EFAULT$ 错误。此外,对于 $ECANCELED$ 错误,还存在特殊情况:
- 创建一个 $CLCOK_-REALTIME$ / $CLOCK_-REALTIME_-ALARM$ 定时器,并启用 $TFD_-TIMER_-ABSTIME$ 和 $TFD_-TIMER_-CANCEL_-ON_-SET$ 标志;
- 调用 $timerfd_-settime$ 启动定时器;
- 对 $CLOCK_-REALTIME$ 时钟进行非连续修改 ( 比如 $settimeofday$ );
- 再次调用 $timerfd_-settime$ 启动定时器。
在上述情况下,$timerfd_-settime$ 将返回 $-1$ ,同时设置 $ECANCELED$ 错误,这使得调用者得知上一次启动因为非连续修改而中止。虽然这次调用返回了错误,但是不意味着设置失败,定时器还是会按照调用中传递的定时时间设置并启动。
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
#include <inttypes.h> // definition of PRIu64
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h> // definition of uint64_t
#define handle_error(msg) \
do { \
perror(msg); \
exit(EXIT_FAILURE); \
} while(0)
static void print_elapsed_time() {
static struct timespec start;
struct timespec curr;
static int first_call = 1;
int secs, nsecs;
if (first_call) {
first_call = 0;
if (clock_gettime(CLOCK_MONOTONIC, &start) == -1)
handle_error("clock_gettime");
}
if (clock_gettime(CLOCK_MONOTONIC, &curr) == -1)
handle_error("clock_gettime");
secs = curr.tv_set - start.tv_sec;
nsecs = curr.tv_nsec - start.tv_nsec;
if (nsecs < 0) {
secs--;
nsecs += 1000000000;
}
printf("%d.%03d: ", secs, (nsecs + 500000) / 1000000);
}
int main(int argc, char **argv) {
struct itimerspec new_value;
int max_exp, fd;
struct timespec now;
uint64_t exp, tot_exp;
if ((argc != 2) && (argc != 4)) {
fprintf(stderr, "%s init-secs [interval-secs max-exp]\n", argv[0]);
exit(EXIT_FAILURE);
}
if (clock_gettime(CLOCK_REALTIME, &now) == -1)
handle_error("clock_gettime");
/* Create a CLOCK_REALTIME absolute timer with initial
expiration and interval as specified in command line. */
new_value.it_value.tv_sec = now.tv_sec + atoi(argv[1]);
new_value.it_value.tv_nsec = now.tv_nsec;
if (argc == 2) {
new_value.it_interval.tv_sec = 0;
max_exp = 1;
} else {
new_value.it_interval.tv_sec = atoi(argv[2]);
max_exp = atoi(argv[3]);
}
new_value.it_interval.tv_nsec = 0;
if ((fd = timerfd_create(CLOCK_REALTIME, 0)) == -1)
handle_error("timerfd_create");
if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1)
handle_error("timerfd_settime");
print_elapsed_time();
printf("timer started\n");
for (tot_exp = 0; tot_exp < max_exp; ) {
if (read(fd, &exp, sizeof(uint64_t)) != sizeof(uint64_t))
handle_error("read");
tot_exp += exp;
print_elapsed_time();
printf("read: %" PRIu64 "; total=%" PRIu64 "\n", exp, tot_exp);
}
exit(EXIT_SUCCESS);
}