梳理epoll的相关流程

epoll 是 Linux 内核提供的高效 I/O 事件通知机制,适用于大量文件描述符的事件监控。epoll 的主要工作流程包括创建 epoll 实例、添加或删除文件描述符、等待事件并处理这些事件。

结合Dragonos代码,以下为epoll的主要结构

pub struct EventPoll {
    /// epoll_wait用到的等待队列
    epoll_wq: WaitQueue,
    /// 维护所有添加进来的socket的红黑树
    ep_items: RBTree<i32, Arc<EPollItem>>,
    /// 接收就绪的描述符列表
    ready_list: LinkedList<Arc<EPollItem>>,
    /// 是否已经关闭
    shutdown: AtomicBool,
    self_ref: Option<Weak<SpinLock<EventPoll>>>,
}

以及EPoll_Item(epoll管理的主要对象)

pub struct EPollItem {
    /// 对应的Epoll
    epoll: Weak<SpinLock<EventPoll>>,
    /// 用户注册的事件,即为记录的是用户对该文件描述符感兴趣的事件类型。
    event: RwLock<EPollEvent>,
    /// 监听的描述符
    /// 以创建tcp连接为例,管理的是tcp socket对应的文件描述符
    fd: i32,
    /// 对应的文件
    file: Weak<File>,
}

主要功能函数分为epoll_create(), epoll_ctl() ,epoll_wait()以及epoll的回调函数wakeup_epoll(),以下是epoll管理的相关流程

1.首先,使用 epoll_createepoll_create1 创建一个 epoll 实例。

通过调用执行函数do_create_epoll(flags: FileMode) → Result<usize, SystemError>

     - flags: 创建的epoll文件的FileMode
 
     - 成功则返回Ok(fd),否则返回Err,fd为创建epoll的文件描述符
   在do_create_epoll()中还创建了epoll的inode对象,以文件的形式管理epoll

其中 epoll_create1epoll_create 的扩展版本,可以接受一个标志参数(如 EPOLL_CLOEXEC),用于设置文件描述符的执行关闭标志。

2. 使用 epoll_ctl 添加文件描述符到 epoll 实例,并指定要监控的事件类型(如读、写、异常等)。

通过调用执行函数do_epoll_ctl(
epfd: i32,
op: EPollCtlOption,
fd: i32,
epds: &mut EPollEvent,
nonblock: bool,
) → Result<usize, SystemError>

    /// ### 参数
    /// - epfd: 操作的epoll文件描述符
    /// - op: 对应的操作,包括有EPollCtlOption::Add,Del,Mod
    /// - fd: 操作对应的文件描述符
    /// - epds: 从用户态传入的event,若op为EpollCtlAdd,则对应注册的监听事件,若op为EPollCtlMod,则对应更新的事件,删除操作不涉及此字段
    /// - nonblock: 定义这次操作是否为非阻塞(有可能其他地方占有EPoll的锁)

以创建的socket为例,通过SYS_SOCKET系统调用创建相应的socket,绑定相应的端口并开始监听socket后,通过EPOLL_CTL系统调用的Add将socket文件和其fd以及用户态传来的epds封装成一个epoll_item对象,并将该epitem插入对应的epoll的ep_items红黑树当中

epoll_ctl的其他op操作执行

  • 对于 EPOLL_CTL_ADD 操作:
    • 将新的文件描述符 fd 添加到 epoll 实例的监控列表中,并将 event 指定的事件类型关联到该文件描述符。
    • 重复添加同一文件描述符会报错
  • 对于 EPOLL_CTL_MOD 操作:
    • 修改已在 epoll 实例中的文件描述符 fd 的事件类型。
    • 只有在文件描述符已经被添加的情况下才能修改
  • 对于 EPOLL_CTL_DEL 操作:
    • 将文件描述符 fdepoll 实例的监控列表中删除。
    • 删除不存在的文件描述符会报错

3. 使用 epoll_wait 等待事件发生。epoll_wait 会阻塞,直到一个或多个文件描述符变得就绪(即有指定的事件发生)。

通过调用执行函数do_epoll_wait(
epfd: i32,
epoll_event: &mut [EPollEvent],
max_events: i32,
timespec: Option,
) → Result<usize, SystemError>

    /// ### 参数
    /// - epfd: 操作的epoll文件描述符
    /// - epoll_event: epoll_event 结构体数组,用于返回内核检测到的事件给用户态
    /// - max_events: 这是要监听的事件的最大数量,即 epoll_event 数组的大小
    /// - timespec: 这是等待事件发生的超时时间
  • epoll_wait 的超时参数 timespec 可以是负数、零或正数,分别代表无限等待、立即返回和指定时间的等待。
  • epoll_wait 返回值是就绪的事件数,如果为 0 表示超时。

epoll_wait()根据epfd获取并确定是epoll文件后,进入一个循环判断是否有就绪事件发生(通过检测epoll对象中的ready_list是否为空来判断),如果有事件就将事件拷贝到用户态中的epoll_event中,通过调用函数 ep_send_events(
epoll: LockedEventPoll, //对应的epoll
user_event: &mut [EPollEvent], //用户空间传入的epoll_event地址
max_events: i32, //处理的最大事件数量
)来返回就绪事件

            // 判断epoll上有没有就绪事件
            let mut available = epoll_guard.ep_events_available();
            drop(epoll_guard);
            loop {
                if available {
                    // 如果有就绪的事件,则直接返回就绪事件

                    return Self::ep_send_events(epoll.clone(), epoll_event, max_events);
                }
            ......
             }

ep_send_events的具体实现:通过对获取的epoll的就绪队列ready_list中的epitem执行绑定文件的poll方法,并获取到感兴趣的事件,然后将该事件拷贝到用户空间的user_event(epoll_event)中,成功则返回获取事件个数

epoll_wait循环过程中若没有事件到来则自旋一段时间判断是否有事件到来,若还未等待到事件发生,则睡眠将当前进程推入epoll的等待队列epoll_wq中(通过调用epoll_wq.sleep_without_schedule()实现),并注册一个计时器在睡眠中记录是否超时,超时则返回OK(0),退出epoll_wait(),等待下一次系统调用epoll_wait

 // 自旋等待一段时间
                available = {
                    let mut ret = false;
                    for _ in 0..50 {
                        if let Ok(guard) = epoll.0.try_lock_irqsave() {
                            if guard.ep_events_available() {
                                ret = true;
                                break;
                            }
                        }
                    }
                    // 最后再次不使用try_lock尝试
                    if !ret {
                        ret = epoll.0.lock_irqsave().ep_events_available();
                    }
                    ret
                };

4.epoll的回调函数,支持epoll的文件有事件到来时直接调用该方法即可

wakeup_epoll(
        // 支持epoll管理的对象对应的eptiems的队列
        epitems: &SpinLock<LinkedList<Arc<EPollItem>>>,
        // 相应对象通过poll获取的事件类型
        pollflags: EPollEventType,
    ) -> Result<(), SystemError>

回调函数是在文件描述符变得就绪时被调用的,通过获取epitems队列中项epitem的事件来以及pollflags来检查事件合理性以及是否有感兴趣的事件,若有事件则将该epitem加入到epoll的就绪队列ready_list中并唤醒相应的进程

            // 检查事件合理性以及是否有感兴趣的事件
            if !(ep_events
                .difference(EPollEventType::EP_PRIVATE_BITS)
                .is_empty()
                || pollflags.difference(ep_events).is_empty())
            {
                                
                // 首先将就绪的epitem加入等待队列
                epoll_guard.ep_add_ready(epitem.clone());

                if epoll_guard.ep_has_waiter() {
                    if ep_events.contains(EPollEventType::EPOLLEXCLUSIVE)
                        && !pollflags.contains(EPollEventType::POLLFREE)
                    {
                        // 避免惊群
                        epoll_guard.ep_wake_one();
                    } else {
                        epoll_guard.ep_wake_all();
                    }
                }
            }

将epitem加入到就绪队列中后,在epoll_wait()中检查到有就绪事件发生(通过检测epoll对象中的ready_list是否为空来判断),之后返回内核中检测到的事件给用户态

总结

  • 创建 epoll 实例: 使用 epoll_createepoll_create1 创建一个 epoll 实例,通过 do_create_epoll 函数实现。
  • 管理文件描述符: 使用 epoll_ctl 添加、修改或删除文件描述符到 epoll 实例,并指定要监控的事件类型。通过 do_epoll_ctl 函数实现。
  • 等待事件发生: 使用 epoll_wait 等待事件发生。epoll_wait 会阻塞,直到一个或多个文件描述符变得就绪。通过 do_epoll_wait 函数实现。
  • 处理回调函数: 当支持 epoll 的文件有事件到来时,直接调用 wakeup_epoll() 方法,将就绪的 epitem 加入到 ready_list 中,并唤醒相应的进程。
1 个赞