eventfd
ventfd是Linux一种轻量化的线程间通信方式。eventfd(2)
会产生一个”eventfd object”,该对象包含一个uint64_t
的计数器。使用eventfd(2)
成功后会返回一个fd。在使用阻塞I/O时,eventfd的逻辑如下:
- 对eventfd进行
read
- 如果计数器的值为0,则该次
read
调用阻塞,直到计数器的值不为零。 - 如果计数器的值不为0,则将计数器的值置0,并返回原来原来计数器的值。
- 如果计数器的值为0,则该次
- 对eventfd进行
write
- 将buffer中的数值加到计数器上,如果相加后计数器的值超过的
UINT64_MAX
,则write
阻塞直到有其他进程调用read
- 将buffer中的数值加到计数器上,如果相加后计数器的值超过的
eventfd的“阻塞”功能,是通过wait_queue
来实现的,下面我们来分析eventfd的一部分的代码。
eventfd的数据结构与初始化
struct eventfd_ctx {
struct kref kref;
wait_queue_head_t wqh;
__u64 count;
unsigned int flags;
int id;
};
wqh
是一个等待队列头wait_queue_head_t
,而计数器就是count
。
eventfd读取数据
eventfd(2)
系统调用通过do_eventfd
来完成evnetfd的创建和初始化:
#1
对等待队列头进行了初始化。#2
取得一个未使用的文件描述符#3
创建一个匿名inode#4
将该fd和匿名inode安装到当前线程上
static int do_eventfd(unsigned int count, int flags)
{
struct eventfd_ctx *ctx;
struct file *file;
int fd;
/* Check the EFD_* constants for consistency. */
BUILD_BUG_ON(EFD_CLOEXEC != O_CLOEXEC);
BUILD_BUG_ON(EFD_NONBLOCK != O_NONBLOCK);
if (flags & ~EFD_FLAGS_SET)
return -EINVAL;
ctx = kmalloc(sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return -ENOMEM;
kref_init(&ctx->kref);
init_waitqueue_head(&ctx->wqh); // #1
ctx->count = count;
ctx->flags = flags;
ctx->id = ida_simple_get(&eventfd_ida, 0, 0, GFP_KERNEL);
flags &= EFD_SHARED_FCNTL_FLAGS;
flags |= O_RDWR;
fd = get_unused_fd_flags(flags); // #2
if (fd < 0)
goto err;
file = anon_inode_getfile("[eventfd]", &eventfd_fops, ctx, flags); // #3
if (IS_ERR(file)) {
put_unused_fd(fd);
fd = PTR_ERR(file);
goto err;
}
file->f_mode |= FMODE_NOWAIT;
fd_install(fd, file); // #4
return fd;
err:
eventfd_free_ctx(ctx);
return fd;
}
SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags)
{
return do_eventfd(count, flags);
}
SYSCALL_DEFINE1(eventfd, unsigned int, count)
{
return do_eventfd(count, 0);
}
eventfd并没有使用之前介绍的封装好的wait_event
和wake_up
系列macro,而是使用DECLARE_WAITQUEUE
,__add_wait_queue
等自行维护wait_queue
:
-
#1
在栈上静态初始化一个wait_queue_entry
。其回调函数为default_wake_function
。 -
#2
不允许读取的内容少于8个字节 #3
获得等待队列的自旋锁,该自旋锁不但保护等待队列,也保护counter。可能这就是重新实现一次wait_queue逻辑的原因。因为如果我们使用封装好的wait_evnet
系列宏,就可能需要另一个同步原语来保证计数器的读写。#4
如果计数器的值大于零,不需等待,直接跳到#14
,遍历等待队列,并唤醒等待队列上所有的task。#5
如果计数器的值为零,且不是非阻塞I/O,就进行将该task加入到等待队列的一系列工作,首先将之前声明在栈上的wait_queue_entry
放入等待队列#6
开始进入循环,每次程序被唤醒时从schedule()
中返回,并从#11
处开始执行#7
将该task状态设置为TASK_INTERRUPTIBLE
,准备将该进程置于interruptible sleep#8
检查counter,如果不为零,就结束sleep#9
因为处于interruptible sleep,检查信号,如果有待处理的信号,返回-ERESTARTSYS
#10
马上要放弃CPU进入sleep,因为绝对不可带着自旋锁进入休眠,因此将释放自旋锁。#11
当从该task被唤醒,并从schedule()
返回后,重新取得自旋锁,进行#7
,#8
,#9
,#10
重新进行条件检查。
由此可见,其逻辑基本和wait_event_interruptible
相同。
static ssize_t eventfd_read(struct file *file, char __user *buf, size_t count,
loff_t *ppos)
{
struct eventfd_ctx *ctx = file->private_data;
ssize_t res;
__u64 ucnt = 0;
DECLARE_WAITQUEUE(wait, current); // #1
if (count < sizeof(ucnt)) // #2
return -EINVAL;
spin_lock_irq(&ctx->wqh.lock); // #3
res = -EAGAIN;
if (ctx->count > 0) // #4
res = sizeof(ucnt);
else if (!(file->f_flags & O_NONBLOCK)) {
__add_wait_queue(&ctx->wqh, &wait); // #5
for (;;) { // #6
set_current_state(TASK_INTERRUPTIBLE); // #7
if (ctx->count > 0) { // #8
res = sizeof(ucnt);
break;
}
if (signal_pending(current)) { // #9
res = -ERESTARTSYS;
break;
}
spin_unlock_irq(&ctx->wqh.lock); // #10
schedule();
spin_lock_irq(&ctx->wqh.lock); // #11
}
__remove_wait_queue(&ctx->wqh, &wait); // #12
__set_current_state(TASK_RUNNING); // #13
}
if (likely(res > 0)) {
eventfd_ctx_do_read(ctx, &ucnt);
if (waitqueue_active(&ctx->wqh)) // #14
wake_up_locked_poll(&ctx->wqh, EPOLLOUT); // #15
}
spin_unlock_irq(&ctx->wqh.lock);
if (res > 0 && put_user(ucnt, (__u64 __user *)buf))
return -EFAULT;
return res;
}
// /include/linux/wait.h
#define wake_up_locked_poll(x, m) \
__wake_up_locked_key((x), TASK_NORMAL, poll_to_key(m))
// /kernel/sched/wait.c
void __wake_up_locked_key(struct wait_queue_head *wq_head, unsigned int mode, void *key)
{
__wake_up_common(wq_head, mode, 1, 0, key, NULL);
}