这个作业属于哪个课程 | 广工2023软件工程课程 |
---|---|
这个作业要求在哪里 | 作业要求 |
文章内容 | 第六篇敏捷冲刺 |
目录
一、任务分配与预期任务量
模块 | 主要负责人 |
---|---|
日志模块 | 钟海超 |
配置模块 | 李昊旃 |
线程模块 | 江周勉 |
协程模块 | 宫旭 |
协程调度模块 | 赵光明 |
I/O协程调度模块 | 李伟东 |
Hook模块 | 邱棋浩(组长) |
二、近日任务安排
时间 | 任务 |
---|---|
今天 | I/O协程调度模块 |
明天 | Hook模块 |
三、具体概念
3.1 什么是IO
IO本质是作为输入输出的一个管理。
IO的对象:文件(file), 网络(socket),进程之间的管道(pipe),文件描述符(fd)
我们整个IO协程调度模块的实现,其实本质上是再对Linux中的epoll进行一个改写。
3.1 事件
可读事件,当文件描述符关联的内核读缓冲区可读,则触发可读事件。(可读:内核缓冲区非空,有数据可以读取)
可写事件,当文件描述符关联的内核写缓冲区可写,则触发可写事件。(可写:内核缓冲区不满,有空闲空间可以写入)
3.2 Epoll
epoll是一种I/O事件通知机制,是linux 内核实现IO多路复用的一个实现。其使用一组函数来完成任务,将用户关心的文件描述符上的事件放在内核里的一个时间表中,无需像select和poll每次调用都要重复传入文件描述符集或事件集。epoll需要使用一个额外的文件描述符,来比唯一标识内核中的事件表。
IO多路复用是指,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。
epoll的通俗解释是一种当文件描述符的内核缓冲区非空的时候,发出可读信号进行通知,当写缓冲区不满的时候,发出可写信号通知的机制
3.2 .1 op操作类型
op参数说明操作类型: | |
EPOLL_CTL_ADD | 往事件表注册fd上的事件 |
EPOLL_CTL_DEL | 删除fd上的事件 |
EPOLL_CTL_MOD | 修改fd上的注册事件 |
3.2 .2 epoll函数
int epoll_create(int size) | 内核会产生一个epoll 实例数据结构并返回一个文件描述符,size:要监听文件描述符的最大值 |
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) | 将被监听的描述符添加到红黑树或从红黑树中删除或者对监听事件进行修改 |
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) | 阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中。 处于ready状态的那些文件描述符会被复制进ready list中,epoll_wait用于向用户进程返回ready list。 |
3.2 .3 epoll事件描述
常用的epoll事件描述如下: | |
EPOLLIN | 描述符处于可读状态 |
EPOLLOUT | 描述符处于可写状态 |
EPOLLET | 将epoll event通知模式设置成edge triggered |
EPOLLONESHOT | 第一次进行通知,之后不再监测 |
EPOLLHUP | 本端描述符产生一个挂断事件,默认监测事件 |
EPOLLRDHUP | 对端描述符产生一个挂断事件 |
EPOLLPRI | 由带外数据触发 |
EPOLLERR | 描述符产生错误时触发,默认检测事件 |
3.2 .4 epoll数据结构定义
// epoll_event结构定义
struct epoll_event{
__uint32_t events; // epoll事件
epoll_data_t data; // 用户数据
};
// epoll_data_t是一个联合体,fd指定事件从属的目标描述符;ptr指定fd相关用户数据。
typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
四、实现
4.1 实现的功能
继承与协程调度器,封装了epoll(Linux),并支持定时器功能(使用epoll实现定时器,精度毫秒级),支持Socket读写时间的添加,删除,取消功能。支持一次性定时器,循环定时器,条件定时器等功能
OManger(epoll) ---> Scheduler
|
|
|
idle (epoll_wait)
PutMessage(msg,) + 信号量1,single()
message_queue
|
|---- Thread
|---- Thread
wait()-信号量1, RecvMessage(msg,)
异步IO,等待数据返回,epoll_wait
epoll_create, epoll_ctl,epoll_wait
4.2 会使用到的模块
Scheduler.h // 调度模块
Log.h // 日志模块
4.3 结构体的定义
struct FdContext {
typedef Mutex MutexType;
struct EventContext {
Scheduler* scheduler = nullptr; // 事件执行的scheduler
Fiber::ptr fiber; // 事件协程
std::function<void()> cb; // 事件的回调函数
};
EventContext& getContext(Event event); // 获取事件的类型
void resetContext(EventContext& ctx); // 重置事件
void triggerEvent(EventContext& ctx); // 触发事件
EventContext read; // 读事件
EventContext write; // 写事件
int fd = 0; // 事件关联的句柄
Event events = None; // 已经注册的事件
MutexType mutex;
}
4.4 所需要实现的头文件
需要实现的接口
namespace sylar {
/**
* @brief 基于Epoll的IO协程调度器
*/
class IOManager : public Scheduler, public TimerManager {
public:
typedef std::shared_ptr<IOManager> ptr;
typedef RWMutex RWMutexType;
/**
* @brief IO事件
*/
enum Event {
/// 无事件
NONE = 0x0,
/// 读事件(EPOLLIN)
READ = 0x1,
/// 写事件(EPOLLOUT)
WRITE = 0x4,
};
private:
/**
* @brief Socket事件上线文类
*/
struct FdContext {
typedef Mutex MutexType;
/**
* @brief 事件上线文类
*/
struct EventContext {
/// 事件执行的调度器
Scheduler* scheduler = nullptr;
/// 事件协程
Fiber::ptr fiber;
/// 事件的回调函数
std::function<void()> cb;
};
/**
* @brief 获取事件上下文类
* @param[in] event 事件类型
* @return 返回对应事件的上线文
*/
EventContext& getContext(Event event);
/**
* @brief 重置事件上下文
* @param[in, out] ctx 待重置的上下文类
*/
void resetContext(EventContext& ctx);
/**
* @brief 触发事件
* @param[in] event 事件类型
*/
void triggerEvent(Event event);
/// 读事件上下文
EventContext read;
/// 写事件上下文
EventContext write;
/// 事件关联的句柄
int fd = 0;
/// 当前的事件
Event events = NONE;
/// 事件的Mutex
MutexType mutex;
};
public:
/**
* @brief 构造函数
* @param[in] threads 线程数量
* @param[in] use_caller 是否将调用线程包含进去
* @param[in] name 调度器的名称
*/
IOManager(size_t threads = 1, bool use_caller = true, const std::string& name = "");
/**
* @brief 析构函数
*/
~IOManager();
/**
* @brief 添加事件
* @param[in] fd socket句柄
* @param[in] event 事件类型
* @param[in] cb 事件回调函数
* @return 添加成功返回0,失败返回-1
*/
int addEvent(int fd, Event event, std::function<void()> cb = nullptr);
/**
* @brief 删除事件
* @param[in] fd socket句柄
* @param[in] event 事件类型
* @attention 不会触发事件
*/
bool delEvent(int fd, Event event);
/**
* @brief 取消事件
* @param[in] fd socket句柄
* @param[in] event 事件类型
* @attention 如果事件存在则触发事件
*/
bool cancelEvent(int fd, Event event);
/**
* @brief 取消所有事件
* @param[in] fd socket句柄
*/
bool cancelAll(int fd);
/**
* @brief 返回当前的IOManager
*/
static IOManager* GetThis();
protected:
void tickle() override;
bool stopping() override;
void idle() override;
void onTimerInsertedAtFront() override;
/**
* @brief 重置socket句柄上下文的容器大小
* @param[in] size 容量大小
*/
void contextResize(size_t size);
/**
* @brief 判断是否可以停止
* @param[out] timeout 最近要出发的定时器事件间隔
* @return 返回是否可以停止
*/
bool stopping(uint64_t& timeout);
private:
/// epoll 文件句柄
int m_epfd = 0;
/// pipe 文件句柄
int m_tickleFds[2];
/// 当前等待执行的事件数量
std::atomic<size_t> m_pendingEventCount = {0};
/// IOManager的Mutex
RWMutexType m_mutex;
/// socket事件上下文的容器
std::vector<FdContext*> m_fdContexts;
};
}
4.5 IOManager
IOManager(size_t threads = 1, bool use_caller = true, const std::string& name = "");
整个IOManager主要分为:线程数,调用者,命名规则
- 首先产生利用 epoll_create()方法来生成5000大小的文件描述符
- 随后将EventContext进行一个初始化
- 最后开启计时器
4.5.1 IOManager析构
- 关闭计时器
- 释放所开辟的空间
4.5.2 添加事件
- 首先取出fd上下文的类,然后判断上下文的状态。
- 对整个进程上一个读锁,如果空间足够fd使用的话就直接分配给fd,随后解除读锁。
- 如果空间不够的话,将读锁解开上写锁,随后调用ContextResize()函数来扩充空间,再进行fd的分配。
- 如果当前的事件与之前的事件相同的话,表明线程是危险的,这时候我们将这种情况存储到日志当中并且退出。
- 那么我们对当前的操作进行一个判断,监测当前的事件是否需要添加,然后将操作符添加到事件监听树当中,如果需要添加则开始添加操作,先得判断进程,协程和回调都得是空闲的才能够正常的添加,否则就返回日志并返回-1。
4.5.3 删除事件
注:删除事件是不会触发事件的
- 跟添加事件的逻辑差不多,首先我们要对信号量上一个读锁,进行写的操作。
- 如果要删除的文件比存储的文件内容要多的话,表明无法删除那么结束直接结束函数。如果可以删除的话我们获得要删除的文件的索引位置,将之前的读锁打开,对当前的文件上锁进行删除操作,如果当前文件的事件是不存在的,也是无法正常删除的
- 同样的我们获得当前事件的操作数,并将其添加到监听树当中,将待定事件当中移除当前事件然后将事件删除
4.5.4 取消事件/所有事件
取消事件和取消所有的事件都会触发一次事件
整体逻辑和取消事件基本一致,我们只需要在将删除操作改成触发操作即可
4.5.5 虚函数
tickle
判断是否有空闲的进程,如果当前有空闲的进程那么tikcle会向m_tickleFds写一个字节作为通知
stopping
对于IOManager而言,必须得等所有待调度的IO事件都执行完了才可以退出
idle
idle是协程调度器没有任务的时候,即线程啥也不干的时候进行,即调度器无调度任务时会阻塞idle协程上。
对IO调度器而言,idle状态应该关注两件事
- 有没有新的调度任务,对应Schduler::schedule(),如果有新的调度任务,那应该立即退出idle状态,并执行对应的任务;
- 关注当前注册的所有IO事件有没有触发,如果有触发,那么应该执行IO事件对应的回调函数
我们规定用epoll_event来存储事件,并用一个智能指针来表示它(方便后续的释放)
由于我们的epoll_wait是水平触发的,所以我们读取事件的时候要读取完全。
整体的实现思路如下:
- 🤚协程调度器没有任务了,检查epoll_wait当中有哪些事件可以唤醒。
- 对于可以唤醒的进程,我们完整的将事件读入到epoll_event当中存储
- 判断事件的操作类型,为了防止我们的数据没有读完全,我们设置一个一个剩余事件的变量,用于避免事件漏存储用于后续的处理
- 我们根据事件的类型以及事件的上下文fd_ctx去触发相应的事件类型
- 唤醒进程处理的同时,我们要让出进程的控制权,交给Scheduler去做一些外部的处理。
4.6 定时器
4.6.1 为什么需要定时器:
对于一个服务器来说,IO的输入都是基于网络层面来进行的,所以经常会在一定的时间段内执行任务,或者持续的执行一系列的任务,都需要有定时器来完成;
定时器通常至少包含两个成员:超时时间和任务回调函数。如果用链表作为容器来串联所有的定时器,则每个定时器还要包含指向下一个定时器的指针成员(单向链表)。基于时间堆的定时器设计则是将所有定时器中超时时间最小的一个定时器的超时值作为心搏间隔。这样一旦心搏函数tick被调用,超时时间最小的定时器必然到期,从而处理该定时器。依次反复,实现精确定时。
定时器是一个有序的列表,根据智能指针的存在与否来判断是否还需要定时器
循环定时,间隔执行,条件定时器(周期性的执行一定事情)
4.6.2 实现的思路
其所有的定时器是根据超时时间的绝对时间点(系统时间+超时时间)进行排序(利用STL当中的set进行排序),每次取出距离当前时间最近的一个时间点,计算超时需要等待的时间然后进行等待。当超时时间到的时候,啧获取当前的绝对是件,将定时器的最小堆中所有小于这个时间点的定时器都执行回调。
4.7 Timer
包括定时器的绝对超时时间、回调函数、对应的管理类指针和一些重置、刷新、取消方法。
class Timer : public std::enable_shared_from_this<Timer> {
friend class TimerManager;
public:
typedef std::shared_ptr<Timer> ptr;
// 取消定时器
bool cancel();
// 刷新定时器的执行时间
bool refesh();
// 重置定时器时间
bool reset(uint64_t ms, bool from_now);
private:
// 构造函数
Timer(uint64_t ms, std::function<void()> cb, bool recurring, TimerManager* manager);
// next:执行的时间戳
Timer(uint64_t next);
private:
// 是否循环定时器
bool m_recurring = false;
// 执行周期
uint64_t m_ms = 0;
// 精确的执行时间
uint64_t m_next = 0;
// 回调函数
std::function<void()> m_cb;
// 定时器管理器
TimerManager* m_manager = nullptr;
private:
// 定时器比较仿函数
struct Comparator {
// 比较定时器的智能指针的大小(按执行时间排序)
bool operator()(const Timer::ptr& lhs, const Timer::ptr& rhs) const;
};
};
4.8 Timermanager
对定时器进行管理,通过std::set来实现最小堆结构(set中的元素都是排序过的)。
通过epoll_wait一方面检测是否有事件触发,一方面判断是否是超时。同时sylar是支持条件定时器的,及创建定时器时绑定变量,当定时器触发时,判断该变量是否有效,若无效啧定时器取消触发。
// 定时器管理器
class TimerManager{
friend class Timer;
public:
typedef RWMutex RWMutexType;
// 构造函数
TimerManager();
// 析构函数
virtual ~TimerManager();
// 添加定时器
Timer::ptr addTimer(uint64_t ms, std::function<void()> cb, bool recurring = false);
// 添加条件定时器
Timer::ptr addConditionTimer(uint64_t ms, std::function<void()> cb,
std::weak_ptr<void> weak_cond, bool recurring = false);
// 到最近一个定时器执行的时间间隔
uint64_t getNextTimer();
// 获取需要执行的定时器的回调函数列表
void listExpiredCb(std::vector<std::function<void()> >& cbs);
// 是否有定时器
bool hasTimer();
protected:
// 有定时器插入到定时器首部时,执行该函数
virtual void onTimerInsertedAtFront() = 0;
// 添加定时器到管理器
void addTimer(Timer::ptr val, RWMutexType::WriteLock& lock);
private:
// 检测服务器时间是否被调后了
bool detectClockRollover(uint64_t now_ms);
private:
// mutex
RWMutexType m_mutex;
// 定时器集合
std::set<Timer::ptr, Timer::Comparator> m_timers;
// 是否触发onITimerInsertedAtFront
bool m_tickled = false;
// 上次执行时间
uint64_t m_previouseTime = 0;
};
五、UML
六、感想与期望
今天是敏捷冲刺的第六天,内容是实现和测试IO协程调度,这也是整个框架当中最为重要的一个部分,在我们团队的默契配合之下也是完成了这个难度很高的模块,同时这也表明我们的项目也要步入尾声了,希望我们能够继续加油完成整个项目。