• IO多路转接


    目录

    1 基本概念

    1.1 io过程

    1.2 五种io模型

    2 select

    2.1 函数介绍

    2.2 select服务器模型

    2.3 select优缺点

    3 poll

    3.1 函数介绍

     3.2 poll服务器模型

    3.3 优缺点

    4 epoll

    4.1 函数介绍

    4.2 epoll服务器模型

    4.3 特点


    1 基本概念

    1.1 io过程

            io的过程就是等待事件就绪+拷贝数据到缓冲区。提高io的效率就是减少等待的时间。

    1.2 五种io模型

            阻塞等待:在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式。

            非阻塞等待:如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK 错误码。

            信号驱动IO: 内核将数据准备好的时候 , 使用 SIGIO 信号通知应用程序进行 IO 操作。
            IO多路转接 : 虽然从流程图上看起来和阻塞 IO 类似 . 实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。
            异步IO: 由内核在数据拷贝完成时 , 通知应用程序 ( 而信号驱动是告诉应用程序何时可以开始拷贝数据 )。

    2 select

    2.1 函数介绍

            select就是io多路转接下负责等待的函数。read、write等函数也有等待的部分,但他们只能等待一个文件描述符fd,而select能同时等待多个文件描述符。其函数原型如下:

     nfds:最大文件描述符fd的值加一,nfds = maxfd +1。

    fd_set:是一个位图结构,比特位的位置代表哪一个sock。

    readfds、wrtiefds、exceptfds:传入用户告诉操作系统需要关心的事件就绪情况,输出内核告诉用用哪些文件描述符事件已就绪。分别只关心读、写、异常。

    timeout:设置等待时间,返回剩余的时间。

    2.2 select服务器模型

    1. #include
    2. #include
    3. #include "Sock.hpp"
    4. #define NUM (sizeof(fd_set)*8)
    5. int fd_array[NUM];//内容大于等于0,合法的fd,如果是-1,该位置没有fd
    6. static void Usage(const std::string& proc)
    7. {
    8. std::cout<<"Usage: "<< proc << " port"<
    9. }
    10. int main(int argc,char* argv[])
    11. {
    12. if(argc != 2)
    13. {
    14. Usage(argv[0]);
    15. exit(1);
    16. }
    17. uint16_t port = (uint16_t)atoi(argv[1]);
    18. int listen_sock = Sock::Socket();
    19. Sock::Bind(listen_sock,port);
    20. Sock::Listen(listen_sock);
    21. for (int i = 0; i < NUM; i++)
    22. {
    23. fd_array[i] = -1;
    24. }
    25. fd_set rfds;
    26. fd_array[0] = listen_sock;
    27. while(true)
    28. {
    29. FD_ZERO(&rfds);
    30. int max_fd = fd_array[0];
    31. for(int i=0;i
    32. {
    33. if(fd_array[i] == -1) continue;
    34. FD_SET(fd_array[i],&rfds);
    35. if(max_fd < fd_array[i])
    36. max_fd = fd_array[i];
    37. }
    38. struct timeval timeout = {5,0};
    39. int n = select(max_fd+1,&rfds,nullptr,nullptr,nullptr);
    40. switch (n)
    41. {
    42. case -1:
    43. std::cerr<<"select error"<
    44. break;
    45. case 0:
    46. std::cout<<"select timeout"<
    47. break;
    48. default:
    49. std::cout<<"有fd对应事件就绪了"<
    50. for(int i=0;i
    51. {
    52. if(fd_array[i] == -1) continue;
    53. if(FD_ISSET(fd_array[i],&rfds))
    54. {
    55. std::cout<< "sock: " <"上面有了读事件"<
    56. if(fd_array[i] == listen_sock)
    57. {
    58. std::cout<< "listen_sock: " <"上面有了新链接"<
    59. int sock = Sock::Accept(listen_sock);
    60. if(sock >=0)
    61. {
    62. std::cout<< "listen_sock: " <"新链接连接成功"<
    63. int pos = 1;
    64. for(;pos
    65. {
    66. if(fd_array[pos] == -1) break;
    67. }
    68. if(pos
    69. {
    70. std::cout<< "新链接: " << sock <<"新链接加入数组"<
    71. fd_array[pos] = sock;
    72. }
    73. else
    74. {
    75. std::cout<< "服务器满载,关闭新链接"<
    76. close(sock);
    77. }
    78. }
    79. }
    80. else
    81. {
    82. // 普通socket读事件
    83. char buffer[1024] = {0};
    84. ssize_t s = recv(fd_array[i],buffer,sizeof(buffer)-1,0);
    85. if (s > 0)
    86. {
    87. std::cout << "sock: " << fd_array[i] << " 上面有普通读取" << std::endl;
    88. buffer[s] = 0;
    89. std::cout << "client[ " << fd_array[i] << "]# " << buffer << std::endl;
    90. }
    91. else if(s == 0)
    92. {
    93. std::cout << "sock: " << fd_array[i] << "关闭了, client退出啦!" << std::endl;
    94. //对端关闭了链接
    95. close(fd_array[i]);
    96. std::cout << "已经在数组下标fd_array[" << i << "]"
    97. << "中,去掉了sock: " << fd_array[i] << std::endl;
    98. fd_array[i] = -1;
    99. }
    100. else
    101. {
    102. close(fd_array[i]);
    103. std::cout << "已经在数组下标fd_array[" << i << "]"
    104. << "中,去掉了sock: " << fd_array[i] << std::endl;
    105. fd_array[i] = -1;
    106. }
    107. }
    108. }
    109. }
    110. break;
    111. }
    112. }
    113. return 0;
    114. }

    2.3 select优缺点

    1. 一次等待多个文件描述符fd,提高io效率。
    2. 每次都要重新设置位图,设置完成后还需要遍历检测。
    3. fd_set中是有上限的,即能同时等待的fd是有限的。
    4. select底层需要轮询检测式检测哪些fd的事件就绪了。
    5. select可能会较为高频率的进行用户到内核,内核到用户的频繁拷贝的问题。

    3 poll

    3.1 函数介绍

            与select的不同,不再使用三个位图来记录事件。而是使用pollfd结构体,记录请求事件和返回事件,再以pollfd数组的方式表示不同的文件描述符的事件情况。

    1. #include
    2. int poll(struct pollfd *fds, nfds_t nfds, int timeout);
    3. // pollfd结构
    4. struct pollfd {
    5. int fd; /* file descriptor */
    6. short events; /* requested events */
    7. short revents; /* returned events */
    8. };

            事件的宏如下:

    查看源图像

     3.2 poll服务器模型

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include "Sock.hpp"
    7. #define NUM 128
    8. struct pollfd fd_array[NUM];
    9. static void Usage(const std::string& proc)
    10. {
    11. std::cout<<"Usage: "<< proc << " port"<
    12. }
    13. int main(int argc,char* argv[])
    14. {
    15. if(argc != 2)
    16. {
    17. Usage(argv[0]);
    18. exit(1);
    19. }
    20. uint16_t port = (uint16_t)atoi(argv[1]);
    21. int listen_sock = Sock::Socket();
    22. Sock::Bind(listen_sock,port);
    23. Sock::Listen(listen_sock);
    24. fd_array[0].fd = listen_sock;
    25. fd_array[0].events = POLLIN;
    26. fd_array[0].revents = 0;
    27. while(true)
    28. {
    29. int n = poll(fd_array,NUM,1000);
    30. switch (n)
    31. {
    32. case -1:
    33. std::cerr<<"poll error"<
    34. break;
    35. case 0:
    36. std::cout<<"poll timeout"<
    37. break;
    38. default:
    39. std::cout<<"有fd对应事件就绪了"<
    40. for(int i=0;i
    41. {
    42. if(fd_array[i].revents & POLLIN)
    43. {
    44. std::cout<< "sock: " << fd_array[i].fd << "上面有了读事件" <
    45. if(fd_array[i].fd == listen_sock)
    46. {
    47. std::cout<< "listen_sock: " <"上面有了新链接" <
    48. int sock = Sock::Accept(listen_sock);
    49. if(sock >=0)
    50. {
    51. std::cout<< "listen_sock: " <"新链接连接成功"<
    52. int pos = 1;
    53. for(;pos
    54. {
    55. if(fd_array[pos].fd == -1) break;
    56. }
    57. if(pos
    58. {
    59. std::cout<< "新链接: " << sock <<"新链接加入数组"<
    60. fd_array[pos].fd = sock;
    61. fd_array[pos].events = POLLIN;
    62. }
    63. else
    64. {
    65. std::cout<< "服务器满载,关闭新链接"<
    66. close(sock);
    67. }
    68. }
    69. }
    70. else
    71. {
    72. // 普通socket读事件
    73. char buffer[1024] = {0};
    74. ssize_t s = recv(fd_array[i].fd,buffer,sizeof(buffer)-1,0);
    75. if (s > 0)
    76. {
    77. std::cout << "sock: " << fd_array[i].fd << " 上面有普通读取" << std::endl;
    78. buffer[s] = 0;
    79. std::cout << "client[ " << fd_array[i].fd << "]# " << buffer << std::endl;
    80. }
    81. else if(s == 0)
    82. {
    83. std::cout << "sock: " << fd_array[i].fd << "关闭了, client退出啦!" << std::endl;
    84. //对端关闭了链接
    85. close(fd_array[i].fd);
    86. std::cout << "已经在数组下标fd_array[" << i << "]"
    87. << "中,去掉了sock: " << fd_array[i].fd << std::endl;
    88. fd_array[i].fd = -1;
    89. }
    90. else
    91. {
    92. close(fd_array[i].fd);
    93. std::cout << "已经在数组下标fd_array[" << i << "]"
    94. << "中,去掉了sock: " << fd_array[i].fd << std::endl;
    95. fd_array[i].fd = -1;
    96. }
    97. }
    98. }
    99. }
    100. break;
    101. }
    102. }
    103. return 0;
    104. }

    3.3 优缺点

    优点:

    1. 文件描述符的监听没有上限
    2. 将监听事件和返回事件分离,接口使用更加方便

    缺点:

    1. poll返回后需要轮询检测获取就绪的文件描述符
    2. 每次调用poll都需要吧大量的pollfd结构从用户态拷贝到内核态
    3. 文件描述符增长,其效率也会下降

    4 epoll

    4.1 函数介绍

            epoll是为处理大量句柄而改进的poll。

    1. // 创建一个epoll句柄
    2. int epoll_create(int size);
    3. // 用户告诉内核需要关注的事件
    4. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
    5. // 内核告知用户哪些文件描述符的事件已就绪
    6. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

    op选项如下:

    EPOLL_CTL_ADD :注册新的fdepfd中;

    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服务器模型

    1. #include
    2. #include
    3. #include
    4. #include
    5. #include
    6. #include "Sock.hpp"
    7. #define SIZE 128
    8. #define NUM 64
    9. static void Usage(std::string proc)
    10. {
    11. std::cerr << "Usage: " << proc << " port" << std::endl;
    12. }
    13. int main(int argc, char *argv[])
    14. {
    15. if (argc != 2)
    16. {
    17. Usage(argv[0]);
    18. exit(1);
    19. }
    20. // 1. 建立tcp 监听socket
    21. uint16_t port = (uint16_t)atoi(argv[1]);
    22. int listen_sock = Sock::Socket();
    23. Sock::Bind(listen_sock, port);
    24. Sock::Listen(listen_sock);
    25. // 2. 创建文件描述符
    26. int epfd = epoll_create(SIZE);
    27. // 3. 添加listen_sock及其他所关心的事件,添加到内核
    28. struct epoll_event ev;
    29. ev.events = EPOLLIN;
    30. ev.data.fd = listen_sock;
    31. epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
    32. // 4. 事件循环
    33. volatile bool quit = false;
    34. struct epoll_event revs[NUM];
    35. while (!quit)
    36. {
    37. int timeout = 1000;
    38. int n = epoll_wait(epfd, revs, NUM, timeout);
    39. switch (n)
    40. {
    41. case 0:
    42. std::cout << "time out ..." << std::endl;
    43. break;
    44. case -1:
    45. std::cout << "epoll error ..." << std::endl;
    46. break;
    47. default:
    48. std::cout << "有事件就绪" << std::endl;
    49. // 5. 处理就绪事件
    50. for (int i = 0; i < n; i++)
    51. {
    52. int sock = revs[i].data.fd;
    53. std::cout << "文件描述符:" << sock << " 上有事件就绪了" << std::endl;
    54. if (revs[i].events & EPOLLIN)
    55. {
    56. std::cout << "文件描述符:" << sock << " 上有读事件就绪就绪了" << std::endl;
    57. if (sock == listen_sock)
    58. {
    59. std::cout << "文件描述符:" << sock << " 链接事件就绪就绪了" << std::endl;
    60. // 5.1 处理链接事件
    61. int fd = Sock::Accept(listen_sock);
    62. if (fd >= 0)
    63. {
    64. std::cout << "获取新链接成功" <
    65. struct epoll_event _ev;
    66. _ev.data.fd = fd;
    67. epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &_ev);
    68. std::cout << "已经将新链接托管给epoll了" <
    69. }
    70. else
    71. {
    72. // fail
    73. }
    74. }
    75. else
    76. {
    77. // 5.2 正常的读取处理
    78. char buffer[1024];
    79. ssize_t s = recv(sock,buffer,sizeof(buffer)-1,0);
    80. if(s>0)
    81. {
    82. buffer[s] = 0;
    83. std::cout << "client ["<"]# "<
    84. }
    85. else if(s==0)
    86. {
    87. std::cout << "client quit... " <
    88. close(sock);
    89. epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
    90. std::cout << "sock: "<" delete from epoll success"<
    91. }
    92. else
    93. {
    94. std::cout << "recv error " <
    95. close(sock);
    96. epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr);
    97. }
    98. }
    99. }
    100. else
    101. {
    102. //处理其他事件
    103. }
    104. }
    105. break;
    106. }
    107. }
    108. close(epfd);
    109. close(listen_sock);
    110. return 0;
    111. }

    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