欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【epoll】epoll的水平触发和边沿触发,及为什么边沿触发必须使用非阻塞?

程序员文章站 2022-06-14 10:58:30
...

目录

 

简答

详细

水平触发(level trigger,LT)与 边沿触发(edge trigger,ET)

下面解释为什么使用边缘触发必须使用非阻塞


简答

ET 模式是一种边沿触发模型,在它检测到有 I/O 事件时,通过 epoll_wait 调用会得到有事件通知的文件描述符,对于每一个"有事”文件描述符,如可读,则必须将该文件描述符一直读到空(返回errno 或EAGAIN 为止),否则下次的 epoll_wait 不会返回余下的数据,会丢掉事件。

而如果你的文件描述符如果不是非阻塞的,那这个一直读(或一直写)到最后读完了(或写完了)不会想非阻塞那样直接返回errno 或EAGAIN ,而是阻塞在那里等待新的数据到来。就会阻塞在那里,不会继续下一个while循环。

epoll ET 一般的调用如下:

while (1)
{
    int nFDs = epoll_wait(nEpollFD, events, 128, &timeout);
    for (int i = 0; i < nFDs; i ++)
    {
        if (events[i].events & EPOLLIN)
        {
            // 套接字有读事件,要读空,直到 recv 返回 -1 而且 errno == EAGAIN
            // 监听套接字可 accept 或套接字可读,都会得到可读通知
        }
         
        ...
    }
}

详细

epoll有两种触发方式

水平触发(level trigger,LT)与 边沿触发(edge trigger,ET)

水平触发与边缘触发的区别:

水平触发:只要缓冲区有数据就会一直触发

边沿触发:只有在缓冲区增加数据的那一刻才会触发

下面举一个例子说明这两者的区别

/* 使用边沿触发 */
 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
 
int main(int argc, char *argv[])
{
    int epfd, nfds;
    struct epoll_event event, events[10];
    int i;
		
    epfd = epoll_create(10);
    event.data.fd = 0; /* 监听标准输入 */
    event.events = EPOLLIN | EPOLLET; /* 读监听、边缘触发 */
    //event.events = EPOLLIN; /* 读监听、边缘触发 */
    epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
	
    while (1) 
    {
        
        nfds = epoll_wait(epfd, events, 10, -1); /* 放回就绪的描述符数量 */

        for (i = 0; i < nfds; i++) 
        {
            if (events[i].data.fd == 0) 
            {
                printf("hello world\n");
            }
        }
    }
	
	return 0;
}

此程序的运行效果,只有当往缓冲区写入数据时才会打印。(注意:由于没有将数据从缓冲区读取,所以此时缓冲区一直有数据)

【epoll】epoll的水平触发和边沿触发,及为什么边沿触发必须使用非阻塞?

如果将 event.events = EPOLLIN | EPOLLET(边沿触发) 改为 event.events = EPOLLIN(水平触发) ,则一旦输入数据,就会循环打印

这是因为输入数据后,并没有将其从缓冲区读取出来,此时epoll是水平触发,一直检测到缓冲区有数据,所以就一直循环打印

以上就是水平触发和边沿触发的区别

下面解释为什么使用边缘触发必须使用非阻塞

在设置边缘触发时,因为每次发消息只会触发一次(不管缓存区是否还留有数据),所以必须把数据一次性读取出来,否则会影响下一次消息

下面的代码实现的是监听文件描述符,每次固定读取5个字节

先看下面这段代码

int main(int argc, char *argv[])
{
    int epfd, nfds;
    int i;
    struct epoll_event event, events[10];
    char buf[5];
    int flag;
	
    epfd = epoll_create(10);
    event.data.fd = 0; /* 监听标准输入 */
    event.events = EPOLLIN | EPOLLET; /* 读监听、边缘触发 */
    //event.events = EPOLLIN; /* 读监听、边缘触发 */
    epoll_ctl(epfd, EPOLL_CTL_ADD, 0, &event);
 
#if 0
	flag = fcntl(0, F_GETFL);
	fcntl(0, F_SETFL, flag | O_NONBLOCK);
#endif	
 
    while (1)
    {
        int i;
        int num = 0;
        char c;
		
        nfds = epoll_wait(epfd, events, 5, -1); /* 返回就绪的文件描述符 */
		
        for (i = 0; i < nfds; ++i) 
        {
            if (events[i].data.fd == STDIN_FILENO) 
            {
                for(i = 0; i < 5; i++)
                {
                    buf[i] = getc(stdin);
                    if(buf[i] == -1)
                        break;
                }
 
                printf("hello world\n");
            }
            
        }
    }
	
    return 0;
}

这段代码的目的是,使用边沿触发,每次触发读取5个字节,此时getc函数是阻塞读取,这就会引起一个问题,当缓存中的数据小于5时,就会在这里阻塞等待,导致无法处理其他IO,这是非常错误的行为,在并发的服务器中,会导致服务器阻塞,无法处理其他客户端

正确的处理方法是将读取的文件描述符设置为非阻塞,循环读取,如果没有数据了就放回错误,这样就不会让服务器阻塞

可以添加下面这两行代码,将标准输入设置为非阻塞IO

flag = fcntl(0, F_GETFL);
fcntl(0, F_SETFL, flag | O_NONBLOCK);

这就解释了为什么边沿触发必须使用非阻塞的问题。

 

讨论:

Q:

你最后的结论“边缘触发必须使用非阻塞”,我的意思是“水平触发也需要使用非阻塞IO” 如果水平触发使用阻塞IO,当缓冲区数据不够read需要的字节数,同样会导致read阻塞,无法监听下一次就绪的读事件,进而形成死锁

A:

1、如果是水平触发非阻塞,然后一直调用读函数确实也会有这种问题,但如果是水平触发的话一般只会调用一次读函数,因为即使这个回合的数据没有读完,epoll依然会触发,从而再次读取,而不会响应下一个回合的数据,也保证这个回合数据的完整性。

2、如果是边缘触发,一旦触发必须得把所有的数据读出来,否则将等到下一个回合的数据到来时epoll触发才能读取,如果没有一次读完,那么这一次的数据将不完整,并且会和下一回合的数据混合在一起。

3、所以在边缘触发时一般都是while读取出所有的数据,直到read函数返回0,这就要求了边缘触发必须是非阻塞。 以上是个人观点~

 

原文:https://blog.csdn.net/weixin_42462202/article/details/86821382

相关标签: C/C++