• 浅谈select模型的实现过程和特点


    目录

    一、select模型的事件就绪条件

    1、读事件就绪

    2、写事件就绪

    二、select模型的执行过程

    1、第一部分:select调用前

    2、第二部分:select调用后

    三、select的优缺点

    1、优点

    2、缺点


    一、select模型的事件就绪条件

    所谓的事件就绪,并非只有数据就绪这种情况,比如监听套接字listen_fd收到了新的连接请求,这也算 读事件就绪的一种,用户可以通过accpet函数将新连接拷贝到上层进行维护,所谓的读不正是将数据从内核拷贝到上层的过程吗?

    1、读事件就绪

    • 最典型的读事件是 缓冲区里的数据就绪。 此时缓冲区里数据所占字节数 大于 低水位标记SO_RCVLOWAT,然后上层便可以无阻塞的读该文件描述符。
    • 监听套接字 listen_fd 收到了新的连接请求。这也算是读事件就绪的一种,然后上层便可以调用accept函数将新连接从内核拷贝到用户层,并返回新连接的 fd
    • 对端的连接断开(recv/read的返回值为0)。
    • socket上有未处理的错误

    2、写事件就绪

    • 最典型的写事件是 缓冲区里有足够的剩余空间。此时缓冲区里剩余空间所占字节数大小 大于 低水位标记SO_SNDLOWAT ,然后上层便可以无阻塞的写入内容。
    • 调用 close 或者 shutdown 函数,此时写操作被关闭了。
    • socket使用非阻塞connect 连接成功或者失败之后。
    • socket上有未读取的错误。

    二、select模型的执行过程

    select的执行过程  以select函数调用为界,分为两部分。第一部分是告诉 select 哪些fd是需要被关注的;第二部分是 事件就绪以后的拷贝工作(调用read/recv/write/send函数)。

    1、第一部分:select调用前

    定义readfds_array数组的原因

    在接收数据的时候,可能存在下面两种情况

    • 一个文件描述符只是接收了一部分数据,此时数据就绪了,剩余尚未接收的数据本该留到下次接收,但是第二个参数readfds 输出时会清空所有的内容。
    • 一个文件描述符读/写事件就绪后,第二个参数readfds 清空所有的内容,将就绪结果告知用户,但是下一次还想要继续写入内容,下一次又该怎么办呢??

    根本原因在于 第二个参数 readfds 是一个输入输出型参数。输入时,用户告诉内核,哪些文件描述符需要被关注;输出时,readfds将原本的内容清空,换上输出结果,以此告诉用户,有哪些文件描述符上的事件已经就绪。

    因此,我们需要一个数组来临时存储下一次仍然需要被关注的fd ,这个数组便是readfds_array

    1. int main()
    2. {
    3. //创建套接字
    4. int listen_fd = TcpSocket::CreateSocket();
    5. //绑定端口号
    6. TcpSocket::Bind(listen_fd, 8080);
    7. //设为监听套接字
    8. TcpSocket::Listen(listen_fd);
    9. #define NUM 1024
    10. int readfds_array[NUM]; //定义一个数组用于保存下一次需要关注的fd
    11. for(int i=0; i< NUM ;i++){
    12. readfds_array[i] = -1; //初始化数组,元素值为-1表示当前位置未被占用
    13. }
    14. readfds_array[0] = listen_fd; //新连接到来的时候,属于读事件就绪
    15. fd_set *readfds = nullptr;
    16. while (1)
    17. {
    18. int max_fd = readfds_array[0]; // select的第一个参数是所有文件描述符的最大值
    19. FD_ZERO(readfds); //清空读事件集合
    20. for (size_t i = 0; i < NUM; i++) //以循环的方式将数组里的fd添加到 读事件集合中
    21. {
    22. if (readfds_array[i] == -1)
    23. continue;
    24. //到这一步,readfds_array[i]一定存了文件描述符
    25. FD_SET(readfds_array[i], readfds);
    26. //获取到文件描述符的最大值
    27. max_fd = max_fd < readfds_array[i] ? readfds_array[i] : max_fd;
    28. }
    29. //设置阻塞时间为5s
    30. struct timeval timeout = {5,0};
    31. int n = select(max_fd,readfds,nullptr,nullptr,&timeout);
    32. switch (n)
    33. {
    34. case -1:
    35. //select调用出错
    36. break;
    37. case 0:
    38. //select阻塞超时(阻塞超时会进入非阻塞状态,这里不算调用出错)
    39. break;
    40. default:
    41. /******************* 第二部分 *****************/
    42. //说明有文件描述符上的事件就绪
    43. break;
    44. }
    45. }
    46. return 0;
    47. }

    2、第二部分:select调用后

    这部分要重点了解的是事件就绪后如何处理的问题。(只不过我们这里只关注读事件就绪)。

    事件就绪有可能是读事件、写事件就绪、异常事件就绪。此时我们并不知道是哪种事件的哪个fd就绪,所以要逐一去判断。如果 readfds_array[i] == -1,说明该位置没有存放文件描述符,直接进行下一次循环。

    1. int main()
    2. {
    3. //创建套接字
    4. int listen_fd = TcpSocket::CreateSocket();
    5. //绑定端口号
    6. TcpSocket::Bind(listen_fd, 8080);
    7. //设为监听套接字
    8. TcpSocket::Listen(listen_fd);
    9. #define NUM 1024
    10. int readfds_array[NUM]; //定义一个数组用于保存下一次需要关注的fd
    11. for(int i=0; i< NUM ;i++){
    12. readfds_array[i] = -1; //初始化数组,元素值为-1表示当前位置未被占用
    13. }
    14. readfds_array[0] = listen_fd; //新连接到来的时候,属于读事件就绪
    15. fd_set *readfds = nullptr;
    16. while (1)
    17. {
    18. int max_fd = readfds_array[0]; // select的第一个参数是所有文件描述符的最大值
    19. FD_ZERO(readfds); //清空读事件集合
    20. for (size_t i = 0; i < NUM; i++)
    21. {
    22. if (readfds_array[i] == -1)
    23. continue;
    24. //到这一步,readfds_array[i]一定存了文件描述符
    25. FD_SET(readfds_array[i], readfds);
    26. //获取到文件描述符的最大值
    27. max_fd = max_fd < readfds_array[i] ? readfds_array[i] : max_fd;
    28. }
    29. //设置阻塞时间为5s
    30. struct timeval timeout = {5,0};
    31. int n = select(max_fd,readfds,nullptr,nullptr,&timeout);
    32. switch (n)
    33. {
    34. case -1:
    35. //select调用出错
    36. break;
    37. case 0:
    38. //select阻塞超时(阻塞超时会进入非阻塞状态,这里不算调用出错)
    39. break;
    40. default:
    41. /******************* 第二部分 *****************/
    42. //说明有文件描述符上的事件就绪
    43. for (size_t i = 0; i < NUM; i++)
    44. {
    45. if (readfds_array[i] == -1) continue;
    46. //判断是不是读事件就绪(判断是否在读事件集合)
    47. if(FD_ISSET(readfds_array[i],readfds)){
    48. //说明该文件描述符上的读事件就绪
    49. if(readfds_array[i] == listen_fd){
    50. //收到了新的连接,此时可以调用accept
    51. int sock = TcpSocket::Accept(readfds_array[i]);
    52. if(sock > 0){
    53. //说明获取成功,此时需要把这个文件描述符加入到 readfds_array[i]中
    54. //因此需要遍历readfds_array数组,看看有哪个位置未被使用,第0个位置固定给监听套接字使用
    55. int pos = 1;
    56. for (; pos < NUM; pos++)
    57. {
    58. if (readfds_array[i] == -1) break; //-1表示该位置未被使用
    59. }
    60. //如果pos
    61. if (pos < NUM)
    62. {
    63. readfds_array[pos] = sock;
    64. }
    65. else{
    66. //没有位置可以放说明服务器满载了
    67. close(sock);
    68. }
    69. }
    70. }
    71. else{
    72. //收到了对方发来的数据,此时可以调用recv/read
    73. char buffer[1024] = {0};
    74. ssize_t s = recv(readfds_array[i],buffer,sizeof(buffer)-1,0);
    75. if (s > 0)
    76. {
    77. // 将数据拷贝到上层,接下来可以处理数据了
    78. }
    79. else if (s == 0)
    80. {
    81. //对端关闭连接了
    82. close(readfds_array[i]);
    83. readfds_array[i] = -1; //空出位置给其他fd使用
    84. }
    85. else{
    86. //读取失败
    87. close(readfds_array[i]);
    88. readfds_array[i] = -1; //空出位置给其他fd使用
    89. }
    90. }
    91. }
    92. //判断是不是写事件就绪
    93. // ... ...
    94. //判断是不是异常事件就绪
    95. // ... ...
    96. }
    97. break;
    98. }
    99. }
    100. return 0;
    101. }

    三、select的优缺点

    1、优点

    可以一次等待多个fd,可以让我们等待的时间重叠,一定程度上可以提高IO的效率。尽管多线程也可以实现,但是多线程的运行是受到CPU调度约束的,阻塞等待的时候会被加入到等待队列,事件就绪的时候再回到运行队列,频繁换队列存在一定的损耗。

    2、缺点

    缺点一

    每次都要重新设置哪些文件描述符需要被关注。以第二个参数readfds为例,readfds是一个输入输出型参数,输入时告诉哪些文件描述符需要被关注;输出的时候,内核通知我们哪些文件描述符上的事件就绪了。

    但是这样一来,我们最开始设置的输入就被输出结果给覆盖了,因此,下一次调用select的时候我们要重新设置第二个参数。

    缺点二

    每次调用select都需要把fd集合从用户层拷贝到内核,这个开销在fd较多时会很大。同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。

    缺点三

    fd_set是一个位图结构,每次可以让select 关注的文件描述符数是有限的,只有1024个。

  • 相关阅读:
    11-【数据库】定义表结构的时间字段的两种方式
    基于Java+SpringBoot+vue前后端分离小徐影城管理系统设计实现
    OWASP TOP 10解析:构建坚不可摧的Web应用安全防线
    14:00面试,14:08就出来了,问的问题有点变态
    谭浩强【C语言程序设计】第一章习题详解
    猴子也能学会的jQuery第十期——jQuery元素操作(下)
    迁移 sqoop测试环境
    Kafka 杂谈
    Spring Boot 注解详解:一步一步了解Spring Boot的核心注解
    C语言——递归题
  • 原文地址:https://blog.csdn.net/challenglistic/article/details/127107837