P36-P39:IO协程调度01-04
前面4节主要内容在协程调度的基础上,基于epoll
设计了IO协程调度,支持为socket句柄加读事件(EPOLLIN
)和写事件(EPOLLOUT
),并且支持删除事件、取消事件功能。IOManager
主要通过FdContext
结构体存储文件描述符fd
、注册的事件event
,执行任务cb/fiber
,其中fd
和event
用于epoll_wait
,cb/fiber
用于执行任务。
在学习几节内容之前,了解一下epoll
的相关概念理解代码会轻松一些,网上都有很多资料,这里我直接引用一位博主概括的笔记(epoll),写得很不错。
一、Epoll
I/O模式
在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
/*
@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的缺点
- 在于单个进程能够监视的文件描述符的数量存在最大限制,在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
将revents
置位(POLLIN
等),然后poll
函数返回。仍然要遍历数组来看是哪个文件描述符来了,并且将revents
置为0,这样就能重复使用pollfd
。
poll
优点
- 解决了
select
的1024上限 - 解决了
select fd_set
不可重用,pollfd
可以通过重置revents
恢复如初
poll
缺点
- 每次调用
poll
都需要将pollfd
拷贝到内核态,有开销 - 不知道是哪个文件描述符的数据来了,所以要遍历,时间复杂度为O(n)
epoll
相对于select
和poll
来说,epoll
更加灵活,没有描述符限制。epoll
使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy
只需一次。
-
epoll_create
//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大 int epoll_create(int size);
这个参数不同于``select()
中的第一个参数,给出最大监听的
fd+1的值,参数
size并不是限制了
epoll`所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议(大于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队列里
使用
copy_from_user
从用户空间将epoll_event
结构copy到内核空间。**if** (ep_op_has_event(op) && copy_from_user(&epds, **event**, **sizeof**(**struct** epoll_event)))
-
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_create:在创建
epoll
句柄时,从slab
缓存中创建一个epollevent
对象,epollfd
本身并不存在一个真正的文件与之对应, 所以内核需要创建一个"虚拟"的文件, 并为之分配真正的struct file
结构, 而且有真正的fd
,而eventpoll
对象保存在struct file
结构的private_data
指针中。返回epollfd
的文件描述符。 -
epoll_ctl:将
epoll_event
结构拷贝到内核空间中并且判断加入的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
就唤醒谁。 -
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
中去。
有点懵
三、Epoll工作模式
LT模式
当epoll_wait
检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait
时,会再次响应应用程序并通知此事件。
LT是缺省的工作方式,并且同时支持block socket
和no-block socket
。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd
进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
ET模式
当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只需一次。
五、Class IOManager
该类继承于Scheduler
事件类型
主要把socket
的事件归类为读事件和写事件
enum Event { // 事件类型
NONE = 0x0, // 无事件
READ = 0x1, // EPOLLIN
WRITE = 0x4 // EPOLLOUT
};
SOCKET事件上下文结构体
struct FdContext {
typedef Mutex MutexType;
// 事件类
struct EventContext {
Scheduler* scheduler = nullptr; // 事件执行的调度器
Fiber::ptr fiber; // 事件协程
std::function<void()> cb; // 事件的回调函数
};
// 获得事件上下文
EventContext& getContext(Event event);
// 重置事件上下文
void resetContext(EventContext& ctx);
// 触发事件
void triggerEvent(Event event);
int fd = 0; // 事件关联的句柄
EventContext read; // 读事件
EventContext write; // 写事件
Event events = NONE; // 已注册的事件
MutexType mutex;
};
成员变量
int m_epfd = 0; // epoll文件句柄
int m_tickleFds[2]; // pipe文件句柄,其中fd[0]表示读端,fd[1] 表示写端
std::atomic<size_t> m_pendingEventCount = {0}; // 等待执行的事件数量
RWMutexType m_mutex; // 互斥锁
std::vector<FdContext*> m_fdContexts; //socket事件上下文容器
构造函数
IOManager::IOManager(size_t threads, bool use_caller, const std::string& name)
: Scheduler(threads, use_caller, name) {
// 创建一个epollfd
m_epfd = epoll_create(5000);
// 断言是否成功
SYLAR_ASSERT(m_epfd > 0);
// 创建管道用于进程通信
int rt = pipe(m_tickleFds);
SYLAR_ASSERT(!rt);
// 创建事件并初始化
epoll_event 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); // fcntl可以改变句柄的属性
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();
}
析构函数
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];
}
}
}
添加事件
int IOManager::addEvent(int fd, Event event, std::function<void()> cb) {
// 初始化一个 FdContext
FdContext* fd_ctx = nullptr;
// 加读锁
RWMutexType::ReadLock lock(m_mutex);
// 先判断句柄是否超出范围
if((int)m_fdContexts.size() > fd) {
// 空间足够
fd_ctx = m_fdContexts[fd];
lock.unlock();
} else {
// 空间不够就扩容1.5倍
lock.unlock();
RWMutexType::WriteLock lock2(m_mutex);
contextResize(fd * 1.5);
fd_ctx = m_fdContexts[fd];
}
// 设置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事件
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.cb && !event_ctx.fiber);
// 对event_ctx的三个成员赋值
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;
}
删除事件
bool IOManager::delEvent(int fd, Event event) {
// 加读锁
RWMutexType::ReadLock lock(m_mutex);
// 如果存放fd上下文的容器数量大小小于当前要删除事件的fd,说明出现问题
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;
}
取消事件
和删除事件类似,唯一的区别在于最后直接触发该事件
fd_ctx->triggerEvent(event);
-- m_pendingEventCount;
取消所有事件
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;
}
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) {
// 睡眠时间不能超过MAX_TIMEOUT
next_timeout = (int)next_timeout > MAX_TIMEOUT ? MAX_TIMEOUT : 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);
// 操作系统中断会返回EINTR,然后重新epoll_wait
if(rt < 0 && errno == EINTR) {
} else {
break;
}
} while(true);
// 找到那些需要执行的定时器,这里调用listExpiredCb返回的应该是那些超时的定时器,难道是超时代表需要在当前时间去处理吗?我之前理解的是超时就丢弃了
std::vector<std::function<void()>> cbs;
listExpiredCb(cbs);
if(!cbs.empty()) {
// 把那些超时任务全部放到队列中去
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;
// 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();
cur.reset();
// 执行完返回scheduler的MainFiber 继续下一轮
raw_ptr->swapOut();
}
}
六、测试内容
#include"sylar/sylar.h"
#include"sylar/iomanager.h"
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<unistd.h>
#include<fcntl.h>
#include<iostream>
#include<sys/epoll.h>
sylar::Logger::ptr g_logger = SYLAR_LOG_ROOT();
int sock = 0;
void test_fiber() {
SYLAR_LOG_INFO(g_logger) << "test_fiber sock = " << sock;
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, "183.2.172.185", &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";
});
sylar::IOManager::GetThis()->addEvent(sock, sylar::IOManager::WRITE, []() {
SYLAR_LOG_INFO(g_logger) << "write callback";
sylar::IOManager::GetThis()->cancelEvent(sock, sylar::IOManager::READ);
close(sock);
});
} else {
SYLAR_LOG_INFO(g_logger) << "else " << errno << " " << strerror(errno);
}
}
void test1() {
sylar::IOManager iom(2,true);
iom.schedule(&test_fiber);
}
int main(int argc, char** argv) {
test1();
return 0;
}
P40 -P41:定时器01-03
参考笔记(由于下半年要参加秋招了,小论文也还没开始写,没有时间像之前那样慢慢debug,导致代码很多理解得不透彻,所以现在的笔记都参考前面链接博主写的,打算第二遍归纳面试问题时再自己整理)
定时器这块还比较容易理解,核心就是在规定的时间结束后调用回调函数(需要执行的子任务)。定时器的设计采用时间堆方式,将所有定时器按照最小堆的方式排列,能够简单的获得当前超时时间最小的定时器,计算出超时需要等待的时间,然后等待超时。超时时间到后,获取当前的绝对时间,并且把时间堆中已经超时的所有定时器都收到一个容器中,执行他们的回调函数。
在定时器的时间计算中,都是使用绝对时间来计算的,按照绝对时间对定时器进行排序。在IOmanager
中,将定时器与epoll_wait
的超时时间相结合,等待时间为定时器中超时时间最小的时间。若是超时导致epoll_wait
返回,那么一定是定时器设置的超时时间到了,把已经超时的定时器的任务加入到任务队列中去执行,但也有可能是别的信号使epoll_wait
返回,那么仍然需要判断是否有定时器超时,所以要比较当前的绝对时间与定时器的绝对时间来判断有无定时器超时。
一、class Timer
成员变量
bool m_recurring = false; // 是否循环定时器
uint64_t m_ms = 0; // 执行周期
uint64_t m_next = 0; // 精确的执行时间
std::function<void()> m_cb; // 回调函数
TimerManager* m_manager = nullptr; // 定时器管理器
比较定时器的大小
struct Comparator {
bool operator() (const Timer::ptr& lhs, const Timer::ptr& rhs) const;
};
bool Timer::Comparator::operator() (const Timer::ptr& lhs, const Timer::ptr& rhs) const {
// 两个都为空返回false
if(!lhs && !lhs) return false;
// 左边为空
if(!lhs) return true;
// 右边为空
if(!rhs) return false;
// 如果左边的执行时间 < 右边的执行时间
if(lhs->m_next < rhs->m_next) return true;
if(rhs->m_ms < lhs->m_ms) return false;
// 如果时间都一样,则比较地址的大小
return lhs.get() < rhs.get();
}
Timer(构造函数)
只能由TimerManager
创建Timer
// Timer构造函数
Timer::Timer(uint64_t ms, std::function<void()> cb, bool recurring, TimerManager* manager)
: m_recurring(recurring)
, m_ms(ms)
, m_cb(cb)
, m_manager(manager) {
m_next = sylar::GetCurrentMS() + m_ms;
}
Timer::Timer(uint64_t next)
: m_next(next) {
}
cancel(取消定时器)
bool Timer::cancel() {
TimerManager::RWMutextType::WriteLock lock(m_manager->m_mutex);
// 如果有回调函数
if(m_cb) {
// 置空
m_cb = nullptr;
// 从定时器管理器中找到需要取消的定时器
auto it = m_manager->m_timers.find(shared_from_this());
// 删除
m_manager->m_timers.erase(it);
return true;
}
return false;
}
refresh(刷新执行时间)
bool Timer::refresh() {
TimerManager::RWMutextType::WriteLock lock(m_manager->m_mutex);
// 如果没有需要执行的回调函数,直接返回刷新失败
if(!m_cb) {
return false;
}
// 找到需要刷新的定时器
auto it = m_manager->m_timers.find(shared_from_this());
if(it == m_manager->m_timers.end()) {
return false;
}
// 一定要先删除再刷新,因为set会自动排序,先刷新后之前获得的迭代器就失效了
m_manager->m_timers.erase(it);
m_next = sylar::GetCurrentMS() + m_ms;
m_manager->m_timers.insert(shared_from_this());
return true;
}
reset(重置定时器时间)
from_now
是否从当前时间开始计算
bool Timer::reset(uint64_t ms, bool from_now) {
// 若周期相同并且不按当前时间计算
if(ms == m_ms && !from_now) {
return true;
}
TimerManager::RWMutextType::WriteLock lock(m_manager->m_mutex);
// 如果没有需要执行的回调函数,直接返回重置失败
if(!m_cb) {
return false;
}
// 找到需要重置的定时器
auto it = m_manager->m_timers.find(shared_from_this());
if(it == m_manager->m_timers.end()) {
return false;
}
// 还是先删除
m_manager->m_timers.erase(it);
// 定义一个起始时间
uint64_t start = 0;
// 从现在开始算
if(from_now) {
start = sylar::GetCurrentMS();
} else {
// 起始时间为当时创建时的起始时间
// m_next = sylar::GetCurrentMS() + m_ms;
start = m_next - m_ms;
}
// 更新
m_ms = ms;
m_next = start + m_ms;
m_manager->addTimer(shared_from_this(), lock);
return true;
}
二、class TimerManager
成员变量
RWMutextType m_mutex;
std::set<Timer::ptr, Timer::Comparator> m_timers; // 定时器集合
bool m_tickled = false; // 是否触发onTimerInsertedAtFront
uint64_t m_previouseTime = 0; // 上一次执行时间
构造函数
TimerManager::TimerManager() {
m_previouseTime = sylar::GetCurrentMS();
}
addTimer(添加定时器)
Timer::ptr TimerManager::addTimer(uint64_t ms, std::function<void()> cb, bool recurring) {
// 创建定时器
Timer::ptr timer(new Timer(ms, cb, recurring, this));
RWMutextType::WriteLock lock(m_mutex);
// 添加到定时器集合中
addTimer(timer,lock);
return timer;
}
void TimerManager::addTimer(Timer::ptr val,RWMutextType::WriteLock& lock) {
// 添加到定时器集合
auto it = m_timers.insert(val).first;
// 如果该定时器是超时时间最短 并且 没有设置触发onTimerInsertedAtFront
bool at_front = (it == m_timers.begin() && !m_tickled);
if(at_front) {
// 设置触发onTimerInsertedAtFront
m_tickled = true; // 频繁修改时不用一直执行onTimerInsertedAtFront,提高效率
}
lock.unlock();
// 触发onTimerInsertedAtFront
if(at_front) {
// 目前该函数只做了一次tickle
onTimerInsertedAtFront();
}
}
addConditionTimer(添加条件定时器)
weak_ptr
是一种弱引用,它不会增加所指对象的引用计数,也不会阻止所指对象被销毁。
而shared_ptr
是一种强引用,它会增加所指对象的引用计数,直到所有shared_ptr
都被销毁才会释放所指对象的内存。
在这段代码中,weak_cond
是一个weak_ptr
类型的对象,通过调用它的lock()
方法可以得到一个shared_ptr
类型的对象tmp
,如果weak_ptr
已经失效,则lock()
方法返回一个空的shared_ptr
对象。
static void OnTimer(std::weak_ptr<void> weak_cond, std::function<void()> cb) {
// 使用weak_cond的lock函数获取一个shared_ptr指针tmp
std::shared_ptr<void> tmp = weak_cond.lock();
// 如果tmp不为空,则调用回调函数cb。
if(tmp) {
cb();
}
}
Timer::ptr TimerManager::addConditionTimer(uint64_t ms, std::function<void()> cb, std::weak_ptr<void> weak_cond, bool recurring) {
// 在定时器触发时会调用 OnTimer 函数,并在OnTimer函数中判断条件对象是否存在,如果存在则调用回调函数cb。
return addTimer(ms, std::bind(&OnTimer, weak_cond, cb), recurring);
}
getNextTimer(最近一个定时器执行的时间间隔)
uint64_t TimerManager::getNextTimer() {
RWMutextType::ReadLock lock(m_mutex);
// 不触发 onTimerInsertedAtFront
m_tickled = false;
// 如果没有定时器要执行,返回一个最大数
if(m_timers.empty()) {
return ~0ull;
}
// 拿到第一个定时器
const Timer::ptr& next = *m_timers.begin();
uint64_t now_ms = sylar::GetCurrentMS();
// 如果当前的时间超过了定时器执行的时间,说明由于某种原因耽搁了定时器的执行,直接返回0立即执行
if(now_ms >= next->m_next) {
return 0;
// 否则返回下一个定时器需要等待的时间
} else {
return next->m_next - now_ms;
}
}
listExpiredCb (返回那些需要执行的定时器)
void TimerManager::listExpiredCb(std::vector<std::function<void()>>& cbs) {
// 获得当前的时间用于对比
uint64_t now_ms = sylar::GetCurrentMS();
// 定义需要执行的定时器数组
std::vector<Timer::ptr> expired;
{
RWMutextType::ReadLock lock(m_mutex);
// 如果定时器为空则直接返回
if(m_timers.empty()) {
return;
}
}
RWMutextType::WriteLock lock(m_mutex);
// 判断服务器时间是否被调整
bool rollover = detectClockRollover(now_ms);
// 如果服务器时间没问题,并且第一个定时器都没有到执行时间,就说明没有任务需要执行
if(!rollover && ((*m_timers.begin())->m_next > now_ms)) {
return;
}
// 记录当前的时间
Timer::ptr now_timer(new Timer(now_ms));
// 找到定时器容器中第一个小于当前时间的值
auto it = rollover ? m_timers.end() : m_timers.lower_bound(now_timer);
// 这一步是为了把定时器容器中与当前时间相同的定时器包含进来
while(it != m_timers.end() && (*it)->m_next == now_ms) {
++ it;
}
// 往超时容器中填充
expired.insert(expired.begin(), m_timers.begin(), it);
// 删除原来定时器容器中的已超时的定时器
m_timers.erase(m_timers.begin(), it);
cbs.reserve(expired.size());
// 将expired的timer放入到cbs中,这样在其它函数中调用listExpiredCb 就可以去执行这些回调函数
for(auto& timer : expired) {
cbs.push_back(timer->m_cb);
// 如果是循环定时器,则再次放入定时器集合中
if(timer->m_recurring) {
timer->m_next = now_ms + timer->m_ms;
m_timers.insert(timer);
} else {
timer->m_cb = nullptr;
}
}
}
detectClockRollover(检测服务器时间是否被调后)
bool TimerManager::detectClockRollover(uint64_t now_ms) {
bool rollover = false;
// 如果当前时间比上次执行时间还小 并且 小于一个小时的时间,相当于时间倒流了
if(now_ms < m_previouseTime && now_ms < (m_previouseTime - 60 * 60 * 1000)) {
rollover = true;
}
// 更新上次执行时间
m_previouseTime = now_ms;
return rollover;
}
三、测试
void test_timer() {
sylar::IOManager iom(2);
s_timer = iom.addTimer(1000, []() {
static int i = 0;
SYLAR_LOG_INFO(g_logger) << "hello timer i = " << i;
if(++i == 3) {
s_timer->reset(3000, false);
// s_timer->cancel();
}
}, true);
}
int main(int argc, char** argv) {
// test1();
test_timer();
return 0;
}
上面测试代码中,我们定义的定时器会按照1s为执行周期,循环去执行回调函数(也就是任务)输出hello timer i =,当i等于3时,把执行周期更新为3s,继续执行直到手动中断。