select, poll, epoll详解(三)

作者感言:

本文,不仅仅是一篇技术介绍文章,更多的是一篇【情怀】文章。

2014年,当时就已经完成了本系列的前面2篇,即第1篇&第2篇,关于select和poll的介绍。当时规划着第3篇介绍epoll,怎知后面耽搁了,一直没有补上第3篇。

时光荏苒,一晃就是11年过去了!!今天整理文章时,草稿箱里一直躺着的这篇未完成文章,终于到了该结尾的时候了。本想一删了之,不发布了,没有太大价值了。今天的linux内核版本,早已不是当年2.6.28时候的样子了。介绍的这些源码已经太过存旧。

然而,作为技术人员,还是得有始有终。当年学习内核代码,没有AI辅助,没有cursor & agent,没有LLM大模型加持,全是凭借自己一行一行代码阅读,理解,消化,测试,然后再整理成文字。现在有AI加持,阅读源码可是比之前轻松太多了。

闲话少叙,今天还是补充完整这篇文章,纪念过去的岁月。

select, poll, epoll详解(一)
select, poll, epoll详解(二)
select, poll, epoll详解(三)

1. epoll源码分析

基于2.6.28内核代码。在内核中对应的系统调用如下:

// sys_epoll_create函数,用于创建一个epoll实例
// 参数size:原本设计用于指定epoll实例的初始大小,但实际上该参数未被使用
// 返回值:成功返回一个新的epoll文件描述符,失败返回负数错误码
asmlinkage long sys_epoll_create(int size)
{
    // 检查传入的size参数是否小于0,如果小于0则返回无效参数错误码
    if (size < 0)
        return -EINVAL;

    // 调用sys_epoll_create1函数,传入参数0
    return sys_epoll_create1(0);
}

传入的 size参数其实并没有用到。在检测完 size 合法性后,直接调用 sys_epoll_create1 函数。

/*
 * Open an eventpoll file descriptor.
 * 打开一个eventpoll文件描述符
 */
// sys_epoll_create1函数,用于创建一个epoll实例
// 参数flags:标志位,用于指定文件描述符的行为
// 返回值:成功-返回一个新的epoll文件描述符,失败-返回负数错误码
asmlinkage long sys_epoll_create1(int flags)
{
    int error, fd = -1;  // error用于存储错误码,fd用于存储文件描述符,初始化为-1表示无效
    struct eventpoll *ep;  // 指向eventpoll结构体的指针

    /* Check the EPOLL_* constant for consistency.  */
    // BUILD_BUG_ON宏检测条件是否为true.如果是,则代码编译期间会报错。
    // 检查EPOLL_CLOEXEC和O_CLOEXEC是否相等,如果不相等则编译报错
    BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);

    // 检查flags中是否有非法的标志位,如果有则返回无效参数错误码
    if (flags & ~EPOLL_CLOEXEC)
        return -EINVAL;

    // 打印调试信息
    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d)\n",
                 current, flags));

    /*
     * Create the internal data structure ( "struct eventpoll" ).
     * 创建内部数据结构("struct eventpoll")
     */
    // 调用ep_alloc函数分配一个eventpoll结构体
    error = ep_alloc(&ep);
    // 如果分配失败,将错误码赋值给fd,并跳转到错误处理标签error_return
    if (error < 0) {
        fd = error;
        goto error_return;
    }

    /*
     * Creates all the items needed to setup an eventpoll file. That is,
     * a file structure and a free file descriptor.
     * 创建设置eventpoll文件所需的所有项,即一个文件结构和一个空闲的文件描述符
     */
    // 调用anon_inode_getfd函数创建一个匿名inode,并返回一个文件描述符
    fd = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep,
                          flags & O_CLOEXEC);
    // 如果获取文件描述符失败,释放之前分配的eventpoll结构体
    if (fd < 0)
        ep_free(ep);
    // 增加用户的epoll设备计数
    atomic_inc(&ep->user->epoll_devs);

error_return:
    // 打印调试信息
    DNPRINTK(3, (KERN_INFO "[%p] eventpoll: sys_epoll_create(%d) = %d\n",
                 current, flags, fd));

    // 返回文件描述符
    return fd;
}

a). 首先来看看BUILD_BUG_ON_ZERO这个宏,定义如下:

// BUILD_BUG_ON_ZERO宏,用于在编译期间检查表达式e是否为零
// 如果e为零,那么结构体中int:-!(e)会导致结构体大小为负数,编译会报错
#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!(e); }))

它的作用是在编译期间检查一个表达式是否为零。如果表达式为零,那么这个结构体的大小将是一个负数,这在C语言中是不允许的,从而导致编译错误。在这个例子中,它用于检查EPOLL_CLOEXEC和O_CLOEXEC是否相等。

b). 接着看ep_alloc函数,它的作用是分配一个eventpoll结构体,并初始化一些字段。

// ep_alloc函数,用于分配一个eventpoll结构体并初始化
// 参数pep:指向eventpoll结构体指针的指针,用于返回分配的结构体指针
// 返回值:成功返回0,失败返回负数错误码
static int ep_alloc(struct eventpoll **pep)
{
    struct eventpoll *ep;  // 指向eventpoll结构体的指针

    // 分配一个eventpoll结构体
    ep = kmalloc(sizeof(struct eventpoll), GFP_KERNEL);
    // 如果分配失败,返回内存不足错误码
    if (!ep)
        return -ENOMEM;

    // 初始化事件就绪链表
    INIT_LIST_HEAD(&ep->rdllist);
    // 初始化文件描述符链表
    INIT_LIST_HEAD(&ep->fllist);
    // 初始化自旋锁
    ep->lock = SPIN_LOCK_UNLOCKED;
    // 初始化溢出列表
    ep->ovflist = NULL;
    // 初始化用户指针
    ep->user = NULL;
    // 初始化红黑树的根节点
    ep->rbr = RB_ROOT;
    // 初始化事件计数
    ep->event_count = 0;
    // 初始化标志位
    ep->flags = 0;

    // 将分配的结构体指针赋值给pep指向的指针
    *pep = ep;
    // 返回成功
    return 0;
}

c). 然后是anon_inode_getfd函数,它的作用是创建一个匿名inode,并返回一个文件描述符。

// anon_inode_getfd函数,用于创建一个匿名inode并返回一个文件描述符
// 参数name:inode的名称
// 参数fops:文件操作结构体指针
// 参数private_data:私有数据指针
// 参数flags:标志位
// 返回值:成功返回文件描述符,失败返回负数错误码
int anon_inode_getfd(const char *name, const struct file_operations *fops,
                     void *private_data, int flags)
{
    struct inode *inode;  // 指向inode结构体的指针
    struct file *file;  // 指向文件结构体的指针
    int error, fd;  // error用于存储错误码,fd用于存储文件描述符

    // 创建一个新的伪inode
    inode = new_inode_pseudo(sock_mnt->mnt_sb, name);
    // 如果创建失败,返回内存不足错误码
    if (!inode)
        return -ENOMEM;

    // 设置inode的文件操作结构体
    inode->i_fop = fops;
    // 设置inode的私有数据
    inode->i_private = private_data;

    // 分配一个文件结构体
    file = alloc_file(sock_mnt, inode, FMODE_READ | FMODE_WRITE,
                      &eventpoll_fops);
    // 如果分配失败,释放之前创建的inode
    if (!file) {
        iput(inode);
        return -ENOMEM;
    }

    // 获取一个未使用的文件描述符
    fd = get_unused_fd_flags(flags);
    // 如果获取失败,释放之前分配的文件结构体
    if (fd < 0) {
        fput(file);
        return fd;
    }

    // 将文件描述符和文件结构体关联起来
    fd_install(fd, file);

    // 返回文件描述符
    return fd;
}

d). 最后是ep_free函数,它的作用是释放一个eventpoll结构体。

// ep_free函数,用于释放一个eventpoll结构体
// 参数ep:指向eventpoll结构体的指针
static void ep_free(struct eventpoll *ep)
{
    // 如果ep指针不为空
    if (ep) {
        // 如果用户指针不为空,减少用户的epoll设备计数
        if (ep->user)
            atomic_dec(&ep->user->epoll_devs);
        // 释放eventpoll结构体
        kfree(ep);
    }
}

2. epoll_ctl源码分析

epoll_ctl函数用于控制epoll文件描述符的行为,它可以添加、修改或删除一个文件描述符的事件。

// sys_epoll_ctl函数,用于控制epoll实例,添加、修改或删除文件描述符的事件
// 参数epfd:epoll实例的文件描述符
// 参数op:操作类型,如EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL
// 参数fd:要操作的文件描述符
// 参数event:指向epoll_event结构体的用户空间指针,用于指定事件
// 返回值:成功返回0,失败返回负数错误码
asmlinkage long sys_epoll_ctl(int epfd, int op, int fd,
                              struct epoll_event __user *event)
{
    struct eventpoll *ep;  // 指向eventpoll结构体的指针
    struct epitem *epi;  // 指向epitem结构体的指针
    struct file *file, *tfile;  // 指向文件结构体的指针
    int error, event_flags;  // error用于存储错误码,event_flags用于存储事件标志

    /*
     * Check the EPOLL_* constant for consistency.  */
    // 检查EPOLL_*常量的一致性
    BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);

    /*
     * Sanity check the operation code.
     * 检查操作码的合法性
     */
    // 如果操作码不在合法范围内,返回无效参数错误码
    if (op < EPOLL_CTL_ADD || op > EPOLL_CTL_DEL)
        return -EINVAL;

    /*
     * Check if the epfd is a valid file descriptor.
     * 检查epfd是否是有效的文件描述符
     */
    // 获取epfd对应的文件结构体
    file = fget(epfd);
    // 如果获取失败,返回错误的文件描述符错误码
    if (!file)
        return -EBADF;

    /*
     * Check if the epfd is an eventpoll file descriptor.
     * 检查epfd是否是一个eventpoll文件描述符
     */
    // 检查文件结构体的操作函数是否是eventpoll的操作函数
    if (!file->f_op || file->f_op != &eventpoll_fops) {
        // 释放文件结构体
        fput(file);
        return -EBADF;
    }

    /*
     * Get the eventpoll structure.
     * 获取eventpoll结构体
     */
    // 获取文件结构体的私有数据,即eventpoll结构体
    ep = file->private_data;

    /*
     * Check if the fd is a valid file descriptor.
     * 检查fd是否是有效的文件描述符
     */
    // 获取fd对应的文件结构体
    tfile = fget(fd);
    // 如果获取失败,释放之前获取的文件结构体
    if (!tfile) {
        fput(file);
        return -EBADF;
    }

    /*
     * Check if the fd is already registered with the eventpoll.
     * 检查fd是否已经注册到eventpoll中
     */
    // 加锁
    spin_lock_irq(&ep->lock);
    // 查找fd对应的epitem结构体
    epi = ep_find(ep, tfile, fd);
    // 解锁
    spin_unlock_irq(&ep->lock);

    // 根据操作码执行不同的操作
    switch (op) {
    case EPOLL_CTL_ADD:
        // 如果fd已经注册,释放文件结构体并返回文件已存在错误码
        if (epi) {
            fput(tfile);
            fput(file);
            return -EEXIST;
        }

        /*
         * Allocate a new epitem structure.
         * 分配一个新的epitem结构体
         */
        // 分配一个epitem结构体
        epi = kmalloc(sizeof(struct epitem), GFP_KERNEL);
        // 如果分配失败,释放文件结构体并返回内存不足错误码
        if (!epi) {
            fput(tfile);
            fput(file);
            return -ENOMEM;
        }

        /*
         * Initialize the epitem structure.
         * 初始化epitem结构体
         */
        // 设置epitem结构体的eventpoll指针
        epi->ep = ep;
        // 设置epitem结构体的文件描述符
        epi->ffd = fd;
        // 设置epitem结构体的文件结构体指针
        epi->file = tfile;
        // 复制事件信息到epitem结构体
        epi->event = *event;
        // 初始化等待计数
        epi->nwait = 0;
        // 初始化下一个指针
        epi->next = NULL;

        /*
         * Add the epitem to the eventpoll.
         * 将epitem添加到eventpoll中
         */
        // 加锁
        spin_lock_irq(&ep->lock);
        // 插入epitem到eventpoll中
        ep_insert(ep, epi);
        // 解锁
        spin_unlock_irq(&ep->lock);

        break;
    case EPOLL_CTL_MOD:
        // 如果fd未注册,释放文件结构体并返回文件不存在错误码
        if (!epi) {
            fput(tfile);
            fput(file);
            return -ENOENT;
        }

        /*
         * Update the event mask of the epitem.
         * 更新epitem的事件掩码
         */
        // 更新epitem的事件信息
        epi->event = *event;

        /*
         * Update the eventpoll.
         * 更新eventpoll
         */
        // 加锁
        spin_lock_irq(&ep->lock);
        // 更新eventpoll
        ep_update(ep, epi);
        // 解锁
        spin_unlock_irq(&ep->lock);

        break;
    case EPOLL_CTL_DEL:
        // 如果fd未注册,释放文件结构体并返回文件不存在错误码
        if (!epi) {
            fput(tfile);
            fput(file);
            return -ENOENT;
        }

        /*
         * Remove the epitem from the eventpoll.
         * 从eventpoll中移除epitem
         */
        // 加锁
        spin_lock_irq(&ep->lock);
        // 移除epitem
        ep_remove(ep, epi);
        // 解锁
        spin_unlock_irq(&ep->lock);

        /*
         * Free the epitem structure.
         * 释放epitem结构体
         */
        // 释放epitem结构体
        kfree(epi);

        break;
    }

    // 释放文件结构体
    fput(tfile);
    fput(file);

    // 返回成功
    return 0;
}

a). 首先检查操作码是否合法,以及epfd和fd是否是有效的文件描述符。

b). 然后获取eventpoll结构和epitem结构(如果存在)。

c). 根据操作码执行相应的操作:
- EPOLL_CTL_ADD:添加一个新的文件描述符到eventpoll中。
- EPOLL_CTL_MOD:修改一个已有的文件描述符的事件掩码。
- EPOLL_CTL_DEL:从eventpoll中删除一个文件描述符。

d). 最后释放文件描述符。

3. epoll_wait源码分析

epoll_wait函数用于等待事件的发生,它会阻塞当前进程,直到有事件发生或者超时。

// sys_epoll_wait函数,用于等待epoll实例上的事件发生
// 参数epfd:epoll实例的文件描述符
// 参数events:指向用户空间的epoll_event结构体数组,用于存储发生的事件
// 参数maxevents:events数组的最大元素个数
// 参数timeout:超时时间,单位为毫秒
// 返回值:成功返回发生事件的数量,超时返回0,失败返回负数错误码
asmlinkage long sys_epoll_wait(int epfd, struct epoll_event __user *events,
                               int maxevents, int timeout)
{
    struct eventpoll *ep;  // 指向eventpoll结构体的指针
    struct file *file;  // 指向文件结构体的指针
    int error, res, eavail;  // error用于存储错误码,res用于存储结果,eavail用于存储可用事件数量

    /*
     * Check if the epfd is a valid file descriptor.
     * 检查epfd是否是有效的文件描述符
     */
    // 获取epfd对应的文件结构体
    file = fget(epfd);
    // 如果获取失败,返回错误的文件描述符错误码
    if (!file)
        return -EBADF;

    /*
     * Check if the epfd is an eventpoll file descriptor.
     * 检查epfd是否是一个eventpoll文件描述符
     */
    // 检查文件结构体的操作函数是否是eventpoll的操作函数
    if (!file->f_op || file->f_op != &eventpoll_fops) {
        // 释放文件结构体
        fput(file);
        return -EBADF;
    }

    /*
     * Get the eventpoll structure.
     * 获取eventpoll结构体
     */
    // 获取文件结构体的私有数据,即eventpoll结构体
    ep = file->private_data;

    /*
     * Check if the events buffer is valid.
     * 检查events缓冲区是否有效
     */
    // 如果maxevents小于等于0或者大于最大事件数,释放文件结构体并返回无效参数错误码
    if (maxevents <= 0 || maxevents > EPOLL_MAX_EVENTS) {
        fput(file);
        return -EINVAL;
    }

    /*
     * Wait for events.
     * 等待事件发生
     */
    // 调用ep_poll函数等待事件发生
    error = ep_poll(ep, events, maxevents, timeout);
    // 如果等待失败,释放文件结构体并返回错误码
    if (error < 0) {
        fput(file);
        return error;
    }

    /*
     * Return the number of events.
     * 返回事件的数量
     */
    // 将ep_poll的返回值赋值给res
    res = error;
    // 获取可用事件数量
    eavail = ep_events_available(ep);
    // 如果有可用事件
    if (eavail > 0) {
        // 如果res小于maxevents,将res更新为eavail
        if (res < maxevents)
            res = eavail;
    } else if (eavail < 0) {
        // 如果eavail小于0,释放文件结构体并返回eavail
        fput(file);
        return eavail;
    }

    // 释放文件结构体
    fput(file);

    // 返回事件数量
    return res;
}

a). 首先检查epfd是否是有效的文件描述符,以及events缓冲区是否合法。

b). 然后获取eventpoll结构。

c). 调用ep_poll函数等待事件的发生。

d). 根据ep_poll函数的返回值,返回事件的数量。

4. epoll的优点

a). 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

select的最大缺点就是进程打开的fd是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案(Apache早期就是这样实现的),但是linux上面创建进程的代价是很大的,加上进程间数据同步远比不上线程间同步的高效,所以它是一种低效的方案。

b). IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。

c). 支持水平触发和边缘触发(只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发)两种方式,理论上边缘触发的性能要更高一些,但是代码实现比较复杂。

d). mmap加速内核与用户空间的信息传递。epoll是通过内核与用户空间mmap同一块内存,避免了无谓的内存拷贝。

5. epoll的应用场景

a). 高并发服务器:epoll可以处理大量的并发连接,提高服务器的性能和稳定性。

b). 网络编程:epoll可以用于网络编程,实现高效的网络通信。

c). 实时系统:epoll可以用于实时系统,实现对事件的快速响应。

d). 其他应用场景:epoll还可以用于其他应用场景,如文件系统、数据库等。

6. 总结

本系列的3篇文章,详细介绍了select、poll和epoll的原理、实现和应用场景。

select和poll是传统的I/O多路复用技术,它们的缺点是监视的描述符数量有限,并且IO的效率会随着监视fd的数量的增长而下降。

epoll是一种新的I/O多路复用技术,它的优点是监视的描述符数量不受限制,并且IO的效率不会随着监视fd的数量的增长而下降。epoll还支持电平触发和边沿触发两种方式,并且可以通过mmap加速内核与用户空间的信息传递。因此,epoll是一种更高效、更灵活的I/O多路复用技术,适用于高并发服务器、网络编程、实时系统等应用场景。


感谢您的阅读。原创不易,如您觉得有价值,请点赞,关注。

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

水草

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值