通过实例来理解 eventfd 函数机制

  eventfd 是 Linux 的一个系统调用,创建一个文件描述符用于事件通知,自 Linux 2.6.22 以后开始支持。

# bionic/libc/include/sys/eventfd.h(Android 8.0源码)
int eventfd(unsigned int initial_value, int flags);

  eventfd() 函数会创建一个 eventfd 对象,用户空间的应用程序可以用这个 eventfd 来实现事件的等待或通知机制,也可以用于内核通知新的事件到用户空间应用程序。 这个对象包含一个 64-bit 的整形计数器,内核空间维护这个计数器,创建这个计数器的时候使用第一个入参 initial_value 来初始化计数器。

#include 
#include 
#include 
#include 
#include 

#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 ...\n", argv[0]);
       exit(EXIT_FAILURE);
   }

   efd = eventfd(0, EFD_SEMAPHORE);
   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);
           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");
   }
}

初始化的计数器值设置为 0 的几种情况

  1. 如果 flags 设置为 0,并且子进程中往文件描述符写入 4、5、6 时,父进程读取并打印的结果是 15,这个时候计数器置为 0;
  2. 如果 flags 设置 EFD_NONBLOCK,并且子进程中不往文件描述符写入(注释 write 代码),会打印一个错误,同时父进程退出(代码中执行 exit 函数)。
wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Parent about to read
read: Resource temporarily unavailable
  1. 如果 flags 没有设置 EFD_NONBLOCK(可设置为 0 或者 EFD_SEMAPHORE),并且子进程中不往文件描述符写入(注释write代码),那么父进程会一直阻塞,程序会阻塞在那里。
wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Parent about to read
一直阻塞在这里......
  1. 如果 flags 设置了 EFD_SEMAPHORE,父进程读取并打印的结果为 1,计数器递减 1。
wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Child writing 4 to efd
Child writing 5 to efd
Child writing 6 to efd
Child completed write loop
Parent about to read
Parent read 1 (0x1) from efd

初始化的计数器值设置为 5 的几种情况

  1. 如果 flags 设置为 0,并且子进程中往文件描述符写入 4、5、6 时,父进程读取并打印的结果是 20,这个时候计数器置为 0;
  2. 如果 flags 设置 EFD_NONBLOCK,并且子进程中不往文件描述符写入(注释 write 代码),不会报错,而是将计数器初始化的非零值(5)给读出来了,这个时候计数器置为 0。
wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Parent about to read
Parent read 5 (0x5) from efd
  1. 如果 flags 没有设置 EFD_NONBLOCK,并且子进程中不往文件描述符写入(注释write代码)。

a) flags 设置为 0,父进程不会阻塞,读取并打印的结果为 5,计数器置为 0。

wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Parent about to read
Parent read 5 (0x5) from efd

b) 在 a) 的场景下,修改代码,加一个循环条件,让其循环读 5 次,父进程将初始值的 5 读完之后,由于计数器置为 0 了,所以第 2 次继续读时,就一直阻塞在那儿了。

for (int i = 0; i < 5; i++) {
    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);
}

wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Parent about to read
Parent read 5 (0x5) from efd
一直阻塞在这里......

c) flags 设置为 EFD_SEMAPHORE,父进程不会阻塞,读取并打印的结果为 1,计数器递减 1。

wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Parent about to read
Parent read 1 (0x1) from efd

d) 在 c) 的场景下,修改代码,加一个循环条件,让其循环读 6 次,父进程将初始值的 5 读完之后,由于计数器置为 0 了,所以第 6 次继续读时,就一直阻塞在那儿了。

wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Parent about to read
Parent read 1 (0x1) from efd
Parent read 1 (0x1) from efd
Parent read 1 (0x1) from efd
Parent read 1 (0x1) from efd
Parent read 1 (0x1) from efd
一直阻塞在这里......
  1. 如果 flags 设置了 EFD_SEMAPHORE,父进程读取并打印的结果为 1,计数器递减 1。
wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6
Child writing 4 to efd
Child writing 5 to efd
Child writing 6 to efd
Child completed write loop
Parent about to read
Parent read 1 (0x1) from efd

小结

通过上述在计数器初始值为 0 和非 0 两种情况下,对 flags 的尝试,可以得出结论:

  1. 只要 flags 设置了 EFD_NONBLOCK,不管计数器初始值是什么值,读到计数器的值为 0 后,再继续读,会直接返回一个错误值,不会阻塞;除非有写入操作使计数器的值为非 0,才能续正常地进行读操作。
  2. 只要 flags 没有设置 EFD_NONBLOCK,不管计数器初始值是什么值,读到计数器的值为 0 后,再继续读,会阻塞;除非有写入操作使计数器的值为非 0,才能续正常地进行读操作。

你可能感兴趣的:(通过实例来理解 eventfd 函数机制)