[IO复用]EPOLL 如何实现ET(边缘触发)

我们在做epoll网络编程的时候,可以选择LT(水平触发)或者ET(边缘触发)。
epoll默认就是水平触发,水平触发也不需要什么特别的设置。所以这里主要研究一下边缘触发怎么弄。

边缘触发(edge-triggered)

什么是边缘触发

关于定义的解释,网上有很多。
我的理解就是,有事件的时候,比如可读了,epoll_wait会触发一次,即使读了一次以后,缓冲区中还有数据,也不会再次触发

问题:

  1. 如何实现边缘触发
  2. 边缘触发如何使用recv & send

如何实现边缘触发

初始化函数,和LT模式没有区别。

//Description:    Ubuntu 16.04.6 LTS
//Release:        16.04
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <netinet/in.h>
#include <errno.h>
#include <arpa/inet.h>
#include <string.h>
#include <fcntl.h> //fctnl()
#define BUFFER_LENGTH 128
#define ARRAY_LENGTH 1024
typedef struct connections
{
    int     fd;    
    char    rbuffer[BUFFER_LENGTH];
    int     rbuff_index;
    //char wbuffer[128];
} connections_t;

int InitServer(int* listenfd, int* epfd)
{
    *listenfd = socket(AF_INET,SOCK_STREAM,0);
    if (-1 == *listenfd) {
        perror("socket");
        return -1;
    }
    struct sockaddr_in svraddr;
    svraddr.sin_family = AF_INET;
    svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    svraddr.sin_port = htons(2048);
    socklen_t len = sizeof(svraddr);
    if(-1 == bind(*listenfd,(struct sockaddr*)&svraddr,len)) {
        perror("bind");
        return -1;
    }
    if(-1 == listen(*listenfd,10)) {
        perror("listen");
        return -1;
    }
    *epfd = epoll_create(1);
    if(-1 == *epfd) {
        perror("epoll_create");
        return -1;
    }
    return 0;
}
int InitConnListItem(int fd, connections_t *connlist)
{
    if(fd < ARRAY_LENGTH) {
        connlist[fd].fd = fd;
        connlist[fd].rbuff_index = 0;
        memset(connlist[fd].rbuffer, 0x00 ,ARRAY_LENGTH);
    } else {
        printf("error:fd out of range.");
        return -1;
    }
    return 0;
}

使用fcntl把fd设置为非阻塞的函数

int SetNonBlockFD(int fd) 
{
    int oldflag = fcntl(fd,F_GETFL);
    int newflag = fcntl(fd,F_SETFL, oldflag | O_NONBLOCK);
    if(newflag == -1)
        return -1;
    return oldflag;
}

main函数
这里只写了个recv。利用recv的调用次数来看是否实现了ET。

int main()
{
    int listenfd, epfd;
    if(InitServer(&listenfd, &epfd) < 0)
        return -1;

    connections_t connlist[ARRAY_LENGTH] = { 0x00 };
    struct epoll_event events[ARRAY_LENGTH] = { 0x00 };

    struct epoll_event ev;
    ev.data.fd = listenfd;
    ev.events = EPOLLIN;
    ev.events |= EPOLLET;  
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);

    int oldflag = SetNonBlockFD(listenfd);
    if(oldflag < 0)
        return -1;

    if(InitConnListItem(listenfd,connlist) < 0) 
        return -1;
    
    while(1) {
       int nready = epoll_wait(epfd,events,ARRAY_LENGTH,-1);
       if(nready > 0)
       {
        int i = 0;
         for(i = 0; i < nready; i++) {
            int eventfd = events[i].data.fd;
            if(events[i].events & EPOLLIN) {
                if(eventfd == listenfd) {
                    struct sockaddr acceptaddr;
                    socklen_t len = sizeof(acceptaddr);
                    int acceptfd = accept(listenfd,&acceptaddr,&len);
                    if(accept < 0) {
                        perror("accept.");
                        continue;
                    }
                    printf("accept fd:%d\n",acceptfd);
                    int oldflag = SetNonBlockFD(acceptfd);
                    if (oldflag < 0)
                        return -1;

                    InitConnListItem(acceptfd,connlist);
                    
                     ev.data.fd = acceptfd;
                     ev.events = EPOLLIN;
                     ev.events |= EPOLLET;
                     epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &ev);
                }   
                else {
                    #define READ_LENGTH 1
                    int count = recv(eventfd, connlist[eventfd].rbuffer, READ_LENGTH, 0);
                    printf("recv, fd:%d,count:%d,msg:%s\n",eventfd, count, connlist[eventfd].rbuffer);
                }
            }
            if(events[i].events & EPOLLOUT) {
                printf("EPOLLOUT\n");
            }
         }
       }
    }

    return 0;    
}

测试:
使用net assistant来模拟客户端,向server发送一个长36Byte的字符串。
根据mainloop中recv处的限定,recv一次只会读1个字节。
如果实现了ET,recv在一定时间内只会调用一次,读一个字节。
如果实现的还是LT,recv会连续调用36次,直到把缓冲区读完。
在这里插入图片描述
可以看到,只recv了一次,读了一个"0",所以实现了ET。

总结&注意

实现ET的代码时的总结:

  1. 把listenfd的 ev.events |= EPOLLET; int oldflag = SetNonBlockFD(listenfd);注释掉,
    还是可以实现recv的边缘触发,所以listenfd的设置,对于acceptfd的边缘触发不是必要条件。
  2. 在mainloop中新增以下代码:
			for(i = 0; i < nready; i++) {
            int eventfd = events[i].data.fd;
            if(events[i].events & EPOLLET) 				//新增
                printf("fd:%d is EPOLLET\n",eventfd); 	//新增
            else 										//新增
                printf("fd:%d is LT\n",eventfd); 		//新增

虽然把listenfd和acceptfd的ev.events 都设置为了|=EPOLLET,但是以上新增的代码,输出一直都是“is LT”,
所以没法用 if(events[i].events & EPOLLET) 这样的方式判断fd是否是ET。
在这里插入图片描述
↑此时已经执行了步骤1,fd 3 (listenfd) 是LT可以理解,fd 5(acceptfd)还是 LT就无法理解了。
这个原因我没搜到。在这里先记录一下,看后续有没有机会调查。
3. if(events[i].events & EPOLLIN) { }的部分,InitConnListItem(acceptfd, connlist); epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &ev); int oldflag = SetNonBlockFD(acceptfd);的执行顺序对ET的实现没有影响。之前我在一份LT代码的基础上改ET,一直不成功,我以为是这三个函数的顺序问题。重写代码后,成功实现ET,调换了这三个的顺序,发现对ET没有影响。
4. 把acceptld的 ev.events |= EPOLLET;注释掉,recv会恢复成水平触发(此时仍是非阻塞的)。

边缘触发如何使用recv & send

recv

使用边缘触发的时候,伴随的是fd的非阻塞。需要一次性读完所有缓冲区内的数据。
需要在一个非阻塞的recv循环中读完:

						while (1)
                        {
                            int count = recv(eventfd, connlist[eventfd].rbuffer, READ_LENGTH, 0);
                            if (count <= 0)
                            {
                                if(count  == 0) {
                                    perror("et recv return 0:");
                                    epoll_ctl(epfd,EPOLL_CTL_DEL,eventfd,NULL);
                                    close(eventfd);
                                    break;
                                }
                                if(count == -1) {
                                    perror("et recv return -1:");
                                    if(errno == EAGAIN) {
                                        printf("errno is EAGAIN,read end.\n");
                                    }
                                    else if(errno == EINTR) {
                                        printf("errno is EINTR,read end.\n");
                                    }
                                    else {
                                        printf("errno is not EAGAIN,read error.\n");
                                        epoll_ctl(epfd,EPOLL_CTL_DEL,eventfd,NULL);
                                        close(eventfd);
                                    }
                                    break;
                                }
                            }
                            printf("recv, fd:%d,count:%d,msg:%s,time:%s\n", eventfd, count, connlist[eventfd].rbuffer, __TIME__);
                        }
recv的返回值
  • 非阻塞情况下:
    recv return -1 且 errno == EINTR || EAGAIN (EWOULDBLOCK):读完成。(perror:Resource temporarily unavailable)
    recv return -1 且 errno != EINTR || EAGAIN (EWOULDBLOCK):出现错误。
    recv return 0 :客户端已断开连接。
  • 阻塞情况下:
    除了EAGAIN这个errno,别的相同。
    返回值参考的文章:linux socket编程中的recv和send的返回值介绍及其含义。
send

在上面的代码中,留了一个EPOLLOUT的响应代码:

 if(events[i].events & EPOLLOUT) {
                printf("EPOLLOUT\n");
            }

根据recv的经验,如果是LT模式,当可写时,就会一直触发EPOLLOUT ,当ET时,就只会触发一次EPOLLOUT。
把accepted的events设置为EPOLLOUT:

ev.data.fd = acceptfd;
events = EPOLLOUT;

测试一下LT模式下的EPOLLOUT执行情况:
不停地触发EPOLLOUT。
在这里插入图片描述
测试一下ET模式下的EPOLLOUT执行情况:
只触发一次EPOLLOUT。
在这里插入图片描述

水平触发(level-trggered)

epoll默认就是是LT的。
当触发条件满足时,会一直触发。
比如读缓冲区中有数据,recv一次没有读完,那缓冲区中还剩余数据,就会再次触发EPOLLIN,可以继续recv。

写一段代码测试一下,代码在 [水平触发完整代码],这里只说一下结果。
用net assistant作为客户端,客户端向服务器发送一条长度为26个字符的消息。
“0123456789abcdefghijklmnopqrstuvwxyz”
每次epoll_wait()前会打印“epoll_wait”。
每次recv后,会打印收到的长度,和完整的fd rbuffer的长度。限定每次最多读10个字符。recv后会把epoll_event改写成EPOLLOUT。
每次send后,会打印send的长度。send完成后会把epoll_event改写成EPOLLIN。
结果如下:
在这里插入图片描述
实际上我只在net assistant进行了一次消息发送。
可以看到自动触发了4次recv(),读完了所有发过去的字符。
而且在recv中,我插入了EPOLLOUT事件,在recv和send来回切换时,还是可以正确读到度缓冲区中的字符的。

LT必须经过wpoll_wati才能再次触发recv。

水平触发代码

这里错误处理弄的不好,后面需要专门总结一下返回值的处理。
这段代码不看也罢。

//Description:    Ubuntu 16.04.6 LTS
//Release:        16.04
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <netinet/in.h>
#include <errno.h>
#include <arpa/inet.h>
#include <string.h>

#define BUFFER_LENGTH 128
typedef struct connections
{
    int     fd;    
    char    rbuffer[BUFFER_LENGTH];
    int     rbuff_index;
    //char wbuffer[128];
} connections_t;

int main()
{
    printf("epoll_event size=%ld\n",sizeof(struct epoll_event));
    int listenfd = socket(AF_INET,SOCK_STREAM,0);
    if (-1 == listenfd) {
        perror("socket");
        return -1;
    }
    struct sockaddr_in svraddr;
    svraddr.sin_family = AF_INET;
    svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    svraddr.sin_port = htons(2048);
    socklen_t len = sizeof(svraddr);
    if(-1 == bind(listenfd,(struct sockaddr*)&svraddr,len)) {
        perror("bind");
        return -1;
    }
    if(-1 == listen(listenfd,10)) {
        perror("listen");
        return -1;
    }
    int epfd = epoll_create(1);
    if(-1 == epfd) {
        perror("epoll_create");
        return -1;
    }
    connections_t connlist[1024] = { 0x00 };

    struct epoll_event events[1024] = { 0x00 };
    struct epoll_event ev;
    ev.events = EPOLLIN;
    ev.data.fd = listenfd;
    epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
    connlist[listenfd].fd = listenfd;
    short iserror = 0;
    while(1) {
        printf("epoll_wait\n");
        int nready = epoll_wait(epfd, events, 1024, -1);       
        for(int i = 0; i < nready; ++i) {
            int eventfd = events[i].data.fd;
            if (events[i].events & EPOLLIN)
            {
                
                if (eventfd == listenfd) {
                    struct sockaddr cliaddr;
                    socklen_t len = sizeof(cliaddr);
                    int acceptfd = accept(eventfd,(struct sockaddr*)&cliaddr,&len);
                    printf("accept,fd:%d\n",acceptfd);
                    
                    ev.events = EPOLLIN;
                    ev.data.fd = acceptfd;
                    epoll_ctl(epfd,EPOLL_CTL_ADD,acceptfd,&ev);

                    connlist[acceptfd].fd = acceptfd;
                    connlist[acceptfd].rbuff_index = 0;
                    memset(connlist[acceptfd].rbuffer,0x00,BUFFER_LENGTH);
                   
                }
                else {
                    char *buff = connlist[eventfd].rbuffer;
                    int *index = &connlist[eventfd].rbuff_index;
                    int recv_length = 10;
                    int msg_count = recv(eventfd, buff + (*index), recv_length, 0);
                    if(msg_count == 0) {
                        perror("recv");
                        printf("disconnect %d\n",eventfd);
                        epoll_ctl(epfd, EPOLL_CTL_DEL, eventfd, NULL);
                        close(eventfd);
                        continue;
                    }
                    if(msg_count == -1) {
                        perror("recv");
                         printf("recv error fd:%d\n",eventfd);
                        iserror = 1;
                        break;
                    }
                    (*index) += msg_count;
                    printf("recv,count:%d,rbuffer:%s\n", msg_count, buff);

                    ev.events = EPOLLOUT;
                    ev.data.fd = eventfd;
                    epoll_ctl(epfd,EPOLL_CTL_MOD,eventfd,&ev);
                }
            }
            if(events[i].events & EPOLLOUT) {
                char *send_msg = connlist[eventfd].rbuffer ;
                int send_msg_length = connlist[eventfd].rbuff_index;
                usleep(100000);
                int msg_count = send(eventfd,send_msg, send_msg_length, 0);
                printf("send,count:%d,msg=%s\n", msg_count, send_msg);

                ev.events = EPOLLIN;
                ev.data.fd = eventfd;            
                epoll_ctl(epfd, EPOLL_CTL_MOD, eventfd, &ev);
            }            
        }
        if(iserror == 1)
             break;
    }
    close(epfd);
    close(listenfd);
    return 0;    
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值