0%

epoll与阻塞非阻塞

1、socket与缓存区

http://c.biancheng.net/cpp/html/3040.html

epoll的ET触发并非缓存区可读可写,而是有新的网络IO事件。如果没有完全读取完毕而返回导致缓存区一直有数据可读。则只能等到下一次有网络IO时间的时候才能读取到。

2、阻塞与非阻塞的网络fd

阻塞与非阻塞是针对fd而言:

  • 阻塞的fd在read不到数据的时候,会一直等待。

  • 在非阻塞的fd中,如果输入操作不能被满足(对于TCP套接口即至少有一个字节的数据可读,对于UDP套接口即有一个完整的数据报可读),则返回-1,并且,设置errno为EWOULDBLOCK错误

2.1、read and write

1、当read()或者write()函数返回值大于0时,表示实际从缓冲区读取或者写入的字节数目

2、

  • 当read()函数返回值为0时,表示对端已经关闭了 socket,这时候也要关闭这个socket,否则会导致socket泄露。netstat命令查看下,如果有closewait状态的socket,就是socket泄露了

  • 当write()函数返回0时,表示当前写缓冲区已满,是正常情况,下次再来写就行了。

3、当read()或者write()返回-1时,一般要判断errno

  • 如果errno == EINTR,表示系统当前中断了,直接忽略

  • 如果errno == EAGAIN或者EWOULDBLOCK,非阻塞socket直接忽略

  • 如果是阻塞的socket,一般是读写操作超时了,还未返回。这个超时是指socket的SO_RCVTIMEO与SO_SNDTIMEO两个属性

ps:read函数读取到缓存区的数据就会返回,不论参数的设置的大小或者fd是否阻塞

所以在使用阻塞socket时,不要将超时时间设置的过小。不然返回了-1,你也不知道是socket连接是真的断开了,还是正常的网络抖动。

一般情况下,阻塞的socket返回了-1,都需要关闭重新连接。

以上部分摘取自:https://www.cnblogs.com/myd620/p/6219994.html

2.2、accept

1、在accept新连接时,建议以非阻塞的方式监听listenfd:如果在触发accept可读的同时,客户端请求关闭连接,此时,此时,会将将其从等待连接的队列中删除,如果队列为空,accept阻塞,直到有新的连接

2、在后续的accept调用中忽略以下错误:EWOULDBLOCK(源自Berkeley的实现,客户终止连接时),ECONNABORTED(POSIX实现,客户终止连接时),EPROTO(SVR4实现,客户终止连接时)和EINTR(如果有信号被捕获)

3、使用举例

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote,   
            (size_t *)&addrlen)) > 0) {  
handle_client(conn_sock);  
}  
if (conn_sock == -1) {  
    if (errno != EAGAIN && errno != ECONNABORTED   
            && errno != EPROTO && errno != EINTR)   
        perror("accept");  
}  

2.3、问题

阻塞的FD为什么被SELECT OR EPOLL触发可读后,读取的时候还是可能会被阻塞?

以读为例:在多路复用中,被SELECT OR EPOLL触发可读后,但是被协议栈检查报文错误,则该报文被丢弃。其实是不可读的,此时线程OR进程会被卡在READ函数处。

对于非阻塞的套接口,如果输入操作不能被满足(对于TCP套接口即至少有一个字节的数据可读,对于UDP套接口即有一个完整的数据报可读)

3、epoll使用举例

1、LT配合非阻塞的fd,见 redis的非阻塞网络模型

2、LT配合阻塞的fd,由于2.2的原因,一般不用多路复用中一般不用阻塞的fd

        /* read call happens here */
        if ((nbytes = read(i, buf, sizeof(buf))) >= 0) {
            handle_read(nbytes, buf);
        } else {
            /* real version needs to handle EINTR correctly */
            perror("read");
            exit(EXIT_FAILURE);
        }

3、ET模式一般不和阻塞的fd一起使用,理由如下:

只说读的情况:
ET只有来数据时才会提醒,因此读数据时,你必须一次性地把缓冲区的数据读完(如果不读完,则缓冲区还残留着未读数据,然后对端又不继续发数据的话,ET是不会再通知你,也就是说你将永远读不到残留数据)为了一次性把缓冲区数据读完,你必须要写一个while循环来read,直到缓冲区里面数据被读完

如果设成阻塞的话,你的程序就无法知道数据什么时候被读完,因为当数据读完时,会卡在while里面的read,一直在等数据,永远退不出while

如果设成非阻塞,当数据被读完,read就会返回,然后将errno设成EAGAIN并退出while,这才是正确的逻辑

4、ET配合非阻塞的fd:

n = 0;
while ((nread = read (sockfd, buf + n, BUFSIZ - 1)) > 0)
{
    n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror ("read error");

需要一直读取,直到返回-1。

如果I/O数据量很大,可能在读取数据的过程中其他文件得不到处理,造成饥饿。解决方法是维护一个就绪列表,在关联数据结构中标记文件描述符为就绪状态,由此可以记住哪些文件在等待,并对所有就绪文件作轮转处理。