IO多路转接模型:select/poll/epoll
对大量描述符进行事件监控(可读/可写/异常)
select模型
- 用户定义描述符的事件监控集合 fd_set(这是一个位图,用于存储要监控的描述符); 用户将需要监控的描述符添加到集合中,这个描述符集合的大小取决于一个宏 _FD_SETSIZE = 1024
- 将集合拷贝到内核中进行监控;在内核中对所有描述符进行轮询遍历判断是否有关心的事件就绪
- 若有描述符就绪,从监控集合中将未就绪的描述符移除;然后调用返回(返回给用户就绪描述符饥集合)
- 用户遍历所有描述符,判断描述符是否在集合中,若在集合中,则这个描述符是就绪描述符
- 用户针对这个就绪的描述符事件进行相应的处理,用户仅仅对大量描述符中就绪的描述符进行处理,sock程序就可以避免accept/recv处因为没有数据到来而阻塞
#include <sys/select.h>
/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h> int select(int nfds, fd_set *readfds,fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- nfds: 监控的文件描述符集里最大文件描述符加1,因为此参数会告诉内核检测前多少个文件描述符的状态
- readfds: 监控有读数据到达文件描述符集合,传入传出参数
- writefds: 监控写数据到达文件描述符集合,传入传出参数
- exceptfds: 监控异常发生达文件描述符集合,如带外数据到达异常,传入传出参数
- timeout: 定时阻塞监控时间,3种情况
- NULL,永远等下去
- 设置timeval,等待固定时间
- 设置timeval里时间均为0,检查描述字后立即返回,轮询
struct timeval
{
long tv_sec; /* seconds */ //秒
long tv_usec; /* microseconds */ //微秒
};
void FD_CLR(int fd, fd_set *set);
//把文件描述符集合里fd清0 将指定的描述符从集合中移除int FD_ISSET(int fd, fd_set *set);
//测试文件描述符集合里fd是否置1 判断指定的描述符是否在集合中void FD_SET(int fd, fd_set *set);
//把文件描述符集合里fd位置1 将指定的描述符添加到集合中void FD_ZERO(fd_set *set);
//把文件描述符集合里所有位清0 清空描述符集合
select优缺点
- select 能监听的文件描述符个数受限于 FD_SETSIZE,一般为 1024,单纯改变进程打开的文件描述符个数并不 能改变 select 监听文件个数
- 每次都需要重新将监控集合拷贝到内核(select会修改集合)
- 解决 1024 以下客户端时使用 select 是很合适的,但如果链接客户端过多,select 采用的是轮询模型,会大大降低服务器响应效率
- select返回给用户就绪的描述符集合(将未就绪的描述符从集合中移除),但是并没有告诉用户具体哪一个描述符就绪,需要用户遍历描述符是否在集合中来判断哪个描述符就绪,这个判断是一个遍历的过程,性能随着描述符增多而下降,并且复杂度更高
- select每次返回都会修改监控集合,因此每次都需要用户重新向集合中添加所有描述符
- select遵循posix标准,支持跨平台;
- 监控的超时等待时间可以精细到微秒
class Select
{
public:Add(TcpSocket &sock); //将用户关心socket描述符添加到监控集合中Del(TcpSocket &sock); //从监控集合中移除不再关心的socket描述符Wait(std::vector<TcpSocket>&list,init timeout_sec,int timeout_sec); //从开始监控,并且向用户返回就绪的socket
private:fd_set _rfds;int_max_fd;
};
实现
select服务端
/** 这个文件封装一个select类,向外界提供更加简单点的select监控接口* 将用户关心socket描述符添加到监控集合中* 从监控集合中移除不再关心的socket描述符* 从开始监控,并且向用户返回就绪的socket*/#include<vector>
#include<sys/select.h>
#include"tcpsocket.hpp"class Select
{public:Select(): _max_fd (-1){FD_ZERO(&_rfds);//清空集合} bool Add(TcpSocket &sock){int fd = sock.GetFd();//void FD_SET(int fd,fd_set *set)//向set描述符集合中添加fd描述符FD_SET(fd,&_rfds);_max_fd = _max_fd > fd ? _max_fd : fd; return true;} bool Del(TcpSocket &sock){int fd = sock.GetFd();//void FD_CLR(int fd, fd_set *set)//从set描述符集合中移除FD_CLR(fd,&_rfds);//从最大的往前遍历for(int i = _max_fd ; i >= 0; i--){//int FD_ISSET(int fd, fd_set *set);//判断fd描述符是否还在set集合中if(FD_ISSET(i,&_rfds)){_max_fd = i;break;}}} bool Wait(std::vector<TcpSocket>&list,int timeout_sec = 3){struct timeval tv; //超时时间tv.tv_sec = timeout_sec;tv.tv_usec = 0;fd_set set = _rfds;int ret =select(_max_fd + 1, &set, NULL ,NULL,&tv);if(ret < 0){perror("select error");return false;}else if(ret == 0){std::cout<< "select wait timeout\n";return false;}for(int i =0 ;i <= _max_fd; i++){if(FD_ISSET(i,&set)){TcpSocket sock;sock.SetFd(i);list.push_back(sock);}}return true;} private:fd_set _rfds;int _max_fd;
};int main()
{TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Bind("192.168.145.132",9000));CHECK_RET(sock.Listen());Select s;s.Add(sock); while(1){std::vector<TcpSocket>list;if(s.Wait(list)==false){continue;}for(int i =0 ; i < list.size(); i++){//判读socket是监听socket还是通信socketif(list[i].GetFd() == sock.GetFd()){TcpSocket clisock;std::string cli_ip;uint16_t cli_port;if(sock.Accept(clisock,cli_ip,cli_port) == false){continue;}s.Add(clisock);}else{std::string buf;if(list[i].Recv(buf) == false){s.Del(list[i]);list[i].Close();continue;}std::cout<<"client say:"<<buf <"\n";}}}sock.Close();return 0;
}
客户端
#include <signal.h>
#include "tcpsocket.hpp"void sigcb(int signo){printf("connection closed\n");
}
int main(int argc, char *argv[])
{if (argc != 3) {std::cout<<"./tcp_cli 192.168.122.132 9000\n";return -1; } std::string ip = argv[1];uint16_t port = atoi(argv[2]);signal(SIGPIPE, sigcb);TcpSocket sock;CHECK_RET(sock.Socket());CHECK_RET(sock.Connect(ip, port));while(1) {std::string buf;std::cout <<"client say:";fflush(stdout);std::cin >>buf;sock.Send(buf);}sock.Close();return 0;
}
poll模型
poll函数接口
#include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);// pollfd结构
struct pollfd { int fd; /* 用户监控的描述符 */ short events; /* 描述符关心的事件 POLLIN/POLLOUT */ short revents; /* 描述符实际就绪的事件 */ }
参数说明
- fds是一个poll函数监听的结构列表. 每一个元素中, 包含了三部分内容: 文件描述符, 监听的事件集合, 返回的事件集合. 描述事件结构数组
- nfds表示fds数组的长度. 要监控事件个数
- timeout表示poll函数的超时时间, 单位是毫秒(ms).
events和revents的取值:
- POLLIN 普通或带外优先数据可读,即POLLRDNORM | POLLRDBAND
- POLLRDNORM 数据可读
- POLLRDBAND 优先级带数据可读
- POLLPRI 高优先级可读数据
- POLLOUT 普通或带外数据可写
- POLLWRNORM 数据可写
- POLLWRBAND 优先级带数据可写
- POLLERR 发生错误
- POLLHUP 发生挂起
- POLLNVAL 描述字不是一个打开的文件
实现原理
- 用户定义描述符事件数组,向数组中添加关心的描述符事件
- 将pollfd事件数组,拷贝到内核中进行遍历轮询监控,判断是否就绪了关心的事件
- 将描述符实际就绪的事件信息,标记到revents中
- 当poll返回。用户遍历pollfd事件数组,通过revents判断描述符就绪了什么事件,进而进行相应操作
使用poll监控标准输入
#include <poll.h>
#include <unistd.h>
#include <stdio.h>int main() { struct pollfd poll_fd; //一个结构就是一个事件poll_fd.fd = 0; poll_fd.events = POLLIN; //可读事件for (;;) { //开始监控int ret = poll(&poll_fd, 1, 1000); //遍历轮询 if (ret < 0) { //出错perror("poll"); continue; }if (ret == 0) { //超时printf("poll timeout\n"); continue; } //ret>0if (poll_fd.revents == POLLIN) { //看事件是否为我们所关心的事件 //对事件进行操作 char buf[1024] = {0}; read(0, buf, sizeof(buf) - 1); printf("stdin:%s", buf); } }
}
poll有缺点分析
优点:
- poll采用事件结构形式对描述符关心的事件进行监控,简化了select三种集合操作的流程
- poll没有描述符上限的设置
缺点
- 不能跨平台,只能用于Linux下
- 在内核中进行轮询遍历判断就绪,性能随着描述符事件增多而下降
- 也不会告诉用户具体哪一个描述符就绪,需要用户轮询遍历判断事件中的revents;进而对描述符进行相应事件操作
revents & POLLIN/POLLOUT
- 需要每次都向内核中拷贝监控信息