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 <sys/eventfd.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
#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, 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 的几种情况
- 如果 flags 设置为 0,并且子进程中往文件描述符写入 4、5、6 时,父进程读取并打印的结果是 15,这个时候计数器置为 0;
- 如果 flags 设置 EFD_NONBLOCK,并且子进程中不往文件描述符写入(注释 write 代码),会打印一个错误,同时父进程退出(代码中执行 exit 函数)。
wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6 Parent about to read read: Resource temporarily unavailable
- 如果 flags 没有设置 EFD_NONBLOCK(可设置为 0 或者 EFD_SEMAPHORE),并且子进程中不往文件描述符写入(注释write代码),那么父进程会一直阻塞,程序会阻塞在那里。
wufan@Frank-Linux:~/Linux/test$ ./rw 4 5 6 Parent about to read 一直阻塞在这里......
- 如果 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 的几种情况
- 如果 flags 设置为 0,并且子进程中往文件描述符写入 4、5、6 时,父进程读取并打印的结果是 20,这个时候计数器置为 0;
- 如果 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
- 如果 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 一直阻塞在这里......
- 如果 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 的尝试,可以得出结论:
- 只要 flags 设置了 EFD_NONBLOCK,不管计数器初始值是什么值,读到计数器的值为 0 后,再继续读,会直接返回一个错误值,不会阻塞;除非有写入操作使计数器的值为非 0,才能续正常地进行读操作。
- 只要 flags 没有设置 EFD_NONBLOCK,不管计数器初始值是什么值,读到计数器的值为 0 后,再继续读,会阻塞;除非有写入操作使计数器的值为非 0,才能续正常地进行读操作。