Linux网络编程 - Eventfd的使用

概述

linux系统提供了各种各样的IPC,管道,信号,消息队列,信号量,共享内存,socket等,各有各的应用场景,今天我们来讲一个linux系统提供的系统调用eventfd,这个系统调用比较新,从linux内核 2.6.22版本加入到内核中的。主要是为了高效的利用系统资源实现通知的管理和送达.

在linux系统中,eventfd是一个用来通知事件的文件描述符,是由内核向用户空间的应用发送通知的机制,可以有效地被用来实现用户空间的事件/通知驱动的应用程序。简而言之,eventfd就是用来触发事件通知的。

eventfd详解

函数原型:

1
2
#include <sys/eventfd.h>
int eventfd(unsigned int initval, int flags);

调用接口eventfd()创建一个eventfd对象,或者也可以理解打开一个eventfd类型的文件,类似普通文件的open操作。

该对象是一个内核维护的无符号64位整型计数器,初始化为initval的值。

第二个参数flags在linux 2.6.26之前版本并没有使用,必须初始化为0,在2.6.27之后的版本flag才被使用。

flags是以下三个标志位OR结果:

  • EFD_CLOEXEC(2.6.27~) : eventfd()返回一个文件描述符,如果该进程被fork的时候,这个文件描述符也会被复制过去,这个时候就会有多个描述符指向同一个eventfd对象,如果设置了这个标志,则子进程在执行exec的时候,会自动清除掉父进程的这个文件描述符。
  • EFD_NONBLOCK(2.6.27~):文件描述符会被设置为O_NONBLOCK,如果没有设置这个标志位,read操作的时候将会阻塞直到计数器中有值,如果设置了这个这个标志位,计数器没有值得时候也会立刻返回-1.
  • EFD_SEMAPHORE(2.6.30~): 这个标志位会影响read操作。具体可以参考read方法.

操作方法

read

读取计数器的值。

  • 如果计数器中的值大于0
    • 设置了EFD_SEMAPHORE标志位,则返回1,且计数器中的值减去1.
    • 没有设置EFD_SEMAPHORE标志位,则返回计数器中的值,且计数器设置为0.
  • 如果计数器中的值为0
    • 设置了EFD_NONBLOCK标志位就直接返回-1.
    • 没有设置EFD_NONBLOCK标志位就会一直阻塞直到计数器中的值大于0.

write

向计数器中写入值。

  • 如果写入的值和小于0xFFFFFFFFFFFFFFFE,则写入成功。
  • 如果写入的值和大于0xFFFFFFFFFFFFFFFE
    • 设置了EFD_NONBLOCK标志位就直接返回-1
    • 如果没有设置EFD_NONBLOCK标志位,则会一直阻塞直到read操作执行。

IO多路复用

epoll()/poll()/select(): 支持 IO 多路复用操作

close

关闭文件描述符

使用事例

父子进程间读写示例:

 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
#include <sys/eventfd.h>
#include <unistd.h>
#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, j;
  uint64_t u;
  ssize_t s;

  if (argc < 2) {
    fprintf(stderr, "Usage: %s <num>...\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  efd = eventfd(0, 0);
  if (efd == -1)
    handle_error("eventfd");

  switch (fork()) {
  case 0:
    for (j = 1; j < argc; j++) {
      printf("Child writing %s to efd\n", argv[j]);
      u = strtoull(argv[j], NULL, 0);
      /* strtoull() allows various bases */
      s = write(efd, &u, sizeof(uint64_t));
      if (s != sizeof(uint64_t))
        handle_error("write");
    }
    printf("Child completed write loop\n");

    exit(EXIT_SUCCESS);

  default:
    //    sleep(2);

    printf("Parent about to read\n");
    s = read(efd, &u, sizeof(uint64_t));
    if (s != sizeof(uint64_t))
      handle_error("read");
    printf("Parent read %llu (0x%llx) from efd\n",
           (unsigned long long) u, (unsigned long long) u);
    exit(EXIT_SUCCESS);

  case -1:
    handle_error("fork");
  }
}

执行结果:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
lzm@ubuntu:~/unpipc/pipe$ gcc eventfd.c -o eventfd
lzm@ubuntu:~/unpipc/pipe$ ./eventfd 1 3 5 7 9
Parent about to read
Child writing 1 to efd
Child writing 3 to efd
Child writing 5 to efd
Child writing 7 to efd
Child writing 9 to efd
Child completed write loop
Parent read 25 (0x19) from efd
lzm@ubuntu:~/unpipc/pipe$

典型应用场景及优势

Eventfd在信号通知的场景下,相对比pipe有非常大的资源和性能优势, 本质其实是counter(计数器)channel(数据信道)的区别。

  • 打开文件数量的差异 由于pipe是半双工的传统IPC实现方式,所有两个线程通信需要两个pipe文件描述符,而用eventfd只需要打开一个文件描述符。 总所周知,文件描述符是系统中非常宝贵的资源,linux的默认值只有1024个而已,pipe只能在两个进程/线程间使用,面向连接,使用之前就需要创建好两个pipe,而eventfd是广播式的通知,可以多对多。

  • 内存使用的差别 eventfd是一个计数器,内核维护的成本非常低,大概是自旋锁+唤醒队列的大小,8个字节的传输成本也微乎其微,而pipe完全不同,一来一回数据在用户空间和内核空间有多达4次的复制,而且最糟糕的是,内核要为每个pipe分配最少4k的虚拟内存页,哪怕传送的数据长度为0.

  • 与epoll完美结合 eventfd设计之初就与epoll完美结合,支持非阻塞读取等,就是为epoll而生的,而pipe是Unix时代就有了,那时候不仅没有epoll,连linux还没诞生。

当pipe只用来发送通知,放弃它,放心的使用eventfd

eventfd配合epoll才是它存在的原因。

0%