Poll API介绍

    先讲下select的缺点 1 每次调用select都需要把fd_set从用户态拷贝到内核态(这样内核才直到要监听哪些文件描述符) 返回传出时需要将fd_set从内核态拷贝到用户态 2 select在内核中需要遍历传进来的置1的fd 在fd_set中有较多置1的文件描述符时 开销会很大 O(n) 3 select能支持的最多的文件描述符数量为1024 太少了 4 fd_set既是传入也是传出参数 不能重用,每次都需要重置

    //poll就是对select的改进
    //poll 的fds可重复使用 且能支持的文件描述符数量无限制 但是缺点1,2还是存在的
    #include
    struct pollfd{
    int fd;//委托内核检测的文件描述符
    short events;//委托内核对fd的什么事件进行检测
    short revents;//文件描述符实际发生的事件 传出参数 return events
    };
    int poll(struct pollfd* fds,nfds_t nfds,int timeout);
    图片1.png
    POLLIN 检测此fd读缓冲区是否有新的数据 同时检测POLLIN|POLLOUT
    fds为struct pollfd结构体类型数组的首地址,保存了要检测的文件描述符的信息
    nfds fds数组的最后一个有效元素的下标+1
    timeout 阻塞时长 传入0表示不阻塞 -1表示永久阻塞直到检测到fds中的fd的一些状态发生改变才解除阻塞函数返回 >0为阻塞的时长单位为毫秒

    失败返回-1 n>0 检测到fds中有n个fd的状态发生变化

    poll代码举例

    1. //io多路复用之poll
    2. #include <sys/time.h>
    3. #include <sys/types.h>
    4. #include <unistd.h>
    5. #include <arpa/inet.h>
    6. #include <string.h>
    7. #include <poll.h>
    8. int main()
    9. {
    10. //创建监听socket
    11. int lfd = socket(PF_INET, SOCK_STREAM, 0);
    12. struct sockaddr_in saddr;
    13. saddr.sin_port = htons(9999);
    14. saddr.sin_family = AF_INET;
    15. saddr.sin_addr.s_addr = INADDR_ANY; //服务器要绑定的地址 服务器任一网卡的ip
    16. //绑定
    17. bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    18. //监听
    19. listen(lfd, 8); //等待连接的最大队列长度为8
    20. //原BIO accept 接收连接 + while死循环与客户端通信
    21. //初始化检测的文件描述符属性数组
    22. struct pollfd fds[1026];
    23. for (int i = 0; i < 1026; i++)
    24. {
    25. fds[i].fd = -1;
    26. fds[i].events = POLLIN; //只检测所有文件描述的读缓存区是否有新数据
    27. }
    28. fds[0].fd = lfd; //监听的文件描述符
    29. int nfds = 0;
    30. while (1)
    31. {
    32. //调用poll 让内核帮助监听文件描述符
    33. int ret = poll(fds, nfds + 1, -1); //只辅助监听 不检测是否能写 不检测是否有一样 永久阻塞
    34. if (ret == -1)
    35. {
    36. perror("poll:");
    37. exit(-1);
    38. }
    39. else if (ret == 0) //超时时间到了 并且在这段时间内要检测的fd的状态没有改变(没有新的可读信息)
    40. {
    41. continue;
    42. }
    43. else if (ret > 0) //ret>0 ret中保存我们监听的fd中有几个的状态发生改变(有几个fd的读缓冲区数据发生变化--即有新的可读信息)
    44. {
    45. if (fds[0].revents & POLLIN) //因为revents的结果不一定只有POLLIN
    46. //在同时检测读和写时revents结果可能是POLLIN|POLLOUT作为结果
    47. //如果我们用fds[0].revents == POLLIN 来判断是否有新数据到来
    48. //当同时 有新数据到来和写缓冲区有空间可写将会导致返回POLLIN|POLLOUT!=POLLIN
    49. //这种情况下用& POLLIN就行可以单独检测我们像检测的状态是否发生变化
    50. {
    51. //判断服务器监听描述符的读缓冲区是否有新的数据
    52. //表示有新的客户端连接进来了!!!!!!!
    53. struct sockaddr_in client_addr;
    54. int len = sizeof(client_addr);
    55. int cfd = accept(lfd, (struct sockaddr *)&client_addr, &len); //接收这个新的连接 这里因为提前直到有新连接到来 调用accept不会阻塞 而是直接接收到那个连接请求
    56. //将新的与客户端通信的fd 加入fd数组中 已完成poll对这个新通信连接的监听
    57. for (int i = 1; i < 1026; i++)
    58. //我们不能直接将数组下标为nfds+1的空间分给这个fd
    59. //因为客户端断开是随机的 在nfds+1之前如果有数组空间空闲了 但我们跳过没用不就浪费了
    60. //所以还是要从头检查数组是否还有空间没被占用(fd!=-1)
    61. {
    62. if (fds[i].fd == -1)
    63. {
    64. fds[i].fd = cfd;
    65. fds[i].events = POLLIN;
    66. nfds = nfds > i ? nfds : i; //数组中的最大可用索引
    67. break;
    68. }
    69. } //从头开始找
    70. }
    71. //遍历其他集合中的文件描述符
    72. for (int i = 0 + 1; i <= nfds; i++)
    73. {
    74. if (fds[i].revents & POLLIN)
    75. {
    76. //fd = i对应的连接的客户端发来了数据
    77. char buf[1024] = {};
    78. int len = read(fds[i].fd, buf, sizeof(buf)); //调用read 因为缓冲区有数据 所以不会阻塞可以直接读
    79. if (len == -1)
    80. {
    81. perror("read:");
    82. exit(-1);
    83. }
    84. else if (len == 0)
    85. {
    86. //对方断开连接
    87. printf("client closed\n");
    88. //将这个通信fd移除
    89. close(fds[i].fd);
    90. fds[i].fd = -1; //=-1表示检测数组中的这个位置空闲了
    91. }
    92. else if (len > 0) //读到了数据
    93. {
    94. //数据处理
    95. printf("from client: %s \n", buf);
    96. write(fds[i].fd, buf, strlen(buf) + 1); //回写
    97. }
    98. }
    99. }
    100. }
    101. }
    102. close(lfd);
    103. return 0;
    104. }

    epoll API介绍

    epoll_create在内核区实例化eventpoll(结构体) 返回epfd,在用户态对epfd操作就能影响内核区中的eventpoll,eventpoll结构体中有struct rb_root rbr 红黑树根节点(保存要检测的fd的信息) struct list_head rdlist 就绪链表头节点(epoll检测到fd的状态改变会将其信息放在这个链表中)。因为eventpoll就是在内核中的 我们不需要进行用户态和内核态之间的拷贝操作
    epoll_ctl函数能够完成对内核区eventpoll添加或删除要检测的fd(如果是添加还要传入指定 要检测fd什么信息 ev)
    epoll_wait 让内核去检测红黑树根fd节点是否有指定的事件发生,如果有则会将rdlist中的一些信息拷贝到用户区(只会返回发生指定事件的fd信息 不需要我们像poll和select一样自己去遍历检查)
    图片2.png

    1. #include <sys/epoll.h>
    2. int epoll_create(int size);
    3. //在内核中创建一个eventpoll(结构体)实例(含 需要检测的fd信息的红黑树 和 双向就绪链表 保存检测到对应事件发生的fd的信息)
    4. //size 目前没有什么意义了但是要大于0(以前是用哈希实现的)
    5. //失败返回-1 大于0 为一个epfd文件描述符用于操作eventpoll实例
    6. int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
    7. //对epfd对应的eventpoll实例进行添加、删除fd或修改fd对应信息的操作
    8. //epfd eventpoll实例对应的文件描述符
    9. //op 要进行什么操作 删除EPOLL_CTL_DEL 添加EPOLL_CTL_ADD 修改EPOLL_CTL_MOD fd
    10. //修改红黑树节点信息
    11. //fd 要检测的文件描述符
    12. //event 具体要检测fd的什么信息
    13. struct epoll_event
    14. {
    15. uint32_t events;//检测的事件 事件非常多 可以在 man epoll_ctl中看到 主要有EPOLLIN(与此fd关联的文件是否可读) EPOLLOUT(与此fd关联的文件是否可写) EPOLLERR
    16. epoll_data_t data;//用户数据信息
    17. };
    18. //epoll_data_t 是个联合体
    19. typedef union epoll_data{
    20. void* ptr;
    21. int fd;
    22. uint32_t u32;
    23. uint64_t u64;
    24. }epoll_data_t;
    25. int epoll_wait(int epfd,struct epoll_event* event,int maxevents,int timeout);
    26. //让内核去检测红黑树根fd节点是否有指定的事件发生,如果有则会将rdlist中的一些信息拷贝到用户区(只会返回发生指定事件的fd信息 不需要我们像poll和select一样自己去遍历检查)
    27. //epfd eventpoll实例对应的文件描述符
    28. //event 作为传出参数 保存了发生了指定事件的文件描述符信息 是个数组 maxevents就是这个传出数组的大小 events不可以是空指针,内核只负责把数据复制到这个 events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高
    29. //timeout 阻塞时长 传入0表示不阻塞 -1表示永久阻塞直到检测到红黑树中的fd的一些状态发生改变才解除阻塞函数返回 >0为阻塞的时长单位为毫秒
    30. //成功返回发生变化的文件描述符的个数 失败返回-1

    epoll举例

    1. //io多路复用之epoll
    2. #include <sys/time.h>
    3. #include <sys/types.h>
    4. #include <unistd.h>
    5. #include <arpa/inet.h>
    6. #include <string.h>
    7. #include <sys/epoll.h>
    8. int main()
    9. {
    10. //创建监听socket
    11. int lfd = socket(PF_INET, SOCK_STREAM, 0);
    12. struct sockaddr_in saddr;
    13. saddr.sin_port = htons(9999);
    14. saddr.sin_family = AF_INET;
    15. saddr.sin_addr.s_addr = INADDR_ANY; //服务器要绑定的地址 服务器任一网卡的ip
    16. //绑定
    17. bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    18. //监听
    19. listen(lfd, 8); //等待连接的最大队列长度为8
    20. //原BIO accept 接收连接 + while死循环与客户端通信
    21. //epoll_create在内核创建event_poll结构体实例
    22. int epfd = epoll_create(1);
    23. //将监听文件描述符的相关信息加到event_poll结构体实例中的红黑树中
    24. struct epoll_event epev;
    25. epev.events = EPOLLIN; //检测是否可读的事件
    26. epev.data.fd = lfd;
    27. epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
    28. struct epoll_event epevs[1026];
    29. int maxevents = 1026;
    30. // event 作为传出参数 保存了发生了指定事件的文件描述符信息 是个数组
    31. // maxevents就是这个传出数组的大小 events不可以是空指针,内核只负责把数据复制到这个
    32. // events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高
    33. while (1)
    34. {
    35. //epoll wait 去检测我们指定的fd是否发生了指定的事件
    36. int ret = epoll_wait(epfd, epevs, maxevents, -1);
    37. //返回ret有几个fd发生了指定的事件 具体是哪几个保存在epevs的data.fd中
    38. if (ret == -1)
    39. {
    40. perror("epoll_wait:");
    41. exit(-1);
    42. }
    43. else if (ret == 0) //超时时间到了 并且在这段时间内要检测的fd的状态没有改变(没有新的可读信息)
    44. {
    45. continue;
    46. }
    47. else if (ret > 0)
    48. {
    49. //ret>0 ret中保存我们监听的fd中有几个的状态发生改变(有几个fd的读缓冲区数据发生变化--即有新的可读信息)
    50. //具体哪几个保存在传出的结构体数组中
    51. for (int i = 0; i < ret; i++)
    52. {
    53. if (epevs[i].data.fd == lfd && (epevs[i].events & EPOLLIN))
    54. {
    55. //监听描述符发生对于事件 并且这个事件是 读缓存区可读
    56. //有新的客户端连入 accept创建通信fd 并将这个通信fd加入epoll实例的红黑树中
    57. struct sockaddr_in client_addr;
    58. int len = sizeof(client_addr);
    59. int cfd = accept(lfd, (struct sockaddr *)&client_addr, &len); //接收这个新的连接 这里因为提前直到有新连接到来 调用accept不会阻塞 而是直接接收到那个连接请求
    60. epev.events = EPOLLIN; //检测是否可读的事件 | EPOLLOUT
    61. epev.data.fd = cfd;
    62. epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
    63. }
    64. else if (epevs[i].events & EPOLLIN) //只检测读事件 非0
    65. {
    66. //用于通信fd 客户端有数据到达
    67. char buf[1024] = {};
    68. int len = read(epevs[i].data.fd, buf, sizeof(buf)); //调用read 因为缓冲区有数据 所以不会阻塞可以直接读
    69. if (len == -1)
    70. {
    71. perror("read:");
    72. exit(-1);
    73. }
    74. else if (len == 0)
    75. {
    76. //对方断开连接
    77. printf("client closed\n");
    78. //将这个通信fd移除
    79. epoll_ctl(epfd, EPOLL_CTL_DEL, epevs[i].data.fd, NULL);
    80. close(epevs[i].data.fd);
    81. //之前都是先关闭后移除 这里是先从红黑树中移除再关闭
    82. }
    83. else if (len > 0) //读到了数据
    84. {
    85. //数据处理
    86. printf("from client: %s \n", buf);
    87. write(epevs[i].data.fd, buf, strlen(buf) + 1); //回写
    88. }
    89. }
    90. }
    91. }
    92. }
    93. close(lfd);
    94. close(epfd);
    95. return 0;
    96. }

    epoll的两种工作模式

    LT(level triggered)模式状态时同时支持阻塞和非阻塞socket。,主线程正在epoll_wait等待事件时,请求到了,epoll_wait返回后没有去处理请求(recv),那么下次epoll_wait时此请求还是会返回(立刻返回了);
    ET(Edge Triggered)模式
    而ET模式状态下要求使用非阻塞socket,这次没处理,下次epoll_wait时将不返回(所以我们应该每次一定要处理),可见很大程度降低了epoll的触发次数(记住这句话先)。内核只会在fd从未就绪变为就绪通知一次(具体ET在什么情况下会触发在Edge Triggered (ET) 边沿触发有写)

    没有将读缓冲区数据读完,下次epoll ET不会提醒(LT会提醒)这个fd读缓冲区还有数据。所以当epoll触发后需要一次性将读缓冲区数据读完(循环读),如果循环读读缓冲区的过程中发生阻塞将会影响对于其他fd检测。

    ET模式减少了epoll事件被重复触发的次数,必须使用非阻塞socket,以避免一个对文件的阻塞读和阻塞写将处理多个fd的任务饿死(不去处理)。
    Level Triggered (LT) 水平触发socket接收缓冲区不为空 有数据可读 读事件一直触发socket发送缓冲区不满 可以继续写入数据 写事件一直触发符合思维习惯,epoll_wait返回的事件就是socket的状态
    在read 无法一次性读完读缓冲区,下次调用epoll wait依然会触发通知
    Edge Triggered (ET) 边沿触发socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件仅在状态变化时触发事件
    设置边沿触发 epoll_event中的event设置为EPOLLIN | EPOLLET

    1. //io多路复用之epoll ET 边沿触发
    2. //socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
    3. #include <sys/time.h>
    4. #include <sys/types.h>
    5. #include <unistd.h>
    6. #include <arpa/inet.h>
    7. #include <string.h>
    8. #include <sys/epoll.h>
    9. #include <fcntl.h> //设置文件描述符非阻塞
    10. #include <errno.h>
    11. int main()
    12. {
    13. //创建监听socket
    14. int lfd = socket(PF_INET, SOCK_STREAM, 0);
    15. struct sockaddr_in saddr;
    16. saddr.sin_port = htons(9999);
    17. saddr.sin_family = AF_INET;
    18. saddr.sin_addr.s_addr = INADDR_ANY; //服务器要绑定的地址 服务器任一网卡的ip
    19. //绑定
    20. bind(lfd, (struct sockaddr *)&saddr, sizeof(saddr));
    21. //监听
    22. listen(lfd, 8); //等待连接的最大队列长度为8
    23. //原BIO accept 接收连接 + while死循环与客户端通信
    24. //epoll_create在内核创建event_poll结构体实例
    25. int epfd = epoll_create(1);
    26. //将监听文件描述符的相关信息加到event_poll结构体实例中的红黑树中
    27. struct epoll_event epev;
    28. epev.events = EPOLLIN; //检测是否可读的事件
    29. epev.data.fd = lfd;
    30. epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &epev);
    31. struct epoll_event epevs[1026];
    32. int maxevents = 1026;
    33. // event 作为传出参数 保存了发生了指定事件的文件描述符信息 是个数组
    34. // maxevents就是这个传出数组的大小 events不可以是空指针,内核只负责把数据复制到这个
    35. // events数组中,不会去帮助我们在用户态中分配内存。内核这种做法效率很高
    36. while (1)
    37. {
    38. //epoll wait 去检测我们指定的fd是否发生了指定的事件
    39. int ret = epoll_wait(epfd, epevs, maxevents, -1);
    40. //返回ret有几个fd发生了指定的事件 具体是哪几个保存在epevs的data.fd中
    41. if (ret == -1)
    42. {
    43. perror("epoll_wait:");
    44. exit(-1);
    45. }
    46. else if (ret == 0) //超时时间到了 并且在这段时间内要检测的fd的状态没有改变(没有新的可读信息)
    47. {
    48. continue;
    49. }
    50. else if (ret > 0)
    51. {
    52. //ret>0 ret中保存我们监听的fd中有几个的状态发生改变(有几个fd的读缓冲区数据发生变化--即有新的可读信息)
    53. //具体哪几个保存在传出的结构体数组中
    54. for (int i = 0; i < ret; i++)
    55. {
    56. if (epevs[i].data.fd == lfd && (epevs[i].events & EPOLLIN))
    57. {
    58. //监听描述符发生对于事件 并且这个事件是 读缓存区可读
    59. //有新的客户端连入 accept创建通信fd 并将这个通信fd加入epoll实例的红黑树中
    60. struct sockaddr_in client_addr;
    61. int len = sizeof(client_addr);
    62. int cfd = accept(lfd, (struct sockaddr *)&client_addr, &len); //接收这个新的连接 这里因为提前直到有新连接到来 调用accept不会阻塞 而是直接接收到那个连接请求
    63. //通信的fd设置读非阻塞
    64. int flag = fcntl(cfd, F_GETFL); //获取cfd属性
    65. fcntl(cfd, F_SETFL, flag | O_NONBLOCK); //添加非阻塞属性
    66. //用于和客户端通信的fd设置为边沿触发
    67. epev.events = EPOLLIN | EPOLLET; //检测是否可读的事件 | EPOLLOUT
    68. epev.data.fd = cfd;
    69. //边沿触发只会提醒一次 即空的接收缓冲区刚接收到数据时触发读事件
    70. //我们需要在触发后将读缓冲区内的数据全部读出来(因为不会二次提醒)
    71. epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &epev);
    72. }
    73. else if (epevs[i].events & EPOLLIN) //只检测读事件 非0
    74. {
    75. //用于通信fd 客户端有数据到达
    76. //循环一次性读出读缓冲区的数据
    77. char buf[5] = {};
    78. int len = 0; //调用read 因为缓冲区有数据 所以不会阻塞可以直接读
    79. //要求一次读 sizeof(buf)个字节 实际读了len个字节
    80. while ((len = read(epevs[i].data.fd, buf, sizeof(buf))) > 0)
    81. {
    82. //raed 返回值>0 表示读缓冲区还有数据 则继续读
    83. // printf("from client: %s \n", buf);
    84. write(STDOUT_FILENO, buf, len); //直接写再标准输出中(屏幕)
    85. write(epevs[i].data.fd, buf, len + 1); //回写
    86. }
    87. if (len == -1)
    88. {
    89. //read 被信号中断 会触发 EINTR错误 此时不应该退出 而是继续读
    90. //raed非阻塞读当读缓冲区读完了 再一次读将会触发EGAIN 此时不应该退出而是继续工作
    91. if (errno == EAGAIN)
    92. printf("buff read finish");
    93. else
    94. {
    95. perror("read:");
    96. exit(-1);
    97. }
    98. }
    99. else if (len == 0) //read 返回值为0 表示客户端断开连接
    100. {
    101. //对方断开连接
    102. printf("client closed\n");
    103. //将这个通信fd移除
    104. epoll_ctl(epfd, EPOLL_CTL_DEL, epevs[i].data.fd, NULL);
    105. close(epevs[i].data.fd);
    106. //之前都是先关闭后移除 这里是先从红黑树中移除再关闭
    107. }
    108. }
    109. }
    110. }
    111. }
    112. close(lfd);
    113. close(epfd);
    114. return 0;
    115. }