目录
[select demo](#select demo)
[poll demo](#poll demo)
五种IO模型
阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。
阻塞IO是最常见的IO模型。

非阻塞IO:如果啮合还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才会使用。

信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。

IO多路转接:虽然从流程图上看起来和阻塞IO类似,但是实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动时告诉应用程序何时可以开始拷贝数据)。

小结:
任何IO过程中,都包含两个步骤:第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让IO更高效,最核心的办法就是让等待的时间尽量减少!


高级IO重要概念
同步通信vs异步通信
同步和异步关注的是消息通信机制。
1.所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回,但是一旦调用返回,就得到返回值了。换句话来说,就是由调用者主动等待这个调用的结果。
2.异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话来说,当一个异步过程调用发出后,调用者不会立刻得到结果,而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。
另外,我们在学习多进程多线程的时候,也有同步和互斥。这里的同步通信和进程之间的同步是完全不相干的概念。
1.进程/线程同步也是进程/线程之间直接的制约关系。
2.视为完成某种任务而建立的两个或者多个线程,这个线程需要再某些位置上协调他们的工作次序而等待、传递信息所产生的制约关系,尤其是在访问临界资源的时候。
所以以后再看到"同步"这个词的时候,一定要搞清楚这个同步,是同步通信,异步通信的同步。还是同步与互斥的同步!
阻塞vs非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
1.阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。
2.非阻塞调用是指在不能立刻得到结果之前,该调用不会阻塞当前线程。
其他高级IO
非阻塞IO,记录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),read和write函数以及存储映射I/O(mmap),这些统称为高级IO。
非阻塞IO
fcntl函数
一个文件描述符,默认都是阻塞IO。
fcntl函数原型如下:
#include <uinstd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );
传入的cmd的值不同,后面追加的参数也不相同。
fcntl函数有5种功能:
1.复制一个现有的描述符(cmd=F_DUPFD)。
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD)。
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL)。
4.获得/设置异步I/O的所有权(cmd=F_GETOWN或F_SETOWN)。
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW)。
我们此处只是用第三种功能,获取/设置文件状态标记,就可以将一个文件描述符设置为非阻塞。
实现函数SetNoBlock
基于fcntl函数,我们实现一个SetNoBlock函数,将文件描述符设置为非阻塞。
cpp
void SetNoBlock(int fd)
{
int fl=fcntl(fd,F_GETFL);
if(fl < 0)
{
perror("fcntl");
return;
}
fcntl(fd,F_SETFL,fl | O_NONBLOCK);
}
使用F_GETFL将当前的文件描述符的属性取出来(这是一个位图)。
然后再使用F_SETFL将文件描述符设置回去,设置回去的同时,加上一个O_NONBLOCK参数。
写一个非阻塞IO的demo
注意:
设置成非阻塞的时候,如果底层fd数据没有就绪,recv/read/write/send,返回值会以出错的形式返回。
可以使用errno进行区分是真的出错,还是底层数据还没有就绪!
cpp
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
void SetNonBlock(int fd)
{
// 先拿到信息
int fl = fcntl(fd, F_GETFL);
if (fl < 0)
{
perror("fcntl");
return;
}
// 重新设置进fl
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
std::cout << "set fd:" << fd << " nonblock success!" << std::endl;
}
int main()
{
char buffer[2014];
SetNonBlock(0);
sleep(1);
while (true)
{
// std::cout << "Please Enter@";
// fflush(stdout);
ssize_t n = read(0, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "echo:" << buffer << std::endl;
}
else if (0 == n)
{
// read done
std::cout << "read done!" << std::endl;
break;
}
else
{
// err 或者底层数据还未就绪!
if (errno == EWOULDBLOCK)
{
std::cout << "0 fd not ready,try again!" << std::endl;
sleep(1);
}
else
{
std::cerr << "read error,n=" << n << ",errno:" << errno << ",err string:" << strerror(errno) << std::endl;
}
}
}
return 0;
}
I/O多路转接之select
初始select
系统提供select函数来实现多路复用输入/输出模型。
1.select系统调用是用来让我们的程序监视多个文件描述符的状态变化的。
2.程序会停在select这里等待,直到被监听的文件描述符有一个或者多个发生了状态改变。
select函数原型
select函数原型如下所示:
#include <sys/select.h>
int select (int nfds, fd_set * readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval * timeout);
参数解释:
fd_set,是一张位图,用来让用户<--->内核传递fd是否就绪的信息的!
1.参数nfds是需要监视的最大文件描述符值+1;
2.rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集合以及异常文件描述符的集合。
3.参数timeout为结构体timeval,用来设置select()函数的等待时间。
参数timeout的取值:
1.NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符发生了事件。
2.0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
3.特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回。

关于fd_set结构体


其实这个结构体就是一个整数数组,更严格的来说,是一个"位图",使用位图中对应的位来表示要监视的文件描述符。
系统提供了一组操作fs_set的接口,来比较方便的操作位图。
void FD_CLR(int fd, fd_set *set); //用来清除描述词组set中相关fd的位。
int FD_ISSET(int fd, fd_set *set); //用来测试描述词组set中相关fd的位是否为真。
void FD_SET(int fd, fd_set *set); //用来设置描述词组set中相关fd的位。
void FD_ZERO(fd_set *set); //用来清除描述词组set的全部位。
关于timeval结构
timeval结构体用于描述一段时间长度,如果在这个时间内,需要监视的描述符没有事件发生则函数返回,返回值为0.

函数返回值:
1.执行成功则返回文件描述符状态已经改变的个数(返回fd就绪个数)。
2.如果返回0,代表在描述符状态改变前已超过timeout时间,没有返回(超时)。
3.当有错误发生时则返回-1,错误原因存于errno,此时参数readfds,writefds,excepfds和timeout的值变成不可预测。
错误值可能为:
1.EBADF:文件描述词为无效的或该文件已关闭。
2.EINTR:此调用被信号所中断。
3.EINVAL:参数n为赋值。
4.ENOMEM:核心内存不足。

常见的程序片段如下:
fd_set readset;
FD_SET(fd, &readset);
select(fd+1, &readset,NULL,NULL,NULL);
if(FD_ISSET(fd, readset) ){ ......}
理解select执行过程
理解select模型的关键在于理解fd_set,为说明方便,取fd_set长度为1字节,fd_set中的每一bit可以对应一个文件描述符fd。则1字节长的fd_set最大可以对应8个fd。
1.执行fd_set set;FD_ZERO(&set); 则set用位表示为0000 0000 。
2.若fd=5,执行FD_SET(fd,&set); 后set变为0001 0000(第5个位置置为1)。
3.若再加入fd=2,fd=1,则set变为0001 0011 。
4.执行select(6,&set,0,0,0);阻塞等待。
5.若fd=1,fd=2上都发生可读事件,则select返回,此时set变为 0000 0011 。注意:没有事件发生的fd=5被清空。
socket就绪条件
读就绪
1.socket内核中,接收缓冲区中的字节数,大于等于低水位标记SO_RCVLOWAT。此时可以无阻塞的读取该文件描述符,并且返回值大于0.
2.socket TCP通信中,对端关闭连接,此时对该socket读,则返回0.
3.监听的socket上有新的连接请求。
4.socket上有未处理的错误。
写就绪
1.socket内核中,发送缓冲区中可用字节数(发送缓冲区的空闲位置大小),大于等于低水位标记SO_SNDLOWAT。此时可以无阻塞的写,并且返回值大于0 。
2.socket的写操作被关闭(close或者shutdown),对一个写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
3.socket使用非阻塞connect连接成功或失败之后。
4.socket上有未读取的错误。
异常就绪(了解)
socket上收到带外数据,关于带外数据,和TCP紧急模式相关(TCP协议头中,有一个紧急指针的字段)。
socket的特点
1.可监控的文件描述符个数取决于sizeof(fd_set)的值,我这边服务器上sizeof(fd_set)=512。每bit表示一个文件描述符,则我服务器上支持的最大文件描述符是512*8=4096.
2.将fd加入select监控集的同时,还要再使用一个数据结构array保存放到select监控集中的fd。
一是用于在select返回后,array作为源数据和fd_set进行FD_ISSET判断。二是select返回后会把以前加入的但是并无事件发生的fd清空,则每次开始select前都要重新array取得fd逐一加入(FD_ZERO最先),扫描array的同时取得fd最大值maxfd,用于select的第一个参数。
注意:fd_set的大小可以调整,可能涉及到重新编译内核。
select的缺点
1.每次调用select,都需要手动设置fd集合,从接口使用角度来说也非常不方便。
2.每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大。
3.同时每次调用select都需要在内核遍历传递进来所有fd,这个开销在fd很多时也很大。
4.select支持的文件描述符数量太小。

select demo
cpp
#pragma once
#include <iostream>
#include <sys/select.h>
#include "Socket.hpp"
#include "Log.hpp"
static uint16_t defaultport = 8888;
const int fd_max_num = (sizeof(fd_set) * 8);
int defaultfd = -1;
class SelectServer
{
public:
SelectServer(uint16_t port = defaultport) : _port(port)
{
for (size_t i = 0; i < fd_max_num; i++)
{
fd_array[i] = defaultfd;
}
}
void Init()
{
// 初始化
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
lg(Info, "selectserver init success!");
}
void Start()
{
fd_array[0] = _listensock.Fd();
for (;;)
{
// 在accept之前,必须得先select。
fd_set rfds;
FD_ZERO(&rfds);
// 每次都要更新一下辅助数组
int maxfd = fd_array[0];
for (size_t i = 0; i < fd_max_num; i++)
{
if (fd_array[i] == defaultfd)
continue;
FD_SET(fd_array[i], &rfds); // 设置进rfds位图里面
if (maxfd < fd_array[i])
{
maxfd = fd_array[i];
lg(Info, "update maxfd success! maxfd:%d", maxfd);
}
}
struct timeval timeout = {0, 0}; // 设置为{0,0}表示
// timeout为NULL时,表示阻塞式等待
int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
switch (n)
{
case 0:
std::cout << "timeout ,time:" << timeout.tv_sec << ":" << timeout.tv_usec << std::endl;
break;
case -1:
std::cout << "select error!" << std::endl;
break;
default:
std::cout << "get a new link!" << std::endl;
Dispatcher(rfds);
break;
}
}
}
void Accepter()
{
// 我们的连接事件就绪了
std::string clientip;
uint16_t clientport;
int sockfd = _listensock.Accept(clientip, clientport);
if (sockfd < 0)
return;
lg(Info, "accept success! %s:%d", clientip.c_str(), clientport);
// sock --->fd_array[]
int pos = 1; // 数组第一位放的是listensockfd。
for (; pos < fd_max_num; pos++)
{
if (fd_array[pos] != defaultfd)
continue;
else
break;
}
if (pos == fd_max_num)
{
lg(Warning, "select array is full,close %d now!", sockfd);
close(sockfd);
}
else
{
fd_array[pos] = sockfd;
PrintFd();
}
}
void Recver(int fd, int i)
{
char buffer[1024];
while (true)
{
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "get a message:" << buffer << std::endl;
}
else if (0 == n)
{
std::cout << "client quit,me too!" << std::endl;
close(fd);
fd_array[i] = defaultfd; // 本质就是把辅助数组中的数字去除
break;
}
else
{
std::cout << "read error!" << std::endl;
close(fd);
fd_array[i] = defaultfd; // 本质就是把辅助数组中的数字去除
break;
}
}
}
void Dispatcher(fd_set &rfds)
{
for (size_t i = 0; i < fd_max_num; i++)
{
int fd = fd_array[i];
if (fd == defaultfd)
continue;
if (FD_ISSET(fd, &rfds))
{
// 在位图里面
if (fd == _listensock.Fd()) // 需要被accept
{
Accepter();
}
else // 在位图里,已经就绪好了的fd
{
Recver(fd, i);
}
}
}
}
void PrintFd()
{
std::cout << "online fd:";
for (size_t i = 0; i < fd_max_num; i++)
{
if (fd_array[i] != defaultfd)
std::cout << fd_array[i] << " ";
}
std::cout << std::endl;
}
~SelectServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
int fd_array[fd_max_num]; // 辅助数组
};
I/O多路转接之poll
poll是为了解决select的硬伤。
poll函数接口
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
// pollfd结构
struct pollfd{
int fd;
short events;
short revents;
}
参数说明
1.fds是一个poll函数监听的结构列表,每一个元素中,包含了三部分内容:文件描述符,监听的事件集合,返回的事件集合。
2.nfds表示fds数组的长度。
3.timeout表示poll函数的超时时间,单位是毫秒(ms)。
events和revents的取值:
|------------|-----------------------------------|---------|---------|
| 事件 | 描述 | 是否可作为输入 | 是否可作为输出 |
| POLLIN | 数据(包括普通数据和优先数据)可读 | 是 | 是 |
| POLLRDNORM | 普通数据可读 | 是 | 是 |
| POLLRDBAND | 优先级带数据可读(Linux不支持) | 是 | 是 |
| POLLPRI | 高优先级数据可读,比如TCP带外数据 | 是 | 是 |
| POLLOUT | 数据(包括普通数据和优先数据)可写 | 是 | 是 |
| POLLWRNORM | 普通数据可写 | 是 | 是 |
| POLLWRBAND | 优先级带数据可写 | 是 | 是 |
| POLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作,它由GNU引入 | 是 | 是 |
| POLLERR | 错误 | 否 | 是 |
| POLLHUP | 挂起。比如管道的写端被关闭后,读端描述符上会收到POLLHUP事件 | 否 | 是 |
| POLLNAVL | 文件描述符没有打开 | 否 | 是 |
返回结果(与select返回值相同)
1.返回值小于0,表示出错。
2.返回值等于0.表示poll函数等待超时。
3.返回值大于0,表示poll由于监听的文件描述符就绪而返回。

socket就绪条件
同select一样。
poll的优点
1.使用更方便的事件管理。poll使用pollfd结构体数组来管理要监听的文件描述符和事件。这种方式比select的"参数-值"分离传递更直观,无需在每次调用前后重置事件集合。
2.没有硬性的文件描述符数量限制。select通常有文件描述符数量的上限,受限于FD_SETSIZE。poll使用数组存储pollfd(但是数量过大后性能也是会下降)。
poll的缺点
poll中监听的文件描述符数量增多时
1.和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
2.每次调用poll都需要大量的pollfd结构从用户态拷贝到内核中。
3.同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监听的描述符数量的增长,其效率也会线性下降。
poll demo
cpp
#pragma once
#include <iostream>
#include <poll.h>
#include "Socket.hpp"
#include "Log.hpp"
static uint16_t defaultport = 8888;
const int fd_max_num = 64;
const int defaultfd = -1;
int non_event = 0;
class PollServer
{
public:
PollServer(uint16_t port = defaultport)
: _port(port)
{
for (size_t i = 0; i < fd_max_num; i++)
{
// 初始化
_event_fds[i].fd = defaultfd;
_event_fds[i].events = non_event;
_event_fds[i].revents = non_event;
}
}
void Init()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
// lg(Info, "PollServer init success!");
}
void Start()
{
_event_fds[0].fd = _listensock.Fd();
_event_fds[0].events = POLLIN;
int timeout = 3000; // 3s
for (;;)
{
int n = poll(_event_fds, fd_max_num, timeout);
switch (n)
{
case 0:
lg(Warning, "timeout !");
break;
case -1:
lg(Fatal, "poll error!");
break;
default:
lg(Info, "get a new link~");
Dispacher();
break;
}
}
}
void Dispacher()
{
for (size_t i = 0; i < fd_max_num; i++)
{
int fd = _event_fds[i].fd;
if (fd == defaultfd)
continue;
if (_event_fds[i].revents & POLLIN)
{
if (fd == _listensock.Fd())
{
Accepter();
}
else
{
Recver(fd, i);
}
}
}
}
void Accepter()
{
std::string clientip;
uint16_t clientport;
int sockfd = _listensock.Accept(clientip, clientport);
if (sockfd < 0)
return;
lg(Info, "accept success,%s:%d", clientip.c_str(), clientport);
int pos = 1;
for (; pos < fd_max_num; pos++)
{
if (_event_fds[pos].fd != defaultfd)
continue;
else
break;
}
if (pos == fd_max_num)
{
lg(Warning, "poll server is full,close %d now!", sockfd);
close(sockfd);
}
else
{
_event_fds[pos].fd = sockfd;
_event_fds[pos].events = POLLIN;
_event_fds[pos].revents = non_event;
PrintFd();
}
}
void Recver(int fd, int i)
{
char buffer[1024];
ssize_t n = read(fd, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "get a message:" << buffer << std::endl;
}
else if (0 == n)
{
std::cout << "client quit,me too!sockfd:" << fd << std::endl;
close(fd);
_event_fds[i].fd = defaultfd;
}
else
{
std::cerr << "read error!" << std::endl;
close(fd);
_event_fds[i].fd = defaultfd;
}
}
void PrintFd()
{
std::cout << "online fd:";
for (size_t i = 0; i < fd_max_num; i++)
{
if (_event_fds[i].fd == defaultfd)
continue;
std::cout << _event_fds[i].fd << " ";
}
std::cout << std::endl;
}
~PollServer()
{
_listensock.Close();
}
private:
Sock _listensock;
uint16_t _port;
struct pollfd _event_fds[fd_max_num];
};
poll虽然解决了select的fd_set上限问题和每次都要对关心的fd进行事件重置的问题,但是poll还是有效率问题!
I/O多路转接之epoll
epoll初始
按照man手册的说法:是为处理大批量句柄而作了改进的poll。
epoll的相关系统调用
epoll有3个相关的系统调用。
epoll_create
int epoll_create(int size);
创建一个epoll的句柄。
自从Linux2.6.8之后,size参数是被忽略的。
用完之后,必须调用close()关闭。
epoll_ctl
int epoll_ctl(int epfd, int op, int fd , struct epoll_event * event);
epoll的事件注册函数
1.它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
2.第一个参数是epoll_create()的返回值(epoll的句柄)。
3.第二个参数表示动作,用三个宏来表示。
4.第三个参数是需要监听的fd。
5.第四个参数是告诉内核需要监听什么事。
第二个参数的取值:
1.EPOLL_CTL_ADD:注册新的fd到epfd中。
2.EPOLL_CTL_MOD:修改已经注册的fd的监听事件。
3.EPOLL_CTL_DEL:从epfd中删除一个fd。
struct epoll_event结构如下:

events可以是以下几个宏的集合:
1.EPOLLIN:表示对应的文件描述符可以读(包括对端SOCKET正常关闭)。
2.EPOLLOUT:表示对应的文件描述符可以写。
3.EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来)。
4.EPOLLERR:表示对应的文件描述符发生错误。
5.EPOLLHUP:表示对应的文件描述符被挂断。
6.EPOLLET:将EPOLL设置为边缘触发(Edge Triggered)模式。这是相对于水平触发(Level Triggered)来说的。
7.EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
epoll_wait
int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);
收集在epoll监控的事件中已经发送的事件。
1.参数events是分配好的epoll_event结构体数组。
2.epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。
3.maxevents告知内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size。
4.参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。
5.如果函数调用成功,返回对应I/O上已经准备好的文件描述符数目,如果返回0,表示已经超时。返回小于0,表示函数失败。

epoll工作原理

当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。
struct eventpoll{....
//红黑树的根结点,这棵树中存储着所有添加到epoll中的需要监控的事件。
struct rb_root rbr;
//双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件。
struct list_head rdlist;
......
}
1.每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。2.这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。
3.而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。
4.这个回调方法在内核中叫做ep_poll_callback,它会将发生的事件添加到rdlist双链表中。
5.在epoll中,对于每一个事件都会建立一个epitem结构体。
struct epitem{struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd;//事件句柄信息
struct eventpoll *ep;//指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
1.当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。2.如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户,这个操作的事件复杂度是O(1)。
总结一下,epoll的使用过程就是三步走:
1.调用epoll_create创建一个epoll句柄。
2.调用epoll_ctl,将要监控的文件描述符进行注册。
3.调用epoll_wait,等待文件描述符就绪。
epoll的优点(和select的缺点对应)
1.接口使用方便:虽然拆分成了三个函数,但是反而使用起来更加方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离开。
2.数据拷贝量:只在合适的时候调用EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁(而select/poll 都是每次循环都要进行拷贝!)
3.事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作的时间复杂度为O(1)。即使文件描述符数目很多,效率也不会受到影响。
4.没有数量限制,文件描述符数目无上限。
注意!
网上有些博客说,epoll使用了内存映射机制,这种说法是不准确的。我们定义的struct epoll_event是我们在用户空间中分配好的内存,势必还是需要将内核的数据拷贝到这个用户空间的内存中的。
内存映射机制:内核直接将就绪队列通过mmap的方式映射到用户态,避免了拷贝内存这样的额外性能开销。
epoll工作方式
epoll有两种工作方式:水平触发(LT)和边缘触发(ET)。
例如:
1.我们已经把一个tcp socket添加到了epoll描述符
2.这个时候socket的另一端被写入了2kb的数据。
3.调用epoll_wait,并且它会返回,说明它已经准备好读取操作。
4.然后调用read,只读取了1kb的数据。
5.继续调用epoll_wait....
水平触发Level Triggered工作模式
epoll默认状态下就是LT工作模式。
1.当epoll检测到socket上事件就绪的时候,可以不立刻进行处理,或者只处理一部分。2.如上面的例子,由于只读了1kb数据,缓冲区中还剩1kb数据,在第二次调用epoll_wait时,epoll_wait仍然会立刻返回并通知socket读事件就绪。
3.直到缓冲区上所有的数据都被处理完,epoll_wait才不会立刻返回。
4.支持阻塞读写和非阻塞读写。
边缘触发Edge Triggered工作模式
如果我们在第1步将socket添加到epoll描述符的时候使用了EPOLLET标志,epoll进入ET工作模式。
1.当epoll检测到socket上事件就绪时,必须立刻处理。2.如上面的例子,虽然只读了1kb的数据,缓冲区还剩下1k的数据,在第二次调用epoll_wait的时候,epoll_wait不会再返回了。
3.也就是说,ET模式下,文件描述符上的事件就绪后,只有一次处理机会。
4.ET的性能比LT性能更高(epoll_wait返回的次数少了很多)。Nginx默认采用ET模式使用epoll。
5.只支持非阻塞的读写。
select和poll其实也是工作在LT模式下,epoll既可以支持LT,也可以支持ET。
对比LT和ET
LT是epoll的默认行为。使用ET能够减少epoll触发的次数,但是代价就是强逼着程序员一次相应就绪过冲中就把所有的数据都处理完。
相当于一个文件描述符就绪之后,不会反复被提示就绪,看起来就比LT更高效一些,但是在LT情况下如果也能做到每次就绪的文件描述符都立刻处理,不让这个就绪被重复提示的话,其实性能也是一样的。
另一方面,ET的代码复杂程度更高了...
理解ET模式和非阻塞文件描述符
使用ET模式的epoll,需要将文件描述符设置为非阻塞,这个不是接口上面的要求,而是"工程实践"上的要求。
假设这样的场景:服务器接受到一个10k的请求,会向客户端返回一个应答数据。如果客户端收不到应答,不会发送第二个10k请求。

如果服务端写的代码是阻塞式的read,并且一次只read 1k数据的话(read不能保证一次就能把所有的数据都读出来,参考man手册的说明,可能被信号打断),剩下9k的数据就会待在缓冲区中。

此时由于epoll是ET模式,并不会认为文件描述符读就绪。epoll_wait就不会再次返回,剩下9k数据会一直在缓冲区中,直到下一次客户端再给服务器写数据。epoll_wait才能返回。
但是问题来了:
1.服务器只读到了1k的数据,要10k读完才会给客户端返回响应的数据。
2.客户端要读到服务器的响应。
3.客户端发送了下一个请求,epoll_wait才会返回,才能去读缓冲区中剩余的数据。

所以,为了解决上述问题(阻塞read不一定能一下把完整的请求读完),于是就可以使用非阻塞轮询的方式来读缓冲区,保证一定能把完整的请求都读出来。
而如果是LT没这个问题,只要缓冲区中的数据没读完,就能够让epoll_wait返回文件描述符读就绪。
epoll的使用场景
epoll的高性能,是有一定的特定的场景的。如果场景选择的不适宜,epoll的性能可能适得其反。
对于多连接,且多连接中只有一部分连接比较活跃时,比较适合epoll。
例如:典型的一个需要处理上万个客户端的服务器,例如各种互联网APP的入口服务器,这样的服务器就很适合epoll。如果只是系统内部,服务器和服务器之间进行通信,只有少数几个连接,这种情况下epoll就并不合适,具体要根据需求和场景特点来决定使用哪种IO模型。