epoll用例详解

在socket网络编程为了避免“Broke Pipe”,所以我们第一件事就应该处理SIG_PIPE避免程序退出

broken pipe经常发生socket关闭之后(或者其他的描述符关闭之后)的write操作中,此时进程会收到SIGPIPE信号,默认动作是进程终止

signal(SIGPIPE,SIG_IGN)

谈及EPOLL首先必定涉及LTET的工作模式。在实际处理过程中,ET得效率高于LT,但是选择符合自己的才是最好的。下面给出两种的工作处理模式。

LT

LT:水平触发:
对于EPOLLIN:只要数据可读则会一直触发EPOLLIN直至用户将所有数据读取完毕。例如socket收到:ABCDEF,此时用户一次读取两个字符,则系统会通知用户3次,用于依次读取:AB->CD->EF,直至读缓冲区为空。
对于EPOLLOUT:只要socke可写,则会一直触发EPOLLOUT事件

读取数据处理逻辑:

if(events[i].events & EPOLLIN)
{
	int count = read(events[i].data.fd,szBuffer,MAX_BUF);
	//下面进行组包拆包即可
}

ET

ET:边沿触发,比LT效率高。
对于EPOLLIN:只有socket上的数据从无到有,EPOLLIN 才会触发,此时用户需要一次性将数据读取完毕,否则将导致数据延后甚至阻塞。例如socket收到:ABCDEF,此时用户一次只读取2个字符,那么当用户读取AB之后,系统并不会再次通知用户去读取CDEF,此时数据CDEF只能等待下次数据再次可读之后才能被取出。此时就会导致数据延迟并且导致缓冲区数据阻塞。
对于EPOLLOUT:只有在socket写缓冲区从不可写变为可写,EPOLLOUT 才会触发(刚刚添加事件完成调用epoll_wait时或者缓冲区从满到不满)

读取数据处理逻辑:一次性读取所有数据

if(events[i].events & EPOLLIN)
{
	//循环读取数据,直至所有数据读取完毕
	while(true)
	{
		int count = read(events[i].data.fd,szBuffer,MAX_BUF);
		if(count  == -1)
		{
			if(errno == EINTER )
			{
				continue;
			}
			else if(errno == EWOULDBLOCK)
			{
				break;
			}
			//读数据出错,进行错误处理
			break;
		}
		//套接字关闭了
		if(count == 0{
			break;
		}
		
		//数据读取完毕
		if(count < MAX_BUF)
		{
			break;
		}
	}
	
	//下面进行组包拆包即可
}

EPOLL事件类型

事件描述
EPOLLIN数据可读时触发(包括普通数据和优先数据)
EPOLLOUT数据可写时触发
EPOLLPRI优先数据可读,eg:tcp的带外数据
EPOLLRDHUPtcp链接被对方关闭,或者关闭了些操作。由GNU引入
EPOLLERR错误引起
EPOLLHUP挂起,比如管道的写端被关闭后
EPOLLNVAL文件描述符没打开
EPOLLET边缘触发模式

这些事件中我们往往只需要关注EPOLLIN 进行读取数据即可。至于EPOLLOUT 往往不会被设置。如果在LT模式设置了EPOLLOUT 该事件,只要缓冲区可写则会一直触发,容易导致cpu浪费。但是如果用户自己维护了数据发送缓冲区,则可以在该事件中进行数据发送,当数据发送完毕之后应该去掉该标记,避免cpu浪费,例如:解决short write问题

EPOLLONESHOT事件

对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或异常事件,且只能触发一次。当事件(读,写,error)处理完成之后应该重置该事件。否则系统会认为上一次事件(读,写,error)没完成,后面的事件(读,写,error)将永远不会触发

如果我们在多线程开发中,建议设置该事件,否则可能导致很多意想不到的错误。例如在多线程中我们处理accpet操作。

if (events[i].data.fd == listen_sock)
{
	int client_sock = accept(listen_sock,(struct sockaddr *)&client_addr,&addr_len);
                
   if(client_sock == -1)
   {
   		printf("[%d]accept error:%s\r\n",thread_id, strerror(errno)); 
        continue;
   }
   intf("[%d]socket[%d] connected\r\n",thread_id,client_sock); 
}

此时可能多个线程都会触发这段代码,但是只有一个线程能执行成功,其他的将报告Resource temporarily unavailable。虽然我们可以忽略这段代码,但是并不是我们期望的。我们期望的是只触发一次。同样的对于读事件,虽然读可以保证read的原子性,但是多线程读取的数据顺序我们没办法保证。例如:客户端发送数据ABCDEF,服务端3个线程进行读取数据,每个线程每次读取两个字符。此时结果可能是,线程A:AB,线程B:EF,线程C:CD,此时数据顺序混乱了,导致数据错误。所以如果在多线程中操作socket的时候我们一定要设置EPOLLONESHOT并在事件操作完之后重置。重置的时候尽可能的在处理事件的线程中处理,避免在其他线程中重置,否则会导致线程不安全,如果一定要在其他线程中重置,则需要开发者自行保证事件对应的线程安全性

void resetOneshotFlag(int epollfd,int fd)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN | EPOLLONESHOT;
    if(-1 == epoll_ctl(epollfd, EPOLL_CTL_MOD,fd,&event))
    {
        printf("modifyfd error:%s\r\n", strerror(errno)); 
    }
}

多线程版本Epoll demo

  1. 处理SIGPIPE信号,并将socket设置为非阻塞模式
  2. 将listen socket 设置EPOLLONESHOT并加入epoll
  3. 多线程epoll_wait,等到事件处理完成之后重置EPOLLONESHOT。
#include<stdio.h>
#include<iostream>
#include<sys/socket.h>
#include<fcntl.h>
#include<unistd.h>
#include<sys/epoll.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<errno.h>
#include<string.h>
#include<mutex>
#include<thread>
#include <sys/syscall.h>
#include <signal.h>

void modifyfd(int epollfd,int fd,bool oneshot,bool notify_write);

const int SEND_BUF_MAX = 10240;
void setnonblocking(int fd)
{
    int flag = fcntl(fd, F_GETFL);
    flag |= O_NONBLOCK;
    fcntl(fd, F_SETFL, flag);
}

void addfd(int epollfd,int fd,bool oneshot,bool notify_write)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if(oneshot)
    {
        event.events |= EPOLLONESHOT;
    }

    if(notify_write)
    {
        event.events |= EPOLLOUT;
    }
    if(-1 == epoll_ctl(epollfd, EPOLL_CTL_ADD,fd,&event))
    {
        printf("addfd error:%s\r\n", strerror(errno)); 
    }
}

void removefd(int epollfd,int fd)
{
    if(-1 == epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,NULL))
    {
        printf("removefd error:%s\r\n", strerror(errno)); 
    }
}

void modifyfd(int epollfd,int fd,bool oneshot,bool notify_write)
{
    struct epoll_event event;
    event.data.fd = fd;
    event.events = EPOLLIN;
    if(oneshot)
    {
        event.events |= EPOLLONESHOT;
    }

    if(notify_write)
    {
        event.events |= EPOLLOUT;
    }
    if(-1 == epoll_ctl(epollfd, EPOLL_CTL_MOD,fd,&event))
    {
        printf("modifyfd error:%s\r\n", strerror(errno)); 
    }
}

int handle_accepter_event(int thread_id,int epoll_id,int listen_sock)
{
    const int MAX_EVENTS = 10;
    struct epoll_event events[MAX_EVENTS];
    while (1)
    {
        int ret = epoll_wait(epoll_id,events,MAX_EVENTS,1000);
        if(ret == -1)
        {
            if(errno == EINTR)
            {
                continue;
            }
            printf("[%d]epoll_wait error:%s\r\n",thread_id, strerror(errno)); 
            return -1;
        }

        for (size_t i = 0; i < ret; i++)
        {
            //listen_sock
            if (events[i].data.fd == listen_sock)
            {   
                
                struct sockaddr_in client_addr = {0};
                socklen_t addr_len = sizeof(client_addr);
                int client_sock = accept(listen_sock,(struct sockaddr *)&client_addr,&addr_len);
                modifyfd(epoll_id,events[i].data.fd,true,false);
                if(client_sock == -1)
                {
                    printf("[%d]accept error:%s\r\n",thread_id, strerror(errno)); 
                    continue;
                }
                printf("[%d]socket[%d] connected\r\n",thread_id,client_sock); 
                addfd(epoll_id,client_sock,true,false);
            }
            else if(events[i].events & EPOLLIN)
            {
                char szBuffer[3] = "";
                int count = read(events[i].data.fd,szBuffer,2);
                if(count <= 0)
                {
                    printf("[%d]socket[%d] close\r\n",thread_id, events[i].data.fd);
                    removefd(epoll_id,events[i].data.fd);
                    close(events[i].data.fd);
                }
                else
                {
                    printf("[%d]socket[%d] recv data:%s\r\n",thread_id, events[i].data.fd,szBuffer);
                    modifyfd(epoll_id,events[i].data.fd,true,false);
                    write(events[i].data.fd,"hellow word",11);
                }
            }
            else if(events[i].events & EPOLLOUT)
            {
                printf("EPOLLOUT\r\n");
            }
        }
        
    }
}

void handle_signal(int signal)
{
    if(signal == SIGPIPE)
    {
        printf("recv sig pipe\r\n");
    }
}

int main()
{
    signal(SIGPIPE,handle_signal);
    int listen_sock = socket(AF_INET, SOCK_STREAM, 0);

    if(listen_sock == -1)
    {
        printf("socket error:%s\r\n", strerror(errno)); 
        return -1;
    }

    int reuse = 1;
    setsockopt(listen_sock,SOL_SOCKET,SO_REUSEADDR,&reuse,sizeof(reuse));
    struct sockaddr_in ser_addr = {0};
    ser_addr.sin_family = AF_INET;
    ser_addr.sin_port = htons(6360);
    ser_addr.sin_addr.s_addr = INADDR_ANY;
    
    if(-1 == bind(listen_sock, (struct sockaddr *)&ser_addr, sizeof(ser_addr)))
    {
        printf("bind socket error:%s\r\n", strerror(errno)); 
        return -1;
    }

    if(-1 == listen(listen_sock,5))
    {
        printf("listen socket error:%s\r\n", strerror(errno)); 
        return -1;
    }

    setnonblocking(listen_sock);

    int epoll_id = epoll_create(5);
    if(epoll_id == -1)
    {
        printf("epoll_create error:%s\r\n", strerror(errno)); 
        return -1;
    }

    addfd(epoll_id,listen_sock,true,false);

    std::thread t(handle_accepter_event,1,epoll_id,listen_sock);
    std::thread t2(handle_accepter_event,2,epoll_id,listen_sock);
    t.join();
    t2.join();
   
    
    printf("hellow word\r\n");
    return 0;
}

这个demo没有考虑short write问题,因为该代码只有监听套接字设置了非阻塞模式accept的套接字没有设置非阻塞模式,所以使用send或者write的时候不存在部分发送导致的short write模式。

缓冲区可以参考:动画图解 socket 缓冲区的那些事儿
关于epoll的多线程demo和short write的解决方案可以参考:tcp 完美解决short write问题

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值