我们先从epoll谈起
EPOLLONESHOT事件
我们使用ET模式下工作,一个socket上的某个事件还是可能被多次触发。这在并发程序中就会引起一个问题。比如一个线程(或进程)在读取完某个socket上的数据后开始处理这些数据,而在数据处理的过程中该socket上又有新数据可读(EPOLLIN再次触发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个或多个线程同时操作一个socket的局面。我们在写服务器的时候当然不想出现这种情况,我们想看到的当然是一个socket在任何时刻都只被一个线程处理。我们就会用到epoll中的EPOLLONESHOT事件。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可读、可写或者异常事件,且只触发一次,除非我们使用epoll_ctl重置该文件描述符上注册的EPOLLONESHOT事件。注册之后,当一个线程正在处理某个socket的时候,其它线程是不可能有机会操作这个socket。注册了EPOLLONESHOT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,确保当有新的事件来临时,它的EPOLLIN事件能够触发,并且可以让其它线程有机会继续处理这个socket。
注意
千万不能将服务器端的监听socket设置为EPOLLONESHOT,否则程序只能处理一个客户连接! 因为后续的客户连接请求将不再触发listenfd上的EPOLLIN事件。
头文件
#ifndef _HEAD_H
#define _HEAD_H
#include<iostream>
#include<sys/types.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<unistd.h>
#include<string.h>
#include<netdb.h>
#include<sys/epoll.h>
#include<pthread.h>
void my_err(std::string str, int line)
{
fprintf(stderr, "line: %d\n", line);
std::cerr << str << std::endl;
exit(0);
}
#endif
#include"head.h"
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 1024
struct fds
{
int epollfd;
int sockfd;
};
/*设置为非阻塞*/
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd, F_SETFL, new_option);
return old_option;
}
/*将fd上的EPOLLIN和EPOLLET事件注册到epollfd指示的epoll内核事件表中,参数oneshot指定是否注册fd上的EPOLLONESHOT事件*/
void addfd(int epollfd, int fd, bool oneshot)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
if(oneshot)
event.events |= EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
}
/*重置fd上的事件。这样操作之后,尽管fd上的EPOLLONESHOT事件被注册,但是操作系统仍然会触发fd上的EPOLLIN事件,且只触发一次*/
void reset_oneshot(int epollfd, int fd)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET | EPOLLONESHOT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
}
/*工作线程*/
void* worker(void *arg)
{
int sockfd = ( (fds*)arg )->sockfd;
int epollfd = ( (fds*)arg )->epollfd;
std::cout << "start new thread to receive data on fd :" << sockfd << "\n";
char buf[BUFFER_SIZE] = {0};
/*循环读取sockfd上的数据,直到遇到EAGAIN错误*/
while(1)
{
int ret = recv(sockfd, buf, BUFFER_SIZE-1, 0);
if(ret == 0)
{
close(sockfd);
std::cout << "foreiner closed the connection\n";
break;
}
else if(ret < 0)
{
if(errno == EAGAIN)
{
reset_oneshot(epollfd, sockfd); //处理完必须重新注册事件,不然该套接字的事件就一直得不到处理
std::cout << "read later\n";
break;
}
}
else
{
std::cout << "get content: " << buf << std::endl;
/*模拟数据处理过程*/
sleep(5);
}
}
std::cout << "end thread receving data on fd: " << sockfd << "\n";
}