在muduo网络库的设计中Poller是个抽象虚类,由EpollPoller和PollPoller继承实现,EpollPoller就是封装了用epoll方法实现的与事件监听有关的各种方法。
与 Channel 的协作:Poller 维护了一个 `ChannelMap`,用于存储需要监听的 Channel 对象。Poller 的 poll 方法会遍历这个 `ChannelMap`,并调用底层的系统调用(如 poll 或 epoll_wait)来监听 I/O 事件。当有事件发生时,Poller 会更新 Channel 的 `revents` 成员变量,并将发生事件的 Channel 加入到 `activeChannels` 列表中返回给 EventLoop。
成员变量
protected:
ChannelMap channels_; // 需要事件监听的Channel集合
private:
EventLoop* ownerLoop_; // 当前Poller 关联的 EventLoop
public:
//Poller关注的Channel
using ChannelList = std::vector<Channel*>;
// 保存fd -> Channel的映射map集合
using ChannelMap = std::map<int, Channel*>;
主要来讲讲EPollPoller
成员变量
private:
// epoll 监视的文件描述符
int epollfd_;
// 用来存储活跃文件描述符的 epoll_event 结构体数组
EventList events_; // epoll_wait 收集事件的集合
public:
using EventList = std::vector<struct epoll_event>;
常量定义
// 定义 Channel 在 Poller 中的状态
static const int kNew = -1; // Channel 要添加到 map 和 epoll 红黑树中
static const int kAdded = 1; // Channel 在 map 和 epoll 的红黑树中
static const int kDeleted = 2; // Channel 在 map 中,但是不在 epoll 的红黑树中
// 默认监听事件数量
static const int kInitEventListSize = 16;
重要方法
通过epoll_wait将发生事件的channel通过`activeChannels`告知给EventLoop
在这个函数中,实际上就是调用了`epoll_wait`得到了事件发生的集合,然后调用`fillActiveChannels` 将发生的事件装入`activeChannels`
`activeChannels` 是 `ChannelList = std::vector<Channel*>`;类型,将监听到该fd发生的事件写进这个Channel中的`revents`成员变量中。这样获取到了发生事件的集合,然后把这个Channel装进`activeChannels`中,当外界调用完poll之后就能拿到事件监听器的监听结果,在EventLoop中就可以对它进行处理
Timestamp poll(int timeoutMs, ChannelList *activeChannels)
{
// 调用 epoll_wait 等待事件发生
int numEvents = ::epoll_wait(epollfd_, events_.data(), events_.size(), timeoutMs);
int savedErrno = errno;
Timestamp now(Timestamp::Now());
if (numEvents > 0) //有事件到达
{
// 填充活跃的 Channel 到 activeChannels 中
fillActiveChannels(numEvents, activeChannels);
// 如果事件数量达到 events_ 的大小,将 events_ 扩容
if (static_cast<size_t>(numEvents) == events_.size())
{
events_.resize(events_.size() * 2);
}
}
else if (numEvents == 0) //没有事件到达
{
LOG_TRACE << " nothing happended";
}
else
{
// 处理错误情况
if (savedErrno != EINTR)
{
errno = savedErrno;
LOG_SYSERR << "EPollPoller::poll()";
}
}
return now;
}
值得注意的是,我们平常使用epoll_wait传入的是数组 ,而在此传的是vector,通过.data(),得到vector的frst指针,指向vector容器首元素的地址;比数组优势的一点在于扩容。
void fillActiveChannels(int numEvents, ChannelList *activeChannels) const
{
assert(static_cast<size_t>(numEvents) <= events_.size());
for (int i = 0; i < numEvents; ++i)
{
Channel *channel = static_cast<Channel *>(events_[i].data.ptr);
// 确保 Channel 在 channels_ 中
assert(channels_.find(channel->fd()) != channels_.end());
assert(channels_.find(channel->fd())->second == channel);
// 设置 Channel 发生的事件
channel->set_revents(events_[i].events);
// 将 Channel 添加到 activeChannels 中
activeChannels->push_back(channel);
}
}
对于events_[i].data.ptr进行进一步解释:
struct epoll_event {
uint32_t events; // 触发的事件类型(如 EPOLLIN、EPOLLOUT)
epoll_data_t data; // 用户数据
};
typedef union epoll_data {
void *ptr; // 指向用户数据的指针
int fd; // 文件描述符
uint32_t u32; // 32位整数
uint64_t u64; // 64位整数
} epoll_data_t;
`data` 是一个联合体(union),只能存储其中一种类型的值。Muduo 选择使用 `ptr` 字段,将其指向对应的 `Channel` 对象。
这么设计的好处也显而易见将 `epoll` 返回的事件与对应的 `Channel` 对象高效绑定,实现 O (1) 时间复杂度的事件分发。
那接着又有一个疑问了, Channel *和void *之间的转换安全吗?
答案是肯定的,陈硕这个大佬,这么设计的思路也是很nb了。因为:
- 存入和取出时使用相同的类型转换。
取出就是我看见的fillActiveChannels函数里用到的,
存入则在updata函数里使用
// 更新 channel 通道,本质是调用了 epoll_ctl
void update(int operation, Channel *channel)
{
struct epoll_event event;
bzero(&event, sizeof event);
event.events = channel->events(); //注册fd感兴趣的事件
event.data.ptr = channel;
int fd = channel->fd();
// 调用 epoll_ctl 进行添加、修改或删除操作
if (::epoll_ctl(epollfd_, operation, fd, &event) < 0)
{
if (operation == EPOLL_CTL_DEL)
{
LOG_SYSERR << "epoll_ctl op=" << operation << " fd= " << fd;
}
else
{
LOG_SYSFATAL << "epoll_ctl op=" << operation << " fd= " << fd;
}
}
}