目录
io的过程就是等待事件就绪+拷贝数据到缓冲区。提高io的效率就是减少等待的时间。
阻塞等待:在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。
非阻塞等待:如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK 错误码。
select就是io多路转接下负责等待的函数。read、write等函数也有等待的部分,但他们只能等待一个文件描述符fd,而select能同时等待多个文件描述符。其函数原型如下:

nfds:最大文件描述符fd的值加一,nfds = maxfd +1。
fd_set:是一个位图结构,比特位的位置代表哪一个sock。
readfds、wrtiefds、exceptfds:传入用户告诉操作系统需要关心的事件就绪情况,输出内核告诉用用哪些文件描述符事件已就绪。分别只关心读、写、异常。
timeout:设置等待时间,返回剩余的时间。
- #include
- #include
- #include "Sock.hpp"
- #define NUM (sizeof(fd_set)*8)
- int fd_array[NUM];//内容大于等于0,合法的fd,如果是-1,该位置没有fd
- static void Usage(const std::string& proc)
- {
- std::cout<<"Usage: "<< proc << " port"<
- }
- int main(int argc,char* argv[])
- {
- if(argc != 2)
- {
- Usage(argv[0]);
- exit(1);
- }
- uint16_t port = (uint16_t)atoi(argv[1]);
- int listen_sock = Sock::Socket();
- Sock::Bind(listen_sock,port);
- Sock::Listen(listen_sock);
-
- for (int i = 0; i < NUM; i++)
- {
- fd_array[i] = -1;
- }
-
- fd_set rfds;
- fd_array[0] = listen_sock;
- while(true)
- {
-
- FD_ZERO(&rfds);
- int max_fd = fd_array[0];
- for(int i=0;i
- {
- if(fd_array[i] == -1) continue;
- FD_SET(fd_array[i],&rfds);
- if(max_fd < fd_array[i])
- max_fd = fd_array[i];
- }
-
- struct timeval timeout = {5,0};
- int n = select(max_fd+1,&rfds,nullptr,nullptr,nullptr);
- switch (n)
- {
- case -1:
- std::cerr<<"select error"<
- break;
- case 0:
- std::cout<<"select timeout"<
- break;
- default:
- std::cout<<"有fd对应事件就绪了"<
- for(int i=0;i
- {
- if(fd_array[i] == -1) continue;
- if(FD_ISSET(fd_array[i],&rfds))
- {
- std::cout<< "sock: " <
"上面有了读事件"< - if(fd_array[i] == listen_sock)
- {
- std::cout<< "listen_sock: " <
"上面有了新链接"< - int sock = Sock::Accept(listen_sock);
- if(sock >=0)
- {
- std::cout<< "listen_sock: " <
"新链接连接成功"< - int pos = 1;
- for(;pos
- {
- if(fd_array[pos] == -1) break;
- }
- if(pos
- {
- std::cout<< "新链接: " << sock <<"新链接加入数组"<
- fd_array[pos] = sock;
- }
- else
- {
- std::cout<< "服务器满载,关闭新链接"<
- close(sock);
- }
- }
- }
- else
- {
- // 普通socket读事件
- char buffer[1024] = {0};
- ssize_t s = recv(fd_array[i],buffer,sizeof(buffer)-1,0);
- if (s > 0)
- {
- std::cout << "sock: " << fd_array[i] << " 上面有普通读取" << std::endl;
- buffer[s] = 0;
- std::cout << "client[ " << fd_array[i] << "]# " << buffer << std::endl;
- }
- else if(s == 0)
- {
- std::cout << "sock: " << fd_array[i] << "关闭了, client退出啦!" << std::endl;
- //对端关闭了链接
- close(fd_array[i]);
- std::cout << "已经在数组下标fd_array[" << i << "]"
- << "中,去掉了sock: " << fd_array[i] << std::endl;
- fd_array[i] = -1;
- }
- else
- {
- close(fd_array[i]);
- std::cout << "已经在数组下标fd_array[" << i << "]"
- << "中,去掉了sock: " << fd_array[i] << std::endl;
- fd_array[i] = -1;
- }
-
- }
- }
- }
- break;
- }
- }
- return 0;
- }
2.3 select优缺点
- 一次等待多个文件描述符fd,提高io效率。
- 每次都要重新设置位图,设置完成后还需要遍历检测。
- fd_set中是有上限的,即能同时等待的fd是有限的。
- select底层需要轮询检测式检测哪些fd的事件就绪了。
- select可能会较为高频率的进行用户到内核,内核到用户的频繁拷贝的问题。
3 poll
3.1 函数介绍
与select的不同,不再使用三个位图来记录事件。而是使用pollfd结构体,记录请求事件和返回事件,再以pollfd数组的方式表示不同的文件描述符的事件情况。
- #include
- 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 */
- };
事件的宏如下:

3.2 poll服务器模型
- #include
- #include
- #include
- #include
- #include
- #include "Sock.hpp"
- #define NUM 128
- struct pollfd fd_array[NUM];
- static void Usage(const std::string& proc)
- {
- std::cout<<"Usage: "<< proc << " port"<
- }
- int main(int argc,char* argv[])
- {
- if(argc != 2)
- {
- Usage(argv[0]);
- exit(1);
- }
- uint16_t port = (uint16_t)atoi(argv[1]);
- int listen_sock = Sock::Socket();
- Sock::Bind(listen_sock,port);
- Sock::Listen(listen_sock);
-
- fd_array[0].fd = listen_sock;
- fd_array[0].events = POLLIN;
- fd_array[0].revents = 0;
- while(true)
- {
- int n = poll(fd_array,NUM,1000);
- switch (n)
- {
- case -1:
- std::cerr<<"poll error"<
- break;
- case 0:
- std::cout<<"poll timeout"<
- break;
- default:
- std::cout<<"有fd对应事件就绪了"<
- for(int i=0;i
- {
- if(fd_array[i].revents & POLLIN)
- {
- std::cout<< "sock: " << fd_array[i].fd << "上面有了读事件" <
- if(fd_array[i].fd == listen_sock)
- {
- std::cout<< "listen_sock: " <
"上面有了新链接" < - int sock = Sock::Accept(listen_sock);
- if(sock >=0)
- {
- std::cout<< "listen_sock: " <
"新链接连接成功"< - int pos = 1;
- for(;pos
- {
- if(fd_array[pos].fd == -1) break;
- }
- if(pos
- {
- std::cout<< "新链接: " << sock <<"新链接加入数组"<
- fd_array[pos].fd = sock;
- fd_array[pos].events = POLLIN;
- }
- else
- {
- std::cout<< "服务器满载,关闭新链接"<
- close(sock);
- }
- }
- }
- else
- {
- // 普通socket读事件
- char buffer[1024] = {0};
- ssize_t s = recv(fd_array[i].fd,buffer,sizeof(buffer)-1,0);
- if (s > 0)
- {
- std::cout << "sock: " << fd_array[i].fd << " 上面有普通读取" << std::endl;
- buffer[s] = 0;
- std::cout << "client[ " << fd_array[i].fd << "]# " << buffer << std::endl;
- }
- else if(s == 0)
- {
- std::cout << "sock: " << fd_array[i].fd << "关闭了, client退出啦!" << std::endl;
- //对端关闭了链接
- close(fd_array[i].fd);
- std::cout << "已经在数组下标fd_array[" << i << "]"
- << "中,去掉了sock: " << fd_array[i].fd << std::endl;
- fd_array[i].fd = -1;
- }
- else
- {
- close(fd_array[i].fd);
- std::cout << "已经在数组下标fd_array[" << i << "]"
- << "中,去掉了sock: " << fd_array[i].fd << std::endl;
- fd_array[i].fd = -1;
- }
-
- }
- }
- }
- break;
- }
- }
- return 0;
- }
-
3.3 优缺点
优点:
- 文件描述符的监听没有上限
- 将监听事件和返回事件分离,接口使用更加方便
缺点:
- poll返回后需要轮询检测获取就绪的文件描述符
- 每次调用poll都需要吧大量的pollfd结构从用户态拷贝到内核态
- 文件描述符增长,其效率也会下降
4 epoll
4.1 函数介绍
epoll是为处理大量句柄而改进的poll。
- // 创建一个epoll句柄
- int epoll_create(int size);
- // 用户告诉内核需要关注的事件
- int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
- // 内核告知用户哪些文件描述符的事件已就绪
- int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
op选项如下:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;
epoll_event结构如下:
events
可以是以下几个宏的集合:
EPOLLIN :
表示对应的文件描述符可以读
(
包括对端
SOCKET
正常关闭
);
EPOLLOUT :
表示对应的文件描述符可以写
;
EPOLLPRI :
表示对应的文件描述符有紧急的数据可读
(
这里应该表示有带外数据到来
);
EPOLLERR :
表示对应的文件描述符发生错误
;
EPOLLHUP :
表示对应的文件描述符被挂断
;
EPOLLET :
将
EPOLL
设为边缘触发
(Edge Triggered)
模式
,
这是相对于水平触发
(Level Triggered)
来说的
.
EPOLLONESHOT
:只监听一次事件
,
当监听完这次事件之后
,
如果还需要继续监听这个
socket
的话
,
需要 再次把这个socket
加入到
EPOLL
队列里。
4.2 epoll服务器模型
- #include
- #include
- #include
- #include
- #include
- #include "Sock.hpp"
- #define SIZE 128
- #define NUM 64
- static void Usage(std::string proc)
- {
- std::cerr << "Usage: " << proc << " port" << std::endl;
- }
-
- int main(int argc, char *argv[])
- {
- if (argc != 2)
- {
- Usage(argv[0]);
- exit(1);
- }
- // 1. 建立tcp 监听socket
- uint16_t port = (uint16_t)atoi(argv[1]);
- int listen_sock = Sock::Socket();
- Sock::Bind(listen_sock, port);
- Sock::Listen(listen_sock);
- // 2. 创建文件描述符
- int epfd = epoll_create(SIZE);
- // 3. 添加listen_sock及其他所关心的事件,添加到内核
- struct epoll_event ev;
- ev.events = EPOLLIN;
- ev.data.fd = listen_sock;
-
- epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
-
- // 4. 事件循环
- volatile bool quit = false;
- struct epoll_event revs[NUM];
- while (!quit)
- {
- int timeout = 1000;
- int n = epoll_wait(epfd, revs, NUM, timeout);
- switch (n)
- {
- case 0:
- std::cout << "time out ..." << std::endl;
- break;
- case -1:
- std::cout << "epoll error ..." << std::endl;
- break;
-
- default:
- std::cout << "有事件就绪" << std::endl;
- // 5. 处理就绪事件
- for (int i = 0; i < n; i++)
- {
- int sock = revs[i].data.fd;
- std::cout << "文件描述符:" << sock << " 上有事件就绪了" << std::endl;
- if (revs[i].events & EPOLLIN)
- {
- std::cout << "文件描述符:" << sock << " 上有读事件就绪就绪了" << std::endl;
- if (sock == listen_sock)
- {
- std::cout << "文件描述符:" << sock << " 链接事件就绪就绪了" << std::endl;
- // 5.1 处理链接事件
- int fd = Sock::Accept(listen_sock);
- if (fd >= 0)
- {
- std::cout << "获取新链接成功" <
- struct epoll_event _ev;
- _ev.data.fd = fd;
- epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &_ev);
- std::cout << "已经将新链接托管给epoll了" <
- }
- else
- {
- // fail
- }
- }
- else
- {
- // 5.2 正常的读取处理
- char buffer[1024];
- ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
- if(s>0)
- {
- buffer[s] = 0;
- std::cout << "client ["<
"]# "< - }
- else if(s==0)
- {
- std::cout << "client quit... " <
- close(sock);
- epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
- std::cout << "sock: "<
" delete from epoll success"< - }
- else
- {
- std::cout << "recv error " <
- close(sock);
- epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
- }
- }
- }
- else
- {
- //处理其他事件
- }
- }
- break;
- }
- }
- close(epfd);
- close(listen_sock);
- return 0;
- }
4.3 特点
1.接口使用方便:
拆分成了三个函数
,
使用起来更方便高效
.
不需要每次循环都设置关注的文件描述符,
也做到了输入输出参数分离开
2.数据拷贝轻量: 只在合适的时候调用
EPOLL_CTL_ADD
将文件描述符结构拷贝到内核中
,
这个操作并不频繁(
而
select/poll
都是每次循环都要进行拷贝
)
3.事件回调机制: 避免使用遍历
,
而是使用回调函数的方式
,
将就绪的文件描述符结构加入到就绪队列中
, epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪
.
这个操作时间复杂度
O(1).
即使文件描述符数目很多,
效率也不会受到影响
.
4.没有数量限制:
文件描述符数目没有上限。
-
相关阅读:
MySQL中为什么要使用索引合并(Index Merge)?
kaggle服务器使用问题
OpenWrt使用Privoxy插件修改UA
大数据高级开发面试题总结及答案汇总之[Spark系列](持续更新中)
小白系统初始化配置资源失败怎么办
C++【IO流】
Set集合(HastSet&hashCode)
排序1---插入排序
[ Windows ] ping IP + Port 测试 ip 和 端口是否通畅
C 基础语法1 —— 数据类型、运算符与表达式
-
原文地址:https://blog.csdn.net/xd6905/article/details/126794152