Linux网络编程:epoll
1.IO多路转接---epoll
1.1.接口认识
epoll多路转接的实现是基于三个系统调用的,而这些系统调用底层是epoll模型的构建,和设置的结构体、数据结构之间的交互,我们需要一步步地进行epoll的学习!
epoll_create( )
如图:epoll_create是创建一个句柄, 也就是标识一个对象的值,而在操作系统层面,创建的这个句柄本质上是一个文件描述符,指向着epoll模型。也就是epoll_create的调用是创造一个epoll模型,那什么是epoll模型?
- 我们知道多路转接事件管理器本质上是需要检测维护的套接字的关心事件是否就绪,所以天然的我们需要一个数据结构(这里为红黑树)来维护文件描述符和关心的事件,一个数据结构(队列)来维护就绪的事件。
- 另外当检测的文件描述符中的事件就绪时,系统将对应的节点插入就绪队列中。这里我们可以看出:我们确定是否事件就绪,复杂度是O(1),获取到所有的就绪事件,复杂度为O(n)
这里我们也可以看到,epoll_create函数创建的epoll模型中,操作系统会构建一颗用来存储、查询文件描述符下关心事件的红黑树,和存储关心事件就绪的就绪队列,并且和形成回调函数,那么结合一下epoll_create创建的句柄。
因为Linux下一切皆文件,而这个句柄本质上就是一个文件描述符,指向的是epoll_file,然后内部有一个指针,这个指针指向的就是操作系统通过系统调用实现的epoll模型。软件实现就是再通过指针,指向红黑树和就绪队列!!!
一言以蔽之:epoll_create函数的作用就是告知操作系统创建一个epoll模型!!!并且返回一个epoll模型对应的文件描述符,调用成功返回对应的epoll句柄(文件描述符)
epoll_ctl()
在我们完成了epoll模型构建之后,我们只是实现了数据结构,并没有对文件套接字进行事件的关心,而epoll_ctl就是用来实现事件的关心(监听) 。
struct epoll_event结构如下:
typedef union epoll_data
{void *ptr;int fd;uint32_t u32;uint64_t u64;
} epoll_data_t;struct epoll_event
{uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */
} __EPOLL_PACKED;
所以epoll_ctl的作用:告知内核在哪个epoll模型中监听哪一个文件描述符的什么事件,值得一提的是如果调用成功返回0,失败返回-1
epoll_wait()
在我们完成了epoll模型的构建,和设置了关心的事件,接下来就是获取就绪的事件了。而epoll_wait的作用就是如此。
1.2.epoll多路转接原理
- 首先服务器启动时,通过epoll_create创建一个epoll模型,接着用epoll_ctl添加关心的事件,然后循环调用epoll_wait进行轮询
- 当数据从网络中被网卡获取时,通过硬件中断,最终被epoll_wait检测到事件就绪。
- 并且在底层这些添加到epoll的事件都会和网卡驱动构建回调关系,检测到数据就绪时,就会调用回调方法,构建就绪队列,这时就获取到了epoll_wait的返回值
- 这样子我们就跳过了IO的等待,直接进行IO的数据拷贝!!!
1.3. epoll如何进行多路转接
其他模块代码从epoll_server demo - Gitee.com 获取!!!
ps:这个代码模块中,我们对epoll接口的使用封装成了epoller对象!!!具体需要在epoll_object模块中对应1.1.代码接口
#include <iostream>
#include <memory>
#include "epoll_object.hpp"
#include "socket.hpp"
#include "Log.hpp"const static int back_log = 32;class EpollServer
{static const int max_events = 64;private:void HandlerEvent(int event_num){lg.LogMessage(Debug, "ready event num = %d\n", event_num);for (size_t i = 0; i < event_num; i++){int sock_fd = _events[i].data.fd;uint32_t event = _events[i].events;// 可以用封装多一层打印event为字符串lg.LogMessage(Debug, "ready fd: %d, Event is: %u\n", sock_fd, event);if (event & EPOLLIN){if (_listen_sock->GetSockFd() == sock_fd){std::string client_ip;uint16_t client_port;// 可以封装成Accepter函数int fd = _listen_sock->AcceptConnection(&client_ip, &client_port);if (fd < 0){lg.LogMessage(Error, "accept failed, error is %s, code is %d\n", strerror(errno), errno);}// 将新增的文件描述符添加到关心事件中_epoller->AddEvent(fd, EPOLLIN);lg.LogMessage(Info, "accept client success, client[%s:%d]\n", client_ip.c_str(), client_port);}else{char buffer[1024];ssize_t n = recv(sock_fd, buffer, sizeof(buffer) - 1, 0);if (n > 0){buffer[n] = 0;std::cout << "client message# " << buffer << std::endl;std::string message = "server message: ";message+=buffer;send(sock_fd, message.c_str(), message.size(), 0);}else{if (n == 0){// 对端关闭lg.LogMessage(Info, "client close fd...");}else{lg.LogMessage(Error, "read failed, error is %s, code is %d\n", strerror(errno), errno);}_epoller->DelEvent(sock_fd);close(sock_fd);}}}}}public:EpollServer(int port) : _port(port), _isrunning(false), _listen_sock(new NetWork::TcpSocket) {}bool InitServer(){// 设置listen套接字_listen_sock->BuildListenSocketMethod(_port, back_log);lg.LogMessage(Info, "init listen_sock success, fd = %d\n", _listen_sock->GetSockFd());// 构建Epoll模型_epoller->InitEpoller();// 将listen套接字添加到为关心事件_epoller->AddEvent(_listen_sock->GetSockFd(), EPOLLIN);return true;}void Loop(){_isrunning = true;while (1){int timeout = 2000;int num_event = _epoller->WaitEpoller(&_events, max_events, timeout);if (num_event > 0){lg.LogMessage(Info, "events are ready...\n");HandlerEvent(num_event);}else if (num_event == 0){lg.LogMessage(Info, "time out, events unready...\n");}else{lg.LogMessage(Error, "epoll wait failed\n");}}}~EpollServer(){}private:std::unique_ptr<NetWork::Socket> _listen_sock;std::unique_ptr<Epoll::Epoller> _epoller;bool _isrunning;int _port;struct epoll_event _events[max_events];
};
epoll的使用逻辑:(这一部分结合一下gitee的epoll_object.hpp这里节省篇幅)
- 首先我们调用epoll_create函数获取当前进程对应的epfd,这时在底层我们构建了一棵添加关心事件的红黑树,我们进行节点操作的复杂度近似为O(logn),与此同时我们也实现了回调函数和就绪队列的形成
- 当我们添加事件是,我们通过epoll_ctl函数,接着传入相应的参数,这时我们就添加了红黑树节点
- 此时调用epoll_wait函数等待时间的就绪
- 最终当我们在网络中获取到数据,触发硬件中断,接着发生回调函数,进而将事件就绪的红黑树节点插入到就绪队列中
- 从epoll_wait函数中获取到就绪事件个数,接着通过外部传入的epoll_event结构体数组开始解读!!!
解读epoll_server:
- 在变量的设置上,我们依旧需要一个维护文件描述符的数据结构,但是epoll中给了我们一个原生的epoll_event结构体,所以我们就实现一个epoll_event的结构体数组即可。另外我们封装了一个epoller的对象,让编程更加简洁!!!
- 首先,我们在初始化服务器时,天然的需要初始化epoller对象,与此同时需要添加监听套接字到关心的事件
- 接着我们在loop循环中,需要对此时的epoll_event的结构体数组进行检测事件是否就绪,当事件就绪时我们转入HandlerEvent函数。
- 在HandlerEvent函数中,从epoll_wait函数中获取就绪事件个数,此时就对这几个进行读取,fd再进行相应的操作。值得一提的是:如果是普通就绪事件(不是套接字就绪),当事件结束后我们需要关闭这个文件描述符。
1.4.epoll的工作模式
工作模式本质上是epoll提供给用户的一种通知策略!!!具体来说就是关心的事件就绪时,epoll_wait函数是不断地进行通知,还是只通知一次(就算数据没有取完)!!!
1.4.1.水平触发Level Triggered
LT模式:底层只要有数据,epoll就需要一直通知的策略
LT模式就像是在家吃饭,家里做好了饭了,你妈妈叫你吃饭,但是这时你在打游戏,你说等等吃,或者是随便夹了一点菜来吃,然后不久后你妈妈又叫你吃饭,不断的叫你吃饭,尽管你在打游戏,但是只要饭没吃完她就叫你吃饭。
很显然,LT模型需要不断的通知,而这个通知是需要消耗系统资源的,所以可能会出现效率低的问题。(除非我们一次通知就取完了所有数据,不让有多次通知的机会)
1.4.2.边缘触发Edge Triggered
ET模式:底层有数据,通知一次就不再通知,直到下次数据发生变化(收到新数据)时,才通知。
而ET模型,就是家里做好午饭了,你妈妈叫你吃饭,你在打游戏,你说等等吃或者随便加了一点菜吃,但是你妈妈就通知你一次,后面她就懒得理你,直到晚上做了饭之后,她就再次通知你吃饭。
ET模式更加高效:
- 通知策略时,每一次的通知都是有效的,没有无效的通知。
- 另外读取数据时倒逼上层,取数据时需要将本轮的数据全部获取。底层上,给发送方通告一个更大的窗口,发送方的滑动窗口就变得更大。进而从概率上提高了双方的通信效率(在通信允许下)。
面试题:为什么ET模式,必须以非阻塞状态进行IO
因为ET模式需要读取完本轮的所有数据,而单次的recv函数不能保证数据读完,所以我们需要循环调用recv,确保数据全部读完。当数据读完时,就会出现IO的阻塞,这时为了ET模式的高效,我们就需要以非阻塞状态进行IO操作。(体现在epoll的设置上!!!)
看到这里,我们也知道了epoll事件管理器的ET模式,需要将文件描述符设置为非阻塞状态。另外我们实现了一个基于Reactor框架的epoll事件管理器的ET模式的服务器demo,大家可以在gitee中通过注释进行学习!!!Linux_code: 存放Linux中的代码 - Gitee.com