浅谈 non-blocking I/O Multiplexing + poll/epoll 的正确使用

在前面的文章中曾经粗略讲过poll,那时是用阻塞IO实现,在发送和接收数据量都较小情况下和网络状况良好的情况下是基本没有问题的,read 不会只接收部分数据,write 也不会一直阻塞。但实际上poll IO复用经常是跟非阻塞IO一起使用的,想想如果现在内核接收缓冲区一点数据没有,read 阻塞了,或者内核发送缓冲区不够空间存放数据,write 阻塞了,那整个事件循环就会延迟响应,比如现在又有一个新连接connect上来了,也不能很快回到循环去accept 它。

在前面的文章中也曾粗略讲过epoll,使用的是ET 边沿触发模式,每次accept 返回需要将conn 设置为非阻塞,ET模式可能存在的问题是有可能只读取了部分数据,剩下的epoll_wait 就再也不会返回可读事件了。

这篇文章来谈谈如何正确使用non-blocking I/O Multiplexing + poll/epoll。

1、首先来回顾下poll / epoll 函数的原型

#include <poll.h> int poll(struct pollfd *fds, nfds_t nfds, int timeout); struct pollfd {      int   fd;     /* file descriptor */      short events;     /* requested events */      short revents;     /* returned events */ };

#include <sys/epoll.h> int epoll_create(int size); //size 并不代表能够容纳的事件个数 int epoll_create1(int flags); // EPOLL_CLOEXEC 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); typedef union epoll_data {      void    *ptr;      int      fd;      uint32_t u32;      uint64_t u64; } epoll_data_t; struct epoll_event {      uint32_t     events;     /* Epoll events */      epoll_data_t data;     /* User data variable */ };

具体的参数介绍参考以前的文章。

2、关于SIGPIPE 信号的产生和处理

如果客户端关闭套接字close,而服务器调用一次write, 服务器会接收一个RST segment(tcp传输层)

如果服务器端再次调用了write,这个时候就会产生SIGPIPE信号,默认终止进程。可以在程序中直接忽略掉,如 signal(SIGPIPE, SIG_IGN);

3、TIME_WAIT 状态对 服务器的影响

如果服务器端 主动断开连接(先于client 调用close),服务器端就会进入TIME_WAIT 状态。应尽可能在服务器端避免TIME_WAIT 状态,因为它会在一定时间内hold住一些内核资源。协议设计上,应该让客户端主动断开连接,这样就把TIME_WAIT状态分散到大量的客户端。如果客户端不活跃了,一些不客户端不断开连接,这样就会占用服务器端的连接资源。服务器端也要踢掉不活跃的连接close。

4、使用 C++ erase 的注意点

即erase 返回的是下一个元素的iterator

5、新的accept4 系统调用

accept - accept a connection on a socket

      #include <sys/types.h>          /* See NOTES */

       #include <sys/socket.h>        int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);        #define _GNU_SOURCE             /* See feature_test_macros(7) */        #include <sys/socket.h>        int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags);

可以使用accept4 这个新的系统调用,多了一个flags 参数,可以设置以下两个标志:

SOCK_NONBLOCK   Set the O_NONBLOCK file status flag on the new open file description.  Using this flag saves  extra  calls to fcntl(2) to achieve the same result.

 SOCK_CLOEXEC    Set  the close-on-exec (FD_CLOEXEC) flag on the new file descriptor.  See the description of the O_CLOEXEC  flag in open(2) for reasons why this may be useful.

注意,这两个标志是设置accept 回来的conn 标志的,当然也可以使用fcntl (F_SETFL / F_SETFD) 设置,但少了两次系统调用,可以稍微提高点性能。

7、poll  的处理流程和存在的问题

存在的问题和解决办法:

(1)、read 可能一次并没有把connfd 所对应的接收缓冲区(内核)的数据都读完(粘包问题),那么connfd 下次仍然是活跃的 应该把读到的数据保存在connfd 的应用层接收缓冲区,每次都追加在末尾。需要处理协议以区分每条消息的边界

(2)、write 可能一次并不能把所有数据都写到发送缓冲区(内核),所以应该有一个应用层发送缓冲区,将未发送完的数据添加到应用层发送缓冲区,关注connfd 的POLLOUT 事件。POLLOUT事件到来,则取出应用层发送缓冲区数据发送write,如果应用层发送缓冲区数据发送完毕,则取消关注POLLOUT事件。 POLLOUT 事件触发条件:connfd的发送缓冲区(内核)不满(可以容纳数据) 注:connfd 的接收缓冲区(内核)数据被接收后会被清空,当发出数据段后接收到对方的ACK段后,发送缓冲区(内核)数据段会被清空。write只是将应用层发送缓冲区数据拷贝到connfd 对应的内核发送缓冲区就返回;read 只是从connfd对应的内核接收缓冲区数据拷贝到应用层接收缓冲区就返回。

9、epoll  的两种模式处理流程和存在的问题

Level-Triggered //跟poll 基本类似

LT 电平触发(高电平触发):

EPOLLIN 事件

内核中的某个socket接收缓冲区     为空          低电平 内核中的某个socket接收缓冲区     不为空       高电平

EPOLLOUT 事件

内核中的某个socket发送缓冲区     不满          高电平 内核中的某个socket发送缓冲区     满             低电平

注:只要第一次write没写完整,则下次调用write直接把数据添加到应用层缓冲区OutBuffer,等待EPOLLOUT事件。

如果采用Level-Triggered,那什么时候关注EPOLLOUT事件?会不会造成busy-loop(忙等待)?

Edge-Triggered:

ET 边沿触发:

低电平-》高电平      触发

推荐epoll使用LT模式的原因:

与poll兼容 LT模式不会发生漏掉事件的BUG,但POLLOUT事件不能一开始就关注,否则会出现busy loop(即暂时还没有数据需要写入,但一旦连接建立,内核发送缓冲区为空会一直触发POLLOUT事件),而应该在write无法完全写入内核缓冲区的时候才关注,将未写入内核缓冲区的数据添加到应用层output buffer,直到应用层output buffer写完,停止关注POLLOUT事件。 读写的时候不必等候EAGAIN,可以节省系统调用次数,降低延迟。(注:如果用ET模式,读的时候读到EAGAIN,写的时候直到output buffer写完或者写到EAGAIN)

注:在使用 ET 模式时,可以写得更严谨,即将 listenfd 设置为非阻塞,如果accpet 调用有返回,除了建立当前这个连接外,不能马上就回到 epoll_wait ,还需要继续循环accpet,直到返回-1 且errno == EAGAIN 才退出。代码示例如下:

if(ev.events & EPOLLIN)
{
    do
    {
        struct sockaddr_in stSockAddr;
        socklen_t iSockAddrSize = sizeof(sockaddr_in);
        
        int iRetCode = accept(listenfd, (struct sockaddr *) &stSockAddr, iSockAddrSize);
        if (iRetCode > 0)
        {
           // ...建立连接
           // 添加事件关注
        }
        else
        {
            //直到发生EAGAIN才不继续accept
            if(errno == EAGAIN)
            {
                break;
            }
        }
    }
    while(true);
    
    // ... 其他 EPOLLIN 事件
}

10、accept(2)返回EMFILE的处理(文件描述符已经用完)

(1)、调高进程文件描述符数目 (2)、死等 (3)、退出程序 (4)、关闭监听套接字。那什么时候重新打开呢? (5)、如果是epoll模型,可以改用edge trigger。问题是如果漏掉了一次accept(2),程序再也不会收到新连接(没有状态变化) (6)、准备一个空闲的文件描述符。遇到这种情况,先关闭这个空闲文件,获得一个文件描述符名额;再accept(2)拿到socket连接的文件描述符;随后立刻close(2),这样就优雅地断开了与客户端的连接;最后重新打开空闲文件,把“坑”填上,以备再次出现这种情况时使用。

如下面的代码片段:

int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
connfd = accept4(listenfd, (struct sockaddr *)&peeraddr,
                 &peerlen, SOCK_NONBLOCK | SOCK_CLOEXEC);

/*          if (connfd == -1)
                ERR_EXIT("accept4");
*/

if (connfd == -1)
{
    if (errno == EMFILE)
    {
        close(idlefd);
        idlefd = accept(listenfd, NULL, NULL);
        close(idlefd);
        idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
        continue;
    }
    else
        ERR_EXIT("accept4");
}

参考:

muduo manual.pdf

《linux 多线程服务器编程:使用muduo c++网络库》

http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏架构之路

【网络编程系列】二:socket通信原理及实践

我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或...

4796
来自专栏Golang语言社区

Linux下socket双向通信

linux下的socket与windows下的类似,就是少一个初始化的过程。 服务端 客户端 1 创建socke...

3708
来自专栏用户2442861的专栏

TCP socket心跳包示例程序

http://blog.csdn.net/lisonglisonglisong/article/details/51327695#comments

1242
来自专栏技术小黑屋

读懂Java中的Socket编程

Socket,又称为套接字,Socket是计算机网络通信的基本的技术之一。如今大多数基于网络的软件,如浏览器,即时通讯工具甚至是P2P下载都是基于Socket实...

692
来自专栏哲学驱动设计

asp.net MVC 应用程序的生命周期

  首先我们知道http是一种无状态的请求,他的生命周期就是从客户端浏览器发出请求开始,到得到响应结束。那么MVC应用程序从发出请求到获得响应,都做了些什么呢?...

1679
来自专栏高性能服务器开发

(三)服务器端的程序架构介绍1

通过上一节的编译与部署,我们会得到TeamTalk服务器端以下部署程序: db_proxy_server file_server http_msg_server...

2926
来自专栏Janti

基础巩固——长连接 、短连接、心跳机制与断线重连

本文将从长连接和短连接的概念切入,再到长连接与短连接的区别,以及应用场景,引出心跳机制和断线重连,给出代码实现。

1051
来自专栏Android源码框架分析

Android后台杀死系列之三:LowMemoryKiller原理(4.3-6.0)

本篇是Android后台杀死系列的第三篇,前面两篇已经对后台杀死注意事项,杀死恢复机制做了分析,本篇主要讲解的是Android后台杀死原理。相对于后台杀死恢复,...

1205
来自专栏用户2442861的专栏

non-blocking I/O Multiplexing + poll/epoll 的正确使用

在前面的文章中曾经粗略讲过poll,那时是用阻塞IO实现,在发送和接收数据量都较小情况下和网络状况良好的情况下是基本没有问题的,read 不会只接收部分数据,...

812
来自专栏安富莱嵌入式技术分享

【安富莱】【RL-TCPnet网络教程】第10章 RL-TCPnet网络协议栈移植(FreeRTOS)

本章教程为大家讲解RL-TCPnet网络协议栈的FreeRTOS操作系统移植方式,学习了第6章讲解的底层驱动接口函数之后,移植就比较容易了,主要是添加库文件、配...

802

扫码关注云+社区