感觉很多文章都是在说自己转的别人的,或者听别人说的,也一直没有说明白,所以给出实验代码,也想讲明白。
epoll底层机制是红黑树,以fd为key的key-value数据结构。红黑树有俩种类型,一种可以插入多个key,一种不可以插入多个key。经过验证后,我认为epoll底层的红黑树是不可以插入多个key的。
先给出结论
LT模式下EPOLLOUT只要在写缓冲区有空间下,就会就绪
ET模式下EPOLLOUT只要由不可写变为可写,就会就绪,注意特殊的一点是EPOLLOUT在注册时的第一次会就绪。即以前不关注OUT事件,那么就可以认为其不可写,现在关注了,发现是可写,就有一次不可写变为可写脉冲。
ET模式下EPOLLIN和EPOLLOUT不可以同时注册
实验基本文件,后序均是在此基础上实验
#server.cc
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT "8080"
#define IP // 待补充
#define BUFFER_SIZE 4096 /*读缓冲区大小*/
#define my_error(x, str) \
do{\
if((x) < 0)\
{ perror(str);printf("%s\n",str); }\
}while(0)
#define ERR_EXIT(str) \
do{\
perror(str);\
exit(EXIT_FAILURE);\
}while(0)
using namespace std;
using EventVec = vector<struct epoll_event> ;
int main(int argc, char**argv)
{
// 服务器地址和端口设置
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);
// inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(atoi(PORT));
int idlefd = open("/dev/null", O_RDONLY | O_CLOEXEC);
int listenfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0); // 高内核版本直接可以使用
// CLOEXEC,当fork后,会关闭该文件描述符
my_error(listenfd, "socket error");
// 设置了端口立即重启, 即取消time_wait状态
int reuse = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
my_error(ret, "bind error");
ret = listen(listenfd, 5);
my_error(ret, "listen error");
EventVec events(16);
int epollfd = epoll_create1(EPOLL_CLOEXEC);
struct epoll_event event;
event.data.fd = listenfd;
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &event);
vector<int> clients;
int nready = 0;
int connfd = 0;
while(1)
{
nready = epoll_wait(epollfd, events.data(), events.size(), -1);
if(nready == -1)
{
if(errno == EINTR)
continue;
perror("epoll");
exit(-1);
}
else if(nready == 0) // timeout到了,但没有人就绪,目前这种情况下不可能
continue;
else if(nready > 0)
{
if(nready == static_cast<int>(events.size()))
events.resize(2*events.size());
for(int i = 0; i < nready; ++i)
{
if(events[i].data.fd == listenfd && events[i].events & EPOLLIN)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
connfd = accept4(listenfd, (struct sockaddr*)&client_address, &client_addrlength, SOCK_NONBLOCK|SOCK_CLOEXEC);
if(-1 == connfd)
{
/*if(EMFILE == errno) // 这里是用来处理EMFILE错误的,在实验内可以忽略
{
close(idlefd);
idlefd = accept(listenfd, nullptr, nullptr);
close(idlefd);
idlefd = open("/dev/null", O_RDONLY|O_CLOEXEC);
continue;
}
else{*/
ERR_EXIT("accept");
//}
}
cout << "client_address "
<< inet_ntoa(client_address.sin_addr)
<< " fd: "
<< connfd
<< " client connected" << endl;
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
clients.push_back(connfd);
}
for(auto it = clients.begin(); it != clients.end(); ++it)
{
if(events[i].data.fd == *it)
{
connfd = *it;
if(events[i].events & EPOLLIN)
cout << " event read " << endl;
if(events[i].events & EPOLLOUT)
{
cout << " event write " << endl;
}
cout << "one time" << endl;
event.data.fd = connfd;
event.events = EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &event);
char buffer[1024] = {0};
do{
ret = recv(connfd, buffer, sizeof(buffer), 0); // 将收到的字节全部读出来。
}while(ret >= 0 || errno != EAGAIN);
sleep(2);
continue;
}
}
}
}
}
close(listenfd);
return 0;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT "8080"
#define IP "172.16.79.129"
#define my_error(x, str) \
do{\
if((x) < 0)\
printf("%s\n",str);\
}while(0)
int main()
{
int fd = socket(AF_INET, SOCK_STREAM, 0);
my_error(fd, "socket error");
// 设置连接服务器地址
struct sockaddr_in address;
address.sin_family = AF_INET;
address.sin_port = htons(atoi(PORT));
inet_pton(AF_INET, IP, &address.sin_addr);
int ret = connect(fd, (struct sockaddr*) &address, sizeof(address));
my_error(ret, "connect error");
char buffer[1024] = {0};
// 客户端就是从终端收到数据,然后向服务端写,为了触发服务器端读事件
while(1)
{
bzero(buffer, sizeof(buffer));
int nread = read(STDIN_FILENO, buffer, sizeof(buffer));
send(fd, buffer, nread, 0);
}
close(fd);
}
即一个fd只能add一个事件。后面的add不会覆盖。注意我指的就是EPOLL_CTL_ADD这个操作。
我在accept后EPOLL_CTL_ADD写事件EPOLLOUT。
然后在通信逻辑中EPOLL_CTL_ADD读事件EPOLLIN。
// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
// 在后面的和connfd通信逻辑中
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD/*EPOLL_CTL_MOD */, connfd, &event);
实验结果
// 在后面的和connfd通信逻辑中
event.data.fd = connfd;
event.events = EPOLLIN | EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &event);
修改代码后,实验出现我们想要的结果。服务器端定时输出event write。客户端每向服务器端发送一次数据后。会有event read输出。
event.events = EPOLLIN | EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
其原因一定是因为这是他这是第二次对这个fd进行ADD操作。我猜测他的逻辑是这样。
读事件就绪后,进入处理逻辑,然后向关注写事件,所以用了ADD,这里应该是用MOD
网上说什么连接时会触发一次,EPOLLIN|EPOLLOUT同时注册会触发一次什么的,很乱。
EPOLLOUT触发条件只有一个,内核缓冲区可写。
// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
直接注册OUT事件,因为该fd的内核写缓冲区一直为空,所以一直输出event write
// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
// 在后面的和connfd通信逻辑中
event.data.fd = connfd;
event.events = EPOLLIN|EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_MOD, connfd, &event);
实验结果同上,网上说的EPOLLIN|EPOLLOUT同时注册的意思,并不是因为同时注册的原因。而是因为其写缓冲区本身就可写,我OUT事件为什么不触发。
// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLIN;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
// 在后面的和connfd通信逻辑中
epoll_ctl(epollfd, EPOLL_CTL_DEL, connfd, NULL);
event.data.fd = connfd;
event.events = EPOLLERR|EPOLLOUT;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
我这里先由读事件就绪,然后将关注的读事件删除,然后再将EPOLLERR|EPOLLOUT添加。依然会触发写事件,这里绕这么多弯弯的原因就是向告诉大家,OUT触发条件很简单。就是该fd的内核写缓冲区为空时,OUT事件就会就绪。
上面仅仅是一些小测试,我们实际关心的还是在ET模式下,配合nonblock r/w来编程。
这里OUT事件触发条件也很简单,就是由不可写变为可写时会触发。
// listenfd的处理逻辑中
event.data.fd = connfd;
event.events = EPOLLOUT|ET;
epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &event);
内核写缓冲区由满变为不满时,write会由不可写变为可写这个大家都很理解。主要是在一开始触发一次即所谓的在连接是触发一次的原因可以这么理解。最开始不存在fd的时候,是没有fd的内核写缓冲区这么一个概念的,所以最开始其是不可写状态。最后该fd的内核写缓冲区建立之后,变为可写状态。所以存在这么一个由不可写变为可写的状态,导致在最一开始ET模式下accept后该fd关注写事件后,从无到有这么一个脉冲导致OUT事件会就绪一次。