You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
// epoll_create, epoll_create1 - open an epoll file descriptor
int epoll_create(int size);
int epoll_create1(int flags);
对 epoll 关注的事件进行增删改
// epoll_ctl - control interface for an epoll file descriptor
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// epfd: 刚才创建的epoll实例返回的fd
// op: 操作,有 EPOLL_CTL_ADD EPOLL_CTL_MOD EPOLL_CTL_DEL可选
// fd: epoll监听的fd
// event:关注的事件,如果是EPOLL_CTL_DEL则忽略
// The event argument describes the object linked to the file descriptor fd.
struct epoll_event {
uint32_t events; /* Epoll events /
epoll_data_t data; / User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events比较多可关注的事件,最常见的有三个
EPOLLIN
The associated file is available for read(2) operations.
EPOLLOUT
The associated file is available for write(2) operations.
EPOLLERR
Error condition happened on the associated file descriptor. This event is also reported for the write
end of a pipe when the read end has been closed. epoll_wait(2) will always report for this event; it
is not necessary to set it in events.
等待 IO 操作
// epoll_wait, epoll_pwait - wait for an I/O event on an epoll file descriptor
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
timeout<0则阻塞直到有事件到来
timeout==0则非阻塞立刻返回
timeout>0为阻塞超时阈值(ms为单位)
前置工具快速上手
这里会介绍两个最常用到的数据结构和一个文件系统的机制,后面我们会逐一深入
list_head
list_head 是内核链表,在 linux 中大量用到,作为一种侵入式容器,较为反直观,通常一个链表是这样实现
template <typename T>
class ListNode {
public:
T data;
ListNode *prev, *next;
};
// poll_table *wait由调用方提供,主要是提供一个_qproc的自定义回调
poll_wait(filp, &pipe->wait, wait); // 允许用户自定义的操作,十分关键 ⭐⭐
// 这个函数翻译过来就是wait->_qproc(filp, &pipe->wait, wait);
// 即允许调用方在f_op->poll()时对pipe->wait和filp“动手脚”
/* Reading only -- no need for acquiring the semaphore. */
nrbufs = pipe->nrbufs;
mask = 0;
if (filp->f_mode & FMODE_READ) {
mask = (nrbufs > 0) ? EPOLLIN | EPOLLRDNORM : 0;
if (!pipe->writers && filp->f_version != pipe->w_counter)
mask |= EPOLLHUP;
}
if (filp->f_mode & FMODE_WRITE) {
mask |= (nrbufs < pipe->buffers) ? EPOLLOUT | EPOLLWRNORM : 0;
/*
* Most Unices do not set EPOLLERR for FIFOs but on Linux they
* behave exactly like pipes for poll().
*/
if (!pipe->readers)
mask |= EPOLLERR;
}
return mask; // 返回可读,可写,错误,挂起等。。。
/*
* This structure is stored inside the "private_data" member of the file
* structure and represents the main data structure for the eventpoll
* interface.
*/
// 这里提到,eventpoll存储于file->private_data,并且是整个epoll接口的主要数据结构
// 在实现过程一般简称eventpoll为ep,它管理的若干事件称为epi(epoll-item)
// 这里为了方便讲解,把内部字段的顺序稍微调整了一下
struct eventpoll {
// 为了线程安全,ep一共用了三种锁 (epmutex mtx lock)
/*
* This mutex is used to ensure that files are not removed
* while epoll is using them. This is held during the event
* collection loop, the file cleanup path, the epoll file exit
* code and the ctl operations.
*/
// 在等待事件时必须用到的互斥锁
struct mutex mtx;
/* Lock which protects rdllist and ovflist */
rwlock_t lock;
/* RB tree root used to store monitored fd structs */
// 红黑树的根,ep维护一棵红黑树用于快速查找是否有对应的epi(感兴趣的event)
// 也就是说,红黑树下的每一个结点都对应一个epi
struct rb_root_cached rbr;
// ep维护两个【等待队列】,稍后讲解wait_queue的原理
// 主要用到wq
/* Wait queue used by sys_epoll_wait() */
wait_queue_head_t wq;
/* Wait queue used by file->poll() */
wait_queue_head_t poll_wait;
// 维护两个就绪队列(ready-list + overflow list)。表明已经就绪的fd
// 一个是侵入式的双向链表(list_head),另一个是单向链表(epitem有指向下一个epitem的字段)
/* List of ready file descriptors */
struct list_head rdllist;
/*
* This is a single linked list that chains all the "struct epitem" that
* happened while transferring ready events to userspace w/out
* holding ->lock.
*/
struct epitem *ovflist;
// 分配获得的file文件
struct file *file;
// 后面的比较细节,跳过
};
总结:核心的数据结构 ep 大概维护以下几大类的成员
锁:要求线程安全
红黑树:要求维护一个存储若干 epi 的数据结构
等待队列:用于阻塞/唤醒机制
就绪队列:事件到来时用到的数据结构
文件:ep 归属于哪个文件中
被监听的红黑树结点 - epitem
epitem 在实现流程简称 epi,由前面的 API 调用介绍的第二步 epoll_ctl
// epoll_ctl - control interface for an epoll file descriptor
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
* Each file descriptor added to the eventpoll interface will
* have an entry of this type linked to the "rbr" RB tree.
* Avoid increasing the size of this struct, there can be many thousands
* of these on a server and we do not want this to take another cache line.
*/
// 上面大概就是说
// 1. 它是ep维护的红黑树的结点,
// 2. 并且因为会大量用到epitem,要尽量减少这个结构体的大小
struct epitem {
// 自身红黑树的结点
union {
/* RB tree node links this structure to the eventpoll RB tree */
struct rb_node rbn;
/* Used to free the struct epitem */
struct rcu_head rcu;
};
/* List header used to link this structure to the eventpoll ready list */
// ep的ready list的结点
struct list_head rdllink;
/*
* Works together "struct eventpoll"->ovflist in keeping the
* single linked chain of items.
*/
// 指向同一ep维护的下一个epi,形成单链表,表示下一个在ovflist的epitem结点
struct epitem *next;
/* The file descriptor information this item refers to */
// epi的file和fd
struct epoll_filefd ffd;
/* The "container" of this item */
// epi的容器,归属于哪一个ep
struct eventpoll *ep;
/* The structure that describe the interested events and the source fd */
// 要监听的事件,由调用方调用epoll_ctl得到
struct epoll_event event;
/*
* The following function implements the controller interface for
* the eventpoll file that enables the insertion/removal/change of
* file descriptors inside the interest set.
*/
// 处理用户态拷贝epds
SYSCALL_DEFINE4(epoll_ctl, int, epfd, int, op, int, fd,
struct epoll_event __user *, event)
{
struct epoll_event epds;
/*
* Differs from ep_eventpoll_poll() in that internal callers already have
* the ep->mtx so we need to start from depth=1, such that mutex_lock_nested()
* is correctly annotated.
*/
// 通过epi得到对应file,并调用其f_op->poll(它本身不是epoll file的话)
static __poll_t ep_item_poll(const struct epitem *epi, poll_table *pt,
int depth)
{
struct file *file = epi->ffd.file;
__poll_t res;
pt->_key = epi->event.events;
if (!is_file_epoll(file)) // f->f_op == &eventpoll_fops, 非嵌套进入该分支
res = vfs_poll(file, pt); // 调用file->f_op->poll(file, pt)
else // 这个分支我们并不关心
res = __ep_eventpoll_poll(file, pt, depth);
return res & epi->event.events; // revents
/*
* This is the callback that is passed to the wait queue wakeup
* mechanism. It is called by the stored file descriptors when they
* have events to report.
*/
// 1. 获取pollflags
// 2. 检查events
// 3. epi插入到overflow-list或者ready-list,如果是overflow-list则结束流程
// 4. 唤醒阻塞在ep->wq的进程
static int ep_poll_callback(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
{
int pwake = 0;
unsigned long flags;
struct epitem *epi = ep_item_from_wait(wait); // 差不多是container_of
struct eventpoll *ep = epi->ep;
__poll_t pollflags = key_to_poll(key); // 转换类型,回调时传入获得的flag
int ewake = 0; // ewake用于return,具体的含义我觉得是允许exclusive wake的意思,具体看__wake_up_common的逻辑
spin_lock_irqsave(&ep->wq.lock, flags);
ep_set_busy_poll_napi_id(epi);
/*
* If the event mask does not contain any poll(2) event, we consider the
* descriptor to be disabled. This condition is likely the effect of the
* EPOLLONESHOT bit that disables the descriptor when an event is received,
* until the next EPOLL_CTL_MOD will be issued.
*/
if (!(epi->event.events & ~EP_PRIVATE_BITS)) // 关注event连基本的EPOLLIN之类的都没有
goto out_unlock;
/*
* Check the events coming with the callback. At this stage, not
* every device reports the events in the "key" parameter of the
* callback. We need to be able to handle both cases here, hence the
* test for "key" != NULL before the event match test.
*/
if (pollflags && !(pollflags & epi->event.events)) // 没有用户感兴趣的event
goto out_unlock;
/*
* If we are transferring events to userspace, we can hold no locks
* (because we're accessing user memory, and because of linux f_op->poll()
* semantics). All the events that happen during that period of time are
* chained in ep->ovflist and requeued later on.
*/
if (ep->ovflist != EP_UNACTIVE_PTR) {
if (epi->next == EP_UNACTIVE_PTR) {
epi->next = ep->ovflist;
ep->ovflist = epi; // 插头
if (epi->ws) {
/*
* Activate ep->ws since epi->ws may get
* deactivated at any time.
*/
__pm_stay_awake(ep->ws);
}
}
goto out_unlock;
}
// 如果插入到ovflist,则不会进入ready-list
/* If this file is already in the ready list we exit soon */
if (!ep_is_linked(epi)) {
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake_rcu(epi);
}
/*
* Wake up ( if active ) both the eventpoll wait list and the ->poll()
* wait list.
*/
if (waitqueue_active(&ep->wq)) {
if ((epi->event.events & EPOLLEXCLUSIVE) &&
!(pollflags & POLLFREE)) {
switch (pollflags & EPOLLINOUT_BITS) {
case EPOLLIN:
if (epi->event.events & EPOLLIN)
ewake = 1;
break;
case EPOLLOUT:
if (epi->event.events & EPOLLOUT)
ewake = 1;
break;
case 0:
ewake = 1;
break;
}
}
wake_up_locked(&ep->wq);
}
if (waitqueue_active(&ep->poll_wait))
pwake++;
/*
* Implement the event wait interface for the eventpoll file. It is the kernel
* part of the user space epoll_wait(2).
*/
// 从epfd获取ep实例
SYSCALL_DEFINE4(epoll_wait, int, epfd, struct epoll_event __user *, events,
int, maxevents, int, timeout)
{
int error;
struct fd f;
struct eventpoll *ep;
/* The maximum number of event must be greater than zero */
if (maxevents <= 0 || maxevents > EP_MAX_EVENTS)
return -EINVAL;
/* Verify that the area passed by the user is writeable */
if (!access_ok(VERIFY_WRITE, events, maxevents * sizeof(struct epoll_event)))
return -EFAULT;
/* Get the "struct file *" for the eventpoll file */
f = fdget(epfd);
if (!f.file)
return -EBADF;
/*
* We have to check that the file structure underneath the fd
* the user passed to us _is_ an eventpoll file.
*/
error = -EINVAL;
if (!is_file_epoll(f.file))
goto error_fput;
/*
* At this point it is safe to assume that the "private_data" contains
* our own data structure.
*/
ep = f.file->private_data;
/* Time to fish for events ... */
error = ep_poll(ep, events, maxevents, timeout); // ⭐
error_fput:
fdput(f);
return error;
}
/**
ep_poll - Retrieves ready events, and delivers them to the caller supplied
event buffer.
@ep: Pointer to the eventpoll context.
@events: Pointer to the userspace buffer where the ready events should be
stored.
@maxevents: Size (in terms of number of events) of the caller event buffer.
@timeout: Maximum timeout for the ready events fetch operation, in
milliseconds. If the @timeout is zero, the function will not block,
while if the @timeout is less than zero, the function will block
until at least one event has been retrieved (or an error
occurred).
Returns: Returns the number of ready events which have been fetched, or an
error code, in case of error.
*/
// 如果timeout == 0
// 则会直接检查event的ready-list,如果ready-list存在entry则ep_send_events,然后直接return res
// 如果timeout > 0
// 1. 先进入fetch流程,同时该流程仅针对就绪队列为空才执行
// 1.1 当前调用epoll_wait的current加入到ep->wq,使用默认的callback
// 1.2 只要被唤醒不符合条件(没event||没超时||没中断),则会再次wait
// 2. 进入到check流程
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout)
{
int res = 0, eavail, timed_out = 0;
unsigned long flags;
long slack = 0;
wait_queue_t wait;
ktime_t expires, *to = NULL;
if (timeout > 0) {
struct timespec end_time = ep_set_mstimeout(timeout);
slack = select_estimate_accuracy(&end_time);
to = &expires;
*to = timespec_to_ktime(end_time);
} else if (timeout == 0) {
/*
* Avoid the unnecessary trip to the wait queue loop, if the
* caller specified a non blocking operation.
*/
// timed_out为true将不会进入for(;;)
timed_out = 1;
spin_lock_irqsave(&ep->lock, flags);
goto check_events;
}
if (!ep_events_available(ep))
ep_busy_loop(ep, timed_out);
spin_lock_irq(&ep->wq.lock);
if (!ep_events_available(ep)) { // 检查rdllist ovflist,当前没有就绪的就进行阻塞
/*
* We don't have any available event to return to the caller.
* We need to sleep here, and we will be wake up by
* ep_poll_callback() when events will become available.
*/
init_waitqueue_entry(&wait, current);
__add_wait_queue_exclusive(&ep->wq, &wait);
for (;;) {
/*
* We don't want to sleep if the ep_poll_callback() sends us
* a wakeup in between. That's why we set the task state
* to TASK_INTERRUPTIBLE before doing the checks.
*/
set_current_state(TASK_INTERRUPTIBLE);
/*
* Always short-circuit for fatal signals to allow
* threads to make a timely exit without the chance of
* finding more events available and fetching
* repeatedly.
*/
if (fatal_signal_pending(current)) {
res = -EINTR;
break;
}
if (ep_events_available(ep) || timed_out)
break;
if (signal_pending(current)) {
res = -EINTR;
break;
}
spin_unlock_irq(&ep->wq.lock);
if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS)) // 调度,sleep until timeout
timed_out = 1;
spin_lock_irq(&ep->wq.lock);
}
__remove_wait_queue(&ep->wq, &wait);
__set_current_state(TASK_RUNNING);
}
check_events: // 检查是否有event就绪
/* Is it worth to try to dig for events ? */
eavail = ep_events_available(ep);
spin_unlock_irq(&ep->wq.lock);
/*
* Try to transfer events to user space. In case we get 0 events and
* there's still timeout left over, we go trying again in search of
* more luck.
*/
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && !timed_out) // ⭐
goto fetch_events; // timeout==0的情况下肯定不会goto
// 如果timeout>0的情况下尚未超时,会不断地在限定时间内尝试fetch_events(因为ep_send_events给出地esed.res为0返回无意义)
// ep_events_available == true 但是 ep_send_events == 0 可能是put_user无法完成等原因导致的
// 只要单次获得大于0的ep_send_events(或出错)则跳出goto循环
return res;
ep_scan_ready_list - Scans the ready list in a way that makes possible for
the scan code, to call f_op->poll(). Also allows for
O(NumReady) performance.
*/
static int ep_scan_ready_list(struct eventpoll *ep,
int (*sproc)(struct eventpoll *, // sproc == ep_send_events_proc
struct list_head *, void *),
void *priv, int depth, bool ep_locked) // priv == esed
{
int error, pwake = 0;
unsigned long flags;
struct epitem *epi, *nepi;
LIST_HEAD(txlist); // transaction list
if (!ep_locked)
mutex_lock_nested(&ep->mtx, depth);
spin_lock_irqsave(&ep->lock, flags);
list_splice_init(&ep->rdllist, &txlist); // 合并两个链表到txlist,然后rdllist重置为empty list
ep->ovflist = NULL; // 允许使用ovflist
spin_unlock_irqrestore(&ep->lock, flags);
/*
* Now call the callback function.
*/
error = (*sproc)(ep, &txlist, priv); // 对txlist进行vfspoll等操作
spin_lock_irqsave(&ep->lock, flags);
/*
* During the time we spent inside the "sproc" callback, some
* other events might have been queued by the poll callback.
* We re-insert them inside the main ready-list here.
*/
for (nepi = ep->ovflist; (epi = nepi) != NULL;
nepi = epi->next, epi->next = EP_UNACTIVE_PTR) {
/*
* We need to check if the item is already in the list.
* During the "sproc" callback execution time, items are
* queued into ->ovflist but the "txlist" might already
* contain them, and the list_splice() below takes care of them.
*/
if (!ep_is_linked(&epi->rdllink)) {
list_add_tail(&epi->rdllink, &ep->rdllist); // 把overflow-list的swap到ready-list
ep_pm_stay_awake(epi);
}
}
/*
* We need to set back ep->ovflist to EP_UNACTIVE_PTR, so that after
* releasing the lock, events will be queued in the normal way inside
* ep->rdllist.
*/
ep->ovflist = EP_UNACTIVE_PTR;
/*
* Quickly re-inject items left on "txlist".
*/
list_splice(&txlist, &ep->rdllist); // 在sproc阶段,如果对于txlist的各个epi,任意一个出错(有revent但无法put_user)会重新插回txlist并退出sproc流程
__pm_relax(ep->ws);
if (!list_empty(&ep->rdllist)) {
/*
* Wake up (if active) both the eventpoll wait list and
* the ->poll() wait list (delayed after we release the lock).
*/
// 可能是监听文件状态变化wakeup导致插入新的ready-list(这种情况在ctl阶段已经wakeup-exclusive了)
// 也可能是前面overflowlist/txlist残留
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
spin_unlock_irqrestore(&ep->lock, flags);
if (!ep_locked)
mutex_unlock(&ep->mtx);
/* We have to call this outside the lock */
if (pwake)
ep_poll_safewake(&ep->poll_wait);
return error;
}
// sproc
// 遍历txlist的epi,进行vfspoll
// 处理要拷贝到用户态的events
// LT逻辑,重新插回ready-list
static __poll_t ep_send_events_proc(struct eventpoll *ep, struct list_head *head, // head == txlist
void *priv) // priv只用于传递esed封装类
{
struct ep_send_events_data *esed = priv;
__poll_t revents;
struct epitem *epi;
struct epoll_event __user *uevent;
struct wakeup_source *ws;
poll_table pt;
init_poll_funcptr(&pt, NULL);
/*
* We can loop without lock because we are passed a task private list.
* Items cannot vanish during the loop because ep_scan_ready_list() is
* holding "mtx" during this call.
*/
for (esed->res = 0, uevent = esed->events;
!list_empty(head) && esed->res < esed->maxevents;) {
epi = list_first_entry(head, struct epitem, rdllink);
list_del_init(&epi->rdllink);
revents = ep_item_poll(epi, &pt, 1); // epi进行vfspoll,此时pt为NULL
if (revents) {
if (__put_user(revents, &uevent->events) ||
__put_user(epi->event.data, &uevent->data)) {
// 出错了
list_add(&epi->rdllink, head);
ep_pm_stay_awake(epi);
if (!esed->res)
esed->res = -EFAULT;
return 0;
}
esed->res++;
uevent++; // 下一个 esed.events大小由用户指定,内核不做额外检查,直接++
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;
else if (!(epi->event.events & EPOLLET)) {
// LT特色,此前已经list_del_init(&epi->rdllink);重新插回ready-list
list_add_tail(&epi->rdllink, &ep->rdllist);
ep_pm_stay_awake(epi);
}
}
}
return 0;
}
int do_epoll_ctl(int epfd, int op, int fd, struct epoll_event *epds,
bool nonblock)
{
// ...
/* Get the "struct file *" for the target file */
tf = fdget(fd);
if (!tf.file)
goto error_fput;
/* The target file descriptor must support poll */
error = -EPERM;
if (!file_can_poll(tf.file)) // 要求file_operations支持poll,内部实现为return file->f_op->poll;
goto error_tgt_fput;
Linux 内核的 epoll 实现
https://ift.tt/WcDAd0w
由于要做年轻人的第一次技术分享,因此我挑了个
epoll
实现原理作为课题,主要是先做下快速介绍,然后直接杠源码(长篇大论没人听警告)涉及到的内容
epoll
是什么,大概怎么用epoll
实现前需要知道的工具epoll 是什么
epoll
全称 eventpoll,是 linux 内核实现 IO 多路复用的一种实现。IO 多路复用的意思是在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
因此,epoll 的行为就是只用一个线程,监听多个
fd
(文件描述符),阻塞直到任意一个 fd 可以进行 IO 操作(视 IO 操作为一个 event),然后返回,交给我们处理怎么用
总共只有三个操作
具体到 API 层面如下所示
创建 epoll 实例
对 epoll 关注的事件进行增删改
等待 IO 操作
前置工具快速上手
这里会介绍两个最常用到的数据结构和一个文件系统的机制,后面我们会逐一深入
list_head
list_head
是内核链表,在linux
中大量用到,作为一种侵入式容器,较为反直观,通常一个链表是这样实现而
list_head
并不会持有任何data
怎么用那当然是“嵌入”地使用
它们主要在指向的地址方面存在差异,尽管如此,
Item
仍然可通过node
的地址计算偏移量,进而得到另一个Item
的首地址由于在实现过程中有大量用到,因此简单给个印象
wait_queue
另一个频繁且至关重要的工具是等待队列
wait_queue
,用于完成两个操作:task
的等待与唤醒(task
并没有限制是什么东西,一般是进程)主要有两个结构体
wait_queue_head
和wait_queue
注意,虽然第二个结构体命名叫
__wait_queue
,但它也是作为结点entry
,而不是一个队列它们之间的关联通过
task_list
连接在一起,我直接画图来表示吧等待的进程会构造关联的
wait_queue
挂到某一个wake_queue_head
上当
wake_queue_head
决定要唤醒时,会逐一遍历整个链表,调用回调函数使得唤醒wait_queue
对应的进程(默认情况下)特殊的情况后面再讲
f_op->poll
这里介绍文件系统的
poll()
机制,它是一种主动获取文件状态变化的接口,具体是怎么实现的,由各自的file
来决定由上面看出,
poll
只是一个函数指针,换句话说,就是提供一个接口,用 C 来实现 OOP可以通过一个简单的实现来大概了解它是怎么做的,这里给出管道文件对应的实现
无非就是三大步骤:
pipe
实例poll_wait
mask
我们可以在
poll_wait
中添加自己的回调,在后面会有关键的应用尝试自己实现一个 IO 多路复用?
其实从前面的
f_op->poll
和wait_queue
的讲解,我们可以简单的猜测出一种 IO 多路复用的实现模型假设我用 1 个线程
t
去监听 10 个fd
,全部只关注是否可读,任意一个可读就不再阻塞这么做似乎是可行的,也略微接近
epoll
的模型那
epoll
是不是这么干的?显然没这么蠢:fd
,要想知道是否就绪,这种实现是\(O(N)\),因为需要逐步遍历,而epoll
可以做到 \(O(1)\)fds
数组到内核态,epoll
可以按需拷贝(绝对不拷贝是不可能的)epoll
不仅比上面的实现更为高效,而且还有更多高级特性:check
肯定是一个LT
操作,但epoll
还有ET
的支持epoll
还支持exclusive
的唤醒方式epoll
自己也可以被监听想知道怎么做到的,就需要了解它的实现流程
限于篇幅,高级特性支持的流程会大量略过
实现流程 1 - epoll_create
我们将通过
epoll_create()
——创建epoll
实例的函数实现,来大概浏览一下epoll
内部是什么样的这个模块相对简单,可以让我们知道
epoll
模型到底使用怎样的数据结构来支撑它的运行PS. 关于
epoll
实现的代码都是采用4.18
版本epoll 实例 - eventpoll
从前面可看出
epoll_create
是返回fd
,但是内核却维护一个eventpoll
的结构体:eventpoll
是epoll
的实例,也是最为核心的数据结构总结:核心的数据结构
ep
大概维护以下几大类的成员epi
的数据结构ep
归属于哪个文件中被监听的红黑树结点 - epitem
epitem
在实现流程简称epi
,由前面的 API 调用介绍的第二步epoll_ctl
可看出,epoll 是创建实例后,需要添加监听的
fd
和事件类型event
其实在内部实现中,内核也是维护另外一个数据结构
epitem
来表示 fd 和 event 在 epoll 实例的关系总结:被监听的
epitem
维护以下特性:rbn
)rdllink / next
)ep
实例容器(ep
)event
)ffd
)这是最为核心的两个数据结构,任意操作流程都会涉及到
可以通过创建
epoll
实例(epoll_create
)和添加监听文件描述符和事件(epoll_ctl(EPOLL_CTL_ADD)
)的流程来了解两个结构体之间的关系,如下所示实现流程 2 - epoll_ctl
这里描述的就是上图【添加监听】的流程(
epoll_ctl
可以添加、删除、修改监听的事件,这里只详细展开添加的流程)从上图我们只知道,每次添加,
ep
维护的红黑树都会新增一个epi
结点但除此以外,并没有明确知道维护的结点有什么用,如何仅通过一个
epi
来实现 IO 多路复用,这些还需要接着了解代码(这里会删减大量非主干的逻辑)
可以看出,插入
epi
的真正实现在ep_insert()
一直到
ep_insert()
之前,大多都只做一些简单的检查这里涉及到关键的操作,
一是构造了一个
epq
的封装类实例,用它来关联epi
和回调函数,并通过这种方式注册一个ep_ptable_queue_proc
回调函数二是针对新构造的
epi
调用了ep_item_poll
来获取revents
(received event),这里把上面提到的回调函数ep_ptable_queue_proc
传入进去三是涉及到
ready-list
的插入,以及ep->wq
的唤醒,但这里我们并不知道ep->wq
里面是什么时候有结点的,以及ready-list
是否还有其它的插入流程后面了解剩下的
ep_item_poll
流程以及pt
(即ep_ptable_queue_proc
)的行为ep_item_poll
只干一件事情,那就是调用vfspoll
接口,它的具体实现,取决于file
的f_op->poll()
前面已经提供,用户可以通过主动地去
poll
来获取这个fd
的就绪事件res
那么在
vfspoll
中接着传递下去的回调ep_ptable_queue_proc
是希望在我们主动poll
的时候做什么样的事情这里的
pwq
也是一个简单的封装类,用于关联epitem
和wait_queue
(whead
指向监听fd
的wait_queue_head
,wait
是epi
自身的等待队列结点)在
poll
的流程中,epi
被插入到fd
的等待队列whead
中(取决于具体实现),并且插入到队列时注册一个ep_poll_callback
函数,当唤醒whead
队列时,轮到epi
对应的wait_queue
则会调用ep_poll_callback
ep_poll_callback
是fd
开始wake up
进行的操作,主要处理ep->wq
实现流程 3 - epoll_wait
epoll_wait
接口会主动引起阻塞 (timeout != 0
时),并随后唤醒返回有多少就绪事件epoll_wait
会进行阻塞直到有事件返回,这个过程分为两部分:fetch_events
和fetch_events
其中,
fetch_events
实现了阻塞的过程,而check_events
不只是检查 event,还会处理遍历ready-list
并传递到用户态的细节流程图
值得思考的问题
下面会提出几个问题,这些都可以在流程里总结出来
epoll 快在哪里
epoll
的实现对比之前上文提到的低配版实现,其实是通过很简单的方式来做到高效vfspoll
只在ready-list
上检查,而不遍历所有关注的事件,只要 \(O(1)\) 判断ready-list
为空就知道必然没有就绪事件epoll_ctl
和epoll_wait
接口,并内部维护容器(红黑树)来避免每次调用都全盘拷贝哪些文件支持 epoll
前面已经提到,
epoll
调用到f_op->poll()
,因此,实现了poll
接口的文件都可以常见支持的文件类型有
socket
,pipe
,timerfd
像普通的磁盘文件 (
ext4
为例) 是不支持的另外,
epoll_create
返回一个epfd
给用户,其实这么写也是为了让epoll
文件自身也支持被epoll
监听LT 与 ET,区别与坑
在前面的流程有写到
函数大概是收集
ready-list
上的epi
的就绪信息,里面有一条细节只要有
revents
并且不是ET
模式,就重新插回ready-list
这意味着:下次
epoll_wait
唤醒,这些曾经有过revents
的epi
也会再次遍历,直到下次vfspoll
没有 revents 返回才不再插回这样满足了只要有就绪事件,就不断可以通知的机制,因此称为 LT 模型(在我们刚才实现的低配版多路复用也是 LT 模型)
而 ET 模型的差别在于,有就绪事件,则只会通知一次,如果不一次性(在下一次
epoll_wait
前)处理完,则不再通知(因为并没有在
ready-list
中)因此,对于
ET
模式的使用,需要epoll_wait
获取事件时,必须单次处理完成所有就绪事件,否则会丢失信息“惊群”问题
当多个
epoll
实例(每个线程各占一个实例)监听同一个fd
,并且都陷入epoll_wait
阻塞时,如果此时有一个事件就绪,将会导致所有epoll_wait
阻塞全部唤醒,这种现象称为惊群原因在于被监听
fd
的等待队列whead
,会遍历队列去执行,那如果是把整个队列都调用了每个 ep 实例所注册的ep_ptable_queue_proc
,则会引起多个epoll
实例唤醒(详细回调见前面流程图的 2.1 和 2.2)
能不能避免这种不必要的惊群?(同一个 fd 只能被某个 epoll 实例处理,多个唤醒属于多余的行为)
Linux
解决这个问题的做法是允许用户在epoll_ctl
添加一个EPOLLEXCLUSIVE
的 flag,这个关联到等待队列的一种特性我们在前面提到了,默认情况下,等待队列会从头结点开始逐一遍历结点并回调相应函数,因此层层递进造成了前面的惊群现象
但是等待队列也允许插入“互斥”的结点(默认是不互斥的),在这里提一下不互斥(简称
N
)与互斥(简称E
)的结点有哪些不同的行为在插入等待队列上:
N
插入头部,E
插入尾部在遍历上:迭代到
N
无条件继续处理,迭代到E
则计数器减一,计数为 0 则终止由于插入行为上的不同,一个等待队列总是会保持前缀为
N
,后缀为E
的性质,即NNNNNNEEEE
,只要遍历时计数器为 1,则每次至多只处理 1 个互斥结点(如果全部都是互斥结点的话)回到
EPOLLEXCLUSIVE
,这个 flag 就是决定前面的插入顺序,Linux
只用如此精简的策略就能避免一定的惊群(互斥唤醒还有更加复杂的细节,比如在
wakeup
时回调为ep_poll_callback
,其返回值ewake
也有限制什么时候允许互斥的分类处理,如果ewake
为 0,即使结点本身是互斥的,遍历时也会仍按照非互斥的方式去处理)图文无关
最近在云通关《海猫鸣泣之时》,绘梨花这副嚣张又吃瘪的模样实在是令人忍俊不禁
23.6.19 update
偶尔回顾一下自己写的文章(特么的写得真好),感觉对当时取的标题不太满意
这篇文章以前就叫epoll in depth,长度上短了点,于是我让 new bing 帮我取了一个新的标题又改了一遍,AI 起的名字还是太长了
via Caturra’s Blog
September 10, 2024 at 11:07AM
The text was updated successfully, but these errors were encountered: