一个从socket中读取数据的代码:
while( (n = read(socketfd, buf, BUFSIZE) ) >0)
if( write(STDOUT_FILENO, buf, n) = n){
printf(“write error”);
exit(1);
}
当代码中的socketfd描述符所对应的套接字无数据将会阻塞,直到有数据从网络的另一端发送过来。如果它是一个服务器程序,它要读写大量的socket,那么在某一个socket上的阻塞很明显会影响与其它socket的交互过程。类似的问题不单单出现在网络上,还可以出现在读写加锁的文件和FIFO等等一系列的情况。
这个也就是网络编程中处理客户端的并发请求的问题。主要思路有:
- 非阻塞IO轮询
- 使用多线程/多进程模型
- 使用IO多路复用模型
- 使用多线程 + IO多路复用模型
I/O多路复用
一、I/O多路复用技术
I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),内核便通知程序进行相应的读写操作。监控多个文件句柄可以提高就绪状态出现的概率,就可以使CPU在大多数时间下都处于忙碌状态,大大提高CPU的性能,达到高效服务器的目的。
IO多路复用适用如下场合:
- 当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。
- 当一个客户同时处理多个套接口时,这种情况是可能的,但很少出现。
- 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。
- 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。
- 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll。但select,pselect,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的;而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
二、select、poll、epoll
1、select
基本原理:select 函数将监视的文件描述符分为3类,分别是writefds、readfds和exceptfds。调用后select函数阻塞,直到有描述符就绪(有数据可读、可写或者有except),或者超时返回。当select函数成功返回后,可以通过遍历各个fdset找到就绪的描述符,可以对这些描述符进行对应的操作。
select接口:
#include <sys/select.h>
#include <sys/time.h>
// 参数2和参数1分别确定以关心事件分类的3个监测描述符集及总个数,
// 最终成功返回的结果描述符集及总个数分别在参数2和函数返回值中。
int select(int maxfdp1, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *tvptr); // 返回值:就绪的描述符数目;超时,返回0;出错,返回-1
struct timeval{
long tv_sec; // seconds
long tv_usec; // microseconds
};
函数参数:
- int maxfdp1: 所有关心描述符的总数,即max fd plus 1(大小限制为FD_SETSIZE, Linux的FD_SETSIZE通常为1024。测试0~maxfdp1-1的描述符);
- fd_set *readfds, *writefds, exceptfds:指定内核要测试的可读、可写、异常条件的描述符集;
- 描述符集指针的一个或全部可以为空指针,表示对相应条件不关心。若3个指针全为NULL,则select提供比sleep更精确的计时器;
- fd_set数据类型指针。fd_set数据类型可执行操作: ① 分配该类型变量; ② 赋给另一个该类型变量; ③ 4个接口宏或函数;(每次使用select之前,要先将所有描述符集均清零再开启关心描述符的对应位)
void FD_ZERO (fd_set *fdset); //清楚所有位
void FD_SET (int fd, fd_set *fdset); //开启一位
void FD_CLR (int fd, fd_set *fdset); //清除一位
int FD_ISSET (int fd, fd_set *fdset);//判断一位 返回值:fd在描述符集中,返回非0;否则,返回0
- struct timeval *tvptr:最长等待时间。
- tvptr==NULL:永远等待。返回: ① 指定的描述符集有一个已准备好; ② 捕捉到一个信号中断等待,此时select返回-1,errno设置为EINTR;
- tvptr.tv->sec==0 && tvptr.tv->usec==0:完全不等待。测试所有的描述符状态,不阻塞select函数并立即返回;
- tvptr.tv->sec!=0 || tvptr.tv->usec!=0:等待指定时间。返回: ① 指定的描述符集有一个已准备好; ② 超时到期且没有一个描述符准备好,返回0; ③ 捕捉到一个信号中断等待,此时select返回-1,errno设置为EINTR。
函数返回值:
- 返回-1:出错。若指定描述符都没准备好前捕捉到一个信号中断等待,则出错。此时一个描述符集都不修改;
- 返回0:所有描述符都没有准备好并且超时返回。此时所有描述符集置0;
- 返回正值:有描述符准备好,返回已准备好描述符数之和。此时描述符集中仍打开的位对应于已准备好的描述符。(若一个描述符已准备好读和写,返回值中计两次,并且在可读、可写描述符集中对应位分别保持打开)
目前支持的异常条件:
- 某个套接口的带外数据的到达。
- 某个已置为分组方式的伪终端存在可从其主端读取的控制状态信息。
“准备好”的含义说明:
- 对读、写描述符集中的一个描述符read、write操作不会阻塞,则认为此描述符是准备好的;
- 对异常条件集中的一个描述符有一个未决异常条件,则认为此描述符已就绪;
- 对于读、写和异常条件,普通文件的文件描述符总是就绪状态;
- 对socket的可读可写,参见其他。
select实现的大致原理:
支持阻塞操作的设备驱动通常会实现一组自身的等待队列(如读/写)用于支持上层(用户层)所需的BLOCK或NONBLOCK操作。当应用程序通过设备驱动访问该设备时(默认为BLOCK操作),若该设备当前没有数据可读\写,则将该用户进程插入到该设备驱动对应的读/写等待队列让其睡眠,等到有数据可读/写时再将该进程唤醒。
select就是巧妙的利用等待队列机制让用户进程适当在没有资源可读/写时睡眠,有资源可读/写时唤醒。
- select的睡眠过程:select会循环遍历它所监测的fd_set内的所有文件描述符对应的驱动程序提供的poll函数。如果有资源可用则立即返回,如果没有任何一个资源可用(即没有一个文件可供操作),则调用进程让出CPU进入睡眠状态(此时调用select的当前进程插入到所有所检测的fd_set关联的驱动内的等待队列,因此任何一个设备有资源时都可以唤醒select),一直等到有资源可用进程被唤醒才继续往下执行。
- select的唤醒过程:睡眠时该进程阻塞在所有监测的文件对应的设备等待队列上,因此在timeout时间内,只要任意个设备变为可操作,都会立即唤醒该进程,从而继续往下执行。
- 通过将select加入到所有监测文件描述符对应的设备驱动内的等待队列,并且可以由任一驱动在有资源时唤醒。这就实现了select的”当有一个文件描述符可操作时就立即唤醒执行“的基本原理。
- poll函数首先会将调用select的用户进程插入到该设备驱动对应资源(读/写)的等待队列上(注意把进程挂到等待队列中并不代表进程已经睡眠了),然后返回一个bitmask告诉select当前哪些资源可用。
- 唤醒该进程的过程通常也是在所监测文件的设备驱动内实现的。当设备驱动发现自身资源变为可读写并且有进程睡眠在该资源的等待队列上时,就会唤醒这个资源等待队列上的进程。
fd_set结构类型:
#define __NFDBITS (8 * sizeof(unsigned long))
#define __FD_SETSIZE 1024
#define __FDSET_LONGS (__FD_SETSIZE/__NFDBITS)
typedef struct {
unsigned long fds_bits[__FDSET_LONGS];
} __kernel_fd_set;
select操作机制:
- 注册所有关心描述符到相关的集合(使用FD_SET);
- select()轮询检测是否有描述符就绪;
- 对所有的所有的注册的描述符循环检查是否是select()返回中的就绪描述符(使用FD_ISSET)。
fd_set readset, writeset;
FD_ZERO(&readset); // 每次使用select之前清空描述符集,再加入关心描述符
FD_ZERO(&writeset);
FD_SET(0,&readset);
FD_SET(1,&writeset);
FD_SET(2,&writeset);
FD_SET(3,&readset);
select(4,&readset,&writeset,NULL,NULL);
FD_ISSET(0,&readset);
FD_ISSET(1,&writeset);
FD_ISSET(2,&writeset);
FD_ISSET(3,&readset);
从上面可以看出select的不足:
- 单个进程能够监视的文件描述符的数量存在最大限制。Linux上一般为1024个。即使通过修改宏定义或者重新编译内核的方式增加监视数量,但效率会呈线性下降趋势。——poll针对此点做了部分改变;
- 对socket的监测采用轮询的方法进行线性扫描,效率较低。并且当套接字比较多时,每次select()无论socket是不是活跃都遍历FD_SETSIZE个socket来完成调度,浪费很多CPU时间。——epoll针对此点做了改进;
- 每次调用select之前都要清空并重新FD_SET,因为一次select之后内核会修改3个描述符集;
- 每次调用select,系统都要将描述符集从用户空间复制到内核空间。——epoll针对此点也做了改进;
- select成功返回后,为了检测一个就绪描述符,需要给所有的描述符执行FD_ISSET();
select的优点:
- select目前几乎在所有的平台上支持,具有良好的跨平台支持;
- 在描述符较少的情况下效率还是挺高的;同时,若处于活跃的socket占比很大,select不一定比epoll效率低;
- 能够操作纳米级别的时间(可以设置个纳秒级别的超时)。
select使用的小技巧:
- 将fd加入select监控集的同时,使用一个数据结构array保存放到select监控集中的fd,一是方便在select返回后,遍历array作为源数据和fd_set进行FD_ISSET判断;二是select返回后会把以前加入的但并无事件发生的fd清空,则每次开始 select前都要先FD_ZERO再重新从array取得fd逐一加入;三是扫描array的同时取得fd最大值并将maxfdp1用于select的第一个参数。
select实例:检测键盘有无输入,有则输出至标准输出上。
#include<stdio.h>
#include<unistd.h>
#include<sys/select.h>
int main(){
char buf[1024];
int ret_select,ret_isset;
fd_set rdfds,wrfds;
struct timeval tv;
FD_ZERO(&rdfds);
FD_SET(0, &rdfds);
FD_SET(1, &wrfds);
tv.tv_sec = 60;
tv.tv_usec = 0;
ret_select = select(1,&rdfds,&wrfds,NULL,&tv);
printf("ret_select:%d\n",ret_select);
if(ret_select < 0)
printf("select error\n");
else if(ret_select == 0)
printf ("time out\n");
else{
ret_isset=FD_ISSET(0,&rdfds);
printf("read ret_isset:%d\n",ret_isset);
ret_isset=FD_ISSET(1,&wrfds);
printf("write ret_isset:%d\n",ret_isset);
scanf("%s",buf); //终端设备行缓冲,select返回时数据已经准备好,等待被写入流缓冲区
printf("%s\n",buf);
}
return 0;
}
select模型的服务器 :
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<stdlib.h>
#include<time.h>
#include<unistd.h>
int array_fds[1024];
int startup(char* ip, short port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0)
{
perror("socket");
exit(2);
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
//bind
if(bind(sock,(struct sockaddr*)&local,sizeof(local)) < 0)
{
perror("bind");
exit(3);
}
//listen
if(listen(sock,10) < 0)
{
perror("listen");
exit(4);
}
return sock;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
printf("Usage:%s [local_ip] [local_port]\n",argv[0]);
return 1;
}
int listen_sock = startup(argv[1],atoi(argv[2]));
int maxfds = 0;//select第一个参数
fd_set rfds;
int array_size = sizeof(array_fds)/sizeof(array_fds[0]);
array_fds[0] = listen_sock;
//初始化array_fds
int i = 1;
for(;i < array_size;++i)
{
array_fds[i] = -1;
}
while(1)
{
struct timeval timeout = {0,0};//非阻塞式等待,NULL为阻塞式的等待
FD_ZERO(&rfds);
maxfds = -1;
for(i = 0; i < array_size;++i)
{
if(array_fds[i] > 0)
{
FD_SET(array_fds[i],&rfds);
if(array_fds[i] > maxfds)//设置maxfds(select第一个参数)
maxfds = array_fds[i];
}
}
switch(select(maxfds+1,&rfds,NULL,NULL,/*&timeout*/NULL ))
{
case 0:
printf("timeout....\n");
break;
case -1:
perror("select");
break;
default://success
{
int j = 0;
for(; j < array_size;++j)
{
if(array_fds[j] < 0)
continue;
if(j == 0 && FD_ISSET(array_fds[j],&rfds))
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int new_fd = accept(array_fds[j],\
(struct sockaddr*)&client,&len);
if(new_fd <0)
{
perror("accept");
continue;
}
else
{
printf("get a client:%s, %d\n",\
inet_ntoa(client.sin_addr),\
ntohs(client.sin_port));
int k = 1;
for(; k < array_size;++k)
{
if(array_fds[k] < 0 )
{
array_fds[k] = new_fd;
break;
}
}
if(k == array_size)//表示没有可用的文件接口
close(new_fd);
}
}
else if(j != 0 && FD_ISSET(array_fds[j],&rfds))
{
char buf[1240];
ssize_t s = read(array_fds[j],\
buf,sizeof(buf)-1);
if(s >0)
{
buf[s] = 0;
printf("client say#:%s\n",buf);
}
else if(s == 0)
{
printf("client quit\n");
close(array_fds[j]);
array_fds[j] = -1;//必须修改掉
}
else
{
perror("read");
close(array_fds[j]);
}
}
}
}
break;
}
}
return 0;
}
Client/Server程序:
使用select写一个TCP回射程序:客户端向服务器发送信息,服务器接收并原样发送给客户端,客户端显示出接收到的信息。
2、poll
poll()系统调用是System V的多元I/O解决方案。它有三个参数,第一个是pollfd结构的数组指针,也就是指向一组fd及其相关信息的指针,因为这个结构包含的除了fd,还有期待的事件掩码和返回的事件掩码,实质上就是将select的中的fd,传入和传出参数归到一个结构之下,也不再把fd分为三组,也不再硬性规定fd感兴趣的事件,这由调用者自己设定。这样,不使用位图来组织数据,也就不需要位图的全部遍历了。按照一般队列地遍历,每个fd做poll文件操作,检查返回的掩码是否有期待的事件,以及做是否有挂起和错误的必要性检查,如果有事件触发,就可以返回调用了。
基本原理:poll本质上和select没有区别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是poll没有最大文件描述符数量的限制。poll和select同样存在一个缺点:包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
poll接口:
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// 返回值:就绪的描述符数目;超时,返回0;出错,返回-1
struct pollfd{
int fd; // 监测的描述符
short events; // 对该描述符关心的事件(监视该文件描述符的事件掩码,由用户设置)
short revents; // 该描述符实际发生了的事件(该文件描述符的操作结果事件掩码,返回时由内核设置)
} // events中请求的任何事件都可能在revents中返回。合法的事件在下边列出。
函数参数:
- struct pollfd *fds:结构数组指针,结构包含了描述符fd,关心的事件events, 实际发生了的事件revents;
- nfds_t nfds:结构元素的个数;
- int timeout:超时时间。(-1:无限等待;+:等待指定时间;0:不等待,直接poll)
poll的事件:
- 合法事件(需要用户设置关心的事件,并且内核可以返回至revents):
- POLLIN:有数据可读。
- POLLRDNORM:有普通数据可读。
- POLLRDBAND:有优先数据可读。
- POLLPRI:有紧迫数据可读。
- POLLOUT:写数据不会导致阻塞。
- POLLWRNORM:写普通数据不会导致阻塞。
- POLLWRBAND:写优先数据不会导致阻塞。
- POLLMSGSIGPOLL :消息可用。
- revents中还可能返回如下事件(用户无需设置异常,内核仍可返回):
- POLLER:指定的文件描述符发生错误。
- POLLHUP:指定的文件描述符挂起事件。
- POLLNVAL:指定的文件描述符非法。
- POLLIN | POLLPRI 等价于select()的读事件,POLLOUT |POLLWRBAND 等价于select()的写事件。POLLIN等价于POLLRDNORM |POLLRDBAND,而POLLOUT则等价于POLLWRNORM。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events为POLLIN |POLLOUT。在poll返回时,我们可以检查revents中的标志,对应于文件描述符请求的events结构体。如果POLLIN事件被设置,则文件描述符可以被读取而不阻塞。如果POLLOUT被设置,则文件描述符可以写入而不导致阻塞。这些标志并不是互斥的:它们可能被同时设置,表示这个文件描述符的读取和写入操作都会正常返回而不阻塞。
出错的errno变量:
- EBADF:一个或多个结构体中指定的文件描述符无效。
- EFAULTfds:指针指向的地址超出进程的地址空间。
- EINTR:请求的事件就绪之前产生一个信号,调用可以重新发起。
- EINVALnfds:参数超出PLIMIT_NOFILE值。
- ENOMEM:可用内存不足,无法完成请求。
poll使用步骤:
- 设置pollfd结构
- poll轮循检测就绪套接字
- 循环所有的结构的revent检查是否发生事件
poll()和select()的区别:
- poll构造一个pollfd结构的数组,每个数组元素指定一个描述符和对其要关心的条件。结构数组的元素个数由参数nfds指定,因此poll摆脱了FD_SETSIZE大小的限制;
- 因为pollfd结构中包含revents,所以内核返回时若有就绪描述符,poll设置结构元素的revents成员,而不是select的修改参数;
- 降低了控制时间timeout的级别(只能到毫秒级);
- 对异常事件的处理:poll()和select()不一样,不需要显式地请求异常情况报告。即无需在events中设置三个异常条件也可以从revents中得到结果。(???再看下)
poll与select的相同点:
- 关键步骤仍是轮询所有的描述符找出就绪状态的描述符。而事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率都会线性下降。
- 尽管对描述符存储方式不一样,但select和poll的每次调用都需要将描述符集或结构数组复制于用户态和内核地址空间之间。
3、select和poll总结
两个I/O多路复用系统调用的关键之处:
- 如何存储描述符及所关心事件:
- select:以关心的事件分3类存储在3个对应的fd_set数据类型中,并记录总个数maxfdp1;
- poll:对每个描述符及其关心的事件单独存储为一个结构数组元素,记录数组大小nfds;
- 区别:有无个数限制;能否不修改初始输入而返回发生事件。相同:仍然都需要在用户空间和内核空间之间复制。
- 轮询机制:
- 都是对所有描述符以轮询的方法进行线性扫描。( ① 所有描述符;② 轮询)
- 超时机制:
- struct timeval *tvptr; 秒+微秒级超时;
- int timeout; 毫秒级超时。
- 返回情况:
- 函数返回值相同,但是发生事件的返回方式不同:select修改参数描述符集,poll分别设置每个发生事件描述符的pollfd结构中的revent。
4、epoll
epoll接口:epoll操作过程需要三个接口,分别如下:
#include <sys/epoll.h>
int epoll_create(int size); //成功,返回epoll句柄;出错,返回-1
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
struct epoll_event{
__uint32_t events; /* epoll事件 */
epoll_data_t data; /* 用于存储用户数据 */
};
//With every descriptor being watched, can associate an integer or a pointer as user data.
typedef union epoll_data{
void *ptr; /* 可用来指定与fd相关的用户数据 */
int fd; /* 最常用:指定事件所从属的目标文件描述符 */
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
1)epoll_create :创建和初始化内核的一个事件表并返回一个epoll句柄。
- size给内核一个提示,告诉内核这个事件表有多大。size参数不同于select()中的maxfdp1。
- 内核的事件表可以长期保存所有需要监视的fd及其事件,并且可以提供动态添加、修改和删除操作,同时,一个事件表由返回的epoll句柄唯一标识。该句柄占用一个fd值,因此在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。因此,关闭该句柄,对应的事件表才会消失。
2)epoll_ctl :给epfd句柄添加、删除或修改指定需要监听的fd及其期待的事件。
- op:指定操作类型,用三个宏来表示:
- EPOLL_CTL_ADD:注册新的fd到epfd中;
- EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
- EPOLL_CTL_DEL:从epfd中删除一个fd;
3)epoll_wait 等待所有注册事件的发生,返回需要处理的事件数目。类似于select()调用。
- events:内核将所有就绪事件从内核的事件表中拷贝到events指向的数组中;
- maxevents:指定最多监听多少个事件,这个值不能大于创建epoll_create()时的事件表大小size;
- timeout:超时时间(毫秒;0会立即返回;-1永久阻塞)。
epoll的事件:前五个和poll的事件基本相同,后两个为epoll独有。
- EPOLLIN :表示对应的文件描述符可读;
- EPOLLOUT:表示对应的文件描述符可写;
- EPOLLPRI:表示对应的文件描述符有紧急的数据可读;
- EPOLLERR:表示对应的文件描述符发生错误;
- EPOLLHUP:表示对应的文件描述符被挂断;
- EPOLLET: 将EPOLL设为边沿触发(Edge Triggered)模式,默认为水平触发(Level Triggered)。
- EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
poll和epoll的使用差别:

工作模式 :ET和LT只是epoll通知事件的不同模式,阻塞/非阻塞是fd的I/O操作方式,两者无直接关系。
LT(level trigger)模式:缺省工作方式,高效的poll,并且同时支持block和no-block。
- 当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件,下次调用epoll_wait时,内核会再次响应应用程序并通知此事件。
- 相当于电平的“水平触发”,只要就绪即触发。
- 这种模式编程出错可能性要小一点。传统的select/poll也使用这种模式。
ET(edge trigger)模式:高速工作方式,只能使用no-block。
- 当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
- 相当于电平的“边沿触发”,只在从未就绪变为就绪的“边沿”才触发。
- 这种模式下,“边沿”时内核才通过epoll告诉用户。然后它会假设你知道文件描述符已经就绪,并且以后的epoll不会再为那个文件描述符发送更多的就绪通知,除非做了某些操作导致那个文件描述符又变回未就绪状态,那么下次“边沿”时,epoll才会又通知用户。
- ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。
- epoll工作在ET模式的时候,必须使用非阻塞轮询处理直至未就绪状态,以避免读时遗漏待处理的数据或写时不能发送要发送的数据(因为epoll不会通知可读/写,程序逻辑就不会读/写)。
ET还是LT:
LT的处理过程:
. accept一个连接,添加到epoll中监听EPOLLIN事件
. 当EPOLLIN事件到达时,read fd中的数据并处理(可读完也可不读完)
. 当需要写出数据时,把数据write到fd中:如果数据较大无法一次性写出,那么在epoll中监听EPOLLOUT事件。当EPOLLOUT事件到达时,继续把数据write到fd中;如果数据写出完毕,那么在epoll中关闭EPOLLOUT事件
ET的处理过程:
. accept一个连接,添加到epoll中监听EPOLLIN|EPOLLOUT事件
. 当EPOLLIN事件到达时,read fd中的数据并处理,read需要一直读,直到返回EAGAIN为止(表示已读完)
. 当需要写出数据时,把数据write到fd中,直到数据全部写完,或者write返回EAGAIN(表示已写不动),此时当EPOLLOUT事件到达时,继续把数据write到fd中,直到数据全部写完,或者write返回EAGAIN重复。
- 从ET的处理过程中可以看到,ET的要求是需要一直读写,直到返回EAGAIN,否则就会遗漏“待处理的数据”。而LT的处理过程中,直到返回EAGAIN不是硬性要求,但通常的处理过程都会读写直到返回EAGAIN,并且不存在数据遗漏。但LT比ET多了一个开关EPOLLOUT事件的步骤。
- 使用ET模式,特定场景下会比LT更快,因为它可以便捷的处理EPOLLOUT事件,省去打开与关闭EPOLLOUT的调用。从而有可能让你的性能得到一定的提升。例如你需要写出1M的数据,写出到socket 256k时,返回了EAGAIN,ET模式下,当再次epoll返回EPOLLOUT事件时,继续写出待写出的数据,当没有数据需要写出时,不处理直接略过即可。而LT模式则需要先打开EPOLLOUT,当没有数据需要写出时,再关闭EPOLLOUT(否则会一直返回EPOLLOUT事件)。
- 总体来说,ET处理EPOLLOUT方便高效些,LT不容易遗漏事件、不易产生bug。如果server的响应通常较小,不会触发EPOLLOUT,那么适合使用LT,例如redis等。而nginx作为高性能的通用服务器,网络流量可以跑满达到1G,这种情况下很容易触发EPOLLOUT,则使用ET。
epoll ET模式为何fd必须要设置为非阻塞??
ET模式下,“边沿”时只通知一次,下次必须导致未就绪状态,才会再次“边沿”触发通知。也就是说,如果要使用ET模式,当数据就绪时不能一次阻塞/非阻塞I/O,必须循环多次直到达到未就绪状态。需要一直read,知道完成或出错为止。但倘若当前fd为阻塞的方式,那么最后一次I/O可能发生阻塞,影响其他fd以及他以后的逻辑,但如果是非阻塞,则会出错返回,程序即可以通过EAGAIN出错判断已进入未就绪状态,就可以继续处理后续逻辑了(处理其他的fd或者进入下一次epoll_wait)。
epoll LT和ET都不用阻塞??
epoll的常见使用方式都是非阻塞的,无论是ET还是LT模式,相信大家没有在实际应用中碰见epoll+阻塞模式socket,下面从理论上分析epoll与阻塞模式socket的使用。
- ET模式:ET模式不支持阻塞模式的socket。因为ET模式要求对read和write的调用必须不断调用,直到返回EAGAIN,只有这样,下一次epoll_wait才有可能返回EPOLLIN或EPOLLOUT。当socket是阻塞方式的,永远也不可能返回EAGAIN,只会阻塞在read/wite调用上。
- LT模式:LT模式能够支持阻塞模式的socket。在阻塞模式下,当有数据到达,epoll_wait返回EPOLLIN事件,此时的处理中调用read读取数据,注意,第一次调用read,可以保证socket里面有数据(EPOLLIN事件说明有数据),read不会阻塞。第二次调用,socket里面有没有数据是不确定的,要是贸然调用,read可能会阻塞,因此不能进行第二次调用,必须等待epoll_wait再次返回EPOLLIN才能再次read。因此LT模式下的阻塞socket使用就只能read/write一次就转到epoll_wait,这对于网络流量较大的应用效率是相当低的,而且一不小心就会阻塞在某个socket上,因此epoll的LT+阻塞式socket几乎不出现在实际应用中。
相比于select、poll的区别和优势:
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。
select/poll有很多重复而无意义的操作:
- 从用户到内核空间拷贝。既然长期监视这几个fd,甚至连期待的事件也不会改变,那拷贝无疑就是重复而无意义的,因此可以让内核长期保存所有需要监视的fd甚至期待事件,或者可以在需要时对部分期待事件进行修改(MOD,ADD,DEL);
- 将当前进程轮流加入到每个fd对应设备的等待队列。这样做无非是哪一个设备就绪时能够通知进程退出调用,聪明的开发者想到,那就找个“代理”的回调函数,代替当前进程加入fd的等待队列好了。这样,驱动程序做poll文件操作发现尚未就绪时,它就调用传入的一个回调函数,这是epoll指定的回调函数,并将这个“代理”的回调函数加入设备的等待队列,由代理的回调函数就等待设备就绪时将它唤醒,然后它就把这个设备fd放到一个指定的地方,同时唤醒可能在等待的进程,到这个指定的地方取fd就好了(ET与LT)。
我们把1和2结合起来就可以这样做了,只拷贝一次fd,一旦确定了fd就可以做poll文件操作,如果有事件当然好啦,马上就把fd放到指定的地方,而通常都是没有的,那就给这个fd的等待队列加一个回调函数,有事件就自动把fd放到指定的地方,当前进程不需要再一个个poll和睡眠等待了。
上面说的就是epoll了,epoll由三个系统调用组成,分别是epoll_create,epoll_ctl和epoll_wait。
- 更加灵活,没有最大并发连接的限制:能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
- 效率提升:只有活跃可用的FD才会调用callback函数,而不是轮询所有FD,因此也不会随着FD数目的增加造成效率下降。即epoll最大的优点在于它只管“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,epoll的效率会远远高于select/poll。
- 内存拷贝开销减少:利用mmap()文件映射内存加速与内核空间的消息传递。即使用mmap减少复制开销。
- epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
- 在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册多个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是epoll的魅力所在。)
- 如果没有大量的idle-connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当遇到大量的idle-connection,就会发现epoll的效率大大高于select/poll。
5、select、poll和epoll区别与总结
- 支持一个进程所能打开的最大连接数:select有限制,poll和epoll无限制。
- fd剧增后带来的IO效率问题:select和poll随fd剧增,效率线性下降;epoll通常不会,取决于活跃数量。
- 消息传递方式:select和poll都需要在用户和内核空间来回拷贝消息;epoll使用共享内存不需要。
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点:
- 表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。epoll的LT模式是大量连接少活跃的poll的高效版本。
- select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。