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_create
或 epoll_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_create1
是 epoll_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
操作:- 将文件描述符
fd
从epoll
实例的监控列表中删除。 - 删除不存在的文件描述符会报错
- 将文件描述符
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_create
或epoll_create1
创建一个epoll
实例,通过do_create_epoll
函数实现。 - 管理文件描述符: 使用
epoll_ctl
添加、修改或删除文件描述符到epoll
实例,并指定要监控的事件类型。通过do_epoll_ctl
函数实现。 - 等待事件发生: 使用
epoll_wait
等待事件发生。epoll_wait
会阻塞,直到一个或多个文件描述符变得就绪。通过do_epoll_wait
函数实现。 - 处理回调函数: 当支持
epoll
的文件有事件到来时,直接调用wakeup_epoll()
方法,将就绪的epitem
加入到ready_list
中,并唤醒相应的进程。