Linux多路转接之epoll(补充)
Linux多路转接之epoll(补充)
- 1.epoll的三个使用函数
- 1.1.epoll_create
- 1.2.epoll_ctl
- 1.3.epoll_wait
- 2.epoll模型
- 3.epoll的工作模式
- 4.LT工作模式vsET工作模式
- 5.Reactor模型
- 6.代码
🌟🌟hello,各位读者大大们你们好呀🌟🌟
🚀🚀系列专栏:【Linux的学习】
📝📝本篇内容:epoll的三个函数;epoll_create;epoll_ctl;epoll_wait;epoll模型;epoll的工作方式;LT工作模式vsET工作模式;Reactor模型;代码
⬆⬆⬆⬆上一篇:Linux高级IO
💖💖作者简介:轩情吖,请多多指教(>> •̀֊•́ ) ̖́-
1.epoll的三个使用函数
1.1.epoll_create
第一个函数是epoll_create,它的作用是创建一个epoll模型,返回一个epoll文件描述符。size参数现在并不起作用,只是给内核一个提示,告诉内核应该如何为内部数据结构划分初始大小。
1.2.epoll_ctl
第二个函数是epoll_ctl,这个函数就是对创建的epoll模型进行操作,简单来说就是操作需要进行IO的文件描述符
函数的第一个参数就是epoll_create的返回值;op就是上图展示的操作方式,其中关于EPOLL_CTL_MOD 操作,它是用于修改一个已经注册的文件描述符的监听事件。这里的“修改”是指对指定文件描述符的监听事件进行整体替换,而不是增量式地添加或删除某些事件。具体来说,当你使用 EPOLL_CTL_MOD 时,你提供的 event 参数中的事件将完全覆盖之前为该文件描述符设置的监听事件
;fd是想要操作的文件描述符;event是什么类型事件,如下图
事件 | 描述 |
---|---|
EPOLLIN | 可读取非高优先级数据(重要,必用) |
EPOLLPRI | 可读取高优先级数据 |
EPOLLOUT | 普通数据可写 (重要,必用) |
EPOLLHUP | 本端描述符产生一个挂断事件,默认监测事件,即当描述符的一端或两端被用户主动关闭时,EPOLLHUP事件会被触发。 |
EPOLLET | 采用边沿触发事件通知(重要) |
EPOLLONESHOT | 在完成事件通知后禁用检查 |
EPOLLRDHUP | 套接字对端关闭 |
EPOLLERR | 有错误发生 |
我们的event是一个结构体类型,我们的成员data是一个用户参数,主要是保存用户的信息,它存在的意义是后续等待的时候可以直接知道哪个文件描述符的哪个事件就绪了(见后续代码);events是我们关心的事件,如表格所示内容
1.3.epoll_wait
这是最后一个函数epoll_wait,这个函数的主要作用是用来进行等待的,返回就绪事件。第一个参数就不用多说了,第二个参数是返回就绪的fd和事件,前面已经讲过它的结构了;maxevent设定最多监听多少个事件,必须大于0,一般设定为65535;
timeout就是超时时间,在函数调用中阻塞时间上限,单位是ms
timeout = -1:表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
timeout = 0:用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
timeout > 0:表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时。
返回值:成功时,epoll_wait()返回为请求的I / O准备就绪的文件描述符的数目;如果在请求的超时毫秒内没有文件描述符准备就绪,则返回零。发生错误时,epoll_wait()返回-1并正确设置errno。
在我们了解我们接口后,这边有一个小知识点要讲一下。
在我们学习网络时,在TCP报头中有一个PSH字段,PSH到这里就能更好的理解了,假设我们的客户端要向服务器发送一堆数据,但是我们的操作系统在忙别的事,导致我们的客户端的滑动窗口变成了0,这时候就可以向服务器发送PSH字段,告诉我们的操作系统将对应的文件描述符的就绪情况告诉给epoll,poll或者select来进行高效IO。
2.epoll模型
上图就是epoll模型的大致框架,我们来谈谈它
1.首先我们来谈谈两个函数,epoll_ctl和epoll_wait。epoll_ctl可以添加fd的所关心的事件,那么其实在底层,其实就是把它(以结点的形式)放入红黑树中;当有事件就绪时就把结点node(图中所展示的,并不一定真叫node)放入到就绪队列中,因此这个结点既属于红黑树又属于就绪队列
。
2.我们epoll模型中的红黑树其实和我们在select中的数组很像,都是存储的是需要等待的fd事件,只不过现在的OS帮我们维护的,并且使用的是红黑树效率高。
2.我们的node结点的内部既包含红黑树的链接方式,也包含队列的链接方式,因此不用担心怎么从红黑树移动到就绪队列
3.与select和poll对比,会有很大的效率提升,poll和select每次都需要将需要监视的文件描述符和对应的事件拷贝到内核,并在其中进行遍历,这样的效率非常低下。
但是epoll不一样,基于红黑树的特性以及回调机制。这边要重点谈的是回调机制,当数据来到网卡后,会通过CPU的针脚向CPU发送中断信号,CPU中的某一个寄存器会记录当前发送中断的针脚的编号,这个编号就叫做“ 中断号 ”。CPU 通过读取寄存器的中断号,就会得知那个外设资源就绪了,就会通过中断号调用中断向量表中的函数来处理外设请求。此时我们的网卡会再经过协议栈,OS会将数据拷贝到文件的缓冲区中,按照以往的文件,OS的任务就结束了,但是我们这次调用的是epoll,我们的OS还会调用回调函数,将结点放入到就绪队列中。到这个时候我们OS的任务才会完成。
总的来说在执行epoll_ctl的add操作时,不仅将文件描述符放到红黑树上,而且也注册了回调函数,内核在检测到某文件描述符可读/可写时会调用回调函数,该回调函数将node放到就绪链表中。其实node中的fd的背后就是文件,我们可以理解为文件中有一个属性是cb_t func=callback;默认情况下是为NULL。
4.并且我们的就绪队列在检测时的效率也是非常高的,只需要进行判断链表是否为空。不过在使用epoll_wait时会将数据进行线性拷贝到events中,并且这些数据时从左向右连续有效的。因为我们的就绪队列里的fd的事件都是已经就绪的。
5.在前面介绍函数的使用的时候,我们谈到过epoll_create的返回值是一个文件描述符,那文件描述符只能是从进程而来,进程当然是指的是调用epoll的进程,如上图的左侧所示,通过文件描述符我们可以找到我们的epoll模型
。所以说每次我们调用epoll_ctl和epoll_wait时需要这个文件描述符。
3.epoll的工作模式
epoll有两种工作模式,一种是ET工作模式,另一种是LT工作模式
首先来讲ET工作模式,它的全称是Edge Triggered,即边缘触发,当我们的epoll设置为这个模式后,当有事件就绪后,我们的epoll_wait只会进行一次有效的通知,当数据从无到有会通知一次,从有到多的时候通知第二次,再增加时,会通知第三次...
举个例子,客户端发来4096字节数据,但是我们一次只能读取2048个字节,这次读取完后,还剩下2048个字节没有读取,如果不再次执行读取操作,我们的epoll_wait不会再进行通知。这就会导致我们无法拿到所有的有效数据。因此在我们设置为ET工作模式后,我们必须循环的读取,并且我们还需要将读取的文件描述符fd设置为非阻塞模式(这里说的是文件描述符,和epoll阻塞不阻塞没关系),否则进行read和write的时候就会阻塞。因此在ET的工作模式下,所有的读取和写入,都必须是非阻塞接口
,如下面代码所展示
//将所有的添加操作都使用一个函数来解决
void AddConnection(const int fd, int events, const uint16_t clientport = 8888, const string &clientip = "127.0.0.1")
{
// 1. 设置fd是非阻塞
//使用判断能够保证即使是修改代码为LT模式,也能正常使用此函数
if (events & EPOLLET)
Util::NonBlock(fd);
// 2. 构建connection对象,交给connections_来进行管理
Connection *con = new Connection(fd, clientip, clientport);
//注意这边的写法,如果是listen有链接到来,给予的回调函数是Accepter,其余的writer,excepter不重要
if (fd == _listensock.GetSock())
{
//con->_reader = bind(&EpollServer::Accepter, this, placeholders::_1);
con->Register(bind(&EpollServer::Accepter, this, placeholders::_1),nullptr,nullptr);
}
//其余的都是网络通信套接字,就可以默认都是Reader,Writer,Excepter,调用时会根据不同的就绪事件来选择
else
{
con->Register(bind(&EpollServer::Reader, this, placeholders::_1),
bind(&EpollServer::Writer, this, placeholders::_1),
bind(&EpollServer::Excepter, this, placeholders::_1));
// con->_reader = bind(&EpollServer::Reader, this, placeholders::_1);
// con->_writer = bind(&EpollServer::Writer, this, placeholders::_1);
// con->_excepter = bind(&EpollServer::Excepter, this, placeholders::_1);
}
con->_events = events;
con->_ptr = this;
_connections.insert(make_pair(fd, con));
// 3.fd && events 写透到内核中
_epoller.AddModEvent(fd, events, EPOLL_CTL_ADD);
LogMessage(Debug, "AddConnection is done....");
}
// 连接管理器
void Accepter(Connection *con)
{
//由于ET模式在有链接到来时只会通知一次,因此要循环读取,保证不会有遗漏
do
{
string clientip;
uint16_t clientport;
int errcode;//错误码
// 1. 新连接到来
int sock = _listensock.Accept(clientip, clientport, errcode);
if (sock > 0)
{
LogMessage(Debug, "%s:%d已经连上服务器了", clientip.c_str(), clientport);
// 1.1 此时在这里,我们能不能进行recv/read ? 不能,只有epoll知道sock上面的事件情况,将sock添加到epoll中
AddConnection(sock, EPOLLIN | EPOLLET, clientport, clientip);
}
else
{
//当错误码是这个时,说明底层暂时没有连接了
if ((errcode== EWOULDBLOCK) || (errcode == EAGAIN))
{
break;
}
else if (errcode== EINTR)
{
continue;
}
else
{
LogMessage(Warning, "Listensock error,errno:%d,errorstring:%s", errno, strerror(errno));
continue;
}
}
} while (con->_events & EPOLLET);
//之所以要使用do while(con->_events&EPOLLET)是因为这样能够保证如果是ET模式,就会不停地循环,
//假设又进行改变了代码,变成了LT模式,也只会执行一次循环,正常工作
LogMessage(Debug, "Accepter is done...");
}
LT工作模式,即Level Triggered,水平触发工作模式。它的工作模式和ET相反,
每次底层只要有数据就会通知一次,如果没把数据读取完或者不读取,就会一直通知
。从概念上讲我们的select和poll也是LT工作模式,我们的epoll默认是LT工作模,如下面的代码展示,根本不需要使用循环来读取,因为只要没读取完就会通知
void HandleEvents(int n)
{
//不需要将整个eparray都遍历一遍,因为我们的输出的参数只会将就绪的拷贝出来
for (int i = 0; i < n; i++)
{
if (eparray[i].events & EPOLLIN)
{
if (eparray[i].data.fd == _listensock.GetSock())
{
// 1.处理新连接到来
string clientip;
uint16_t clientport;
int sock = _listensock.Accept(clientip, clientport);
if (sock < 0)
{
continue;
}
LogMessage(Debug, "%s:%d已经连上服务器了", clientip.c_str(), clientport);
bool r = _epoller.AddEvent(sock, EPOLLIN);
assert(r);
(void)r;
}
else
{
// 2.读取
//这就是在epoll_ctl增加时为什么要传递fd的原因,这边可以直接使用
int fd = eparray[i].data.fd;
char buff[1024];
int n = read(fd, buff, sizeof(buff) - 1); // 读取会被阻塞吗?不会
if (n > 0)
{
//这边-2的原因是使用telnet链接,telnet会将换行也会传递过来,并且传递的换行时\t\n,所以说需要-2,找到\t的位置添加\0
buff[n-2] = 0;
cout << "client echo:" << buff << endl;
string send_to = buff;
send_to += "[epoll server echo]\n";//基于它的telnet的特性,换行也就传递\r\n(经过测试\n也可以)
cout << send_to << endl;
write(fd, send_to.c_str(), send_to.size());
}
else
{
if (n == 0)
{
LogMessage(Information, "client want to end...");
}
else
{
LogMessage(Warning, "read error,errno:%d,string:%s", errno, strerror(errno));
}
// 在处理异常的时候,先从epoll中移除,然后再关闭,否则fd就是不合法的
_epoller.DelEvent(fd);
close(fd); // 不能忘记,remember
}
}
}
}
}
注:上述代码并不完整,只是为了更好的理解工作模式
4.LT工作模式vsET工作模式
不管什么东西,只要是成双出现的,就会有对比,那么它们两个谁更加的高效率呢?
1.从上面的描述来看,我们的ET工作模式效率更高,具体来说是通知效率高
,因为它只需要通知一次,然后循环的非阻塞读取,不像LT工作模式,只要底层有数据没有读取完就还要通知。我们的一次通知就是一次系统调用返回,我们的ET工作模式能够有效的减少系统调用的次数。
2.第二点,我们的ET工作模式必须是循环非阻塞读取的,否则会有数据的丢失,这是必须的强硬的,使得调用者不得不循环读取,将所有的数据尽快取走。这其实有利于底层的TCP底层能够更新出更大的接收窗口,从较大概率上,使得滑动窗口的变大,提高发送效率。
(这边用较大概率是因为发送效率是由滑动窗口=min(对端主机的16位窗口大小,网络的拥塞窗口)决定的,这边还要考虑网络拥塞的情况)
3.但是其实我们的LT工作模式其实进行设计一下,也可以变成只通知一次或者进行循环非阻塞读取,也能达到同样的效果,但是就像前面说的,ET工作模式是强硬的循环读取,我们的LT还是有的选,这就导致了它的不确定性,这算是ET工作模式的优点
但是总的来说LT工作模式也能达到ET工作模式的效率,所以说我们一般认为ET工作模式>=LT工作模式
5.Reactor模型
Reactor是
基于多路转接的包含事件派发器,连接管理器等半同步半异步的IO服务器
Reactor的意思是反应堆的意思,简单来讲就是epoll服务器中会等待就绪事件,当事件一旦就绪就会调用对应文件描述符的回调函数来读取或者写入。一有事件就绪就处理,一有事件就绪就产生反应,即Reactor。其实类似于一种打地鼠的情况,出现一个地鼠就拿锤子砸一下,出现一个地鼠就砸一下
Reactor的半同步指的是事件派发-交给回调函数处理-业务处理
Reactor的半异步指的是我们的epoll服务器可以只进行事件派发和读写,业务处理交给线程去做
6.代码
EpollServerV1—LT工作模式+基本使用
EpollServerV2—ET工作模式+大部分代码
EpollServerV3—ET工作模式+完整代码
🌸🌸Linux多路转接之epoll(补充)的知识大概就讲到这里啦,博主后续会继续更新更多Linux的相关知识,干货满满,如果觉得博主写的还不错的话,希望各位小伙伴不要吝啬手中的三连哦!你们的支持是博主坚持创作的动力!💪💪