IO协程调度模块概述
在协程调度模块的基础上,封装了epoll
。支持对IO事件的调度功能,可以为socket
句柄添加读事件(EPOLLIN
)和写事件(EPOLLOUT
),并且支持删除事件功能。IOManager主要通过FdContext
结构体存储文件描述符fd, 注册的事件event,执行任务cb/fiber
,其中fd
和event
用于epoll_wait
,cb/fiber
用于执行任务。当有任务时,使用管道pipe
来唤醒epoll_wait()
先执行其他任务。
IO模式
先梳理一遍epoll
的相关概念,这样能够更好的理解IO协程调度模块。
在IO操作时,操作系统会将 I/O 的数据缓存在文件系统的页缓存( page cache )中,也就是说,第一阶段: 数据会先被拷贝到操作系统内核的缓冲区中,第二阶段: 然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。
阻塞I/O
在默认情况下,所有的socket
都是被阻塞的,也就是阻塞I/O,这样就会导致两个阶段的阻塞:等待数据 、从内核拷贝数据到用户空间 。
非阻塞I/O
可以通过设置socket
变为non-blocking
,如果内核还没有准备好数据,那么并不会阻塞用户进程,而是立刻返回一个error
,可以通过系统调用获得数据,一旦内核的数据准备好了,并且又再次收到了用户进程的system call
,那么它马上就将数据拷贝到了用户内存(第二阶段的阻塞:在拷贝的过程中进程也是会被block),然后返回。
异步I/O
在两个阶段都不会被阻塞。第一阶段: 例如,当用户进程发起read
操作后,内核收到用户进程的system call
会立刻返回,不会对用户进程产生任何的阻塞,第二阶段: 当内核准备好了数据,将数据拷贝到用户空间,当这一切都完成之后,内核才会给用户进程发送信号表示操作完成,所以第二阶段也不会被阻塞。整个过程都没有被阻塞。
只有异步I/O是真正的异步,其他的模式包括阻塞I/O、异步I/O、I/O多路复用都是同步I/O。对于真正的I/O操作,指的是第二阶段:当内核收到用户进程发来的system call
,将数据拷贝到用户空间中,这一步骤只有异步I/O是非阻塞的,其他的I/O模式都会被阻塞。
I/O多路复用
服务器要跟多个客户端建立连接,就需要处理大量的socket fd
,通过单线程或单进程同时监测若干个文件描述符是否可以执行IO操作,这就是IO多路复用。
当用户进程调用了select
,那么整个进程都会被阻塞,同时,内核会监视所有select
负责的socket fd
,当任何一个socket
中的数据准备好了,select
就会返回,这个时候用户进程在调用recvfrom
操作,内核收到system call
就将数据拷贝到用户进程。
在Linux中,主要使用select
、poll
、epoll
三种方式处理。通常我们将socket
设置为O_NONBLOCK
,由于在I/O操作时,整个用户进程是被阻塞的,但是这个进程是被select
阻塞的,而不是被socket IO
给阻塞的。
或许有个疑问,A: 当select
返回了,那一定是socket
中的数据准备好了, 那么recvfrom
也不会阻塞了,所以设不设置socket
为非阻塞模式似乎都没有什么区别了?Q: 在 Linux 下,select
可以将套接字文件描述符报告为“准备读取”,同时后续读取块。 例如,当数据到达但检查时校验和错误并被丢弃时,可能会发生这种情况。 在其他情况下,文件描述符可能会被虚假地报告为就绪。 因此,在不应阻塞的套接字上使用O_NONBLOCK
可能更安全。
select
/*
@param: n 最大文件描述符+1
@param: readfds 读文件描述符
@param writefds 写文件描述符
@param exceptfds 异常文件描述符
@param timeout 超时事件
*/
int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
当进程调用select
时将会被阻塞,fd_set
的数据结构为bitmap
,通过FD_SET
方法将需要监听的文件描述符集合fdset
对应的bitmap
置为1(例如文件描述符集合为4,9,那么就将bitmap
的第4位和第9位置为1),select
会截取bitmap
前n
位进行监听。select
会将需要关注的fd_set
拷贝到内核态监听,当有数据来时,内核将有数据的fd_set
置位(bitmap
对应的文件描述符置位为相应的操作,读、写、异常),select
返回。因为不知道是哪个文件描述符来数据了,所以再遍历fdset
寻找就绪的文件描述符。
select
目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。
select
的缺点
- 在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
fd_set
是不可重用的,每次需要使用FD_ZERO
方法清空- 每次调用
select
都需要将fd_set
拷贝到内核态,有开销 - 不知道是哪个文件描述符的数据来了,所以要遍历,时间复杂度为O(n)
poll
/*
param fds fd事件
param nfds fd数量
param timeout 超时时间
*/
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
poll
数据结构与select
不同,poll
采用数组存储pollfd
,并将fd
和关注的事件(POLLIN
等)分别保存到pollfd
的fd
和events
中。
poll
与select
工作原理相同,但要注意的是,当数据来时,poll
将revents
置位(POLLIN
等),然后poll
函数返回。仍然要遍历数组来看是哪个文件描述符来了,并且将revents
置为0,这样就能重复使用pollfd
。
poll
优点
- 解决了
select
的1024上限 - 解决了
select fd_set
不可重用,pollfd
可以通过重置revents
恢复如初
poll
缺点
- 每次调用
poll
都需要将pollfd
拷贝到内核态,有开销 - 不知道是哪个文件描述符的数据来了,所以要遍历,时间复杂度为O(n)
epoll
epoll
是在2.6内核中提出的,是之前的select
和poll
的增强版本。相对于select
和poll
来说,epoll
更加灵活,没有描述符限制。epoll
使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy
只需一次。
epoll 操作流程
epoll
通过以下3个接口操作
- epoll_create
//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大
int epoll_create(int size);
创建一个epoll
的句柄,size
用来告诉内核这个监听的数目一共有多大,这个参数不同于`select()
中的第一个参数,给出最大监听的fd+1
的值,参数size
并不是限制了epoll
所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。但看了源码,只要size
大于0就可以了,没有实质性的作用。
当创建好epoll
句柄后,它就会占用一个fd
值,在linux下如果查看/proc/进程id/fd/
,是能够看到这个fd
的,所以在使用完epoll
后,必须调用close()
关闭,否则可能导致fd
被耗尽。
通过源码得知,每创建一个epollfd
, 内核就会分配一个eventpoll
结构体与之对应,其中维护了一个RBTree
来存放所有要监听的struct epitem(表示一个被监听的fd)
- epoll_ctl
从用户空间将epoll_event
结构copy到内核空间
/*
@param epfd epoll_create()的返回值
@param op 添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD事件
@param event 告诉内核需要监听什么事
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
/*
* epoll事件关联数据的联合体
* fd: 表示关联的文件描述符。
* ptr:表示关联的指针。
*/
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
//events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
通过源码得知,同一个fd
不能重复添加。内核会自动添加这两个事件epds.events |= POLLERR | POLLHUP;
并且使用copy_from_user
从用户空间将epoll_event
结构copy到内核空间。
if (ep_op_has_event(op) &&
copy_from_user(&epds, event, sizeof(struct epoll_event)))
3. epoll_wait
/*
@param epfd epoll_create() 返回的句柄
@param events 分配好的 epoll_event 结构体数组,epoll 将会把发生的事件复制到 events 数组中
events不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存,但是内核会检查空间是否合法
@param maxevents 表示本次可以返回的最大事件数目,通常 maxevents 参数与预分配的 events 数组的大小是相等的;
@param timeout 表示在没有检测到事件发生时最多等待的时间(单位为毫秒)
如果 timeout 为 0,则表示 epoll_wait 在 rdllist 链表为空时,立刻返回,不会等待。
rdllist:所有已经ready的epitem(表示一个被监听的fd)都在这个链表里面
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
收集在 epoll
监控的事件中已经发生的事件,如果epoll
中没有任何一个事件发生,则最多等待timeout
毫秒后返回。epoll_wait
的返回值表示当前发生的事件个数,如果返回 0,则表示本次调用中没有事件发生,如果返回 -1,则表示发生错误,需要检查errno
判断错误类型。
通过源码得知,通过__put_user
将数据从内核空间拷贝到用户空间
if (__put_user(revents, &uevent->events) ||
__put_user(epi->event.data, &uevent->data)) {
list_add(&epi->rdllink, head);
return eventcnt ? eventcnt : -EFAULT;
}
epoll 源码工作流程
这里只讲了些主要的代码,可以对照源码自行查看
epoll
主要数据结构
/* 创建epollfd时创建eventpoll结构体,内核维护的就是这个 */
struct eventpoll {
/* Protect the this structure access */
spinlock_t lock;
/* 保证线程安全 */
struct mutex mtx;
/* 系统调用等待队列 */
wait_queue_head_t wq;
/* epollfd被poll() */
wait_queue_head_t poll_wait;
/* 已经ready的epitem */
struct list_head rdllist;
/* 所有要监听的epitem的红黑树头节点 */
struct rb_root rbr;
/* 这是一个链表,它链接了所有“结构 epitem”
* epoll_wait()已返回但是又来了新的事件,就保存到这里 */
struct epitem *ovflist;
/* 这里保存了一些用户变量 */
struct user_struct *user;
};
/* epitem 表示一个被监听的fd */
struct epitem {
/* 所有要监听的epitem红黑树节点 */
struct rb_node rbn;
/* 链表节点, 所有已经ready的epitem都会被链到eventpoll的rdllist中 */
struct list_head rdllink;
/*
* 协同工作 “struct eventpoll”->ovflist 以保持单链接的项目链
*/
struct epitem *next;
/* The file descriptor information this item refers to */
/* epitem对应的fd和struct file */
struct epoll_filefd ffd;
/* 附加到轮询操作的活动等待队列数 */
int nwait;
/* 包含轮询等待队列的列表 其实就是被eppoll_entry */
struct list_head pwqlist;
/* 当前epitem属于哪个eventpoll */
struct eventpoll *ep;
/* 用于将此项目链接到“结构文件”项目列表的列表标题 */
struct list_head fllink;
/* 当前的epitem关系哪些events, 这个数据是调用epoll_ctl时从用户态传递过来 */
struct epoll_event event;
};
/* poll所用到的钩子Wait structure used by the poll hooks */
struct eppoll_entry {
/* List header used to link this structure to the "struct epitem" */
struct eppoll_entry *next;
/* The "base" pointer is set to the container "struct epitem" */
struct epitem *base;
/*
* Wait queue item that will be linked to the target file wait
* queue head.
*/
wait_queue_t wait;
/* The wait queue head that linked the "wait" wait queue item */
wait_queue_head_t *whead;
};
/* 轮询队列使用的包装器结构 */
struct ep_pqueue {
poll_table pt;
struct epitem *epi;
};
/* 保存了文件结构 */
struct epoll_filefd {
struct file *file;
int fd;
};
/* ep_send_events()中使用,作为回调函数的参数 */
struct ep_send_events_data {
int maxevents;
struct epoll_event __user *events;
};
epollfd
对应的file_operations
就封装了两个操作
static const struct file_operations eventpoll_fops = {
.release = ep_eventpoll_release,
.poll = ep_eventpoll_poll
};
- epoll_create
在创建epoll
句柄时,从slab
缓存中创建一个epollevent
对象,epollfd
本身并不存在一个真正的文件与之对应, 所以内核需要创建一个"虚拟"的文件, 并为之分配真正的struct file
结构, 而且有真正的fd
,而eventpoll
对象保存在struct file
结构的private_data
指针中。返回epollfd
的文件描述符。
// 使用kzalloc为epollevent分配内存,等同于kmalloc() + memset()
// GFP_KERNEL 内核内存的正常分配. 可能睡眠.
ep = kzalloc(sizeof(*ep), GFP_KERNEL);
2. epoll_ctl
将epoll_event
结构拷贝到内核空间中
if (ep_op_has_event(op) &&
copy_from_user(&epds, event, sizeof(struct epoll_event)))
goto error_return;
并且判断加入的fd
的f_op->poll
是否支持poll
结构(epoll,poll,select
I/O多路复用必须支持poll
操作)。
通过epollfd
取得struct file
中private_data
获取eventpoll
,根据op
区分是添加,修改还是删除。
在添加fd
时,首先在eventpoll
结构中的红黑树查找是否已经存在了相对应的epitem
,没找到就支持插入操作,否则报重复的错误,并且会调用被监听的fd
的poll
方法查看是否有事件发生。
当poll
时,通过poll_wait
执行回调函数ep_ptable_queue_proc
,初始化等待队列成员时绑定ep_poll_callback
回调函数,然后将等待队列成员加入到等待队列头,等待队列头是由fd
驱动所拥有的。当数据来时,等待队列头会挨个通知等待队列成员,这样epoll
就知道数据来了,然后执行回调函数ep_poll_callback
将准备好的epitem
加入到rdllist
。
最后会将epitem
放到eventpoll
的红黑树中,如果此时已经有数据来了,谁在epoll_wait
就唤醒谁。
该过程不会阻塞进程。
// 这里主要涉及linux驱动方面的知识
--------------------------------------------------------------------------------------------------------------------------------
/*
* 等待队列
* 等待队列的思想有点像信号量的pv操作,可以自行了解一下
*/
struct wait_queue_head_t {
spinlock_t lock;
struct list_head task_list;
};
/*
* 初始化一个等待队列,指定func为唤醒时的回调函数,wa 就是 wait_queue_head_t
* 当我们监听的fd发生状态改变时, 也就是队列头被唤醒时,指定的回调函数将会被调用。
*/
void init_waitqueue_func_entry(struct wait_queue_entry *wq_entry,
wait_queue_func_t func)
{
wq_entry->func = func;
wq_entry->private = NULL;
}
/*
* func签名如下
*/
int func(wait_queue_t *wait, unsigned mode, int flags, void *key) {}
/*
* poll 原型
*/
unsigned int (*poll)(struct file *filp, struct poll_table *wait);
/*
* poll_wait 原型
*/
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}
--------------------------------------------------------------------------------------------------------------------------------
epoll_ctl
/* 首先做一些初始化 */
/* 初始化自己睡在 wq 这个等待队列上,只有数据准备好了,才会唤醒
* 初始化的是 epoll 的 等待队列头 */
init_waitqueue_head(&ep->wq);
init_waitqueue_head(&ep->poll_wait);
/* 根据op判断是什么操作,插入、修改还是删除 */
/* 在添加时,会自动添加事件 错误和挂起 */
epds.events |= POLLERR | POLLHUP;
/* 然后调用这个
* tfile就是要监听的这个fd的file结构体*/
error = ep_insert(ep, &epds, tfile, fd);
epoll_ctl
在做插入操作时会调用ep_insert()
// ep_insert() 插入操作
/* 前面主要做一些初始化操作 */
/* 函数里执行的就相当于这句 poll_table->_qproc = ep_ptable_queue_proc
* 在执行 poll_wait() 时就会执行回调 ep_ptable_queue_proc 函数 */
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
/* 首先对 tfile 的等待队列调用 poll_wait()[不同的fd有不同的调用路径,但最后都会调用到poll_wait()],最后调用到ep_ptable_queue_proc函数
/* tfile:被监听fd的strct file,&epq.pt:poll_table */
revents = tfile->f_op->poll(tfile, &epq.pt);
/* 将 epitem 加入到 eventpoll 的红黑树中 */
ep_rbtree_insert(ep, epi);
/* 如果监听的fd已经有事件来了那么就处理 */
if ((revents & event->events) && !ep_is_linked(&epi->rdllink)) {
/* 将当前的epitem加入到ready list中去 */
list_add_tail(&epi->rdllink, &ep->rdllist);
/* Notify waiting tasks that events are available */
/* 谁在epoll_wait, 就唤醒它... */
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
/* 谁在epoll当前的epollfd, 也唤醒它... */
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
ep_ptable_queue_proc()
在这个方法中将epitem
和指定fd
使用等待队列关联起来
--------------------------------------------------------------------------------------------------------------------------------
/*
* pwd(eppoll_entry):poll所用到的钩子,里面有等待队列头whead(被监听fd所持有,驱动持有这个等待队列头)、等待队列成员wait、epitem结构体base、epitem链
* 表llink
*/
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead, poll_table *pt)
/* 该函数将等待队列项的func成员变量设置为指定的函数指针
* wq_entry是一个指向等待队列项的指针,func是一个函数指针,用于指定在等待队列中等待的回调函数。
*/
void init_waitqueue_func_entry(struct wait_queue_entry *wq_entry, wait_queue_func_t func)
{
wq_entry->func = func;
wq_entry->private = NULL;
}
--------------------------------------------------------------------------------------------------------------------------------
/* container_of 根据结构体成员 poll_table pt 获得父结构体 ep_pqueue */
struct ep_pqueue *epq = container_of(pt, struct ep_pqueue, pt);
/* 拿到要监听的epitem */
struct epitem *epi = epq->epi;
struct eppoll_entry *pwq;
/* 首先初始化等待队列,并且设置回调函数ep_poll_callback,当我们监听的fd发生状态改变时, 也就是队列头被唤醒时,指定的回调函数将会被调用。 */
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
/* 保存队列头 */
pwq->whead = whead;
/* 保存epitem */
pwq->base = epi;
/* 将刚分配的等待队列成员加入到头中, 头是由fd持有的 */
add_wait_queue(whead, &pwq->wait);
/* 将pwq放到epi->pwqlist队头 */
pwq->next = epi->pwqlist;
/* 更新epi的pwqlist队列 */
epi->pwqlist = pwq;
ep_poll_callback()
当我们监听的fd
发生状态改变时, 它会被调用
--------------------------------------------------------------------------------------------------------------------------------
/*
* wait:等待队列,key:unsigned long类型 ,携带events
*/
static int ep_poll_callback(wait_queue_t *wait, unsigned mode, int sync, void *key)
--------------------------------------------------------------------------------------------------------------------------------
/* ep_item_from_wait中是 container_of 操作,根据结构体成员 wait 拿到父结构体 eppoll_entry 中的 base,也就是epitem*/
struct epitem *epi = ep_item_from_wait(wait);//从等待队列获取epitem.需要知道哪个进程挂载到这个设备
/* 获得当前 epitem 所在的 eventpoll */
struct eventpoll *ep = epi->ep;
/*
* 如果该callback被调用的同时, epoll_wait()已经返回了,也就是说, 此刻应用程序有可能已经在循环获取events
* 这种情况下, 内核将此刻发生event的epitem用一个单独的链表链起来, 不发给应用程序, 也不丢弃
* 而是在下一次epoll_wait时返回给用户。
*
* ovflist初始化为EP_UNACTIVE_PTR,在epoll_wait()中,会将其置为NULL
* epi->next初始化也为EP_UNACTIVE_PTR,在此时会置为空;再处理ovflist时epi->next会置为EP_UNACTIVE_PTR
*/
if (unlikely(ep->ovflist != EP_UNACTIVE_PTR)) {
if (epi->next == EP_UNACTIVE_PTR) {
epi->next = ep->ovflist;
ep->ovflist = epi;
}
goto out_unlock;
}
/* 将当前的epitem放入ready list */
if (!ep_is_linked(&epi->rdllink))
list_add_tail(&epi->rdllink, &ep->rdllist);
/* 唤醒epoll_wait... */
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
3. epoll_wait
epoll_wait()
要将数据从内核copy
到用户空间,内存需要用户空间自己提供,但内核会验证内存空间是否合法,然后执行ep_poll()
。- 在
ep_poll()
中当rdllist
不为空时,使用等待队列把当前进程挂到epoll
的等待队列头,无限循环 { 若rddllist
有数据或者已经过了超时事件,又或者有信号来了,就跳出循环唤醒进程。无事件发生,就使用schedule_timeout
睡觉,数据来时,调用ep_poll_callback()
唤醒了epoll
的等待队列头时,就不用睡眠了。},然后将当前进程从epoll
的等待队列中移除。然后调用ep_send_events()
。 - 在
ep_send_events()
中调用ep_scan_ready_list()
- 在
ep_scan_ready_list()
中先将rdllist
剪切到txlist
中,执行ep_send_events_proc()
将txlist
中的epitem
处理,把未处理完的epitm
重新加入到rdllist
中。然后将ovflist
加入到rddlist
中,若仍有事件没有处理完,则唤醒epoll
的等待队列头。 - 在
ep_send_events_proc()
中调用被监听fd
的poll
方法拿到准备好的事件,若与我们监听的事件相同,那么就将数据从内核将拷贝到用户空间中。若设置了为边缘触发模式,则不会将当前epitem
放回到rdllist
中,也就是说,只有再次调用epoll_wait
时,通过本函数的poll
步骤,当信号来时调用了ep_poll_pollback()
才会将epitem
重新放回到rldlist
中;若设置了水平触发模式,则不管有没有有效事件就放回到rdllist
中去。
--------------------------------------------------------------------------------------------------------------------------------
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
--------------------------------------------------------------------------------------------------------------------------------
/* 先获取epfd 的 struct file */
file = fget(epfd);
/* 获取 eventpoll */
ep = file->private_data;
/* 执行 ep_poll 睡觉, 等待事件到来 */
error = ep_poll(ep, events, maxevents, timeout);
// 从ep_poll中得到处理了多少个事件
return error
ep_poll
将执行epoll_wait
的进程阻塞
--------------------------------------------------------------------------------------------------------------------------------
static int ep_poll(struct eventpoll *ep, struct epoll_event __user *events,
int maxevents, long timeout)
/*
* 此函数的第一个输入参数代表等待队列中的一个元素,在此被初始化,此结构体的定义详细参看abort_exclusive_wait( )。
* 第二个输入参数p是struct task_struct类型的指针变量,代表进程的描述符信息,保存进程的基本信息。
*/
static inline void init_waitqueue_entry(wait_queue_t *q, struct task_struct *p)
{
q->flags = 0;
q->private = p;
q->func = default_wake_function;
}
/*
* 当调用schedule_timeout函数时,当前进程将进入睡眠状态,并等待timeout个jiffies的时间。如果在等待期间有信号到达,则会唤醒进程并返回剩余的等待时间。
* 如果等待时间已经过去,则函数返回0。
*/
unsigned long schedule_timeout(unsigned long timeout);
--------------------------------------------------------------------------------------------------------------------------------
/* epoll 的等待队列成员 */
wait_queue_t wait;
retry:
/* 如果 rdllist 为空 */
if (list_empty(&ep->rdllist)) {
/* 初始化等待队列成员,将当前进程current添加到等待队列中 */
init_waitqueue_entry(&wait, current);
/* 将 wait 等待队列成员 挂在到 ep 的等待队列头 wq */
__add_wait_queue_exclusive(&ep->wq, &wait);
/* 无线循环中 */
for(;;) {
/* 这是针对等待某事件或其他资源而睡眠的进程设置的。在内核发送信号给该进程时表明等待的事件已经发生或资源已经可用,进程状态变为 TASK_RUNNING,此时只要被调度器选中就立即可恢复运行。 */
/* 将该进程状态设置为睡眠,该过程可以被外部信号唤醒。当内核发送型号给该进程是表示等待的事件已经发生了,进程状态变为 TASK_RUNNING */
set_current_state(TASK_INTERRUPTIBLE);
/* 如果 rdllist 有成员或者超时时间已经过了,就直接唤醒 */
if (!list_empty(&ep->rdllist) || !jtimeout)
break;
/* 判断当前进程是否有未处理的信号的函数(例如 ctrl—c) */
if (signal_pending(current)) {
res = -EINTR;
break;
}
/* 无事就进入睡眠状态 */
jtimeout = schedule_timeout(jtimeout);
}
/* break后将当前进程的等待队列成员从等待队列中删除 */
__remove_wait_queue(&ep->wq, &wait);
/* 醒来了 */
set_current_state(TASK_RUNNING);
}
/* 判断有没有已经准备好的epitem或者是上次epoll_wait之后才准备好的epitem */
eavail = !list_empty(&ep->rdllist) || ep->ovflist != EP_UNACTIVE_PTR;
/* res为0,且有事件,但是处理的事件数为0,超时时间还有剩余
* retry重试
*/
if (!res && eavail &&
!(res = ep_send_events(ep, events, maxevents)) && jtimeout)
goto retry;
/* 最后返回处理事件数量 */
return res;
ep_send_events()
会调用ep_scan_ready_list()
--------------------------------------------------------------------------------------------------------------------------------
static int ep_send_events(struct eventpoll *ep,
struct epoll_event __user *events, int maxevents)
--------------------------------------------------------------------------------------------------------------------------------
struct ep_send_events_data esed;
esed.maxevents = maxevents;
esed.events = events;
return ep_scan_ready_list(ep, ep_send_events_proc, &esed);
ep_scan_ready_list()
扫描列表
--------------------------------------------------------------------------------------------------------------------------------
/*
* @ep:指向 epoll 私有数据结构的指针,eventpoll。
* @sproc:指向扫描回调的指针,ep_send_events_proc。
* @priv:传递给@sproc回调的私有不透明数据,也就是ep_send_events_data esed。
*/
static int ep_scan_ready_list(struct eventpoll *ep,
int (*sproc)(struct eventpoll *,
struct list_head *, void *),
void *priv)
--------------------------------------------------------------------------------------------------------------------------------
/* 所有监听到 events 的 epitem 都链到 rdllist 上了
* 但是这一步之后, 所有的 epitem 都转移到了 txlist 上, 而 rdllist 被清空了 */
list_splice_init(&ep->rdllist, &txlist);
/* 将ovflist置空,这样在ep_poll_callback()中就会将epitem放入到该链表中,而不是放到rdllist */
ep->ovflist = NULL;
/* 在 sproc 也就是 ep_send_events_proc 中处理 txlist 每个 epitem */
/* error为处理的事件的数量 */
error = (*sproc)(ep, &txlist, priv);
/* 接下来处理 ovflist 中的 epitem
* 这些 epitem 都是我们在传递数据给用户空间时监听到了事件 */
for (nepi = ep->ovflist; (epi = nepi) != NULL;
nepi = epi->next, epi->next = EP_UNACTIVE_PTR) {
/* 将这些直接放入readylist */
if (!ep_is_linked(&epi->rdllink))
list_add_tail(&epi->rdllink, &ep->rdllist);
}
/* 将ovflist重新初始化 */
ep->ovflist = EP_UNACTIVE_PTR;
/* ep_send_events_proc中没有处理完的epitem, 重新插入到ready list */
list_splice(&txlist, &ep->rdllist);
/* ready list不为空, 直接唤醒 */
if (!list_empty(&ep->rdllist)) {
if (waitqueue_active(&ep->wq))
wake_up_locked(&ep->wq);
if (waitqueue_active(&ep->poll_wait))
pwake++;
}
/* 最后返回处理事件数量 */
return error;
ep_send_events_proc()
处理已经准备好的epitem
,也就是处理txlist
--------------------------------------------------------------------------------------------------------------------------------
static int ep_send_events_proc(struct eventpoll *ep, struct list_head *head,
void *priv)
--------------------------------------------------------------------------------------------------------------------------------
/* 扫描整个链表 */
/* 取出第一个成员 */
epi = list_first_entry(head, struct epitem, rdllink);
/* 然后从链表里面移除 */
list_del_init(&epi->rdllink);
/* 读取events,
* 注意events我们ep_poll_callback()里面已经取过一次了, 为啥还要再取?
* 1. 我们当然希望能拿到此刻的最新数据, events是会变的
* 2. 不是所有的poll实现, 都通过等待队列传递了events(也就是ep_poll_callback中的参数key), 有可能某些驱动压根没传
* 必须主动去读取. */
revents = epi->ffd.file->f_op->poll(epi->ffd.file, NULL) &
epi->event.events;
/* 如果取到了事件 */
if (revents) {
/* 将epoll事件的结果写入用户空间的缓冲区中 */
if (__put_user(revents, &uevent->events) ||
__put_user(epi->event.data, &uevent->data)) {
/* 如果写入失败,则将epi添加回等待队列中 */
list_add(&epi->rdllink, head);
/* 返回处理了多少个事件,如果一个都没写成功就返回错误码 */
return eventcnt ? eventcnt : -EFAULT;
}
// 如果只监听1次
if (epi->event.events & EPOLLONESHOT)
epi->event.events &= EP_PRIVATE_BITS;
// 如果是ET模式
else if (!(epi->event.events & EPOLLET)) {
/* 如果是ET, epitem是不会再进入到rdllist,
* 除非fd再次发生了状态改变, ep_poll_callback被调用.
* 如果是非ET, 不管你还有没有有效的事件或者数据,
* 都会被重新插入到rdllist, 再下一次epoll_wait
* 时, 会立即返回, 并通知给用户空间. 当然如果这个
* 被监听的fds确实没事件也没数据了, epoll_wait会返回一个0,
* 空转一次.
*/
list_add_tail(&epi->rdllink, &ep->rdllist);
}
}
/* 最后返回处理事件数量 */
return eventcnt;
epoll 工作模式
LT模式(level triggered) :
当epoll_wait
检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait
时,会再次响应应用程序并通知此事件。
LT是缺省的工作方式,并且同时支持block socket
和no-block socket
。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd
进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
ET模式(edge-triggered) :
当epoll_wait
检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait
时,不会再次响应应用程序并通知此事件。
ET是高速工作方式,只支持no-block socket
。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll
告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK
错误)。但是请注意,如果一直不对这个fd
作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll
事件被重复触发的次数,因此效率要比LT模式高。epoll
工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
ET模式下: 如果read
返回0,那么说明已经接受所有数据 如果errno=EAGAIN
,说明还有数据未接收,等待下一次通知 如果read
返回-1,说明发生错误,停止处理
epoll 优点
-
监视的描述符数量不受限制
- 它所支持的
fd
上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat/proc/sys/fs/file-max
察看,一般来说这个数目和系统内存关系很大
- 它所支持的
-
IO的效率不会随着监视fd的数量的增长而下降
epoll
不同于select
和poll
轮询的方式,而是通过每个fd
定义的回调函数来实现的。只有就绪的fd
才会执行回调函数ep_poll_callback()
。ep_poll_callback()
的调用时机是由被监听的fd
的具体实现, 比如socket
或者某个设备驱动来决定的,因为等待队列头是他们持有的,epoll
和当前进程只是单纯的等待。
-
epoll使用一个文件描述符管理多个描述符
- 将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
select/poll/epoll之间的区别
select | poll | epoll | |
---|---|---|---|
数据结构 | bitmap | 数组 | 红黑树 |
最大连接数 | 1024 | 无上限 | 无上限 |
fd拷贝 | 每次调用select拷贝 | 每次调用poll拷贝 | fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝 |
工作效率 | 轮询:O(n) | 轮询:O(n) | 回调:O(1) |
详解
关于epoll
说的有点多,现在切入正题
class IOManager
class IOManager
继承自class Scheduler
在IOManager
中,主要把socket
的事件归类为读事件和写事件
enum Event
{
// 无事件
NONE = 0x0,
// 读事件(EPOLLIN)
READ = 0x1,
// 写事件(EPOLLOUT)
WRITE = 0x4,
};
class IOManager
封装了socket事件上下文
结构体
struct FdContext {
typedef Mutex MutexType;
/**
* @brief 事件上下文
*/
struct EventContext {
Scheduler* scheduler = nullptr; //事件执行的scheduler
Fiber::ptr fiber; //事件协程
std::function<void()> cb; //事件执行的回调函数
};
/**
* @brief 获取对应事件上下文
*/
EventContext& getContext(Event event) {
switch (event) {
case IOManager::READ:
return read;
case IOManager::WRITE:
return write;
default:
SYLAR_ASSERT2(false, "getContext");
}
}
/**
* @brief 重置事件上下文
*/
void resetContext(EventContext& ctx) {
ctx.scheduler = nullptr;
ctx.fiber.reset();
ctx.cb = nullptr;
}
/**
* @brief 触发事件
*/
void triggerEvent(Event event) {
SYLAR_ASSERT(events & event);
// 触发该事件就将该事件从注册事件中删掉
events = (Event)(events & ~event);
EventContext &ctx = getContext(event);
if (ctx.cb) {
// 使用地址传入就会将cb的引用计数-1
ctx.scheduler->schedule(&ctx.cb);
} else {
// 使用地址传入就会将fiber的引用计数-1
ctx.scheduler->schedule(&ctx.fiber);
}
// 执行完毕将协程调度器置空
ctx.scheduler = nullptr;
return;
}
EventContext read; //读事件
EventContext write; //写事件
int fd; //事件关联的句柄
Event events = NONE; //已经注册的事件
MutexType mutex;
};
mumber(成员变量)
//epoll文件句柄
int m_epfd = 0;
// pipe文件句柄,其中fd[0]表示读端,fd[1] 表示写端
int m_tickleFds[2];
// 等待执行的事件数量
std::atomic<size_t> m_pendingEventCount = {0};
// 互斥锁
RWMutexType m_mutex;
//socket事件上下文容器
std::vector<FdContext*> m_fdContexts;
IOManager(构造函数)
IOManager::IOManager(size_t threads, bool use_caller, const std::string &name)
:Scheduler(threads, use_caller, name) {
// 创建一个epollfd
m_epfd = epoll_create(5000);
// 成功时,这些系统调用将返回非负文件描述符。如果出错,则返回-1,并且将errno设置为指示错误。
SYLAR_ASSERT(m_epfd != -1);
// 创建管道,用于进程间通信
int rt = pipe(m_tickleFds);
// 成功返回0,失败返回-1,并且设置errno。
SYLAR_ASSERT(!rt);
epoll_event event;
// 用0初始化event
memset(&event, 0, sizeof(epoll_event));
// 注册读事件,设置边缘触发模式
event.events = EPOLLIN | EPOLLET;
// fd关联pipe的读端
event.data.fd = m_tickleFds[0];
// 对一个打开的文件描述符执行一系列控制操作
// F_SETFL: 获取/设置文件状态标志
// O_NONBLOCK: 使I/O变成非阻塞模式,在读取不到数据或是写入缓冲区已满会马上return,而不会阻塞等待。
rt = fcntl(m_tickleFds[0], F_SETFL, O_NONBLOCK);
SYLAR_ASSERT(!rt);
// 将pipe的读端注册到epoll
rt = epoll_ctl(m_epfd, EPOLL_CTL_ADD, m_tickleFds[0], &event);
SYLAR_ASSERT(!rt);
// 初始化socket事件上下文vector
contextResize(32);
// 启动调度器
start();
}
void IOManager::contextResize(size_t size) {
m_fdContexts.resize(size);
for (size_t i = 0; i < m_fdContexts.size(); ++i) {
// 没有才new新的
if (!m_fdContexts[i]) {
m_fdContexts[i] = new FdContext;
m_fdContexts[i]->fd = i;
}
}
}
~IOManager(析构函数)
IOManager::~IOManager() {
// 停止调度器
stop();
// 释放epoll
close(m_epfd);
// 释放pipe
close(m_tickleFds[0]);
close(m_tickleFds[1]);
// 释放 m_fdContexts 内存
for (size_t i = 0; i < m_fdContexts.size(); ++i) {
if (m_fdContexts[i]) {
delete m_fdContexts[i];
}
}
}
addEvent(添加事件)
int IOManager::addEvent(int fd, Event event, std::function<void()> cb) {
// 初始化一个 FdContext
FdContext* fd_ctx = nullptr;
RWMutexType::ReadLock lock(m_mutex);
// 从 m_fdContexts 中拿到对应的 FdContext
if ((int)m_fdContexts.size() > fd) {
fd_ctx = m_fdContexts[fd];
lock.unlock();
} else {
lock.unlock();
RWMutexType::WriteLock lock2(m_mutex);
// 不够就扩充
contextResize(fd * 1.5);
fd_ctx = m_fdContexts[fd];
}
FdContext::MutexType::Lock lock2(fd_ctx->mutex);
// 一个句柄一般不会重复加同一个事件, 可能是两个不同的线程在操控同一个句柄添加事件
if (fd_ctx->events & event) {
SYLAR_LOG_ERROR(g_logger) << "addEvent assert fd = " << fd
<< ", event = " << event
<< ", fd_ctx.event = " << fd_ctx->events;
SYLAR_ASSERT(!(fd_ctx->events & event));
}
// 若已经有注册的事件则为修改操作,若没有则为添加操作
int op = fd_ctx->events ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;
epoll_event epevent;
// 设置边缘触发模式,添加原有的事件以及要注册的事件
epevent.events = EPOLLET | fd_ctx->events | event;
// 将fd_ctx存到data的指针中
epevent.data.ptr = fd_ctx;
// 注册事件
int rt = epoll_ctl(m_epfd, op, fd, &epevent);
if (rt) {
SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ","
<< op << ", " << fd << ", " << epevent.events << ") :"
<< rt << " (" << errno << ") (" << strerror(errno) << ")";
return -1;
}
// 等待执行的事件数量+1
++m_pendingEventCount;
// 将 fd_ctx 的注册事件更新
fd_ctx->events = (Event)(fd_ctx->events | event);
// 获得对应事件的 EventContext
FdContext::EventContext &event_ctx = fd_ctx->getContext(event);
// EventContext的成员应该都为空
SYLAR_ASSERT(!event_ctx.scheduler
&& !event_ctx.fiber
&& !event_ctx.cb);
// 获得当前调度器
event_ctx.scheduler = Scheduler::GetThis();
// 如果有回调就执行回调,没有就执行该协程
if (cb) {
event_ctx.cb.swap(cb);
} else {
event_ctx.fiber = Fiber::GetThis();
SYLAR_ASSERT(event_ctx.fiber->getState() == Fiber::EXEC);
}
return 0;
}
delEvent(删除事件)
bool IOManager::delEvent(int fd, Event event) {
RWMutexType::ReadLock lock(m_mutex);
if ((int)m_fdContexts.size() <= fd) {
return false;
}
// 拿到 fd 对应的 FdContext
FdContext* fd_ctx = m_fdContexts[fd];
lock.unlock();
FdContext::MutexType::Lock lock2(fd_ctx->mutex);
// 若没有要删除的事件
if (!(fd_ctx->events & event)) {
return false;
}
// 将事件从注册事件中删除
Event new_events = (Event)(fd_ctx->events & ~event);
// 若还有事件则是修改,若没事件了则删除
int op = new_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
epoll_event epevent;
// 水平触发模式,新的注册事件
epevent.events = EPOLLET | new_events;
// ptr 关联 fd_ctx
epevent.data.ptr = fd_ctx;
// 注册事件
int rt = epoll_ctl(m_epfd, op, fd, &epevent);
if (rt) {
SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ","
<< op << ", " << fd << ", " << epevent.events << ") :"
<< rt << " (" << errno << ") (" << strerror(errno) << ")";
return false;
}
// 等待执行的事件数量-1
--m_pendingEventCount;
// 更新事件
fd_ctx->events = new_events;
// 拿到对应事件的EventContext
FdContext::EventContext& event_ctx = fd_ctx->getContext(event);
// 重置EventContext
fd_ctx->resetContext(event_ctx);
return true;
}
cancelEvent(取消事件)
取消事件会触发该事件
bool IOManager::cancelEvent(int fd, Event event) {
RWMutexType::ReadLock lock(m_mutex);
if ((int)m_fdContexts.size() <= fd) {
return false;
}
FdContext* fd_ctx = m_fdContexts[fd];
lock.unlock();
FdContext::MutexType::Lock lock2(fd_ctx->mutex);
if (!(fd_ctx->events & event)) {
return false;
}
Event new_events = (Event)(fd_ctx->events & ~event);
int op = new_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
epoll_event epevent;
epevent.events = EPOLLET | new_events;
epevent.data.ptr = fd_ctx;
int rt = epoll_ctl(m_epfd, op, fd, &epevent);
if (rt) {
SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ","
<< op << ", " << fd << ", " << epevent.events << ") :"
<< rt << " (" << errno << ") (" << strerror(errno) << ")";
return false;
}
// 触发当前事件
fd_ctx->triggerEvent(event);
--m_pendingEventCount;
return true;
}
cancelAll(取消所有事件)
取消事件会触发该事件
bool IOManager::cancelAll(int fd) {
RWMutexType::ReadLock lock(m_mutex);
if ((int)m_fdContexts.size() <= fd) {
return false;
}
FdContext* fd_ctx = m_fdContexts[fd];
lock.unlock();
FdContext::MutexType::Lock lock2(fd_ctx->mutex);
if (!fd_ctx->events) {
return false;
}
// 删除操作
int op = EPOLL_CTL_DEL;
epoll_event epevent;
// 没有事件
epevent.events = 0;
epevent.data.ptr = fd_ctx;
int rt = epoll_ctl(m_epfd, op, fd, &epevent);
if (rt) {
SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ","
<< op << ", " << fd << ", " << epevent.events << ") :"
<< rt << " (" << errno << ") (" << strerror(errno) << ")";
return false;
}
// 有读事件执行读事件
if (fd_ctx->events & READ) {
fd_ctx->triggerEvent(READ);
--m_pendingEventCount;
}
// 有写事件执行写事件
if (fd_ctx->events & WRITE) {
fd_ctx->triggerEvent(WRITE);
--m_pendingEventCount;
}
SYLAR_ASSERT(fd_ctx->events == 0);
return true;
}
GetThis(获得当前IO调度器)
IOManager* IOManager::GetThis() {
return dynamic_cast<IOManager *>(Scheduler::GetThis());
}
tickle(通知有任务)
void IOManager::tickle() {
// 没有在执行 idel 的线程
if (!hasIdleThread()) {
return;
}
// 有任务来了,就往 pipe 里发送1个字节的数据,这样 epoll_wait 就会唤醒
int rt = write(m_tickleFds[1], "T", 1);
SYLAR_ASSERT(rt == 1);
}
stopping(停止条件)
bool IOManager::stopping() {
uint64_t timeout = 0;
return stopping(timeout);
}
bool IOManager::stopping(uint64_t& timeout) {
// 获得下次任务执行的时间
timeout = getNextTimer();
// 定时器为空 && 等待执行的事件数量为0 && scheduler可以stop
return timeout == ~0ull
&& m_pendingEventCount == 0
&& Scheduler::stopping();
}
idle(调度器无任务时执行)
void IOManager::idle() {
epoll_event* events = new epoll_event[64]();
// 使用智能指针托管events, 离开idle自动释放
std::shared_ptr<epoll_event> shared_events(events, [](epoll_event *ptr)
{ delete[] ptr; });
while (true) {
// 下一个任务要执行的时间
uint64_t next_timeout = 0;
// 获得下一个执行任务的时间,并且判断是否达到停止条件
if (stopping(next_timeout)) {
SYLAR_LOG_INFO(g_logger) << "name = " << getName()
<< ", idle stopping exit";
break;
}
int rt = 0;
do {
// 毫秒级精度
static const int MAX_TIMEOUT = 3000;
// 如果有定时器任务
if (next_timeout != ~0ull) {
// 睡眠时间为next_timeout,但不超过MAX_TIMEOUT
next_timeout = (int)next_timeout > MAX_TIMEOUT
? MAX_TIMEOUT : (int)next_timeout;
} else {
// 没定时器任务就睡眠MAX_TIMEOUT
next_timeout = MAX_TIMEOUT;
}
/*
* 阻塞在这里,但有3中情况能够唤醒epoll_wait
* 1. 超时时间到了
* 2. 关注的 soket 有数据来了
* 3. 通过 tickle 往 pipe 里发数据,表明有任务来了
*/
rt = epoll_wait(m_epfd, events, 64, (int)next_timeout);
/* 这里就是源码 ep_poll() 中由操作系统中断返回的 EINTR
* 需要重新尝试 epoll_Wait */
if(rt < 0 && errno == EINTR) {
} else {
break;
}
} while (true);
std::vector<std::function<void()> > cbs;
// 获取已经超时的任务
listExpiredCb(cbs);
// 全部放到任务队列中
if (!cbs.empty()) {
Scheduler::schedule(cbs.begin(), cbs.end());
cbs.clear();
}
// 遍历已经准备好的fd
for (int i = 0; i < rt; ++i) {
// 从 events 中拿一个 event
epoll_event &event = events[i];
// 如果获得的这个信息时来自 pipe
if (event.data.fd == m_tickleFds[0]) {
uint8_t dummy;
// 将 pipe 发来的1个字节数据读掉
while (read(m_tickleFds[0], &dummy, 1) == 1);
continue;
}
// 从 ptr 中拿出 FdContext
FdContext *fd_ctx = (FdContext*)event.data.ptr;
FdContext::MutexType::Lock lock(fd_ctx->mutex);
/* 在源码中,注册事件时内核会自动关注POLLERR和POLLHUP */
if (event.events & (EPOLLERR | EPOLLHUP)) {
// 将读写事件都加上
event.events |= (EPOLLIN | EPOLLOUT) & fd_ctx->events;
}
int real_events = NONE;
// 读事件好了
if (event.events & EPOLLIN) {
real_events |= READ;
}
// 写事件好了
if (event.events & EPOLLOUT) {
real_events |= WRITE;
}
// 没事件
if ((fd_ctx->events & real_events) == NONE) {
continue;
}
// 剩余的事件
int left_events = (fd_ctx->events & ~real_events);
// 如果执行完该事件还有事件则修改,若无事件则删除
int op = left_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
// 更新新的事件
event.events = EPOLLET | left_events;
// 重新注册事件
int rt2 = epoll_ctl(m_epfd, op, fd_ctx->fd, &event);
if (rt2) {
SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ","
<< op << ", " << fd_ctx->fd << ", " << event.events << ") :"
<< rt2 << " (" << errno << ") (" << strerror(errno) << ")";
continue;
}
// 读事件好了,执行读事件
if (real_events & READ) {
fd_ctx->triggerEvent(READ);
--m_pendingEventCount;
}
// 写事件好了,执行写事件
if (real_events & WRITE) {
fd_ctx->triggerEvent(WRITE);
--m_pendingEventCount;
}
}
// 执行完epoll_wait返回的事件
// 获得当前协程
Fiber::ptr cur = Fiber::GetThis();
// 获得裸指针
auto raw_ptr = cur.get();
// 将当前idle协程指向空指针,状态为INIT
cur.reset();
// 执行完返回scheduler的MainFiber 继续下一轮
raw_ptr->swapOut();
}
}
总结
举例说明:
int sock = 0;
void test_fiber() {
SYLAR_LOG_INFO(g_logger) << "test_fiber";
sock = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sock, F_SETFL, O_NONBLOCK);
sockaddr_in addr;
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(80);
inet_pton(AF_INET, "112.80.248.75", &addr.sin_addr.s_addr);
if (!connect(sock, (const sockaddr*)&addr, sizeof(addr))) {
} else if(errno == EINPROGRESS) {
SYLAR_LOG_INFO(g_logger) << "add event errno=" << errno << " " << strerror(errno);
sylar::IOManager::GetThis()->addEvent(sock, sylar::IOManager::READ, [](){
SYLAR_LOG_INFO(g_logger) << "read callback";
char temp[1000];
int rt = read(sock, temp, 1000);
if (rt >= 0) {
std::string ans(temp, rt);
SYLAR_LOG_INFO(g_logger) << "read:["<< ans << "]";
} else {
SYLAR_LOG_INFO(g_logger) << "read rt = " << rt;
}
});
sylar::IOManager::GetThis()->addEvent(sock, sylar::IOManager::WRITE, [](){
SYLAR_LOG_INFO(g_logger) << "write callback";
int rt = write(sock, "GET / HTTP/1.1\r\ncontent-length: 0\r\n\r\n",38);
SYLAR_LOG_INFO(g_logger) << "write rt = " << rt;
});
} else {
SYLAR_LOG_INFO(g_logger) << "else " << errno << " " << strerror(errno);
}
}
void test01() {
sylar::IOManager iom(2, true, "IOM");
iom.schedule(test_fiber);
}
int main(int argc, char** argv) {
g_logger->setLevel(sylar::LogLevel::INFO);
test01();
return 0;
}
2023-06-07 17:11:40 901 IOM_0 5 [INFO] [root] tests/test_iomanager.cc:15 test_fiber
2023-06-07 17:11:40 901 IOM_0 5 [INFO] [root] tests/test_iomanager.cc:28 add event errno=115 Operation now in progress
2023-06-07 17:11:40 901 IOM_0 5 [INFO] [root] tests/test_iomanager.cc:52 test_fiber end
2023-06-07 17:11:40 901 IOM_0 5 [INFO] [root] tests/test_iomanager.cc:42 write callback
2023-06-07 17:11:40 901 IOM_0 5 [INFO] [root] tests/test_iomanager.cc:44 write rt = 38
2023-06-07 17:11:40 900 IOM 6 [INFO] [root] tests/test_iomanager.cc:31 read callback
2023-06-07 17:11:40 900 IOM 6 [INFO] [root] tests/test_iomanager.cc:36 read:
[HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: no-cache
Connection: keep-alive
Content-Length: 9508
Content-Security-Policy: frame-ancestors 'self' https://chat.baidu.com http://mirror-chat.baidu.com https://fj-chat.baidu.com https://hba-chat.baidu.com https://hbe-chat.baidu.com https://njjs-chat.baidu.com https://nj-chat.baidu.com https://hna-chat.baidu.com https://hnb-chat.baidu.com http://debug.baidu-int.com;
Content-Type: text/html
Date: Wed, 07 Jun 2023 09:11:41 GMT
P3p: CP=" OTI DSP COR IVA OUR IND COM "
P3p: CP=" OTI DSP COR IVA OUR IND COM "
Pragma: no-cache
Server: BWS/1.1
Set-Cookie: BAIDUID=6102D94CFB4C3C8E44DBD4BAA6A58951:FG=1; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
Set-Cookie: BIDUPSID=6102D94CFB4C3C8E44DBD4BAA6A58951; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
]et-Cookie: PSTM=1686129101; expires=Thu, 31-Dec-37 23:55:55 GMT; max-age=2147483647; path=/; domain=.baidu.com
2023-06-07 17:11:40 900 IOM 4 [INFO] [system] sylar/iomanager.cc:303 IOM is done
2023-06-07 17:11:43 901 IOM_0 3 [INFO] [system] sylar/iomanager.cc:303 IOM_0 is done
- 在
test01()
中创建了IO协程调度器,并且会start()
启动1个线程(901)执行任务run()
,然后在出函数时析构执行stop()
让当前线程(900)去执行任务run()
。 - 当线程901在
run()
中拿到test_fiber
执行cb_fiber
协程,它创建socket
,与百度建立连接,并且注册了读事件和写事件,此时test_fiber
执行完毕,回到run()
中,继续执行idle()
。 - 在
idle()
中epoll_wait()
监听到写事件,所以将写事件的回调函数加入到任务队列中,然后交出自己的执行权,回到run()
中。 - 线程901拿到了写任务,执行写操作,然后继续陷入
idle()
中。 - 在
idle()
中,当消息回来时,epoll_wait()
监听到了读事件,此时将读事件的回调函数加入到任务队列中,然后交出自己的执行权,回到run()
中。 - 线程900拿到了读任务,执行写操作,然后继续陷入
idle()
中,但此时所有任务已经结束了,达到结束条件。与此同时,由于已经没有事件能够唤醒epoll_wait()
,所以线程901在idle()
中睡了3秒,等线程900处理完读任务后达到了停止条件,也结束了。 - 此时任务都已经执行完毕了,达到了停止条件,
idle()
结束,线程901结束任务,线程900回到stop()
中,继续完成析构函数,此时test01()
真正的结束了。