目录
非阻塞读取
fcntl函数
I/O多路转接之select
select函数
fd_set结构
select的模拟实现
select的优缺点
I/O多路转接之poll
poll函数
struct pollfd结构体
poll函数的使用示例
poll的模拟实现
poll的优缺点
I/O多路转接之epoll
epoll的三个系统调用
epoll的工作原理
epoll的模拟实现
Linux中五种IO模型(阻塞、非阻塞、多路转接、信号驱动、异步IO)中,只有前三种比较常用,后两种不太常用,所以下面只学习前三种IO模型
首先IO = 等 + 数据拷贝,在网络通信时大部分时间都在等,等IO类事件就绪,一旦就绪了,我们就可以从内核拷贝到用户,所以我们为了提高IO效率,也就是为了减少等的比重,也就是让我们单位时间内,拷贝的数据量变得更多,IO的效率也就更高了
同步异步IO的区别其实就是:是否有参与到 等 + 拷贝 的这个流程中来
对于读来讲:底层有数据
对于写来讲:底层有空间
就叫做IO类事件就绪
非阻塞读取
说起阻塞读取,我们最常见的就是0号文件描述符标准输入了,如下所示:
此时运行代码:
我若是不输入内容,就会一直阻塞式等待,这就是阻塞,输入一行内容就打印一行内容:
如果想设置文件描述符为非阻塞式等待,需要用到fcntl函数:
fcntl函数
fcntl就是对文件描述符进行指定命令的操作,如果失败返回值是-1
而我们设置文件描述符为非阻塞,也并不想影响其他的文件描述符选项 ,所以第三个参数是在原有的基础上,按位或新的标记位
获取标志位,第二个参数填F_GETFL
设置标志位,第二个参数填F_SETFL,后面是可变参数,想设置什么就往后面加
此时将函数改为如下所示:
运行结果如下:
当没有数据时,就会打印"当前0号fd数据没有就绪, 请重复试试"
当输入数据时,就会打印输入的数据
这就叫做非阻塞读取
非阻塞读取的意义就是,当我们的线程走到这里时:
如果返回的错误码是:EWOULDBLOCK或是EAGAIN,就表示并不是出错了,只是数据还没有就绪
如果是EINTR,就表示被其他信号锁中断了,也并不是出错
所以如果发现返回的信号是上述的两种情况,就continue继续,并不是出错
非阻塞读取就可以让当前进程做其他的事情,而阻塞读取就只能被挂起,什么也干不了
I/O多路转接之select
多路转接是给我们提供更高效等的方案,一次等待多个文件描述符,下面是select的主要工作:
- 帮用户进行一次等待多个文件sock
- 当哪些文件sock就绪了,select就要通知用户,对应就绪的sock有哪些,然后用户再调用recv/recvfrom/read等进行数据读取
select函数
select函数的函数原型如下:
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
select需要包含头文件:sys/select.h
函数参数:
nfds:需要监视的文件描述符中,最大的文件描述符值+1
readfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的读事件是否就绪,返回时内核告知用户哪些文件描述符的读事件已经就绪
writefds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的写事件是否就绪,返回时内核告知用户哪些文件描述符的写事件已经就绪
exceptfds:输入输出型参数,调用时用户告知内核需要监视哪些文件描述符的异常事件是否就绪,返回时内核告知用户哪些文件描述符的异常事件已经就绪
timeout:输入输出型参数,调用时由用户设置select的等待时间,返回时表示timeout的剩余时间
参数timeout的取值:
timeout的类型struct timeval是一个结构体
其中tv_sec是秒,表示可以获取到秒级别的时间戳
tv_usec是微秒,表示可以获取到微秒级别的时间戳
struct timeval的使用如下所示,gettimeofday的第一个参数就是struct timeval类型的:
结果为:
timeout作为输入型参数的含义:
select等待多个fd,等待策略可以选择:
①阻塞式 nullptr
②非阻塞式 {0, 0}
③可以设置timeout时间,时间内阻塞,时间到,立马返回 {5, 0}
设置为nullptr,表示阻塞式
设置为{0, 0},表示非阻塞式
设置为{5, 0},表示5秒内阻塞,时间到后立马返回
timeout作为输出型参数的含义:
其中第三点,传入{5, 0}表示5秒内阻塞,但是在等待时间内假设过了2秒,有fd就绪,此时就可以表示它的输出性,此时这个参数中保存的就是3秒,表示距离下次timeout还剩多长时间
函数返回值:
表示就绪的fd的个数,至少只要有一个fd数据就绪 或 空间就绪,就可以进行返回了
fd_set结构
fd_set被称为文件描述符集,本质是一个位图结构,用位图中对应的位来表示要监视的文件描述符
调用select函数之前就需要用fd_set结构定义出对应的文件描述符集,然后将需要监视的文件描述符添加到文件描述符集当中
这个添加的过程本质就是在进行位操作,但是这个位操作不需要用户自己进行,系统提供了一组专门的接口,用于对fd_set类型的位图进行各种操作,如下所示:
void FD_CLR(int fd, fd_set *set); //用来清除描述词组set中相关fd的位
int FD_ISSET(int fd, fd_set *set); //用来测试描述词组set中相关fd的位是否为真
void FD_SET(int fd, fd_set *set); //用来设置描述词组set中相关fd的位
void FD_ZERO(fd_set *set); //用来清除描述词组set的全部位
因为fd_ set是一个固定大小位图,直接决定了select能同时关心的fd的个数是有上限的
而fd_set的大小为1024byte,所以一个select服务器同时能监管的文件描述符总数是1024:
结果为:
之所以*8是因为,sizeof算的是字节数,字节数*8才是比特位数
第二、三、四个参数的类型都是fd_set,下面具体说明第二个参数fd_set *readfds,其他两个参数以此类推即可
参数readfds:
a.输入时:用户告诉内核,我的比特位中,比特位的位置表示文件描述符值,比特位的内容表示是否关心
例如:0000 1010,就代表关心1和3号文件描述符的读事件
b.输出时:内核告诉用户,我是OS,用户你让我关心的多个fd有结果了,比特位的位置表示文件描述符值,比特位的内容表示是否就绪
例如:0000 1000,就代表3号文件描述符的读事件已经就绪了,所以后续用户可以直接读取3号文件描述符,而不会被阻塞
由上述对于输入输出型参数readfds的解释,我们可以得出以下结论:
①用户和内核都会修改同一个位图结构
②这个参数用一次之后,一定需要进行重新设定
同样的道理,writefds和exceptfds与readfds的含义是一样的,都是通过位图置0置1,使得内核和用户相互传递信息
select的模拟实现
select的模拟实现中只完成读取功能,写入和异常不做处理,在模拟实现epoll中会写完整
select的一般编写代码的模式:
需要有一个第三方的数组,用来保存所有合法的fd
不断循环:
while(true)
{
遍历数组,更新出最大值
遍历数组,添加所有需要关心的fd到_fd_set位图中
调用select事件检测
遍历数组,找到就绪的事件,根据就绪事件,完成对应的动作
①Accepter ②Recver
}
select的模拟实现代码如下,具体细节都在代码中的注释中有体现:
Sock.hpp:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "Log.hpp"class Sock
{
private:const static int gbacklog = 20;public:Sock(){}// 创建套接字static int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){// logMessage(FATAL, "create socket error,%d:%s", errno, strerror(errno));exit(2);}int opt = 1;setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));// 文件描述符012默认打开,所以再创建就是3// logMessage(NORMAL, "create socket success, listensock: %d", listensock);return listensock;}// bindstatic void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"){// bind 文件 + 网络struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 4字节ip->转为网络// binf失败if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){// logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));exit(3);}}//监听static void Listen(int sock){if (listen(sock, gbacklog) < 0){// 监听失败// logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));exit(4);}// logMessage(NORMAL, "init server success");}//获取连接(server端)static int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len = sizeof(src);int serversock = accept(listensock, (struct sockaddr *)&src, &len);if (serversock < 0){// 获取连接失败// logMessage(ERROR, "accept error,%d : %s", errno, strerror(errno));return -1;}//拿到客户端的IP和portif(port) *port = ntohs(src.sin_port);if(ip) *ip = inet_ntoa(src.sin_addr);return serversock;}//连接函数(client端,发起连接请求)static bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);//主机->网络server.sin_addr.s_addr = inet_addr(server_ip.c_str());//inet_addr->点分十进制->4字节IPif(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;else return false;}~Sock(){}
};
Log.hpp:
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <ctype.h>
#include "Log.hpp"class Sock
{
private:const static int gbacklog = 20;public:Sock(){}// 创建套接字static int Socket(){int listensock = socket(AF_INET, SOCK_STREAM, 0);if (listensock < 0){// logMessage(FATAL, "create socket error,%d:%s", errno, strerror(errno));exit(2);}int opt = 1;setsockopt(listensock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));// 文件描述符012默认打开,所以再创建就是3// logMessage(NORMAL, "create socket success, listensock: %d", listensock);return listensock;}// bindstatic void Bind(int sock, uint16_t port, std::string ip = "0.0.0.0"){// bind 文件 + 网络struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);inet_pton(AF_INET, ip.c_str(), &local.sin_addr); // 4字节ip->转为网络// binf失败if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){// logMessage(FATAL, "bind error,%d:%s", errno, strerror(errno));exit(3);}}//监听static void Listen(int sock){if (listen(sock, gbacklog) < 0){// 监听失败// logMessage(FATAL, "listen error,%d:%s", errno, strerror(errno));exit(4);}// logMessage(NORMAL, "init server success");}//获取连接(server端)static int Accept(int listensock, std::string *ip, uint16_t *port){struct sockaddr_in src;socklen_t len = sizeof(src);int serversock = accept(listensock, (struct sockaddr *)&src, &len);if (serversock < 0){// 获取连接失败// logMessage(ERROR, "accept error,%d : %s", errno, strerror(errno));return -1;}//拿到客户端的IP和portif(port) *port = ntohs(src.sin_port);if(ip) *ip = inet_ntoa(src.sin_addr);return serversock;}//连接函数(client端,发起连接请求)static bool Connect(int sock, const std::string &server_ip, const uint16_t &server_port){struct sockaddr_in server;memset(&server, 0, sizeof(server));server.sin_family = AF_INET;server.sin_port = htons(server_port);//主机->网络server.sin_addr.s_addr = inet_addr(server_ip.c_str());//inet_addr->点分十进制->4字节IPif(connect(sock, (struct sockaddr*)&server, sizeof(server)) == 0) return true;else return false;}~Sock(){}
};
main.cc
#include "selectServer.hpp"
#include <memory>int main()
{unique_ptr<selectServer> svr(new selectServer());svr->Start();return 0;
}
selectServer.hpp
#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__#include <sys/select.h>
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>#include "Sock.hpp"
#include "Log.hpp"using namespace std;#define BITS 8
#define NUM (sizeof(fd_set) * BITS)
#define FD_NONE -1class selectServer
{
public:selectServer(const uint16_t& port = 8080):_port(port){_listensock = Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);logMessage(DEBUG, "create base socket success");// 将 _fd_array 数组初始化为-1// 再将 _fd_array[0] = _listensockfor(int i = 0; i < NUM; i++) _fd_array[i] = FD_NONE;_fd_array[0] = _listensock;}void Start(){while(true){// struct timeval timeout = {0, 0};// 将_listensock添加到读文件描述符集rdfds中// FD_SET(_listensock, &rdfds);// nfds: 添加到select中的sock越来越多,所以每一次都会变化// 二三四个参数:输入输出型参数,每一次都是不一样的,所以每一次都要重新添加// timeout:输入输出型参数,每一次都要重置// 所以需要将合法的文件描述符保存起来,存到 _fd_array数组中DebugPrint();fd_set rdfds;FD_ZERO(&rdfds);int maxfd = _listensock;for(int i = 0; i < NUM; i++){if(_fd_array[i] == FD_NONE) continue;FD_SET(_fd_array[i], &rdfds);if(maxfd < _fd_array[i]) maxfd = _fd_array[i];}int n = select(maxfd+1, &rdfds, nullptr, nullptr, nullptr);switch(n){case 0:logMessage(DEBUG, "time out...");break;case -1:logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));break;default:logMessage(DEBUG, "get a new link event...");HandlerEvent(rdfds);break;}}}~selectServer(){if(_listensock >= 0)close(_listensock);}
private:void HandlerEvent(const fd_set& rdfds){for(int i = 0; i < NUM; i++){// 去掉不合法的fdif(_fd_array[i] == FD_NONE) continue;// 合法了不一定就绪,只有在文件描述符集 rdfds 中,才表示就绪if(FD_ISSET(_fd_array[i], &rdfds)){// 读事件就绪,连接事件到来(_listensock)if(_fd_array[i] == _listensock) Accepter();// 读事件就绪,INPUT事件到来: read / recvelse Recver(i);}}}void Accepter(){string clientip;uint16_t clientport = 0;// 表示listensock上面的读事件就绪了,可以读取了,此时不会阻塞int sock = Sock::Accept(_listensock, &clientip, &clientport);if(sock < 0){logMessage(WARNING, "accept error");return;}logMessage(DEBUG, "get a new link success : [%s:%d] : %d", clientip.c_str(), clientport, sock);// 这里不能直接read/recv,因为不能确定sock上的数据什么到来,此时就有可能会被阻塞// 所以这里也让select帮我们检测sock上是否有新的数据,如果有再通知我,此时就不会被阻塞了// 直接将 sock 放入数组 _fd_array 中即可int pos = 1;for(; pos < NUM; pos++){if(_fd_array[pos] == FD_NONE) break;}if(pos == NUM){// 文件描述符集已经满了logMessage(WARNING, "select server already full, close: %d", sock);close(sock);}else{// 找到了文件描述符集中值为 FD_NONE 的位置_fd_array[pos] = sock;}}void Recver(int i){logMessage(DEBUG, "message in, get IO event: %d", _fd_array[i]);char buffer[1024];int n = recv(_fd_array[i], buffer, sizeof(buffer)-1, 0);if(n > 0){ buffer[n] = 0;logMessage(DEBUG, "client[%d]# %s", _fd_array[i], buffer);}else if(n == 0){logMessage(DEBUG, "client[%d] quit, me too...", _fd_array[i]);// 1. 关闭不需要的描述符close(_fd_array[i]);// 2. 数组对应位置置为FD_NONE,select也不需要关心了_fd_array[i] = FD_NONE;}else{logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[i], errno, strerror(errno));// 1. 关闭不需要的描述符close(_fd_array[i]);// 2. 数组对应位置置为FD_NONE,select也不需要关心了_fd_array[i] = FD_NONE; }}void DebugPrint(){cout << "_fd_array[]: ";for(int i = 0; i < NUM; i++){if(_fd_array[i] == FD_NONE) continue;cout << _fd_array[i] << " ";}cout << endl;}private:uint16_t _port;int _listensock;int _fd_array[NUM]; // 数组保存合法的fd
};#endif
select的优缺点
优点:
①相比于之前效率比较高,因为在单位时间内等的比重大大减少了,单位时间内任何文件描述符就绪的概率比之前大
②应用场景:有大量的连接,但是只有少量是活跃的,省资源
缺点:
①为了维护第三方数组,select服务器会充满大量数组
②每一次都要对select输出参数进行重新设定
③能够同时管理的fd的个数是有上限的(1024)
④因为几乎每一个参数都是输入输出型的,所以select一定会频繁的进行用户到内核、内核到用户的数据拷贝
⑤编码比较复杂
I/O多路转接之poll
下面的poll和epoll都是针对于select的缺点进步的
poll主要针对了select的如下两个缺点:
①输入输出参数一体
②管理fd个数是有上限
poll针对于这两个缺点做以改进
poll与select一样,也是只负责等
用户告诉内核:你要帮我关心哪些fd上的哪些事件
内核告诉用户:哪些fd已经就绪了
poll函数的第一个和第二个参数就用于解决上述的两个问题,与select的输入输出参数不同的是,select的输入输出参数是位图结构,所以需要改动
而poll的参数是结构体,结构体中可以有很多成员,有些成员是解决第一个问题,有些成员是解决第二个问题的
poll函数
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
函数参数:
fds:struct pollfd的结构体的起始地址,每一个元素包含三部分内容:文件描述符、监视的事件集合、就绪的事件集合
nfds:表示fds数组的长度
timeout:表示poll函数在多长时间后返回,单位是毫秒(ms)
参数timeout的取值:
大于0:每隔多少秒,timeout一次
0:以非阻塞的方式等
-1:以阻塞的方式等
函数返回值:
大于0:是多少就表示有多少个文件描述符就绪
等于0:说明超时了
小于0:表示函数调用失败,同时错误码会被设置
struct pollfd结构体
fd表示文件描述符
events:需要监视该文件描述符上的哪些事件
revents:poll函数返回时告知用户该文件描述符上的哪些事件已经就绪
poll中,区分读、写、异常的方式是给events设置进POLLIN、POLLOUT、POLLERR就可以了
其中POLLIN、POLLOUT是宏
poll函数的使用示例
简易使用示例代码:
#include <iostream>
#include <poll.h>
#include <cstdio>
#include <unistd.h>using namespace std;int main()
{struct pollfd poll_fd;poll_fd.fd = 0;// 关心读事件就绪poll_fd.events = POLLIN;while(true){int ret = poll(&poll_fd, 1, 1000);if(ret < 0){perror("poll");continue;}else if(ret == 0){cout << "poll timeout..." << endl;continue;}// 表示成功返回if(poll_fd.revents == POLLIN){cout << "poll event ready!" << endl;char buff[1024] = {0};read(0, buff ,sizeof(buff)-1);cout << "stdin: " << buff << endl;}}return 0;
}
运行结果:
可以看到,在示例代码中,在while死循环中,让poll关注读事件,如果返回值小于等于0,表示没有就绪,此时继续循环
如果返回值大于0,表示就绪了,此时判断revents是否是POLLIN,如果是就表示读事件就绪,调用read接口读取内容,并打印出来
poll与select的模拟实现的代码非常相似,并且是在select的基础上做以改进,比select的视线更简单
poll的模拟实现
同样的Sock.hpp和Log.hpp与select一样,makefile和main.cc也是相差不大,下面只体现pollServer.hpp的代码实现:
#ifndef __POLL_SVR_H__
#define __POLL_SVR_H__#include <poll.h>
#include <iostream>
#include <cstring>
#include <string>
#include <unistd.h>#include "Sock.hpp"
#include "Log.hpp"using namespace std;#define FD_NONE -1class PollServer
{
public:static const int nfds = 100;
public:PollServer(const uint16_t& port = 8080):_port(port), _nfds(nfds){_listensock = Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);logMessage(DEBUG, "create base socket success");_fds = new struct pollfd[_nfds];// 初始化 struct pollfd结构体for(int i = 0; i < _nfds; i++){_fds[i].fd = FD_NONE;_fds[i].events = _fds[i].revents = 0;} // 将_listensock 填入0号位置_fds[0].fd = _listensock;_fds[0].events = POLLIN;_timeout = 1000;}void Start(){while(true){DebugPrint();int n = poll(_fds, _nfds, _timeout);switch(n){case 0:logMessage(DEBUG, "time out...");break;case -1:logMessage(WARNING, "poll error: %d : %s", errno, strerror(errno));break;default:HandlerEvent();break;}}}~PollServer(){if(_listensock >= 0)close(_listensock);if(_fds)delete []_fds;}
private:void HandlerEvent(){for(int i = 0; i < _nfds; i++){// 去掉不合法的fdif(_fds[i].fd == FD_NONE) continue;// _fds[i].revents 中如果存在POLLIN,就说明读事件就绪if(_fds[i].revents & POLLIN){// 读事件就绪,连接事件到来(_listensock)if(_fds[i].fd == _listensock) Accepter();// 读事件就绪,INPUT事件到来: read / recvelse Recver(i);}}}void Accepter(){string clientip;uint16_t clientport = 0;// 表示listensock上面的读事件就绪了,可以读取了,此时不会阻塞int sock = Sock::Accept(_listensock, &clientip, &clientport);if(sock < 0){logMessage(WARNING, "accept error");return;}logMessage(DEBUG, "get a new link success : [%s:%d] : %d", clientip.c_str(), clientport, sock);int pos = 1;for(; pos < _nfds; pos++){if(_fds[pos].fd == FD_NONE) break;}if(pos == _nfds){logMessage(WARNING, "poll server already full, close: %d", sock);close(sock);}else{_fds[pos].fd = sock;_fds[pos].events = POLLIN;}}void Recver(int i){logMessage(DEBUG, "message in, get IO event: %d", _fds[i].fd);char buffer[1024];int n = recv(_fds[i].fd, buffer, sizeof(buffer)-1, 0);if(n > 0){ buffer[n] = 0;logMessage(DEBUG, "client[%d]# %s", _fds[i].fd, buffer);}else if(n == 0){logMessage(DEBUG, "client[%d] quit, me too...", _fds[i].fd);// 1. 关闭不需要的描述符close(_fds[i].fd);// 2. _fds对应位置置为FD_NONE,poll也不需要关心了_fds[i].fd = FD_NONE;_fds[i].events = 0;}else{logMessage(WARNING, "%d sock recv error, %d : %s", _fds[i].fd, errno, strerror(errno));// 1. 关闭不需要的描述符close(_fds[i].fd);// 2. _fds对应位置置为FD_NONE,poll也不需要关心了_fds[i].fd = FD_NONE; _fds[i].events = 0;}}void DebugPrint(){cout << "_fd_array[]: ";for(int i = 0; i < _nfds; i++){if(_fds[i].fd == FD_NONE) continue;cout << _fds[i].fd << " ";}cout << endl;}private:uint16_t _port;int _listensock;// poll的三个参数都设为成员变量struct pollfd* _fds;int _nfds;int _timeout;
};#endif
poll的优缺点
优点:
①相比于之前效率比较高,同样在单位时间内等的比重大大减少了,单位时间内任何文件描述符就绪的概率比之前大
②应用场景:有大量的连接,但是只有少量是活跃的,同样节省资源
③输入输出参数是分离,不需要每次都进行重置
④poll参数级别,没有可以管理的fd的上限
缺点:
①poll服务器依旧需要不少的遍历,在用户层检测时间就绪,在内核检测fd就绪
②poll需要内核到用户的拷贝
③poll代码编写也比较复杂,但是相比于select容易一些
I/O多路转接之epoll
epoll也是系统提供的一个多路转接接口
epoll的三个系统调用
epoll_create函数
epoll_create函数用于创建一个epoll模型:
int epoll_create(int size);
参数:
size参数一般是被忽略的,但size的值必须设置为大于0的值,至于为什么不废弃,是因为需要向前向后兼容
返回值:
epoll模型创建成功返回其对应的文件描述符,否则返回-1,同时错误码会被设置
epoll_ctl函数
epoll_ctl函数用于向指定的epoll模型进行操作:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
参数:
epfd:指定的epoll模型(epoll_create的返回值)
op:表示具体的动作,用三个宏来表示(增加、删除、修改)
fd:需要监视的文件描述符
event:需要监视该文件描述符上的哪些事件
op的取值:
EPOLL_CTL_ADD
:增加EPOLL_CTL_MOD
:修改EPOLL_CTL_DEL
:删除
返回值:
函数调用成功返回0,调用失败返回-1,同时错误码会被设置
epoll_wait函数
epoll_wait函数用于收集监视的事件中已经就绪的事件
int epoll_wait(int epfd, struct epoll_event *events,int maxevents,int timeout)
参数:
在特定的epfd中,获取已经就绪的事件,maxevents是events数组中的元素个数
events是输出型参数,内核告诉用户的已经就绪的事件
timeout与poll中的timeout是一样的含义
大于0:每隔多少秒,timeout一次
0:以非阻塞的方式等
-1:以阻塞的方式等
结构体epoll_event
有两个成员,分别是events和data
events是哪个事件就绪,例如EPOLLIN
data是一个联合体,每次只使用一个成员,这里我们使用fd成员
返回值:
返回已经就绪的文件描述符的个数
epoll返回的时候,会将就绪的event按照顺序放入events数组中,即从0下标开始,一共有返回值个
如果底层就绪的sock非常多,events装不下,那么一次拿不完就下一次再拿
epoll的工作原理
在学习epoll的工作原理之前,先想想前面所学习的select和poll的共识:
①无论是select和poll,都是需要用户自己维护一个数组,来进行保存特定的fd与特定的事件
②select和poll都需要遍历
③select和poll的工作模式:
a. 用户告诉内核,需要你帮我关心哪些文件描述符上的哪些事件
b. 内核告诉用户,哪些文件描述符上的哪些事件已经就绪
epoll模型:
当某一进程调用epoll_create函数时,Linux内核会创建一个epoll模型
红黑树与就绪队列:
红黑树:本质就是用户告诉内核,需要监视哪些文件描述符上的哪些事件,调用epll_ctl函数实际就是在对这颗红黑树进行对应的增删改操作
就绪队列:本质就是内核告诉用户,哪些文件描述符上的哪些事件已经就绪了,调用epoll_wait函数实际就是在从就绪队列当中获取已经就绪的事件
红黑树需要key值,而这里的fd就是一个天然的key值
回调机制:
对于select和poll来说,操作系统在监视多个文件描述符上的事件是否就绪时,需要让操作系统主动对这多个文件描述符进行轮询检测,这一定会增加操作系统的负担
而对于epoll来说,操作系统不需要主动进行事件的检测,当红黑树中监视的事件就绪时,会自动调用对应的回调方法,将就绪的事件添加到就绪队列当中
当用户调用epoll_wait函数获取就绪事件时,只需要关注底层就绪队列是否为空,如果不为空则将就绪队列当中的就绪事件拷贝给用户即可
采用回调机制最大的好处,就是不再需要操作系统主动对就绪事件进行检测了,当事件就绪时会自动调用对应的回调函数进行处理
下面是总结的epoll的五个细节:
①文件描述符可以作为红黑树天然的key值
②用户只需要设置关系,获取结果即可,不用关心任何对fd和event的管理细节
③epoll高效是因为:
第一、底层是用的红黑树管理的,之前是数组管理的,所以增删改的效率高
第二、之前文件描述符是否就绪需要操作系统遍历,现在只需要就绪以后回调即可,所以操作系统并不需要浪费精力在文件描述符的时间监测上
第三、以前想要获取就绪的文件描述符,依旧需要操作系统遍历,现在epoll有就绪队列,只需要调用epoll_wait从就绪队列中获取就绪结点即可,可以以O(1)的方式直接监测队列是否有数据
④epoll底层只要有fd就绪了,OS会自己构建节点,插入到就绪队列中,上层只需要不断地从就绪队列中将数据拿走,就完成了获取就绪事件的任务
(生产者消费者模型,就绪队列本质是共享资源,epoll保证所有epoll接口是线程安全的,也就是进行了加锁)
⑤如果底层没有就绪事件,那么上层只能阻塞等待
epoll的模拟实现
同样只实现读取
main.cc:
#include <memory>
#include "EpollServer.hpp"using namespace std;void Print(string request)
{cout << "change: " << request << endl;
}int main()
{unique_ptr<EpollServer> svr(new EpollServer(Print));svr->Start();return 0;
}
Epoll.hpp(封装的三个epoll的函数):
#include <iostream>
#include <sys/epoll.h>class Epoll
{
public:static const int gsize = 256; // 随便一个大于0的数都可以public:static int CreateEpoll(){int epfd = epoll_create(gsize);if(epfd > 0) return epfd;exit(5);}static bool CtlEpoll(int epfd, int op, int sock, uint32_t events){struct epoll_event ev;ev.events = events;ev.data.fd = sock;int n = epoll_ctl(epfd, op, sock, &ev);return n == 0;}static int EpollWait(int epfd, struct epoll_event *events, int maxevents, int timeout){return epoll_wait(epfd, events, maxevents, timeout);}
};
EpollServer.hpp:
#ifndef __EPOLL_SERVER_H__
#define __EPOLL_SERVER_H__#include <iostream>
#include <sys/epoll.h>
#include <string>
#include <functional>
#include <cassert>
#include <unistd.h>#include "Sock.hpp"
#include "Log.hpp"
#include "Epoll.hpp"using namespace std;class EpollServer
{
public:using func_t = function<void(string)>;static const int gmax = 100;
public:EpollServer(func_t HandlerRequest, const uint16_t& port = 8080):_port(port), _maxevent(gmax), _HandlerRequest(HandlerRequest){// 0. 申请对应的空间_events = new struct epoll_event[_maxevent];// 1. 创建listensock_listensock = Sock::Socket();Sock::Bind(_listensock, _port);Sock::Listen(_listensock);// 2. 创建epoll模型_epfd = Epoll::CreateEpoll();logMessage(DEBUG, "init success, listensock: %d, epfd: %d", _listensock, _epfd);// 将listensock先添加到epoll中,让epoll帮我们管理起来if(!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, _listensock, EPOLLIN)) exit(6);logMessage(DEBUG, "add listensock to epoll success.");}void Accepter(int listensock){string clientip;uint16_t clientport;int sock = Sock::Accept(listensock, &clientip, &clientport);if(sock < 0){logMessage(WARNING, "accept error");return;}// 将新的sock,添加给epollif(!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, sock, EPOLLIN)) return;logMessage(DEBUG, "add new sock : %d to epoll success", sock);}void Recver(int sock){char buffer[1024];ssize_t n = recv(sock, buffer, sizeof(buffer)-1, 0);if(n > 0){// 假设读到了完整的报文buffer[n] = 0;// 处理数据_HandlerRequest(buffer);}else if(n == 0){// 1. 先在epoll模型中去掉对 sock 的关心bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);assert(res);(void)res;// 2. 再close文件close(sock);logMessage(NORMAL, "client %d quit, me too...", sock);}else{// 1. 先在epoll模型中去掉对 sock 的关心bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);assert(res);(void)res;// 2. 再close文件close(sock);logMessage(NORMAL, "client recv %d error, close error sock", sock); }}void HandlerEvents(int n){assert(n > 0);for(int i = 0; i < n; i++){uint32_t revents = _events[i].events;int sock = _events[i].data.fd;// 读事件就绪if(revents & EPOLLIN){if(sock == _listensock) Accepter(_listensock); // listensock就绪else Recver(sock); // 一般的sock就绪 -- read}}}// 循环一次的函数void LoopOnce(int timeout){int n = Epoll::EpollWait(_epfd, _events, _maxevent, timeout);switch(n){case 0:logMessage(DEBUG, "timeout...");break;case -1:logMessage(WARNING, "epoll wait error: %s", strerror(errno));break;default:// 等待成功logMessage(DEBUG, "get a event");HandlerEvents(n);break;}}void Start(){int timeout = -1;while(true){LoopOnce(timeout);}}~EpollServer(){if(_listensock >= 0) close(_listensock);if(_epfd >= 0) close(_epfd);if(_events) delete[] _events;}
private:int _listensock;uint16_t _port;int _epfd; // epoll模型struct epoll_event* _events; // 结构体数组int _maxevent; // _events的容量func_t _HandlerRequest; // Recver函数中的处理方法
};#endif
IO多路转接之select、poll、epoll到此结束啦