碌碌无为,则余生太长;
欲有所为,则人生苦短。
--- 中岛敦 《山月记》---
从零开始使用多路转接IO
1 前言
上一篇文章我们学习了多路转接中的Select,其操作很简单,但有一些缺陷:
- 每次调用 select,都需要手动设置 fd 集合, 从接口使用角度来说也非常不便。
- 每次调用 select, 都需要把 fd 集合从用户态拷贝到内核态, 这个开销在 fd 很多时会很大。这个是多路转接IO无法避免的问题!
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时很大。
- select 支持的文件描述符数量太小!虽然操作系统中文件描述符也有限制,但是这是操作系统的缺陷。同样select也是缺点
而poll方案可以解决其中的两个缺点:
- select支持的文件描述符少,poll理论上可以支持无限个文件描述符。
- select每次调用接口都需要手动设置fd集合,poll不需要!
那么接下来我们就来看poll是怎样实现的。
1 poll接口介绍
首先poll的作用与select一模一样:等待多个文件描述符!只负责等待!
我们来看看poll接口:
OLL(2) Linux Programmer's Manual POLL(2)
NAME
poll, ppoll - wait for some event on a file descriptor
SYNOPSIS
#include<poll.h>intpoll(structpollfd*fds,nfds_t nfds,int timeout);#define_GNU_SOURCE/* See feature_test_macros(7) */#include<signal.h>#include<poll.h>intppoll(structpollfd*fds,nfds_t nfds,conststructtimespec*tmo_p,constsigset_t*sigmask);
poll接口中只有三个参数:
struct pollfd *fds
:这时一个文件描述符数组,其中每个元素是一个结构体,其中包含文件描述符,需要处理的事件类型。nfds_t nfds
:表示文件描述符的数量!timeout
:输入性参数,这里直接采用的是毫秒,不使用结构体!等于0时是非阻塞IO,等于-1时是阻塞IO!- 返回值表示是否成功:大于0 即有n个就绪了;等于0表示超时了;小于0就是poll出错了!
我们来看看
struct pollfd
内部是怎么样的 :
structpollfd{int fd;/* file descriptor */short events;/* requested events */short revents;/* returned events */};
我们对比一下select,select需要传入三个事件集,输入输出性参数,每次都会发生改变!所以才需要每次调用都要进行初始化。而poll使用一个结构体,对于这个文件描述符有两种事件:
requested events 与 returned events
!输入输出并不互相干扰!那么就解决了select需要不断初始化的问题。
那么事件类型有哪些呢?
宏定义描述POLLIN普通或优先级带数据可读POLLRDNORM同POLLINPOLLRDBAND数据可读(优先级带数据)POLLPRI高优先级数据可读POLLOUT普通数据可写POLLWRNORM同POLLOUTPOLLWRBAND数据可写(优先级带数据)POLLERR发生错误POLLHUP挂起,对方关闭连接POLLNVAL描述字不是一个打开的文件
这些都是宏定义,
short events;
是一个16位位图,可以通过宏定义进行匹配设置!我们想要查看哪些事件,或者有哪些事件就绪了,就都可以通过位运算进行判断就可以了!
通过结构体的两个位图:
- 用户就可以告诉内核需要帮我们对fd的哪些事件进行等待了
- 内核也可以通过位图告诉用户fd的哪些事件就绪了!
3 代码编写
我们仅仅需要对select的代码做出一些修改即可:
首先,poll需要一个struct pollfd数组,这里储存需要处理的fd。初始化事遍历进行将对应fd设置为-1,事件设置为0,将listen套接字加入就可以:
voidInitserver(){// 对数组进行初始化for(int i =0; i < gnum; i++){
fd_array[i].fd = gdefault;
fd_array[i].events =0;
fd_array[i].revents =0;}// 加入监听套接字
fd_array[0].fd = _listensock->GetSockfd();
fd_array[0].events = POLLIN;}//...// pollstructpollfd fd_array[gnum];
然后对Loop函数进行修改,我们不在需要对数据遍历更新rfds了,这样代码看起来就整洁了许多!
voidLoop(){// 进入服务while(true){// 创建timeoutint timeout =1000;// 进行selectint n =::poll(fd_array, gnum, timeout);switch(n){case0:// 超时LOG(DEBUG,"timeout \n");break;case-1:// 出错了LOG(ERROR,"select error\n");break;default:// 正常LOG(INFO,"have event ready: n = %d\n", n);// 处理事件HandlerEvent();PrintDebug();break;}}}
接下来就是HandlerEvent函数,进行判断的策略依然是遍历,这里只关心读事件:
voidHandlerEvent(){// 遍历fd_array判断是否有就绪的新事件for(int i =0; i < gnum; i++){if(fd_array[i].fd == gdefault)continue;// 如果有新事件if(fd_array[i].revents & POLLIN){// 进行判断是scokfd 还是普通fdif(fd_array[i].fd == _listensock->GetSockfd()){Accepter();}// 普通fd 进行正常读写else{HandlerIO(fd_array[i]);}}}}
然后就是对于普通套接字和监听套接字的处理,针对数组进行稍微修改即可:
voidAccepter(){// 连接事件就绪
InetAddr addr;int sockfd = _listensock->Accepter(&addr);// 已经就绪 ,不会阻塞// 这时会得到一个新连接if(sockfd >0){LOG(DEBUG,"get a new link , client info %s:%d\n", addr.Ip().c_str(), addr.Port());// 将新获取的fd加入到数组中LOG(INFO,"get new fd :%d\n", sockfd);bool flag =false;for(int i =0; i < gnum; i++){if(fd_array[i].fd == gdefault){
flag =true;
fd_array[i].fd = sockfd;
fd_array[i].events = POLLIN;break;}elsecontinue;}if(flag ==false){LOG(WARNING,"fd_array have fill!\n");return;// 可以进行扩容}}}voidHandlerIO(structpollfd&sp){char buffer[1024];int n =::recv(sp.fd, buffer,sizeof(buffer)-1,0);if(n >0){// 读取到了数据
buffer[n]=0;
std::string echo_str ="[client say]#";
echo_str += buffer;
std::cout << echo_str << std::endl;// 返回一个报文
std::string content ="<html><body><h1>hello bite</h1></body></html>";
std::string ret_str ="HTTP/1.0 200 OK\r\n";
ret_str +="Content-Type: text/html\r\n";
ret_str +="Content-Length: "+ std::to_string(content.size())+"\r\n\r\n";
ret_str += content;// echo_str += buffer;::send(sp.fd, ret_str.c_str(), ret_str.size(),0);// 临时方案}elseif(n ==0){// 此时fd退出了LOG(INFO,"fd:%d quit!\n", sp.fd);::close(sp.fd);
sp.fd = gdefault;
sp.events =0;
sp.revents =0;}else{LOG(ERROR,"recv error! errno:%d\n", errno);::close(sp.fd);
sp.fd = gdefault;}}
来看效果:
很好的实现了我们的需求!代码也比select更加的简单了!
4 总结
Poll的底层其实也是遍历,对我们传入的数据进行遍历,这样的效率其实比select并不能高出太多!也就是说poll依然有这样的缺点:
- 每次调用 select, 都需要把 fd 集合从用户态拷贝到内核态, 这个开销在 fd 很多时会很大。这个是多路转接IO无法避免的问题!
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时很大。
这样poll 的处境就很尴尬,没有select资历早,适配性不如select。性能又比不过epoll!
下一篇文章我们来学习epoll!
版权归原作者 叫我龙翔 所有, 如有侵权,请联系我们删除。