目录
1.先说说什么是IO多路复用
IO多路复用指的是除了多进程、多线程以外用来解决并发的手段。
以网络IO为例子:当大量网络链接建立时,不需要创建多线程来消耗大量系统资源。
以一个进程作为入口,用一个监视器(select poll epoll)来监视链接,当链接发来数据时,select会发生响应,没有数据时我们可以进行其他正常操作。
这样,我们就节省了等待的时间,当发生IO时就立即处理。
那么IO多路复用的使用场景也就很明确了:可以维护大量IO,但同时IO不能过于密集
2.select
函数原型:int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
fd_set是一个位图结构,用位来标识要监视的fd。
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的全部位
nfds表示最大的fd,readfds表示需要关注读的fd,writedfs表示需要关注写的fd。
exceptfds表示出错的fd,timeout用来定时,超时返回0。
错误返回-1,正确返回改变的fd个数
来段使用select思想的socket网络代码:
#pragma once
#include "sock.hpp"
#include "log.hpp"
#include <iostream>
#include <sys/select.h>
#include <sys/time.h>
#include <unistd.h>
using namespace std;
#define BITS 8
#define NUM (sizeof(fd_set) * BITS)
#define FD_NONE -1
class SelectServer
{
public:
SelectServer(uint16_t port = 8080)
{
_port = port;
//socket套接字编程,直接调的系统接口
_listensock = Sock::Socket();
Sock::Bind(_listensock, _port);
Sock::Listen(_listensock);
//需要使用一个数组来存储需要关注的fd
for (int i = 0; i < NUM; i++)
_fd_set_array[i] = FD_NONE;
//listensock存在0下标
_fd_set_array[0] = _listensock;
}
~SelectServer()
{
if (_listensock >= 0)
close(_listensock);
}
void Start()
{
while (true)
{
//只关心读
fd_set rfds; // fd集合
FD_ZERO(&rfds); // 先初始化
int maxfd = _listensock; // 找最大fd
for (int i = 0; i < NUM; i++)
{
if (_fd_set_array[i] == FD_NONE)
continue;
//非空就进行设置
FD_SET(_fd_set_array[i], &rfds);
//找到最大fd
if (maxfd < _fd_set_array[i])
maxfd = _fd_set_array[i];
}
//监视rfds,这里只关心读事件
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
//日志,不用在意具体实现
logMessage(DEBUG, "%s", "time out...");
break;
case -1:
logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
break;
default:
logMessage(DEBUG, "get a new link event...");
HandEvent(rfds);
break;
}
}
}
private:
void HandEvent(const fd_set &rfds)
{
for (int i = 0; i < NUM; i++)
{
if (_fd_set_array[i] == FD_NONE)
continue;
//判断rfds中是否存在_fd_set_array[i]
if (FD_ISSET(_fd_set_array[i], &rfds))
{
//是listensock,表示需要accept创建接收sock
if (_fd_set_array[i] == _listensock)
Accepter();
else
//表示接收sock
Recver(i);
}
}
}
void Accepter()
{
string clientip;
uint16_t clientport;
//accept接收
int sock = Sock::Accept(_listensock, &clientip, &clientport);
logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
int pos = 1;
for (; pos < NUM; pos++)
{
if (_fd_set_array[pos] == FD_NONE)
break;
}
if (pos == NUM)
{
logMessage(WARNING, "%s:%d", "select server already full , close: %d", sock);
close(sock);
}
else
{
//找到一个位置放上接收sock
_fd_set_array[pos] = sock;
}
}
void Recver(int pos)
{
logMessage(DEBUG, "message in, get IO event: %d", _fd_set_array[pos]);
char buffer[1024];
//从接收sock中接收数据 暂且不考虑粘包问题
int s = recv(_fd_set_array[pos], buffer, sizeof(buffer) - 1, 0);
if (s > 0)
{
buffer[s] = 0;
logMessage(DEBUG, "client[%d]# %s", _fd_set_array[pos], buffer);
}
else if (s == 0)
{
//读端关闭
logMessage(DEBUG, "client[%d] quit, me too...", _fd_set_array[pos]);
//关闭sock
close(_fd_set_array[pos]);
_fd_set_array[pos] = FD_NONE;
}
else
{
logMessage(WARNING, "%d sock recv error, %d : %s", _fd_set_array[pos], errno, strerror(errno));
close(_fd_set_array[pos]);
_fd_set_array[pos] = FD_NONE;
}
}
private:
int _listensock;
int _fd_set_array[NUM];
uint16_t _port;
};
select的缺点:
每次使用select都需要设定fd集合,将fd集合从用户态拷贝到内核态。
需要使用一个数组来存储fd,需要频繁的修改。
fd_set大小有限,支持存储fd大小有限。
3.poll
函数原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);
pollfd结构:
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events */
short revents; /* returned events */
};
fd表示需要关注的文件描述符
events表示这个描述符关注的事件,常见的有三个:
POLLIN:可读 POLLOUT:可写 POLLERR:错误
revents表示返回的标记
nfds同样表示关注的最大文件描述符
timeout表示时间限制,单位是毫秒
改写上面select版的socket通信,改成poll版
#pragma once
#include"sock.hpp"
#include"log.hpp"
#include<unistd.h>
#include<poll.h>
#define FD_NONE -1
using namespace std;
class PollServer
{
public:
const static int nfds=100;
const static int timeout=1000; //1秒
PollServer(uint16_t port=8080)
:_port(port)
{
_nfds=nfds;
_timeout=timeout;
_listensock=Sock::Socket();
Sock::Bind(_listensock,_port);
Sock::Listen(_listensock);
//创建pollfd组
_fds=new struct pollfd[_nfds];
//初始化
for(int i=0;i<_nfds;i++)
{
_fds[i].fd=FD_NONE;
_fds[i].events=_fds[i].revents=0;
}
//listensock只关注读
_fds[0].fd=_listensock;
_fds[0].events=POLLIN;
}
void Start()
{
while(true)
{
int n=poll(_fds,_nfds,_timeout);
//帮用户检测文件描述符是否就绪
switch(n)
{
case 0:
logMessage(DEBUG, "%s", "time out...");
break;
case -1:
logMessage(WARNING, "poll error: %d : %s", errno, strerror(errno));
break;
default:
HandEvent();
break;
}
}
}
void HandEvent()
{
for(int i=0;i<_nfds;i++)
{
if(_fds[i].fd == FD_NONE)
continue;
//只关注读
if(_fds[i].revents & POLLIN)
{
//需要创建读取的sock
if(i == 0)
Accepter();
else
//从读取sock读取数据
Recver(i);
}
}
}
void Accepter()
{
string clientip;
uint16_t clientport;
int sock=Sock::Accept(_listensock,&clientip,&clientport);
logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
int pos=1;
for(;pos<_nfds;pos++)
{
if(_fds[pos].fd == FD_NONE)
break;
}
//不够用了,可以通过增加nfds的值来搞
if(pos == _nfds)
{
logMessage(WARNING, "%s:%d", "poll server already full ,close: %d", sock);
close(sock);
}
else
{
//设置进pollfd中
_fds[pos].fd=sock;
//只关注读
_fds[pos].events=POLLIN;
}
}
void Recver(int pos)
{
char buffer[1024];
//读取数据
int s=recv(_fds[pos].fd,buffer,sizeof buffer-1,0);
if(s == 0)
{
//读端关闭
logMessage(DEBUG, "client[%d] quit, me too...", _fds[pos].fd);
close(_fds[pos].fd);
//不要忘记设置pos位置无效
_fds[pos].fd=FD_NONE;
_fds[pos].events=0;
}
else if(s < 0)
{
logMessage(WARNING, "%d sock recv error, %d : %s", _fds[pos].fd, errno, strerror(errno));
_fds[pos].fd=FD_NONE;
_fds[pos].events=0;
}
else
{
buffer[s]=0;
logMessage(DEBUG, "client[%d]# %s", _fds[pos].fd, buffer);
}
}
private:
int _listensock;
uint16_t _port;
struct pollfd* _fds;
int _nfds;
int _timeout;
};
poll相比select,编写更容易。
但是,同样需要对pollfd进行遍历,需要不断地从用户态和内核态之间转换
4.epoll
epoll相比与select和poll而言,最大的区别是不需要用户自己来维护fd数组了。底层可以通过epoll_ctl将你关心的fd都挂载到一颗红黑树上,同时设置一个回调函数,当sock对应事件发生时,回调开始。回调最终会产生一段就绪队列,就绪队列可以通过epoll_wait返回。
函数原型:
int epoll_create(int size);
创建一个epoll模型,size已经被弃用了,返回一个fd表示epoll模型
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op可以取以下值:
EPOLL_CTL_ADD:添加fd
EPOLL_CTL_MOD:修改fd
EPOLL_CTL_DEL:删除fd
epoll_event结构内容:
struct epoll_event
{
uint32_t events; //需要关系的事件
epoll_data_t data; //里面有fd,其他不重要
/*****/
}
events可以取的值
*EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
*EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
这里的events是输出型参数,返回就绪队列长度。
再用epoll重写一个socket网络代码(只关注读)
epoll.hpp
#pragma once
#include<sys/epoll.h>
#include<unistd.h>
#include<iostream>
class Epoll
{
public:
const static int g_num=256;
static int CreateEpoll() //创建epoll模型
{
int epfd=epoll_create(g_num);
if(epfd > 0)
return epfd;
else
exit(1);
}
//依据不同方式(传参)来实现对特定fd的特定功能的插入删除或修改
static bool CtlEpoll(int epfd,int op,int sock,uint32_t events)
{
struct epoll_event ev;
//也需要在events中设置一下sock
ev.data.fd=sock;
ev.events=events;
int n=epoll_ctl(epfd,op,sock,&ev);
return n==0;
}
//OS进行等待事件发生,返回值表示n个fd准备就绪
//同时所有结果存放至ev中,从下标0到n-1
static int WaitEpoll(int epfd,struct epoll_event ev[],int num,int timeout)
{
return epoll_wait(epfd,ev,num,timeout);
}
};
epollserver.hpp
#pragma once
#include "sock.hpp"
#include "log.hpp"
#include "epoll.hpp"
#include <unistd.h>
#include <assert.h>
#include <sys/epoll.h>
#include <functional>
using namespace std;
const static int g_num = 64;
class EpollServer
{
public:
using func_t = function<void(string)>;
EpollServer(func_t hander, const uint16_t &port = 8080)
: _port(port), _func(hander), _revsnum(g_num)
{
//申请空间
_revs = new struct epoll_event[_revsnum];
_listensock = Sock::Socket();
Sock::Bind(_listensock, _port);
Sock::Listen(_listensock);
// 创建ep模型_epfd 并将_listensock以读等待的方式进行ctl
_epfd = Epoll::CreateEpoll();
logMessage(DEBUG, "init success, listensock: %d, epfd: %d", _listensock, _epfd);
if (!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, _listensock, EPOLLIN))
exit(2);
logMessage(DEBUG, "add listensock to epoll success.");
}
void Accepter()
{
string clientip;
uint16_t clientport;
int sock = Sock::Accept(_listensock, &clientip, &clientport);
Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, sock, EPOLLIN);
logMessage(DEBUG, "add new sock : %d to epoll success", sock);
}
void Recver(int sock)
{
char buffer[1024];
int s = recv(sock, buffer, sizeof buffer - 1, 0);
if (s > 0)
{
buffer[s] = 0;
_func(buffer);
}
else if (s == 0)
{
bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
assert(res);
(void)res;
close(sock);
logMessage(NORMAL, "client %d quit, me too...", sock);
}
else
{
bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
assert(res);
(void)res;
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 = _revs[i].events;
int sock = _revs[i].data.fd;
if (revents & EPOLLIN)
{
if (sock == _listensock)
Accepter();
else
Recver(sock);
}
}
}
void LoopOnce(int timeout)
{
//_revs返回就绪的events
//返回就绪fd数组的长度
int n = Epoll::WaitEpoll(_epfd, _revs, _revsnum, 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; // 阻塞等
// int timeout=1000;
while (true)
{
//处理一次
LoopOnce(timeout);
}
}
~EpollServer()
{
if (_listensock >= 0)
close(_listensock);
if (_epfd >= 0)
close(_epfd);
if (_revs)
delete[] _revs;
}
private:
int _listensock;
uint16_t _port;
int _epfd;
struct epoll_event *_revs;
int _revsnum;
func_t _func;
};
5.总结
epoll是IO多路复用的最佳解决方案。通过一套模型,不会像select那样fd过多就无法满足需求,也不会出现大量循环导致用户内核频繁切换导致的性能消耗(添加fd,使用epoll_ctl直接ADD即可)。