大家好,欢迎来到IT知识分享网。
文章目录
1. poll
poll函数参数解析
输入 man poll
poll的第一个参数是文件描述符
poll的第二个参数为 等待的多个文件描述符(fd)数字层面 最大的+1
poll函数中的最后一个参数 timeout 是一个 纯输入型参数,单位是毫秒
若 timeout 为-1,则表示永久阻塞,直到文件描述符就绪
若 timeout为0,则表示 非阻塞
若timeout 大于0,则表示 在timeout事件以内 以阻塞等待,超时则进行非阻塞等待
poll的返回值的含义与select 相同
第一种 大于0表示有几个文件描述符 是就绪的
第二种 等于0进入timeout状态 ,即 5s以内没有任何一个文件描述符 就绪
第三种 小于0等待失败 返回-1如:想要等待下标为1 和2的文件描述符,但是下标为2的文件描述符根本不存在,就会等待失败
在pollfd 结构体 中
fd 表示 文件描述符
events: 用户告诉内核,需要关心那些文件描述符上的那些事件
revents :内核会告诉用户,关心的那些文件描述符上的那些事件已经就绪
poll将 输入参数 和输出参数进行分离
poll就有对应的事件
常用的有
POLLIN 表示 有数据可以读
POLLOUT 表示 当前写的时候不会被阻塞
POLLIN 表示第一个比特位为1
POLLOUT 表示 第三个比特位为1
代码解析
主要将第一个初始版本的select代码进行修改
由于poll 自带结构体,内部包含
fd (文件描述符)
events (用户告诉操作系统 那些文件描述符上的事件需要关心)
revents (操作系统告诉用户 关心的那些文件描述符上的事件已经就绪)
此时的fdaaray作为结构体指针,可以通过该指针 去指向 pollfd结构体成员
当想要使用 数组当前元素表示对应的文件描述符时,需指向对应的fd成员
想要表示 (用户告诉操作系统 那些文件描述符上的事件需要关心)
需要通过指针去指向对应的成员 events
想要表示 (操作系统告诉用户 关心的那些文件描述符上的事件已经就绪)
需要通过指针去指向对应的成员 revents
PollServer代码
#include<iostream> #include<string> #include<sys/poll.h> #include<cstring> #include"Sock.hpp" #include"Log.hpp" #include"Err.hpp" using namespace std; const static int gport=8888; const static int N=4096; const static short defaultevent=0; typedef pollfd type_t; class PollServer {
public: PollServer(uint16_t port=gport) :port_(port),fdarray_(nullptr) {
} void InitServer()//初始化 {
listensock_.Socket();//创建套接字 listensock_.Bind(port_);//绑定 listensock_.Listen();//设置监听状态 fdarray_=new type_t[N]; //对fdarray数组进行初始化 for(int i=0;i<N;i++) {
fdarray_[i].fd= defaultfd; fdarray_[i].events= defaultevent; fdarray_[i].revents=defaultevent; } } void Accepter()//获取新连接的动作 {
//这里再使用accept 就不会阻塞了 //listen套接字底层一定有就绪的事件 即连接已经到来了 string clientip; uint16_t clientport; int sock=listensock_.Accept(&clientip,&clientport);//获取客户端IP和端口号 if(sock<0) {
return; } //当得到对应新连接的sock套接字,是不能进行read/recv //并不知道sock上的数据是否就绪的 //所以需要将sock交给select,由select进行管理 logMessage(Debug,"[%s:%d],sock:%d",clientip.c_str(),clientport,sock ); //只需把新获取的sock 添加到 数组中 int pos=1; for(;pos<N;pos++) {
if(fdarray_[pos].fd==defaultfd)//说明没有被占用 {
break; } } if(pos>=N)//整个数组中的位置全被占用了 {
//由于fdarray_是动态开辟空间的,所以可以动态扩容 //若扩容失败,则close close(sock); logMessage(Warning,"sockfd[] array full"); } else //找到了对应的位置 {
fdarray_[pos].fd=sock; fdarray_[pos].events=POLLIN; fdarray_[pos].revents=defaultevent; } } void HandlerEvent()//处理就绪事件 {
for(int i=0;i<N;i++) {
int fd=fdarray_[i].fd; int revent= fdarray_[i].revents; if( (fd==defaultfd)&&(revent &POLLIN))//读事件就绪 {
continue; } //合法fd //若套接字为listensock套接字,并且读事件就绪 if(fd==listensock_.Fd() &&(revent &POLLIN)) {
Accepter(); } //若套接字不是listensock套接字,并且读事件就绪 即普通的读取数据就绪 else if ((fd != listensock_.Fd()) && (revent &POLLIN)) {
char buffer[1024]; ssize_t s=recv(fd,buffer,sizeof(buffer)-1,0); //读取不会被阻塞 if(s>0)//读取成功 {
buffer[s-1]=0; cout<<"client# "<<buffer<<endl; //发送回去 也要被select管理 string echo=buffer ; echo+= "[select server echo ]"; send(fd,echo.c_str(),echo.size(),0);//发送消息 将echo内的数据 交给fd } else {
if(s==0)//读到文件结尾 {
logMessage(Info,"client quit...,fdarray_[i] -> defaultfd:%d->%d",fd,defaultfd); } else //读取失败 {
logMessage(Warning,"recv error,client quit...,fdarray_[i] -> defaultfd:%d->%d",fd,defaultfd); } close(fd); fdarray_[i].fd=defaultfd; fdarray_[i].events=defaultevent; fdarray_[i].revents=defaultevent; } } } } void DebugPrint() {
cout<<"fdarray_[]:"<<endl; for(int i=0;i<N;i++) {
if(fdarray_[i].fd==defaultfd) {
continue; } cout<<fdarray_[i].fd<<" "; } cout<<"\n"; } void Start() //启动 {
//在网络中,新连接到来被当作 读事件就绪 //对应不同的事件就绪,做出不同的动作 fdarray_[0].fd=listensock_.Fd(); fdarray_[0].events=POLLIN;//数据可读 while(true) {
int timeout= -1;//永久阻塞 int n= poll(fdarray_,N,timeout); //timeout 设为nullptr后,全部为阻塞等待 switch(n) {
case 0: //表示没有任何一个文件描述符就绪 logMessage(Debug,"timeout,%d: %s",errno,strerror(errno)); break; case -1: //等待失败 返回-1 logMessage(Warning,"%d: %s",errno,strerror(errno)); break; default: //大于0 ,则表示成功 返回有多少文件描述符就绪 logMessage(Debug,"有一个就绪事件发生了:%d",n); HandlerEvent();//处理就绪事件 DebugPrint();//打印数组内容 break; } } } ~PollServer() {
listensock_.Close(); if(fdarray_) {
delete[]fdarray_; } } private: uint16_t port_;//端口号 Sock listensock_;//创建Sock对象 type_t* fdarray_;//自己定义一个数组,与位图大小相同,来进行已经获得的sock进行管理 };
poll 特点
poll 就相当于在 select 的基础上进行优化
poll自带结构体,只需将读写 异常 放入 events 事件即可
poll 跟 select 一样 也是以数组的形式 传递多个文件描述符,传进去后,需要操作系统继续遍历
- 每次调用poll,都需要把fd集合从用户态拷贝到内核态,在fd很多时开销会很大
(每次都需要用户需要告诉内核,那些文件描述符的那些事件需要关心) - 每次调用poll,都需要在内核遍历传递过来的所有fd,在fd很多时 开销会很大
- (每次都需要内核需要告诉用户,关心的文件文件描述符上的那些事件就绪)
poll 解决了文件描述符 有上限的问题
(定义的数组是在堆上开辟的,若空间满了,还可以动态扩容)
select由于定义的是一个固定长度的数组大小,当到达整个数组长度时,就只能打印信息
2. epoll
epoll 是为处理大批句柄而作改进的poll
认识接口
epoll_create
输入 man epoll_create
参数size可以被忽略,但是必须大于0
返回值 :
若返回epoll文件描述符,则表示返回成功
若返回-1,则表示返回失败
epoll_create 作用:创建出epoll模型
epoll_ctl
输入 man epoll_ctl
第一个参数 epfd 为 epoll_create 的返回值
第二个参数 op 表示你想作什么样的操作
一般常见设置为三个值
EPOLL_CTL_ADD 添加
EPOLL_CTL_MOD 修改
EPOLL_CTL_DEL 删除
第三个参数 fd 表示 哪一个文件描述符
最后一个参数 event 表示关心什么事件
events 表示 输入
fd表示 输入时 表示那些文件描述符上的什么样事件要关心
epoll_ctl 作用: 用户告诉内核,帮我关心 增加/修改/删除那个文件描述符上的那一个事件
epoll_wait
输入 man epoll_wait
返回值含义 与select和poll相同
第一种 大于0表示有几个文件描述符 是就绪的
第二种 等于0进入timeout状态 ,即 5s以内没有任何一个文件描述符 就绪
第三种 小于0等待失败 返回-1如:想要等待下标为1 和2的文件描述符,但是下标为2的文件描述符根本不存在,就会等待失败
第一个参数 epfd v 为 epoll_create的返回值
最后一个参数 timeout 与poll中含义相同
第二个参数 events 为 返回的就绪事件
第三个参数 maxevents为 epoll模型的最大个数
epoll_wait作用:内核告诉 用户 那些文件描述符上的那些事件就绪
与poll的宏基本一致
主要使用 EPOLLIN 和 EPOLLOUT
EPOLLIN 表示 有数据可以读
EPOLLOUT 表示 当前写的时候不会被阻塞
基本原理
红黑树
创建epoll时,在底层就会创建一颗红黑树
使用红黑树 使用户告诉操作系统 来关心 增加/修改/删除那个文件描述符上的那一个事件
点击查看:红黑树概念
红黑树的节点假设为 sruct rb_node
内部包含 文件描述符fd 和 对应事件 event
eopll_ctl 本质 为 通过epoll模型来对红黑树操作
向红黑树中新增 删除 修改 某一个节点
而每一个节点 都对应的是文件描述符和对应的事件
即 epoll_ctl 用来对红黑树 进行增删改 操作
在内核中,一个结构体对象,既可以属于结构A,又可以属于结构B
所以struct rb_node 既可以属于红黑树,又可以属于其他结构
就绪队列
创建epoll时,同时也会创建一个就绪队列
当特定的文件描述符上有对应的事件发生了,就可以将对应已经发生事件的节点 链入就绪队列中
(所以struct rb_node 既可以属于红黑树,又可以属于就绪队列)
就绪队列中只保存已经准备好的文件描述符上的对应事件
作为就绪队列的节点,需要包含文件描述符fd 以及 revent (操作系统告诉用户 关心的文件描述符的那些事件就绪)
epoll_wait 以事件复杂度为O(1)的方式,检测有没有事件就绪 即检测就绪队列是否为空
数据就绪 形成节点放入就绪队列中 ,将红黑树中节点关系 也添加到就绪队列中
这样一个结构体对象就可以既属于红黑树 ,又属于就绪队列了
整体称为 epoll
当调用 epoll_create 时,就是创建epoll模型
epoll避免使用 遍历,而是通过回调函数的方式,将就绪的文件描述符加入 就绪队列中
epoll_wait 返回直接访问 就绪队列 就知道那些文件描述符就绪
工作模式
select poll epoll 基本的情况下:一旦有事件就绪,如果上层不取,底层会一直通知我事件就绪
这种情况,称为 LT 模式
LT—— Level Triggered 水平触发
还有一种 ET 模式
ET—— Edge Triggered 边缘触发
特点为 有效通知只有一次,数据从无到有,从有到多变化的时候,才有第二次通知
epoll默认处于 LT 模式
LT与ET的区别
故事1
假设在菜鸟驿站中有几个快递员,如:张三 李四 王五
小王 住在 宿舍楼的五楼,而且在淘宝上买了很多东西
驿站派出张三来 小王所在的宿舍楼 送快递
当张三到了宿舍楼下后,给小王打了电话通知下去取快递,
小王虽然回答回去取,但是因为正在打游戏所以并没有立刻下去取快递
张三等待了一会后,发现小王依旧没有下来,所以就又打了电话
可小王依旧在打游戏,所以只是敷衍说马上下来取
就这样墨迹了半天,小王终于下来取快递了
张三手里共有两个属于小王的快递,但小王表示身体不好,只能拿一个,一会再来拿另一个,就上楼了
可是小王上去,又打游戏去了,当张三再次打给小王电话时,又开始了敷衍 就是不下去取剩下的一个快递
李四手里也有属于小王的几个快递,只不过李四是先去另一栋楼送的快递
但李四返回时,发现张三还在小王宿舍楼下,李四就把小王的几个快递给了张三
李四又打电话通知小王,新增了几个快递
小王又墨迹了好半天,才叫室友一起下楼把快递全都取走
故事2
假设小王 喜欢打游戏,结果成天打,把键盘和鼠标搞坏了
所以小王就在淘宝买了一个键盘和鼠标
驿站派出李四来 小王所在的宿舍楼 送属于小王的两个快递
由于李四是老快递员,当李四到了宿舍楼下后
给小王打电话,并表示只打一次,要是不拿就不管了
没有办法,小王只好下楼去取,但是小王身体不行,只能拿走一个快递
可小王并不知道李四手中还有自己的一个快递,同样李四也不会告诉小王这件事情
李四就要下班了
李四在路上遇见张三,张三就想让李四把他手里小王的一个快递送一下
于是李四就又去了小王宿舍楼下
又开始通知小王一次,告诉小王 就打着一次电话,不来就走了
小王带着室友下楼来取快递,把快递全部拿走了
(李四刚开始只通知一次,当手中快递变多了时,才进行下一次通知)
张三和李四两人的派发快递的过程中
李四通知效率高(每次只打一个电话通知)
同时 IO效率也高(打一个电话,小王就下来取快递了)
倒逼上层,尽快取走数据
把张三的模式叫做LT模式
把李四的模式叫做ET模式
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/158412.html