IO多路转接:select、poll、epoll

目录

非阻塞读取

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到此结束啦

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/147031.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

当你在Linux系统中使用MySQL命令行工具查询数据库时,如果中文显示为问号(?)或其他乱码,简单解决办法。(2)

文章目录 1、问题出现2、解决办法 1、问题出现 2、解决办法 mysql -u [username] -p --default-character-setutf8 [database_name]rootab66508d9441:/# mysql -uroot -p123456 --default-character-setutf8 tingshu_album mysql: [Warning] Using a password on the command …

Redis 字符串类型的典型应用场景

目录 1. 缓存功能 2. 计数功能 3. 共享会话&#xff08;Session&#xff09; 4. 手机验证码 前言 这里将详细介绍 Redis 字符串类型在实际开发中的几个典型应用场景&#xff0c;并提供相应的伪代码示例。 1. 缓存功能 场景描述 在许多Web应用中&#xff0c;数据通常需要…

使用AVL树实现Map

一、数组在裂变扩容时可能会出现环、在数组元素转为链表之后选择尾插法插入节点、数组到链表到AVL到RBT的转换 1、数组在裂变扩容时链表中的节点计算出来的位置可能也会发生变化&#xff0c;在多线程情况下调整节点位置可能会出现环。 2、数组中的数组元素转为链表后插入新节点…

在大模型训练中,为什么GPU 通常比 CPU 更重要

在大模型训练中&#xff0c;GPU 通常比 CPU 更重要&#xff0c;原因主要有以下几点&#xff1a; 一、并行计算能力 GPU 拥有强大的并行计算能力。在大模型训练中&#xff0c;需要处理海量的数据和复杂的计算任务。例如&#xff0c;深度学习模型中的矩阵运算、卷积运算等&…

13. 了解人工智能可能存在的偏见

这篇文章没有太多技术和代码细节&#xff0c;更多的是作为一份有趣的报告。 这里没有任何模型会被训练。 这篇文章也为生成式人工智能导论课程中 HW8: Safety Issues of Generative AI 提供中文引导。 代码文件下载 文章目录 为什么人工智能存在偏见&#xff1f;动手试试加载模…

算法_BFS解决多源最短路问题---持续更新

文章目录 前言引入矩阵题目要求题目解析代码如下 飞地的数量题目要求题目解析代码如下 地图中的最高点题目要求题目解析代码如下 地图分析题目要求题目解析代码如下 前言 本文将会向你介绍有关宽度优先搜索&#xff08;BFS&#xff09;解决多源最短路问题的相关题型&#xff1…

故障诊断│GWO-DBN灰狼算法优化深度置信网络故障诊断

1.引言 随着人工智能技术的快速发展&#xff0c;深度学习已经成为解决复杂问题的热门方法之一。深度置信网络&#xff08;DBN&#xff09;作为深度学习中应用比较广泛的一种算法&#xff0c;被广泛应用于分类和回归预测等问题中。然而&#xff0c;DBN的训练过程通常需要大量的…

机器人速度雅可比矩阵(机器人动力学)

博途PLC矩阵求逆 矩阵求逆 博图SCL_博图矩阵运算-CSDN博客文章浏览阅读839次。本文介绍如何用C语言实现矩阵求逆的过程,详细解析了相关代码,适合线性代数和编程爱好者学习。https://rxxw-control.blog.csdn.net/article/details/122367883 1、二自由度平面关节机器人速度雅…

项目第十二弹:功能联调

项目第十二弹&#xff1a;功能联调 一、发布订阅功能测试1.生产者2.消费者3.演示4.持久化信息查看1.消息2.SQLite3数据库 二、持久化恢复测试1.代码2.gc3.演示 三、虚拟机和信道隔离测试1.责任划分2.如何测试3.生产者4.消费者5.演示 一、发布订阅功能测试 我们直接上TOPIC交换…

MySQL中的逻辑条件

逻辑条件组合两个比较条件的结果来产生一个基于这些条件的单个的结果&#xff0c;或者逆转一个单个条件的结果。当所有条件的结果为真时&#xff0c;返回行。 SQL的三个逻辑运算符是&#xff1a; AND、OR、NOT 可以在WHERE子句中用AND和OR运算符使用多个条件。 示例一&#…

惊爆!高通要收购英特尔,巨头也会被时代抛弃!

今天看到的外媒消息&#xff0c;高通要收购英特尔&#xff0c;看到消息的时候&#xff0c;其实&#xff0c;还是挺吃惊的。 高通是移动芯片的王者&#xff0c;英特尔是 PC 芯片的王者。当然了&#xff0c;英特尔这个可能需要再加上两个字&#xff1a;曾经的 PC 芯片王者。 其实…

植物大战僵尸【源代码分享+核心思路讲解】

植物大战僵尸已经正式完结&#xff0c;今天和大家分享一下&#xff0c;话不多说&#xff0c;直接上链接&#xff01;&#xff01;&#xff01;&#xff08;如果大家在运行这个游戏遇到了问题或者bug&#xff0c;那么请私我谢谢&#xff09; 大家写的时候可以参考一下我的代码思…

在VMware16中安装Windows 10:完整教程

在VMware中安装Windows 10&#xff1a;完整教程 1.安装环境准备2.创建虚拟机 1.安装环境准备 1.虚拟机: VMware-workstation-full-16.2.2-19200509 2.系统镜像:win10 2.创建虚拟机 1.自定义 2.下一步 3.稍后安装系统 3.默认下一步 4.虚拟机取名和选择存放路径(按需更改…

利士策分享,江西新余悲剧背后的深思:安全与责任的重构

利士策分享&#xff0c;江西新余悲剧背后的深思&#xff1a;安全与责任的重构 在这个信息瞬息万变的时代&#xff0c;每一次突发事件都能迅速触动社会的神经&#xff0c; 而江西新余近期发生的悲剧&#xff0c;更是让我们在悲痛之余&#xff0c;不得不深刻反思安全管理与社会…

AVL树与红黑树

目录 AVL树 AVL树节点的定义 AVL树的插入 AVL树的旋转 右单旋 左单旋 左右双旋 右左双旋 AVL树的验证 AVL树的性能 红黑树 红黑树的性质 红黑树节点的定义 红黑树结构 红黑树的插入操作 按照二叉搜索的树规则插入新节点 检测新节点插入后&#xff0c;红黑树的性…

升级你的HarmonyOS体验:一窥功能引导与拖拽交换的独家技巧

文章目录 前言项目目录结构开发流程主要步骤讲解关键配置Index.ets 页面讲解高光组件相关HeaderApp 总结 前言 在当今的移动应用开发领域&#xff0c;为了提供更加友好和直观的用户体验&#xff0c;开发者们通常会集成多种交互功能来增强应用的互动性和易用性。在这些功能中&a…

【机器学习】12-决策树1——概念、特征选择

机器学习10-决策树1 学习样本的特征&#xff0c;将样本划分到不同的类别&#xff08;分类问题&#xff09;或预测连续的数值&#xff08;回归问题&#xff09;。 选择特征&#xff0c;划分数据集&#xff0c;划分完成形成模型&#xff08;树结构&#xff09;&#xff0c;一个…

JavaSE——多线程基础

概述 现代操作系统&#xff08;Windows&#xff0c;macOS&#xff0c;Linux&#xff09;都可以执行多任务。多任务就是同时允许多个任务。例如&#xff1a;播放音乐的同时&#xff0c;浏览器可以进行文件下载&#xff0c;同时可以进行QQ消息的收发。 CPU执行代码都是一条一条顺…

Matlab R2018a怎么下载安装?Matlab R2018a保姆级详细安装教程

Matlab R2018a下载方法&#xff1a; Matlab R2018a安装教程&#xff1a; 1、右击下载好的压缩包&#xff0c;选择解压到Matlab R2018a 2、打开文件夹【R2018a_win64】&#xff0c;右击下面的setup.exe&#xff0c;选择【以管理员身份运行】 3、点击选择【使用文件安装密钥】&a…

IDEA连接数据库报错:Access denied for user ****

使用IDEA开发时&#xff0c;通过Databse连接数据库。多次连接报错&#xff1a;Access denied for user **** 如下所示&#xff1a; ​ ‍ ‍ ​ ‍ 花了不少时间排查&#xff0c;确认账号、密码&#xff0c;后面发现账号后多了个空格&#xff0c;而且不容易发现&#xf…