目录
一 什么是多路复用
传统的read,write,都需要等对端写/缓冲区剩余空间足够才能写,相当于大部分时间都在等某个资源就绪,这样会不会太慢了?再比如appect获取连接,如果一直没人来连,就会一直阻塞等待。换句话说,一次性只能处理一个连接,有没有能同时等待多个事件?多路复用就是解决上诉问题的方法:原理是一次性等待多个事件就绪,然后上层在进行统一处理,在同一时间段效率比传统的阻塞式等待效率要高,就好比如钓鱼,一次抛一个鱼竿和一次抛100个鱼竿,肯定是一次性抛100个鱼竿钓到鱼的概率要大些。
二 select
1. 作用
只是用来检测事件(多个文件描述符)是否就绪,其他的操作应用层处理。
2. 接口
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
nfds(输入参数):等待的多个文件描述符最大的那个+1
struct timeval(输入输出参数):设置超时时间
1. 比如设置{10,0}表示每10秒返回一次,如果10秒内有时间就绪,直接返回,timeval会记录还剩下多长时间,比如3秒有事件就绪,还剩7秒.
2. 如果设置{0,0}表示一直返回
3. 设置为nullptr表示阻塞式等待事件就绪,没有则一直阻塞直到有事件就绪
fd_set:位图结构
比如设置{1,2,3,4,5,6}个文件描述符对应二进制 000000,从右到左从0开始,是几就在第几个bit位上,如果该位置为0/1表示事件是否就绪
readfds(输入输出参数):只关心读事件
writefds(输入输出参数):只关心写事件
exceptfds(输入输出参数):只关心异常事件
select返回值:
1. 大于0 说明就几个事件就绪
2. 等于0 说明超时了
3. 小于0 说明select出错返回
3. 代码示例
下面测试读事件是否就绪(采用appect测试/简易代码)
// select 默认的文件集(fd_set)大小为1024
const static size_t Maxbites = sizeof(fd_set) * 8;
class SelectServer
{
public:
SelectServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>())
{
// 服务器创建
_listensock->BuildListenSocket(_port);
}
~SelectServer() {}
void InitServer()
{
// 初始化管理事件集合
for (int i = 0; i < Maxbites; i++)
{
arr[i] = -1;
}
// 将listensocket添加进集合
arr[0] = _listensock->Sockfd();
}
void Listenevent()
{
// accept一定不会阻塞
InetAddr addr;
int sockfd = _listensock->Accepter(&addr);
if (sockfd == -1)
{
std::cout << "appect 失败" << std::endl;
}
// 找没有使用的位置添加进去
for (int pos = 1; pos < Maxbites; pos++)
{
if (arr[pos] == -1)
{
arr[pos] = sockfd;
break;
}
else continue;
}
}
void RWevent(int i)
{
// 读一定不会阻塞
char buff[1024];
int n = ::read(arr[i], buff, sizeof(buff));
if (n > 0)
{
// 这里写默认当他就绪(一般新建立的连接,默认是tcp发送,接收缓冲区没有数据)
buff[n] = 0;
std::string mes = "client say: ";
mes += buff;
std::cout << mes << std::endl;
write(arr[i], mes.c_str(), mes.size());
}
else if (n == 0)
{
// 客户端退出,关闭连接并把对应的事件移除出arr[]事件集合里
::close(arr[i]);
arr[i] = -1;
}
else
{
// 读出错
std::cout << "read failed" << std::endl;
}
}
void handerevent(fd_set *rfds)
{
// rfds有事件就绪,遍历arr[]就绪的事件
for (int i = 0; i < Maxbites; i++)
{
// 不在集合跳过
if (FD_ISSET(arr[i], rfds) == false)
continue;
else
{
// 处理就绪的事件
if (arr[i] == _listensock->Sockfd())
{
// accept连接事件
Listenevent();
}
else
{
// 其他事件(读事件)
RWevent(i);
}
}
}
}
void Loop()
{
while (true)
{
// fd_set是位图结构,每次返回都会改变他,所以每次都需要进行重置
fd_set rfds;
// 系统提供的接口初始化 fd_set 位图集合
FD_ZERO(&rfds);
// 记录最大的文件描述符
int Max = -1;
for (int i = 0; i < Maxbites; i++)
{
// 将添加的事件添加在 fd_set 集合里
if (arr[i] != -1)
{
FD_SET(arr[i], &rfds);
Max = std::max(Max, arr[i]);
}
else
continue;
}
// 设置超时事件
timeval t = {3, 0};
// 只关心读事件
int n = select(Max + 1, &rfds, nullptr, nullptr, &t);
if (n > 0)
{
// 有事件就绪
std::cout << "事件就绪" << std::endl;
// 处理事件,不然一直会提示事件就绪
handerevent(&rfds);
}
else if (n == 0)
{
// 超时
std::cout << "超时" << std::endl;
}
else
{
// select返回出错
std::cout << "select 错误" << std::endl;
}
print();
}
}
void print()
{
for (int i = 0; i < 10; i++)
{
std::cout << arr[i] << " ";
}
std::cout << '\n';
}
private:
// 服务器端口号
uint16_t _port;
// 服务器
std::unique_ptr<Socket> _listensock;
// 维护事件个数
int arr[Maxbites];
};
select的缺点:
1. 每次select之前都要重新设置位图结构,因为调用select返回的时候,之前设置的可能有的就绪了,没有就绪的直接清空,有事件就绪还要判断是listen监听就绪还是普通文件描述符就绪,listen监听就绪还要判断哪个位置没有被使用过,充满大量循环。
2. select支持的最多文件描述符个数是1024,并不能处理大量请求。
3. select调用时,用户通知内核关心哪些文件描述符,一次拷贝,哪些文件描述符就绪由内核通知用户,一次拷贝,内核处理的时候不断检测关心的描述符是否就绪,也就是循环遍历。
三 poll
1. 作用
和select一样,也是同时监听多个文件描述符,但使用不一样
2. 接口
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
1. timeout:毫秒为单位,输入参数,不能获取剩余时间
2. nfds:struct pollfd* 的个数
3. struct pollfd:
struct pollfd { int fd; /* file descriptor */ short events; /* requested events */ short revents; /* returned events */ };fd:关心的fd
events:关心什么事件(用户通知内核)
revents:什么事件就绪(内核通知用户)
4. 和select的不同点在于poll不用每次调用之前对参数进行重置,select重置的原始是关心的事件,返回时会改变他,也就是绑在一起了,都用在同一个bit位的位置,poll把关心的fd和关心的事件,就绪的事件都分离开了,也就是fd,events,revents不指向相同的位置,都有各自的空间,所以每次调用poll之前不用对参数进行 重置
2. poll的代码和select的代码几乎一样,调用poll之前不用对参数进行重置,只需要对struct pollfd设置文件描述符和events通知事件,将来通过revents来获取事件。
1. poll相对于select的优点在于:调用poll之前不用重新设置参数,理论上poll没有文件描述符的最大限制
2. 但poll 仍然有用户通知内核,内核通知用户2次拷贝,内核不断检测事件是否就绪,也是循环检测问题
四 epoll
1. 作用
和select/poll一样管理多个文件描述符对应的事件。
2. 接口
#include <sys/epoll.h>
int epoll_create(int size);
size:只要设置大于0即可,无任何含义
返回值:返回一个文件fd,epoll会在底层创建一个epoll模型并与返回的fd关联起来,详细看后面
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epfd:epoll_create返回的文件描述符
返回值:成功0,失败-1
op:添加/删除关心的文件描述符
fd:关心的文件描述符
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 */ };
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);返回值:和select/poll一样
timeout:和poll一样
epfd:epoll_create返回的文件描述符
events:一段缓冲区,将来就绪事件放在缓冲区里面,并严格排序
maxevents:events对象个数
3. epoll工作模型
当epoll_create 返回时,返回一个文件描述符,该文件描述符会和epoll模型关联起来,epoll会在底层构建一颗红黑树和一个就绪队列,当添加新的文件描述符,就会在红黑树添加一个节点,key值为fd,删除也就是移除节点,然后在设置一个回调方法,将来比如有新的事件来了,通过回调方法判断是哪个节点的事件就绪,在把该节点直接链入至就绪队列当中,通知上层去取数据,这种就不需要进行遍历,就绪队列里的节点就是已经就绪了的事件,不需要反复进行遍历。

4. 代码示例
下面测试读事件是否就绪(采用appect测试/简易代码)
class Epoll
{
public:
// 初始化服务器,绑定listen套接字并创建epoll模型
Epoll(uint16_t port):_port(port),listensock(std::make_unique<TcpSocket>())
{
listensock->BuildListenSocket(_port);
epfd = epoll_create(1);
if(epfd<0)
{
std::cout<<"epoll_create failed "<<std::endl;
::exit(-1);
}
std::cout<<epfd<<std::endl;
}
// 将listen套接字添加到epoll里
void InitServer()
{
epoll_event ev;
ev.data.fd=listensock->Sockfd();
ev.events=EPOLLIN;
int n = epoll_ctl(epfd,EPOLL_CTL_ADD,listensock->Sockfd(),&ev);
if(n<0)
{
std::cout<<"epoll_ctl failed "<<std::endl;
::exit(-1);
}
}
// 处理新连接
void Accept()
{
InetAddr addr;
int fd=listensock->Accepter(&addr);
if(fd<0)
{
std::cout<<"Accepter failed "<<std::endl;
::exit(-1);
}
epoll_event ee;
ee.data.fd=fd;
ee.events=EPOLLIN;
int n=epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ee);
if(n<0)
{
std::cout<<"Accepter failed "<<std::endl;
::exit(-1);
}
std::cout<<fd<<std::endl;
}
// 处理IO
void IO(int fd)
{
char buff[1024];
int n=read(fd,buff,sizeof(buff)-1);
if(n>0)
{
buff[n]=0;
std::string mes="HTTP/1.0 \r\n\r\n";
std::string rsp="<html><body>hello world</body></html>";
mes+=rsp;
int k=write(fd,mes.c_str(),mes.size());
}
else if(n==0)
{
// 需先移除关心的事件,在close,epoll不能移除不健康,不合法的fd,可能出错
std::cout<<"client quit "<<std::endl;
int k = epoll_ctl(epfd,EPOLL_CTL_DEL,fd,nullptr);
if(k<0)
{
std::cout<<"epoll_ctl failed "<<std::endl;
::exit(-1);
}
::close(fd);
}
else
{
std::cout<<"read failed "<<std::endl;
::exit(-1);
}
}
// 事件派发
void hander(int n)
{
for(int i=0;i<n;i++)
{
int fd=ev[i].data.fd;
if(ev[i].events==EPOLLIN)
{
if(fd==listensock->Sockfd())
{
Accept();
}
else
{
IO(fd);
}
}
}
}
void Loop()
{
// 轮询检测事件
while(true)
{
int n=epoll_wait(epfd,ev,1024,3000);
if(n>0)
{
std::cout<<"有事件"<<std::endl;
hander(n);
}
else if(n==0)
{
std::cout<<"超时"<<std::endl;
}
else
{
std::cout<<"epoll_wait failed "<<std::endl;
::exit(-1);
}
}
}
~Epoll(){if(epfd>=0)::close(epfd),listensock->Close();}
private:
uint16_t _port;
// epoll模型
int epfd;
// 将来就绪的事件填放在该缓冲区里
epoll_event ev[1024];
std::unique_ptr<Socket> listensock;
};
5.epoll相对于select/poll的区别
1. select/poll都需要用户维护各个事件,也就是用数组保存起来,虽然poll每次调用poll()之前不用重置事件集,但事件就绪依旧需要遍历整个数组来判断每个下标是否有事件,但epoll事件就绪就会把已经就绪的事件添加到epoll_event数组里,并严格排序,比如数组大小1024.只有10个事件就绪,epoll_wait则会返回10,将来只用遍历0-9下标即可,更大的差别在于epoll的事件由内核维护,并且是红黑树,内核使用select/poll会不断进行循环检测是否就绪,epoll则通过设置回调方法让就绪的事件自动通知操作系统并链入到就绪队列中(并非拷贝),效率大大提高
2. epoll的几乎解决了select/poll的所有缺点,但是当调用epoll_ctl时内核依旧会把用户数据拷贝一份,调用epoll_wait时,用户也必须从就绪队列中把数据拷贝到缓冲区里,这是避免不了的,epoll除了遍历新的就绪事件,不用在像select/poll一样重置事件集,事件就绪也不用把整个数组都遍历一遍,而且事件集是由内核维护,并采用红黑树和就绪队列进行添加事件/通知就绪的事件
总结:
1. epoll的接口虽然有3个接口,但不需要自己维护事件集,编码比select/poll简单,每个接口分工明确,不像select/poll一样一个接口承担很多事情
2. 添加事件epoll添加/删除事件在红黑树上操作(logn),select/poll在数组遍历则是O(n)
3. epoll通过回调机制通知操作系统有事件就绪,不需要像select/poll一样不断遍历事件集
4. epoll和poll一样文件描述符理论没有上限
五 边缘触发(ET) VS 水平触发(LT)
之前触发事件不处理就会一直通知导致刷屏,为什么?默认他们的工作模式为水平触发模式。
1. 水平触发
当有事件就绪,底层会一直通知,直到处理完,select/poll/epoll默认的工作模式,且select/poll只有这一种工作模式。
2. 边缘触发
当有事件就绪,底层只会通知一次,且只有poll有这种模式并且要设置epoll_ctl(op字段)。
谁的效率更高?在同一时间段,明显边缘触发效率更高,因为只会通知一次,水平触发如果上层不处理事件就会一直通知,边缘触发通知有效的事件更多,另一个方面边缘触发通知一次倒逼上层尽快处理事件,如果read一次性读完一个事件tcp接收窗口的窗口大小更大,分多次读完窗口更小,IO次数也就比较少,但要一次性读完必须采用非阻塞循环读,虽然水平触发也可以采用非阻塞循环读,但边缘触发是必须循环读,水平触发可用可不用。


1676

被折叠的 条评论
为什么被折叠?



