一、IO多路复用函数
高效并发的网络框架大多离不开io多路复用函数,Linux下有三种
- select
- poll
- epoll
Libevent源码中讲解了对epoll/poll/select的封装,是用c语言写的库,通过函数指针实现多态。我学习的muduo源码是c++11版本的,利用c++进行封装。
二、复习:C++面向对象和基于对象的区别
- 面向对象
面向对象的三大特点:封装,继承,多态缺一不可
·
封装:数据和处理数据的函数统一起来,封装在一个class中
·
继承:通过继承某个类派生出一个新类,被继承的类称作基类,派生出的类称作派生类。派生类是对基类的补充,二者之间满足一定的归属关心,如动物(基类),鸟(派生类)。继承可以是public/private/protected继承,也可以是虚继承(用于解决多重继承带来的重复问题),基类可以是抽象基类(不能被实例化),但是派生类需要重新实现基类定义的每个纯虚函数。
·
多态:在继承的基础上通过基类指针指向派生类的实例化对象,达到调用派生类虚函数的目的,多态又被叫做运行时多态,是在运行期根据基类指针实际指向的对象类型判断调用哪个函数的方式。
- 基于对象
无继承,无多态,只有封装
·
利用类封装好的接口实现对数据的操作
三、如何禁止编译器自动生成拷贝构造函数/赋值运算符
- 继承boost::noncopyable
- 自定义空基类,基类中将两个函数放在private域,派生类private继承该基类
- 在自己的private域中声明两个函数,不予实现
c++11版本采用第2种,boost版本采用第1种,第3种效果不好,因为错误是在链接期发现,前两个是在编译期
四、关于muduo
1. 基于/面向对象
现如今大多C++程序都是基于对象的,面向对象只在整个程序中占一小部分比重。
muduo采用的也是基于对象的手法,但是对IO多路复用的封装采用的是面向对象:即定义一个基类,派生出不同的派生类。
2. 基类Poller
muduo只派生了poll/epoll两个类封装,因为二者在实现上有相似性,可以共用基类Poller。
基类Poller主要用于设计统一接口,两个派生类EPollPoller/PollPoller用于实现各自的操作。Poller是EventLoop的间接成员,只供其owner EventLoop在IO线程调用,因此无须加锁。其生命期与EventLoop相等。Poller并不拥有Channel,Channel在析构之前必须自己unregister(EventLoop::removeChannel()),避免空悬指针。
Poller定义如下:
/* 禁止编译器自动生成拷贝构造函数/赋值操作运算符 */
class Poller : noncopyable
{
public:
typedef std::vector<Channel*> ChannelList;
Poller(EventLoop* loop);
virtual ~Poller();
/// Polls the I/O events.
/// Must be called in the loop thread.
/*
* 监听函数,对于epoll是epoll_wait,对于poll是poll
* 返回epoll_wait/poll返回的时间
*/
virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels) = 0;
/// Changes the interested I/O events.
/// Must be called in the loop thread.
/* 更新监听事件,增删改对fd的监听事件 */
virtual void updateChannel(Channel* channel) = 0;
/// Remove the channel, when it destructs.
/// Must be called in the loop thread.
/* 删除监听事件 */
virtual void removeChannel(Channel* channel) = 0;
virtual bool hasChannel(Channel* channel) const;
static Poller* newDefaultPoller(EventLoop* loop);
void assertInLoopThread() const
{
ownerLoop_->assertInLoopThread();
}
protected:
/*
* Channel,保存fd和需要监听的events,以及各种回调函数(可读/可写/错误/关闭等)
* 类似libevent的struct event
*/
typedef std::map<int, Channel*> ChannelMap;
/* 保存所有事件Channel,类似libevent中base的注册队列 */
ChannelMap channels_;
private:
/*
* EventLoop,事件驱动主循环,用于调用poll函数
* 类似libevent的struct event_base
*/
EventLoop* ownerLoop_;
};
类中采用前向声明(点击查看解释),即在定义Poller之前声明一下class Channel;,好处是避免使用头文件 #include <muduo/net/Channel.h> ,不必要的头文件会增加编译时间。因为头文件中并没有使用Channel,只是定义了这个类型的变量,所以只声明就好了,而在成员函数的实现中需要使用Channel的接口,这就需要让编译器知道Channel是怎么定义的,就需要在 .cpp 文件中 #include <muduo/net/Channel.h>。另外,因为Channel是Poller的成员变量,当Poller析构时也会调用Channel的析构函数,这就需要让编译器知道Channel析构函数的定义,所以Poller的析构函数需要在 .cpp 中定义。
以上也是大多数muduo类采用的方法(在定义类Poller之前声明类Channel),这种方法可以降低依赖关系,如果Channel文件改变,不需要重新编译Poller文件。
3、派生类EPollPoller
派生类EPollPoller的实现就是重新实现基类Poller声明的纯虚函数,简单地调用epoll的接口。在poll返回后也会将就绪的fd(muduo是由Channel管理,libevent是由struct event管理)添加到激活队列中。
/*
* 对epoll函数的封装,继承自Poller
*/
class EPollPoller : public Poller
{
public:
EPollPoller(EventLoop* loop);
virtual ~EPollPoller();
/* epoll_wait */
virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels);
/* ADD/MOD/DEL */
virtual void updateChannel(Channel* channel);
/* DEL */
virtual void removeChannel(Channel* channel);
private:
static const int kInitEventListSize = 16;
/* EPOLL_CTL_ADD/MOD/DEL转成字符串 */
static const char* operationToString(int op);
/* epoll_wait返回后将就绪的文件描述符添加到参数的激活队列中 */
void fillActiveChannels(int numEvents,
ChannelList* activeChannels) const;
/* 由updateChannel/removeChannel间接调用,执行epoll_ctl */
void update(int operation, Channel* channel);
typedef std::vector<struct epoll_event> EventList;
int epollfd_;
EventList events_;
};
类的声明中的EventList (31行)记录着所有监听的epoll_event,.cpp中就是实现上述函数,进行增删改等,主要记录一些没接触过的知识。
4、epoll_create1
int epoll_create(int size); //早期创建监听epollfd的函数
功能:创建一个epoll的句柄,size用来告诉内核要监听的数目。需要注意的是,当创建好epoll句柄后,它就会占用一个fd值,在Linux下如果查看 /poc/ 进程的 id/fd/,是能够看到这个fd值的,所以在使用完epoll后,必须调用 close() 关闭,否则可能导致fd被耗尽。
- 创建epollfd,早期linux引入的创建监听epollfd的函数,传入的参数size作为给内核的一个提示
- 内核会根据这个size分配一块这么大的数据空间用来监听事件(struct epoll_event)
- 当在使用的过程中出现大于size的值时,内核会重新分配内存空间。
- 目前:这个size已经没有作用,内核可以动态改变数据空间大小,但仍然需要传入大于0的数
int epoll_create1(int flag); //新版创建监听epollfd的函数
在linux 2.6.27中新加入的, 和epoll_create不同的是epoll_create1函数的参数是flag。
- 当flag = 0 时, 表示和epoll_create函数完全一样, 不需要size的提示了。
- 当flag = EPOLL_CLOEXEC, 创建的epollfd会设置FD_CLOEXEC, 它是fd的一个标识说明,
用来设置文件close-on-exec状态的,当程序exec执行新程序时自动close epollfd,防止fd被耗尽。 - 当flag = EPOLL_NONBLOCK, 创建的epfd会设置为非阻塞。
5、static_assert; //静态断言(编译期)
C++0x中引入了static_assert这个关键字,用来做编译期间的断言,因此叫做静态断言。
在编译期间发现更多的错误,用编译器来强制保证一些契约,并帮助我们改善编译信息的可读性,尤其是用于模板的时候。static_assert可以用在全局作用域中,命名空间中,类作用域中,函数作用域中,几乎可以不受限制的使用。
static_assert(bool flag, char *msg);
其语法:static_assert(常量表达式,提示字符串),
- 编译期断言,程序在编译的过程中执行;
- 若flag为真,什么也不做;
- 若flag为假,产生一条编译错误,输出错误信息msg,错误位置为该static_assert语句所在行号;
- 编译器在遇到一个static_assert语句时,通常会立刻将其第一个参数flag作为常量表达式进行演算,但如果该常量表达式依赖于某些模板参数,则延迟到模板实例化时再进行演算,这就让检查模板参数成为了可能。
- 性能方面,由于是static_assert编译期间断言,不生成目标代码,因此static_assert 不会造成任何运行期性能损失。
注: static_assert详解请点击此处。
6、assert; // 运行期断言
assert(bool flag);
- 运行期DEBUG模式下的断言
- 若flag为真,什么也不做
- 若flag为假,终止程序
注意:assert只有在debug模式下才会有效,在release模式下这条语句就被编译器删除了
对此,通常在assert后面有一个(void)n;等语句
int fd = channel_->fd();
assert(channels_[fd] == channel);
(void)fd;
如果以release模式下运行,assert被删除,编译器会发出警告通知,提示变量fd未使用;
而如果在设置编译条件时将警告提升为错误,那么编译就不会继续进行;
(void)fd;意为将fd转为void类型,简单使用一下fd,消除警告。