Linux eventfd分析

2017-07-20



eventfd在linux中是一个较新的进程通信方式,和信号量等不同的是event不仅可以用于进程间的通信,还可以用户内核发信号给用户层的进程。eventfd在virtIO后端驱动vHost的实现中作为vhost和KVM交互的媒介,起到了重大作用。本节结合linux源码就eventfd的具体实现坐下简要分析。

eventfd在用户层下有函数

#include <sys/eventfd.h>

 int eventfd(unsigned int initval, int flags);

该函数返回一个文件描述符,类似于其他的文件描述符操作,可以对该描述符进行一系列的操作,如读、写、poll、select等,当然这里我们仅仅考虑read、write。看下该函数的内核实现

SYSCALL_DEFINE2(eventfd2, unsigned int, count, int, flags)
{
    int fd, error;
    struct file *file;
    error = get_unused_fd_flags(flags & EFD_SHARED_FCNTL_FLAGS);
    if (error < 0)
        return error;
    fd = error;
    file = eventfd_file_create(count, flags);
    if (IS_ERR(file)) {
        error = PTR_ERR(file);
        goto err_put_unused_fd;
    }
    fd_install(fd, file);
    return fd;
err_put_unused_fd:
    put_unused_fd(fd);
    return error;
}

代码本身很是简单,首先 获取一个空闲的文件描述符,这个和普通的文件描述符没有区别。然后调用eventfd_file_create创建了一个file结构。该函数中有针对eventfd的一系列操作,看下该函数

struct file *eventfd_file_create(unsigned int count, int flags)
{
    struct file *file;
    struct eventfd_ctx *ctx;

    /* 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 ERR_PTR(-EINVAL);

    ctx = kmalloc(sizeof(*ctx), GFP_KERNEL);
    if (!ctx)
        return ERR_PTR(-ENOMEM);

    kref_init(&ctx->kref);
    init_waitqueue_head(&ctx->wqh);
    ctx->count = count;
    ctx->flags = flags;

    file = anon_inode_getfile("[eventfd]", &eventfd_fops, ctx,
                  O_RDWR | (flags & EFD_SHARED_FCNTL_FLAGS));
    if (IS_ERR(file))
        eventfd_free_ctx(ctx);

    return file;
}

这里说明下,每个eventfd在内核对应一个eventfd_ctx结构,该结构后面咱们再细讲,函数中首先给该结构分配 了内存然后做初始化,注意有个等待队列和count,等待队列就是当进程需要阻塞的时候挂在对应evnetfd的等待队列上,而count就是read、write操作的值。接着就调用anon_inode_getfile获取一个file对象,具体也没什么好说的,只是注意这里把刚才分配好的eventfd_ctx作为file结构的私有成员即private_data,并且关联了eventfd自身的操作函数表eventfd_fops, 里面实现的函数不多,如下

static const struct file_operations eventfd_fops = {
#ifdef CONFIG_PROC_FS
    .show_fdinfo    = eventfd_show_fdinfo,
#endif
    .release    = eventfd_release,
    .poll        = eventfd_poll,
    .read        = eventfd_read,
    .write        = eventfd_write,
    .llseek        = noop_llseek,
};

我们重点看read和write函数。当用户空间对eventfd文件描述符发起read操作时,最终要调用到上面函数表中的eventfd_read函数,

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 cnt;
    if (count < sizeof(cnt))
        return -EINVAL;
    res = eventfd_ctx_read(ctx, file->f_flags & O_NONBLOCK, &cnt);
    if (res < 0)
        return res;
    return put_user(cnt, (__u64 __user *) buf) ? -EFAULT : sizeof(cnt);
}

首先从private_data获取eventfd_ctx,然后判断请求读取的大小是否满足条件,这里count是64位即8个字节,所以最小读取8个字节,如果不足则错误。没问题就调用eventfd_ctx_read,该函数实际返回eventfd_ctx中的count计数,并清零,如果读取有问题则返回,否则把值写入到用户空间。前面eventfd_ctx_read是读取的核心,什么时候会返回小于0的值呢,我们看下该函数的实现

ssize_t eventfd_ctx_read(struct eventfd_ctx *ctx, int no_wait, __u64 *cnt)
{
    ssize_t res;
    DECLARE_WAITQUEUE(wait, current);

    spin_lock_irq(&ctx->wqh.lock);
    *cnt = 0;
    res = -EAGAIN;
    if (ctx->count > 0)
        res = 0;
    else if (!no_wait) {
        /*add to wait queue*/
        __add_wait_queue(&ctx->wqh, &wait);
        for (;;) {
            /*设置阻塞状态*/
            set_current_state(TASK_INTERRUPTIBLE);
            /*如果信号变为有状态。则break*/
            if (ctx->count > 0) {
                res = 0;
                break;
            }
            /*如果有未处理的信号,也break,进行处理*/
            if (signal_pending(current)) {
                res = -ERESTARTSYS;
                break;
            }
            /*否则触发调度器执行调度*/
            spin_unlock_irq(&ctx->wqh.lock);
            schedule();
            spin_lock_irq(&ctx->wqh.lock);
        }
        /*remove from the wait queue*/
        __remove_wait_queue(&ctx->wqh, &wait);
        /*set processs state*/
        __set_current_state(TASK_RUNNING);
    }
    if (likely(res == 0)) {
        /*read fdcount again*/
        eventfd_ctx_do_read(ctx, cnt);
        /**/
        if (waitqueue_active(&ctx->wqh))
            wake_up_locked_poll(&ctx->wqh, POLLOUT);
    }
    spin_unlock_irq(&ctx->wqh.lock);

    return res;
}

该函数比较长,我们慢慢分析,首先操作eventfd_ctx要加锁保证安全。起初res初始化为-EAGAIN,如果count计数大于0,那么对res置0,否则意味着count=0(count不会小于0),这种情况下看传递进来的参数标志,如果设置了O_NONBLOCK,则就不需等待,直接返回res.这正是前面说的返回值小于0的情况。如果没有指定O_NONBLOCK标志,此时由于读取不到count值(count值为0),就会在这里阻塞。具体把当前进程加入到eventfd_ctx的等待队列,这里有必要说下DECLARE_WAITQUEUE(wait, current),该宏声明并初始化一个wait_queue_t对象,其关联的函数为default_wake_function,是作为唤醒函数存在。OK,接下上面,加入到队列后进入一个死循环,设置当前进程状态为TASK_INTERRUPTIBLE,并不断检查count值,如果count大于0了,意味着有信号了,就设置res=0,然后break,然后把进程从等待队列去掉,然后设置状态TASK_RUNNING。如果count值为0,则检查是否有挂起的信号,如果有信号,同样需要先对信号进行处理,不过这就以为这read失败了。都么有的话就正常阻塞,调用调度器进行调度。break之后,如果res==0,对count值进行读取,这里对应上面循环中判断count值大于0的情况。具体读取通过eventfd_ctx_do_read函数,该函数很简单

static void eventfd_ctx_do_read(struct eventfd_ctx *ctx, __u64 *cnt)
{
    *cnt = (ctx->flags & EFD_SEMAPHORE) ? 1 : ctx->count;
    ctx->count -= *cnt;
}

如果没有指定EFD_SEMAPHORE标志就返回count值,该标志是指定eventfd像信号量一样使用,不过在2.6之后的内核都设置为0了。然后对count做减法,实际上减去之后就为0了。在读取值之后count值就变小了,之前如果有在该eventfd上阻塞的write进程,现在就可以唤醒了,所以这里检查了下,如果等待队列还有进程,则调用wake_up_locked_poll对对应的进程进行唤醒。

用户空间的write操作最终要调用到eventfd_write,不过该函数的实现和上面read操作类似,这里就不重复,感兴趣可以自行分析源码。前面说内核也可以主动的对eventfd发送信号,这里就是通过eventfd_signal函数实现

__u64 eventfd_signal(struct eventfd_ctx *ctx, __u64 n)
{
    unsigned long flags;

    spin_lock_irqsave(&ctx->wqh.lock, flags);
    if (ULLONG_MAX - ctx->count < n)
        n = ULLONG_MAX - ctx->count;
    ctx->count += n;
    /*mainly judge if wait is empty*/
    if (waitqueue_active(&ctx->wqh))
        wake_up_locked_poll(&ctx->wqh, POLLIN);
    spin_unlock_irqrestore(&ctx->wqh.lock, flags);

    return n;
}

该函数和write函数类似,不过不会阻塞,如果指定的n太大导致count加上之后超过ULLONG_MAX,就去n为当前count和ULLONG_MAX的差值,即不会让count溢出。然后如果等待队列有等待的进程,则对其进程唤醒,当然唤醒的应该是需要读操作的进程。

到这里对于eventfd的介绍基本就完成了,总的来说很简单的一个东西,不过经过上面分析不难发现,eventfd应该归结于低级通信行列,即不适用于传递大量数据,仅仅用于通知或者同步操作。

关于eventfd的使用方法,参考手册:https://linux.die.net/man/2/eventfd

以马内利

参考资料:

linux内核3.10.1源码

时间: 2024-10-06 01:43:44

Linux eventfd分析的相关文章

自学linux指令分析-ls

自学linux指令分析-ls 1·命令格式 ls [option] [directory-list] ls [参数][目录名] 2·命令参数    -a, –all 列出目录下的所有文件,包括以 . 开头的隐含文件. -A, –almost-all 列出除了 . 及 .. 以外的任何项目 –author 印出每个文件的作者 -b, –escape 把文件名中不可输出的字符用反斜杠加字符编号的形式列出. –block-size=大小块以指定<大小>的字节为单位 -B, –ignore-backu

自学linux指令分析-head

自学linux指令分析-head 1·命令格式 head [参数][文件] 2·命令参数 -q                     隐藏文件名 -v                     显示文件名 -c<字节>           显示字节数 -n<行数>           显示的行数 3.命令功能 头部  显示文件头部-n, 行数,默认显示头部10行 4.命令范列 [[email protected] ~]# head -5 ett.txt 12345

自学linux指令分析-vi

自学linux指令分析-vi 1·命令格式 vi  file-list vi [文件名] 2.命令功能 vi编辑器是所有Unix及Linux系统下标准的编辑器,它的强大不逊色于任何最新的文本编辑器. 3.使用方式 执行 vi oldboy.txt进入 vi 编辑器(默认是命令模式),点击 a 或者 i 进入编辑模式, 敲入内容I am studying linux,然后按键盘上的esc键退出编辑模式(进入命令模式), 最后敲 ;wq保存并退出,wq解释 write quit. 如果只是查看,可以

《linux 内核分析》 第4周

王一 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.linux 系统的状态 Linux在x86平台下支持0内核态和3用户态.在内核态32位平台能访问0x00000000以上的空间,而用户态只能访问小于0xc0000000一下的地址空间 (此处的地址空间为逻辑地址).当用户态切换到内核态的时候主要方式为中断. 1.当int128调用时,系统会自动的两个状态下的cs:eip,ss:es

《linux 内核分析》 第二周 实验

王一 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 本次课的核心是通过中断机制完成进程的调度 ,在本次课程中__init my_start_kernel作为入口函数,定义0号进程的tPCB结构体,通过复制来制造其他进程的tPCB数据结构,中断时间函数被 my_timer_handler周期性的调用来修改my_need_sched 的值,而0号进程一直在检测my_need_sched 的

Linux内核分析8

周子轩 原创作品转载请注明出处  <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 实验目的: 使用gdb跟踪分析一个schedule()函数,理解Linux系统中进程调度的时机. 实验过程: 登陆实验楼虚拟机http://www.shiyanlou.com/courses/195 打开shell终端,执行以下命令: cd LinuxKernel rm -rf menu git clone https://git

《Linux内核分析》课程第七周学习总结

姓名:何伟钦 学号:20135223 ( *原创作品转载请注明出处*) ( 学习课程:<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-100002900 学习内容:Linux内核如何装载和启动一个可执行程序 理解编译链接的过程和ELF可执行文件格式: 编程使用exec*库函数加载一个可执行文件,动态链接分为可执行程序装载时动态链接和运行时动态链接,编程练习动态链接库的这两种使用方式: 使用gdb跟踪分析一个execve系统调用内核处

LINUX内核分析第七周学习总结——可执行程序的装载

LINUX内核分析第六周学习总结——进程的描述和进程的创建 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 一.知识概要 (一)预处理.编译.链接和目标文件的格式 1.可执行程序是怎么得来的 2.目标文件的格式ELF 3.静态链接的ELF可执行文件和进程的地址空间 (二)可执行程序.共享库和动态加载 1.装载可执行程序之前的工作 2.装载时动态链接和运行时动态链接应用举例 (三)

《Linux内核分析》第六周学习小结

进程的描述和进程的创建 一.进程的描述 进程描述符task_struct数据结构: (1)操作系统的三大功能: 进程管理.内存管理.文件系统 (2)进程的作用: 将信号.进程间通信.内存管理和文件系统联系起来 (3)进程控制块PCB——task_struct数据结构 提供了内核需要了解的信息 (4)task_struct结构庞大,有400多行代码.包含了进程状态.内核堆栈等相关信息的定义. (5)Linux的进程和操作系统原理中描述的进程状态有所不同,实际内核中,就绪和运行状态都用TASK_RU