关于linux:Linux-IO模式及-selectpollepoll详解含部分实例源码

28次阅读

共计 12206 个字符,预计需要花费 31 分钟才能阅读完成。

同步 IO 和异步 IO,阻塞 IO 和非阻塞 IO 别离是什么,到底有什么区别?不同的人在不同的上下文下给出的答案是不同的。

一 概念阐明

在进行解释之前,首先要阐明几个概念:- 用户空间和内核空间 - 过程切换 - 过程的阻塞 - 文件描述符 - 缓存 I/O

用户空间与内核空间

当初操作系统都是采纳虚拟存储器,那么对 32 位操作系统而言,它的寻址空间(虚拟存储空间)为 4G(2 的 32 次方)。操作系统的外围是内核,独立于一般的应用程序,能够拜访受爱护的内存空间,也有拜访底层硬件设施的所有权限。为了保障用户过程不能间接操作内核(kernel),保障内核的平安,操心零碎将虚拟空间划分为两局部,一部分为内核空间,一部分为用户空间。针对 linux 操作系统而言,将最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF),供内核应用,称为内核空间,而将较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF),供各个过程应用,称为用户空间。

过程切换

为了管制过程的执行,内核必须有能力挂起正在 CPU 上运行的过程,并复原以前挂起的某个过程的执行。这种行为被称为过程切换。因而能够说,任何过程都是在操作系统内核的反对下运行的,是与内核严密相干的。

从一个过程的运行转到另一个过程上运行,这个过程中通过上面这些变动:1. 保留处理机上下文,包含程序计数器和其余寄存器。2. 更新 PCB 信息。3. 把过程的 PCB 移入相应的队列,如就绪、在某事件阻塞等队列。4. 抉择另一个过程执行,并更新其 PCB。5. 更新内存治理的数据结构。6. 复原处理机上下文。

注:总而言之就是很耗资源,具体的能够参考这篇文章:过程切换

过程的阻塞

正在执行的过程,因为期待的某些事件未产生,如申请系统资源失败、期待某种操作的实现、新数据尚未达到或无新工作做等,则由零碎主动执行阻塞原语(Block),使本人由运行状态变为阻塞状态。可见,过程的阻塞是过程本身的一种被动行为,也因而只有处于运行态的过程(取得 CPU),才可能将其转为阻塞状态。当过程进入阻塞状态,是不占用 CPU 资源的。

文件描述符 fd

文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的援用的抽象化概念。

文件描述符在模式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个过程所保护的该过程关上文件的记录表。当程序关上一个现有文件或者创立一个新文件时,内核向过程返回一个文件描述符。在程序设计中,一些波及底层的程序编写往往会围绕着文件描述符开展。然而文件描述符这一概念往往只实用于 UNIX、Linux 这样的操作系统。

缓存 I/O

缓存 I/O 又被称作规范 I/O,大多数文件系统的默认 I/O 操作都是缓存 I/O。在 Linux 的缓存 I/O 机制中,操作系统会将 I/O 的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,而后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

缓存 I/O 的毛病:数据在传输过程中须要在应用程序地址空间和内核进行屡次数据拷贝操作,这些数据拷贝操作所带来的 CPU 以及内存开销是十分大的。

须要 C /C++ Linux 服务器架构师学习材料加群 812855908(材料包含 C /C++,Linux,golang 技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg 等),收费分享

二 IO 模式

方才说了,对于一次 IO 拜访(以 read 举例),数据会先被拷贝到操作系统内核的缓冲区中,而后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。所以说,当一个 read 操作产生时,它会经验两个阶段:1. 期待数据筹备 (Waiting for the data to be ready)2. 将数据从内核拷贝到过程中 (Copying the data from the kernel to the process)

正式因为这两个阶段,linux 零碎产生了上面五种网络模式的计划。- 阻塞 I/O(blocking IO)- 非阻塞 I/O(nonblocking IO)- I/O 多路复用(IO multiplexing)- 信号驱动 I/O(signal driven IO)- 异步 I/O(asynchronous IO)

注:因为 signal driven IO 在理论中并不罕用,所以我这只提及剩下的四种 IO Model。

阻塞 I/O(blocking IO)

在 linux 中,默认状况下所有的 socket 都是 blocking,一个典型的读操作流程大略是这样:

当用户过程调用了 recvfrom 这个零碎调用,kernel 就开始了 IO 的第一个阶段:筹备数据(对于网络 IO 来说,很多时候数据在一开始还没有达到。比方,还没有收到一个残缺的 UDP 包。这个时候 kernel 就要期待足够的数据到来)。这个过程须要期待,也就是说数据被拷贝到操作系统内核的缓冲区中是须要一个过程的。而在用户过程这边,整个过程会被阻塞(当然,是过程本人抉择的阻塞)。当 kernel 始终等到数据筹备好了,它就会将数据从 kernel 中拷贝到用户内存,而后 kernel 返回后果,用户过程才解除 block 的状态,从新运行起来。

所以,blocking IO 的特点就是在 IO 执行的两个阶段都被 block 了。

非阻塞 I/O(nonblocking IO)

linux 下,能够通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是这个样子:

当用户过程收回 read 操作时,如果 kernel 中的数据还没有筹备好,那么它并不会 block 用户过程,而是立即返回一个 error。从用户过程角度讲,它发动一个 read 操作后,并不需要期待,而是马上就失去了一个后果。用户过程判断后果是一个 error 时,它就晓得数据还没有筹备好,于是它能够再次发送 read 操作。一旦 kernel 中的数据筹备好了,并且又再次收到了用户过程的 system call,那么它马上就将数据拷贝到了用户内存,而后返回。

所以,nonblocking IO 的特点是用户过程须要 一直的被动询问kernel 数据好了没有。

I/O 多路复用(IO multiplexing)

IO multiplexing 就是咱们说的 select,poll,epoll,有些中央也称这种 IO 形式为 event driven IO。select/epoll 的益处就在于单个 process 就能够同时解决多个网络连接的 IO。它的基本原理就是 select,poll,epoll 这个 function 会一直的轮询所负责的所有 socket,当某个 socket 有数据达到了,就告诉用户过程。

当用户过程调用了 select,那么整个过程会被 block,而同时,kernel 会“监督”所有 select 负责的 socket,当任何一个 socket 中的数据筹备好了,select 就会返回。这个时候用户过程再调用 read 操作,将数据从 kernel 拷贝到用户过程。

所以,I/O 多路复用的特点是通过一种机制一个过程能同时期待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就能够返回。

这个图和 blocking IO 的图其实并没有太大的不同,事实上,还更差一些。因为这里须要应用两个 system call (select 和 recvfrom),而 blocking IO 只调用了一个 system call (recvfrom)。然而,用 select 的劣势在于它能够同时解决多个 connection。

所以,如果解决的连接数不是很高的话,应用 select/epoll 的 web server 不肯定比应用 multi-threading + blocking IO 的 web server 性能更好,可能提早还更大。select/epoll 的劣势并不是对于单个连贯能解决得更快,而是在于能解决更多的连贯。)

在 IO multiplexing Model 中,理论中,对于每一个 socket,个别都设置成为 non-blocking,然而,如上图所示,整个用户的 process 其实是始终被 block 的。只不过 process 是被 select 这个函数 block,而不是被 socket IO 给 block。

异步 I/O(asynchronous IO)

inux 下的 asynchronous IO 其实用得很少。先看一下它的流程:

用户过程发动 read 操作之后,立即就能够开始去做其它的事。而另一方面,从 kernel 的角度,当它受到一个 asynchronous read 之后,首先它会立即返回,所以不会对用户过程产生任何 block。而后,kernel 会期待数据筹备实现,而后将数据拷贝到用户内存,当这所有都实现之后,kernel 会给用户过程发送一个 signal,通知它 read 操作实现了。

总结

blocking 和 non-blocking 的区别

调用 blocking IO 会始终 block 住对应的过程直到操作实现,而 non-blocking IO 在 kernel 还筹备数据的状况下会立即返回。

synchronous IO 和 asynchronous IO 的区别

在阐明 synchronous IO 和 asynchronous IO 的区别之前,须要先给出两者的定义。POSIX 的定义是这样子的:- A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;- An asynchronous I/O operation does not cause the requesting process to be blocked;

两者的区别就在于 synchronous IO 做”IO operation”的时候会将 process 阻塞。依照这个定义,之前所述的 blocking IO,non-blocking IO,IO multiplexing 都属于 synchronous IO。

有人会说,non-blocking IO 并没有被 block 啊。这里有个十分“刁滑”的中央,定义中所指的”IO operation”是指实在的 IO 操作,就是例子中的 recvfrom 这个 system call。non-blocking IO 在执行 recvfrom 这个 system call 的时候,如果 kernel 的数据没有筹备好,这时候不会 block 过程。然而,当 kernel 中数据筹备好的时候,recvfrom 会将数据从 kernel 拷贝到用户内存中,这个时候过程是被 block 了,在这段时间内,过程是被 block 的。

而 asynchronous IO 则不一样,当过程发动 IO 操作之后,就间接返回再也不理会了,直到 kernel 发送一个信号,通知过程说 IO 实现。在这整个过程中,过程齐全没有被 block。

各个 IO Model 的比拟如图所示:

通过下面的图片,能够发现 non-blocking IO 和 asynchronous IO 的区别还是很显著的。在 non-blocking IO 中,尽管过程大部分工夫都不会被 block,然而它依然要求过程去被动的 check,并且当数据筹备实现当前,也须要过程被动的再次调用 recvfrom 来将数据拷贝到用户内存。而 asynchronous IO 则齐全不同。它就像是用户过程将整个 IO 操作交给了别人(kernel)实现,而后别人做完后发信号告诉。在此期间,用户过程不须要去查看 IO 操作的状态,也不须要被动的去拷贝数据。

三 I/O 多路复用之 select、poll、epoll 详解

select,poll,epoll 都是 IO 多路复用的机制。I/ O 多路复用就是通过一种机制,一个过程能够监督多个描述符,一旦某个描述符就绪(个别是读就绪或者写就绪),可能告诉程序进行相应的读写操作。但 select,poll,epoll 实质上都是同步 I /O,因为他们都须要在读写事件就绪后本人负责进行读写,也就是说这个读写过程是阻塞的,而异步 I / O 则无需本人负责进行读写,异步 I / O 的实现会负责把数据从内核拷贝到用户空间。(这里啰嗦下)

select

int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 

select 函数监督的文件描述符分 3 类,别离是 writefds、readfds、和 exceptfds。调用后 select 函数会阻塞,直到有形容副就绪(有数据 可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果立刻返回设为 null 即可),函数返回。当 select 函数返回后,能够 通过遍历 fdset,来找到就绪的描述符。

select 目前简直在所有的平台上反对,其良好跨平台反对也是它的一个长处。select 的一 个毛病在于单个过程可能监督的文件描述符的数量存在最大限度,在 Linux 上个别为 1024,能够通过批改宏定义甚至从新编译内核的形式晋升这一限度,但 是这样也会造成效率的升高。

poll

int poll (struct pollfd *fds, unsigned int nfds, int timeout); 

不同与 select 应用三个位图来示意三个 fdset 的形式,poll 应用一个 pollfd 的指针实现。

struct pollfd {
    int fd; /* file descriptor */
    short events; /* requested events to watch */
    short revents; /* returned events witnessed */
}; 

pollfd 构造蕴含了要监督的 event 和产生的 event,不再应用 select“参数 - 值”传递的形式。同时,pollfd 并没有最大数量限度(然而数量过大后性能也是会降落)。和 select 函数一样,poll 返回后,须要轮询 pollfd 来获取就绪的描述符。

从下面看,select 和 poll 都须要在返回后,通过遍历文件描述符来获取曾经就绪的 socket。事实上,同时连贯的大量客户端在一时刻可能只有很少的处于就绪状态,因而随着监督的描述符数量的增长,其效率也会线性降落。

epoll

epoll 是在 2.6 内核中提出的,是之前的 select 和 poll 的加强版本。绝对于 select 和 poll 来说,epoll 更加灵便,没有描述符限度。epoll 应用一个文件描述符治理多个描述符,将用户关系的文件描述符的事件寄存到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。

一 epoll 操作过程

epoll 操作过程须要三个接口,别离如下:

int epoll_create(int size);// 创立一个 epoll 的句柄,size 用来通知内核这个监听的数目一共有多大
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); 

1. int epoll_create(int size);创立一个 epoll 的句柄,size 用来通知内核这个监听的数目一共有多大,这个参数不同于 select()中的第一个参数,给出最大监听的 fd+ 1 的值,参数 size 并不是限度了 epoll 所能监听的描述符最大个数,只是对内核初始调配外部数据结构的一个倡议。当创立好 epoll 句柄后,它就会占用一个 fd 值,在 linux 下如果查看 /proc/ 过程 id/fd/,是可能看到这个 fd 的,所以在应用完 epoll 后,必须调用 close()敞开,否则可能导致 fd 被耗尽。

2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);函数是对指定描述符 fd 执行 op 操作。- epfd:是 epoll_create()的返回值。- op:示意 op 操作,用三个宏来示意:增加 EPOLL_CTL_ADD,删除 EPOLL_CTL_DEL,批改 EPOLL_CTL_MOD。别离增加、删除和批改对 fd 的监听事件。- fd:是须要监听的 fd(文件描述符)- epoll_event:是通知内核须要监听什么事,struct epoll_event 构造如下:

struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

//events 能够是以下几个宏的汇合:EPOLLIN:示意对应的文件描述符能够读(包含对端 SOCKET 失常敞开);EPOLLOUT:示意对应的文件描述符能够写;EPOLLPRI:示意对应的文件描述符有紧急的数据可读(这里应该示意有带外数据到来);EPOLLERR:示意对应的文件描述符产生谬误;EPOLLHUP:示意对应的文件描述符被挂断;EPOLLET:将 EPOLL 设为边缘触发 (Edge Triggered) 模式,这是绝对于程度触发 (Level Triggered) 来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还须要持续监听这个 socket 的话,须要再次把这个 socket 退出到 EPOLL 队列里 

3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);期待 epfd 上的 io 事件,最多返回 maxevents 个事件。参数 events 用来从内核失去事件的汇合,maxevents 告之内核这个 events 有多大,这个 maxevents 的值不能大于创立 epoll_create()时的 size,参数 timeout 是超时工夫(毫秒,0 会立刻返回,- 1 将不确定,也有说法说是永恒阻塞)。该函数返回须要解决的事件数目,如返回 0 示意已超时。

二 工作模式

epoll 对文件描述符的操作有两种模式:LT(level trigger)ET(edge trigger)。LT 模式是默认模式,LT 模式与 ET 模式的区别如下:LT 模式:当 epoll_wait 检测到描述符事件产生并将此事件告诉应用程序,应用程序能够不立刻解决该事件。下次调用 epoll_wait 时,会再次响应应用程序并告诉此事件。ET 模式:当 epoll_wait 检测到描述符事件产生并将此事件告诉应用程序,应用程序必须立刻解决该事件。如果不解决,下次调用 epoll_wait 时,不会再次响应应用程序并告诉此事件。

1. LT 模式

LT(level triggered)是缺省的工作形式,并且同时反对 block 和 no-block socket. 在这种做法中,内核通知你一个文件描述符是否就绪了,而后你能够对这个就绪的 fd 进行 IO 操作。如果你不作任何操作,内核还是会持续告诉你的。

2. ET 模式

ET(edge-triggered)是高速工作形式,只反对 no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过 epoll 通知你。而后它会假如你晓得文件描述符曾经就绪,并且不会再为那个文件描述符发送更多的就绪告诉,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比方,你在发送,接管或者接管申请,或者发送接管的数据少于一定量时导致了一个 EWOULDBLOCK 谬误)。然而请留神,如果始终不对这个 fd 作 IO 操作(从而导致它再次变成未就绪),内核不会发送更多的告诉(only once)

ET 模式在很大水平上缩小了 epoll 事件被反复触发的次数,因而效率要比 LT 模式高。epoll 工作在 ET 模式的时候,必须应用非阻塞套接口,以防止因为一个文件句柄的阻塞读 / 阻塞写操作把解决多个文件描述符的工作饿死。

3. 总结

如果有这样一个例子:1. 咱们曾经把一个用来从管道中读取数据的文件句柄 (RFD) 增加到 epoll 描述符 2. 这个时候从管道的另一端被写入了 2KB 的数据 3. 调用 epoll_wait(2),并且它会返回 RFD,阐明它曾经筹备好读取操作 4. 而后咱们读取了 1KB 的数据 5. 调用 epoll_wait(2)……

LT 模式:如果是 LT 模式,那么在第 5 步调用 epoll_wait(2)之后,依然能受到告诉。

ET 模式:如果咱们在第 1 步将 RFD 增加到 epoll 描述符的时候应用了 EPOLLET 标记,那么在第 5 步调用 epoll_wait(2)之后将有可能会挂起,因为残余的数据还存在于文件的输出缓冲区内,而且数据收回端还在期待一个针对曾经收回数据的反馈信息。只有在监督的文件句柄上产生了某个事件的时候 ET 工作模式才会汇报事件。因而在第 5 步的时候,调用者可能会放弃期待仍在存在于文件输出缓冲区内的残余数据。

当应用 epoll 的 ET 模型来工作时,当产生了一个 EPOLLIN 事件后,读数据的时候须要思考的是当 recv()返回的大小如果等于申请的大小,那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有解决完,所以还须要再次读取:

while(rs){buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
  if(buflen < 0){
    // 因为是非阻塞的模式, 所以当 errno 为 EAGAIN 时, 示意以后缓冲区已无数据可读
    // 在这里就当作是该次事件已解决处.
    if(errno == EAGAIN){break;}
    else{return;}
  }
  else if(buflen == 0){// 这里示意对端的 socket 已失常敞开.}

 if(buflen == sizeof(buf){rs = 1;   // 须要再次读取}
 else{rs = 0;}
} 

Linux 中的 EAGAIN 含意

Linux 环境下开发常常会碰到很多谬误 (设置 errno),其中 EAGAIN 是其中比拟常见的一个谬误(比方用在非阻塞操作中)。从字面上来看,是提醒再试一次。这个谬误经常出现在当应用程序进行一些非阻塞(non-blocking) 操作 (对文件或 socket) 的时候。

例如,以 O_NONBLOCK 的标记关上文件 /socket/FIFO,如果你间断做 read 操作而没有数据可读。此时程序不会阻塞起来期待数据准备就绪返回,read 函数会返回一个谬误 EAGAIN,提醒你的应用程序当初没有数据可读请稍后再试。又例如,当一个零碎调用 (比方 fork) 因为没有足够的资源 (比方虚拟内存) 而执行失败,返回 EAGAIN 提醒其再调用一次(兴许下次就能胜利)。

三 代码演示

上面是一段不残缺的代码且格局不对,意在表述下面的过程,去掉了一些模板代码。

#define IPADDRESS   "127.0.0.1"
#define PORT        8787
#define MAXSIZE     1024
#define LISTENQ     5
#define FDSIZE      1000
#define EPOLLEVENTS 100

listenfd = socket_bind(IPADDRESS,PORT);

struct epoll_event events[EPOLLEVENTS];

// 创立一个描述符
epollfd = epoll_create(FDSIZE);

// 增加监听描述符事件
add_event(epollfd,listenfd,EPOLLIN);

// 循环期待
for (; ;){
    // 该函数返回曾经筹备好的描述符事件数目
    ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
    // 解决接管到的连贯
    handle_events(epollfd,events,ret,listenfd,buf);
}

// 事件处理函数
static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf) {
     int i;
     int fd;
     // 进行遍历; 这里只有遍历曾经筹备好的 io 事件。num 并不是当初 epoll_create 时的 FDSIZE。for (i = 0;i < num;i++)
     {fd = events[i].data.fd;
        // 依据描述符的类型和事件类型进行解决
         if ((fd == listenfd) &&(events[i].events & EPOLLIN))
            handle_accpet(epollfd,listenfd);
         else if (events[i].events & EPOLLIN)
            do_read(epollfd,fd,buf);
         else if (events[i].events & EPOLLOUT)
            do_write(epollfd,fd,buf);
     }
}

// 增加事件
static void add_event(int epollfd,int fd,int state){
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
}

// 解决接管到的连贯
static void handle_accpet(int epollfd,int listenfd){
     int clifd;     
     struct sockaddr_in cliaddr;     
     socklen_t  cliaddrlen;     
     clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);     
     if (clifd == -1)         
     perror("accpet error:");     
     else {printf("accept a new client: %s:%dn",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);                       // 增加一个客户描述符和事件 
         add_event(epollfd,clifd,EPOLLIN);     
     } 
}

// 读解决
static void do_read(int epollfd,int fd,char *buf){
    int nread;
    nread = read(fd,buf,MAXSIZE);
    if (nread == -1)     {perror("read error:");         
        close(fd); // 记住 close fd 
        delete_event(epollfd,fd,EPOLLIN); // 删除监听 
    }
    else if (nread == 0)     {fprintf(stderr,"client close.n");
        close(fd); // 记住 close fd 
        delete_event(epollfd,fd,EPOLLIN); // 删除监听 
    }     
    else {printf("read message is : %s",buf);        
        // 批改描述符对应的事件,由读改为写 
        modify_event(epollfd,fd,EPOLLOUT);     
    } 
}

// 写解决
static void do_write(int epollfd,int fd,char *buf) {     
    int nwrite;     
    nwrite = write(fd,buf,strlen(buf));     
    if (nwrite == -1){perror("write error:");        
        close(fd);   // 记住 close fd 
        delete_event(epollfd,fd,EPOLLOUT);  // 删除监听 
    }else{modify_event(epollfd,fd,EPOLLIN); 
    }    
    memset(buf,0,MAXSIZE); 
}

// 删除事件
static void delete_event(int epollfd,int fd,int state) {
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
}

// 批改事件
static void modify_event(int epollfd,int fd,int state){     
    struct epoll_event ev;
    ev.events = state;
    ev.data.fd = fd;
    epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
}

// 注:另外一端我就省了 

四 epoll 总结

在 select/poll 中,过程只有在调用肯定的办法后,内核才对所有监督的文件描述符进行扫描,而 epoll 当时通过 epoll_ctl() 来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采纳相似 callback 的回调机制,迅速激活这个文件描述符,当过程调用 epoll_wait() 时便失去告诉。(此处去掉了遍历文件描述符,而是通过监听回调的的机制。这正是 epoll 的魅力所在。)

epoll 的长处次要是以下个方面:1. 监督的描述符数量不受限制,它所反对的 FD 下限是最大能够关上文件的数目,这个数字个别远大于 2048, 举个例子, 在 1GB 内存的机器上大概是 10 万左 右,具体数目能够 cat /proc/sys/fs/file-max 查看, 一般来说这个数目和零碎内存关系很大。select 的最大毛病就是过程关上的 fd 是有数量限度的。这对 于连贯数量比拟大的服务器来说基本不能满足。尽管也能够抉择多过程的解决方案(Apache 就是这样实现的),不过尽管 linux 下面创立过程的代价比拟小,但仍旧是不可漠视的,加上过程间数据同步远比不上线程间同步的高效,所以也不是一种完满的计划。

  1. IO 的效率不会随着监督 fd 的数量的增长而降落。epoll 不同于 select 和 poll 轮询的形式,而是通过每个 fd 定义的回调函数来实现的。只有就绪的 fd 才会执行回调函数。

如果没有大量的 idle -connection 或者 dead-connection,epoll 的效率并不会比 select/poll 高很多,然而当遇到大量的 idle- connection,就会发现 epoll 的效率大大高于 select/poll。

正文完
 0