epoll源码分析---sys_epoll_create()函数

本文深入探讨了Linux epoll机制的内部实现,从sys_epoll_create()的调用流程,到sys_epoll_create1()函数的具体工作,再到find_next_zero_bit()函数的详细解析,为读者提供了epoll工作原理的全面理解。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

eventpoll的优点就不用说了,网上的资料很多,eventpoll的使用也很广泛,特别是在Web服务器中。因为最近要用到epoll,所以好好地看了一下它的实现,把学到的一些东西做下整理,做个记录。

一、sys_epoll_create()

其源码如下:

SYSCALL_DEFINE1(epoll_create, int, size)
{
    if (size <= 0)
        return -EINVAL;
 
    return sys_epoll_create1(0);
}

SYSCALL_DEFINE1(epoll_create, int, size)在预处理之后就是long sys_epoll_create(int size)。从这里可以看到在用户层调用epoll_create时,传入的size参数没有使用。sys_epoll_create()在检查完参数后直接调用sys_epoll_create1()函数来完成主要的工作。因此接下来看看sys_epoll_create1()是怎么实现的。

二、sys_epoll_create1()函数

SYSCALL_DEFINE1(epoll_create1, int, flags)
{
    int error;
    struct eventpoll *ep = NULL;
 
    /*
     * 如果(EPOLL_CLOEXEC != O_CLOEXEC)成立,在编译时就会报错。这种方式
     * 
     */
    BUILD_BUG_ON(EPOLL_CLOEXEC != O_CLOEXEC);
 
    /* flags要么为0,要么为EPOLL_CLOEXEC,否则返回EINVAL错误 */
    if (flags & ~EPOLL_CLOEXEC)
        return -EINVAL;
    /*
     * 分配eventpoll实例并初始化,存储在file结构的private_data成员中。
     * private_data成员用来存储文件描述符真正对应的对象。例如
     * 如果文件描述符是一个套接字的话,其对应的file实例的private_data
     * 成员存储的就是一个socket实例。
     */
    error = ep_alloc(&ep);
    if (error < 0)
        return error;
 
    /*
     * 创建eventpoll文件,这个文件的file_operations为eventpoll_fops,
     * 私有的数据为eventpoll实例
     */
    error = anon_inode_getfd("[eventpoll]", &eventpoll_fops, ep,
                 flags & O_CLOEXEC);
    if (error < 0)
        ep_free(ep);
 
    return error;
}

首先看一看BUILD_BUG_ON宏,该宏用来在编译时检查condition是否为true,如果是true,会报编译错误,

#define BUILD_BUG_ON(condition) ((void)BUILD_BUG_ON_ZERO(condition))

宏BUILD_BUG_ON_ZERO的定义如下:

#define BUILD_BUG_ON_ZERO(e) (sizeof(struct { int:-!!(e); }))

乍看之下,这个宏的定义好像有些乱,是不是不合语法啊?但是细看之下,你就会发现这个宏的巧妙之处。
先看!!(e),这个很容易看懂,就是将e转换为bool值,假设e为10,第一次取反时变为0,第二次取反时又变为1;如果e为0的话,两次取反仍是0. 接着看-!!(e)(注意前面的“-”,就是取负),如果e不为0时,!!(e)返回的是1,-!!(e)返回的就是-1,此时宏 BUILD_BUG_ON_ZERO预处理后为(sizeof(struct { int:-1};)).我们知道定义结构体时,可以指定成员按位来存放,可以取的值为0到对应类型的bit位个数,小于0编译时会报错(在sizeof中可以为0,但是真正定义类型时不能为0)。内核使用的是下限值,还以使用上限值,即指定的位数超过对应成员的类型,例如下面的例子:

#define BUILD_BUG_ON_ZERO1(e) (sizeof(struct {char:(!!e << 9);}))

接下来看ep_alloc()函数,源码和注释如下如下:

/*
 * 分配eventpoll实例并初始化
 */
static int ep_alloc(struct eventpoll **pep)
{
    int error;
    struct user_struct *user;
    struct eventpoll *ep;
 
    user = get_current_user();
    error = -ENOMEM;
    ep = kzalloc(sizeof(*ep), GFP_KERNEL);
    if (unlikely(!ep))
        goto free_uid;
 
    /*
     * 
     */
    spin_lock_init(&ep->lock);
    /*
     * 初始化用于向用户空间传递事件和移除epoll中的文件之间
     * 的互斥锁
     */
    mutex_init(&ep->mtx);
    /*
     * 初始化epoll文件的等待队列。调用epoll_wait的进程
     * 可能在此队列上睡眠, 等待ep_poll_callback()函数唤醒或超时
     */
    init_waitqueue_head(&ep->wq);
    /*
     * poll_wait是eventpoll文件本身的唤醒队列,该队列上睡眠
     * 的进程是等待eventpoll文件本身的某些事件发生。
     */
    init_waitqueue_head(&ep->poll_wait);
    /*
     * 初始化就绪队列,如果当某个文件指定的事件发生时,
     * 会防止到该队列中。
     */
    INIT_LIST_HEAD(&ep->rdllist);
    /*
     * 用于存储文件描述符的红黑树根节点
     */
    ep->rbr = RB_ROOT;
    /*
     * 如果正在向用户空间传递事件,此时状态就绪的文件
     * 描述符相关的结构会暂时放在该队列上,否则会直接
     * 添加到就绪队列rdllist中。
     */
    ep->ovflist = EP_UNACTIVE_PTR;
    ep->user = user;
 
    *pep = ep;
 
    return 0;
 
free_uid:
    free_uid(user);
    return error;
}

上面的注释比较详细了,不再多叙。
最后一个关心的函数是anon_inode_getfd(),该函数的作用类似于sock_map_fd(),就是将eventpoll实例映射到一个文件中

int anon_inode_getfd(const char *name, const struct file_operations *fops,
         void *priv, int flags)
{
    int error, fd;
    struct file *file;
 
    /*
     * 分配一个空闲的文件描述符。
     */
    error = get_unused_fd_flags(flags);
    if (error < 0)
        return error;
    fd = error;
 
    file = anon_inode_getfile(name, fops, priv, 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;
}


该函数首先调用get_unused_fd_flags()分配一个空闲的文件描述符,然后创建一个匿名文件,附加上去。因为涉及到文件系统的操作,不做过多的分析。

三、find_next_zero_bit()函数

anon_inode_getfile()是一个宏定义,对应的函数时alloc_fd(),alloc_fd()中调用find_next_zero_bit()在文件描述符的位图中查找一个空闲的bit位,空闲的bit位的索引即为找到的文件描述符,我对这个函数比较感兴趣,特别研究一个一番,跟大家分享一下。

/*
 * find_next_zero_bit返回的值的范围是0~(size-1),相当于是bit数组中的索引
 * @addr: 位图的地址
 * @size: 位图的bit位个数
 * @offset: 可以理解为bit数组中的索引,也就是说从这个bit位开始查找
 */
unsigned long find_next_zero_bit(const unsigned long *addr, unsigned long size,
                 unsigned long offset)
{
    /*
     * 这里BITOP_WORD用来计算offset对应位图中的unsigned long元素在
     * addr数组中的索引,所以p为offset所在的unsigned long元素的地址
     */
    const unsigned long *p = addr + BITOP_WORD(offset);
    /*
     * 相当于是offset - (offset % BITS_PER_LONG),也就是offset所在的unsigned long
     * 之前所有unsigned long元素的bit位个数
     */
    unsigned long result = offset & ~(BITS_PER_LONG-1);
    unsigned long tmp;
 
    /*
     * 如果偏移量大于等于位图的大小,则直接
     * 返回size。
     */
    if (offset >= size)
        return size;
    /*
     * 计算offset所在的unsigned long及其之后所有的unsigned long元素中
     * 的bit位个数
     */
    size -= result;
    /*
     * 计算offset对应的unsigned long中占用的bit位所在的位置,这个也可以理解
     * 为一个索引。假设计算前offset的值为67,计算后offset的值为3,也就是所在
     * unsigned long中的第4个bit位。
     */
    offset %= BITS_PER_LONG;
    if (offset) {
        /*
         * tmp的值为offset所在的unsigned long的值
         */
        tmp = *(p++);
        /*
         * BITS_PER_LONG - offset计算的offset所在的unsigned long元素中offset所在的
         * bit位及其之后的bit位个数。tmp中offset所对应的bit位及其之后的bit位都保留,而将
         * tmp中offset所对应的bit位之前的bit位都设置为1.
         */
        tmp |= ~0UL >> (BITS_PER_LONG - offset);
        /*
         * 如果size小于BITS_PER_LONG,说明offset在最后一个unsigned long元素。
         */
        if (size < BITS_PER_LONG)
            goto found_first;
        /*
         * 如果tmp取反后为不为0,则说明tmp中有为0的bit位,因此从
         * tmp中查找空闲的bit位。
         */
        if (~tmp)
            goto found_middle;
        /*
         * 如果offset所对应的bit位所在的unsigned long中没有空闲的bit位,
         * 开始从其之后得unsigned long元素中查找。计算剩余的bit位个数,
         * 修改已经查找的bit位个数
         */
        size -= BITS_PER_LONG;
        result += BITS_PER_LONG;
    }
    /*
     * 如果size小于BITS_PER_LONG,则退出循环
     */
    while (size & ~(BITS_PER_LONG-1)) {
        /*
         * 将下一个查找的元素存储在tmp中,如果tmp取反后不为0,则说明tmp中有为0的bit位,因此
         * 从tmp中查找空闲的bit位。
         */
        if (~(tmp = *(p++)))
            goto found_middle;
        /*
         * 计算剩余的bit位个数,修改已经查找的bit位个数
         */
        result += BITS_PER_LONG;
        size -= BITS_PER_LONG;
    }
    /*
     * 如果全部查找后,仍没有找到空闲的bit位,则直接返回result。
     * 此时result的值应该为位图的bit位的个数。
     */
    if (!size)
        return result;
    /*
     * 如果size不为0,则在剩余的最后的bit位(剩余的个数小于BITS_PER_LONG)中查找。
     */
    tmp = *p;
 
found_first:
    /*
     * 因为剩余的bit位个数有可能小于BITS_PER_LONG,因此需要将unsigned long中
     * 不用的bit位置为1,以免干扰后续的查找
     */
    tmp |= ~0UL << size;
    /*
     * 如果所有bit位都为1,则说明没有空余的bit位,
     * 则返回所有的bit位的个数。
     */
    if (tmp == ~0UL)    /* Are any bits zero? */
        return result + size;    /* Nope. */
found_middle:
    /*
     * ffz(tmp)返回的是tmp中第一个为0的bit位的索引
     */
    return result + ffz(tmp);
}

转自:https://blog.youkuaiyun.com/justlinux2010/article/details/8506940

<think>我们正在讨论epoll_create函数的功能和作用。根据引用[1]的内容,epoll_create用于创建epoll实例,它返回一个文件描述符,该描述符用于后续的epoll操作(如epoll_ctl和epoll_wait)。在早期版本中,epoll_create需要一个size参数,该参数用于提示内核关于期望监控的文件描述符数量,但现在已经不再需要(内核会动态调整),但为了向后兼容,size必须大于0。 具体来说: - epoll_create(int size) 创建一个epoll实例,并返回一个文件描述符。 - 这个epoll实例在内核中是一个数据结构,用于存储被监控的文件描述符和它们的事件。 - 在Linux中,epoll实例使用红黑树来高效地管理大量的文件描述符。 另外,引用[1]还提到,epoll_create1是后来引入的,它允许使用标志(如EPOLL_CLOEXEC)来创建epoll实例,从而提供更多的控制。 因此,我们可以这样总结epoll_create的作用: epoll_create函数用于创建一个epoll实例,该实例用于后续通过epoll_ctl添加、修改或删除要监控的文件描述符,并通过epoll_wait等待这些文件描述符上的事件。该函数返回一个文件描述符,代表这个epoll实例。在创建时,虽然size参数已经不再起实际作用(内核动态调整数据结构大小),但为了兼容性,必须传递一个大于0的值。 下面,我们按照要求组织回答:</think>### `epoll_create` 函数详解 `epoll_create` 是 Linux 系统中创建 epoll 实例的核心系统调用,其核心作用是为 I/O 多路复用机制建立事件监控的基础设施。以下是详细说明: --- #### **功能与作用** 1. **创建内核事件表** - 在**内核空间**初始化一个事件监控表(红黑树结构),用于高效存储和管理待监控的文件描述符[^1]。 - 返回一个**epoll 文件描述符**(`epfd`),作为后续操作(`epoll_ctl`/`epoll_wait`)的入口标识。 2. **资源预分配优化**(历史行为) - **早期实现**:参数 `size` 用于提示内核**预期监控的文件描述符数量**,内核据此预分配存储空间(提升性能)[^1]。 - **现代实现**:内核已支持**动态空间分配**,`size` 参数仅保留向后兼容性(但仍需设为 `>0` 的值)。 --- #### **函数原型** ```c #include <sys/epoll.h> int epoll_create(int size); ``` | **参数** | **说明** | |----------|--------------------------------------------------------------------------| | `size` | 预期监控的文件描述符数量(现代内核忽略此值,但必须 `>0`)[^1] | | **返回值** | **说明** | |------------------|----------------------------------------| | `≥0`(成功) | epoll 实例的文件描述符(`epfd`) | | `-1`(失败) | 错误码存入 `errno`(如 `EMFILE` 描述符耗尽) | --- #### **使用示例** ```c // 创建 epoll 实例(size 设为任意大于0的值) int epoll_fd = epoll_create(128); // 128 仅作兼容性占位 if (epoll_fd == -1) { perror("epoll_create failed"); exit(EXIT_FAILURE); } printf("Created epoll instance: fd=%d\n", epoll_fd); ``` --- #### **演进与替代** | **函数** | **引入版本** | **优势** | |--------------------|--------------------|-----------------------------------| | `epoll_create()` | Linux 2.6(2003) | 基础功能 | | `epoll_create1()` | Linux 2.6.27(2008)| 支持标志位(如 `EPOLL_CLOEXEC`)[^1] | ```c // 现代推荐用法:使用 epoll_create1 int epoll_fd = epoll_create1(EPOLL_CLOEXEC); // 子进程不继承描述符 ``` --- #### **关键注意事项** 1. **`size` 参数的演变** - 旧内核:`size` 决定预分配空间大小(若实际监控数 `>size`,需额外扩容) - 新内核:`size` 被忽略(内核动态调整红黑树节点),但**必须传递 `>0` 的值**以兼容旧系统[^1]。 2. **资源管理** - epoll 实例占用一个文件描述符,需通过 `close(epoll_fd)` 显式释放 - 最大实例数受 `/proc/sys/fs/epoll/max_user_instances` 限制 3. **性能本质** - 底层使用**红黑树**管理描述符,实现 $O(\log n)$ 复杂度的增删改操作 - 就绪事件通过**双向链表**传递,`epoll_wait` 可 $O(1)$ 获取事件[^2] --- #### **典型工作流程** 1. 调用 `epoll_create` 创建实例 → 获取 `epfd` 2. 通过 `epoll_ctl` 向 `epfd` 注册需监控的 socket 描述符 3. 调用 `epoll_wait` 阻塞等待事件就绪 4. 处理就绪事件后回到步骤 3 --- ### 总结 `epoll_create` 的核心作用是**创建 epoll 实例的内核数据结构**,为高效 I/O 多路复用提供基础: - 返回的 `epfd` 是 epoll 机制的操作入口 - 历史 `size` 参数仅用于兼容性(必须 `>0`) - 现代开发推荐使用 `epoll_create1(EPOLL_CLOEXEC)` 替代[^1] 该函数与 `epoll_ctl`/`epoll_wait` 协同工作,构成 Linux 高性能网络服务的基石(如 Nginx、Redis)。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值