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$ 错误。对于返回的描述符,可以进行以下操作:

#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$ 参数可以为以下值:

        如果 $old_-value$ 非空,那么原来的定时时间会被返回到 $old_-value$ 指向的地址中。
        $timerfd_-gettime$ 调用会返回当前的定时时间。如果返回的时间值都为 $0$ ,表示当前定时器没有开始计时。无论当前定时器是否指定了 $TFD_-TIMER_-ABSTIME$ ,都将返回相对时间。
        可以对 $timerfd$ 进行以下操作:

        如果 $fd$ 无效,返回 $EBADF$ 错误。如果 $fd$ 不是 $timerfd$ ,返回 $EINVAL$ 错误。如果 $new_-value$ 、$old_-value$ 或者 $curr_-value$ 无效,返回 $EFAULT$ 错误。此外,对于 $ECANCELED$ 错误,还存在特殊情况:

  1. 创建一个 $CLCOK_-REALTIME$ / $CLOCK_-REALTIME_-ALARM$ 定时器,并启用 $TFD_-TIMER_-ABSTIME$ 和 $TFD_-TIMER_-CANCEL_-ON_-SET$ 标志;
  2. 调用 $timerfd_-settime$ 启动定时器;
  3. 对 $CLOCK_-REALTIME$ 时钟进行非连续修改 ( 比如 $settimeofday$ );
  4. 再次调用 $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);
}

Unix网络编程补充:eventfd和timerfd