[网络编程] select/epoll分析

 1、简要分析html

一个socket对应一个数据流,经过I/O操做中的read从流中读入数据,write向流中写入数据。当read时,socket流中没有数据的话,read阻塞,线程睡眠,CPU开始作其余的任务,流中有数据可读时,read返回。linux

在阻塞IO模式下,一个线程只能处理一个IO事件。若是处理多个事件,须要多线程或多进程,可是效率比较低。web

一、若是采用非阻塞方式,须要不断轮训全部的流,假设共有N个socket流streams[N], 以下:sql

// busy poll
while True:
    for stream in streams[N]: if stream has data read all data

这种模式最大的缺点是,若是没有socket可读,也将一直占用CPU,浪费CPU时间。segmentfault

二、为了解决这个问题,引入select,当streams[N]中有k(0 < k <= N)个socket流可操做时才返回,不然阻塞,释放CPU。缓存

// select
while True:
    select(streams[N])            
    for stream in streams[N]: if stream has data read all data

当streams[N]中没有数据时, 线程阻塞在select处,CPU处理其余任务。select返回时表示有k个流可操做,但是select并无通知咱们是那些流,所以咱们须要轮询全部的N个流,安全

时间复杂度为O(N). 在N比较小时,这样处理ok,可是当链接数达到数万甚至几十万时(C10K问题),select的轮询机制会致使效率低下。网络

三、epoll则解决了selec后的轮询。epoll会返回每一个可操做的socket流以及这些流产生了那些IO事件。数据结构

// epoll
while True:
    active_streams[k] = epoll(streams[N])
    for stream in active_streams[k] deal all data

这样epoll将复杂度下降为O(1), 提升了效率[1], 解决了C10K问题。多线程

 

2、Linux IO模式分析(转载: https://segmentfault.com/a/1190000003063859)

注:本文是对众多博客的学习和总结,可能存在理解错误。请带着怀疑的眼光,同时若是有错误但愿能指出。

同步IO和异步IO,阻塞IO和非阻塞IO分别是什么,到底有什么区别?不一样的人在不一样的上下文下给出的答案是不一样的。因此先限定一下本文的上下文。

本文讨论的背景是Linux环境下的network 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 以及内存开销是很是大的。

二 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提示其再调用一次(也许下次就能成功)。

三 代码演示

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

  1 #define IPADDRESS   "127.0.0.1"
  2 #define PORT        8787
  3 #define MAXSIZE     1024
  4 #define LISTENQ     5
  5 #define FDSIZE      1000
  6 #define EPOLLEVENTS 100
  7 
  8 listenfd = socket_bind(IPADDRESS,PORT);
  9 
 10 struct epoll_event events[EPOLLEVENTS];
 11 
 12 //建立一个描述符
 13 epollfd = epoll_create(FDSIZE);
 14 
 15 //添加监听描述符事件
 16 add_event(epollfd,listenfd,EPOLLIN);
 17 
 18 //循环等待
 19 for ( ; ; ){
 20     //该函数返回已经准备好的描述符事件数目
 21     ret = epoll_wait(epollfd,events,EPOLLEVENTS,-1);
 22     //处理接收到的链接
 23     handle_events(epollfd,events,ret,listenfd,buf);
 24 }
 25 
 26 //事件处理函数
 27 static void handle_events(int epollfd,struct epoll_event *events,int num,int listenfd,char *buf)
 28 {
 29      int i;
 30      int fd;
 31      //进行遍历;这里只要遍历已经准备好的io事件。num并非当初epoll_create时的FDSIZE。
 32      for (i = 0;i < num;i++)
 33      {
 34          fd = events[i].data.fd;
 35         //根据描述符的类型和事件类型进行处理
 36          if ((fd == listenfd) &&(events[i].events & EPOLLIN))
 37             handle_accpet(epollfd,listenfd);
 38          else if (events[i].events & EPOLLIN)
 39             do_read(epollfd,fd,buf);
 40          else if (events[i].events & EPOLLOUT)
 41             do_write(epollfd,fd,buf);
 42      }
 43 }
 44 
 45 //添加事件
 46 static void add_event(int epollfd,int fd,int state){
 47     struct epoll_event ev;
 48     ev.events = state;
 49     ev.data.fd = fd;
 50     epoll_ctl(epollfd,EPOLL_CTL_ADD,fd,&ev);
 51 }
 52 
 53 //处理接收到的链接
 54 static void handle_accpet(int epollfd,int listenfd){
 55      int clifd;     
 56      struct sockaddr_in cliaddr;     
 57      socklen_t  cliaddrlen;     
 58      clifd = accept(listenfd,(struct sockaddr*)&cliaddr,&cliaddrlen);     
 59      if (clifd == -1)         
 60      perror("accpet error:");     
 61      else {         
 62          printf("accept a new client: %s:%d\n",inet_ntoa(cliaddr.sin_addr),cliaddr.sin_port);                       //添加一个客户描述符和事件         
 63          add_event(epollfd,clifd,EPOLLIN);     
 64      } 
 65 }
 66 
 67 //读处理
 68 static void do_read(int epollfd,int fd,char *buf){
 69     int nread;
 70     nread = read(fd,buf,MAXSIZE);
 71     if (nread == -1)     {         
 72         perror("read error:");         
 73         close(fd); //记住close fd        
 74         delete_event(epollfd,fd,EPOLLIN); //删除监听 
 75     }
 76     else if (nread == 0)     {         
 77         fprintf(stderr,"client close.\n");
 78         close(fd); //记住close fd       
 79         delete_event(epollfd,fd,EPOLLIN); //删除监听 
 80     }     
 81     else {         
 82         printf("read message is : %s",buf);        
 83         //修改描述符对应的事件,由读改成写         
 84         modify_event(epollfd,fd,EPOLLOUT);     
 85     } 
 86 }
 87 
 88 //写处理
 89 static void do_write(int epollfd,int fd,char *buf) {     
 90     int nwrite;     
 91     nwrite = write(fd,buf,strlen(buf));     
 92     if (nwrite == -1){         
 93         perror("write error:");        
 94         close(fd);   //记住close fd       
 95         delete_event(epollfd,fd,EPOLLOUT);  //删除监听    
 96     }else{
 97         modify_event(epollfd,fd,EPOLLIN); 
 98     }    
 99     memset(buf,0,MAXSIZE); 
100 }
101 
102 //删除事件
103 static void delete_event(int epollfd,int fd,int state) {
104     struct epoll_event ev;
105     ev.events = state;
106     ev.data.fd = fd;
107     epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,&ev);
108 }
109 
110 //修改事件
111 static void modify_event(int epollfd,int fd,int state){     
112     struct epoll_event ev;
113     ev.events = state;
114     ev.data.fd = fd;
115     epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev);
116 }
117 
118 //注:另一端我就省了

 

四 总结

select的调用过程以下所示:

(1)使用copy_from_user从用户空间拷贝fd_set到内核空间

(2)注册回调函数__pollwait

(3)遍历全部fd,调用其对应的poll方法(对于socket,这个poll方法是sock_poll,sock_poll根据状况会调用到tcp_poll,udp_poll或者datagram_poll)

(4)以tcp_poll为例,其核心实现就是__pollwait,也就是上面注册的回调函数。

(5)__pollwait的主要工做就是把current(当前进程)挂到设备的等待队列中,不一样的设备有不一样的等待队列,对于tcp_poll来讲,其等待队列是sk->sk_sleep(注意把进程挂到等待队列中并不表明进程已经睡眠了)。在设备收到一条消息(网络设备)或填写完文件数据(磁盘设备)后,会唤醒设备等待队列上睡眠的进程,这时current便被唤醒了。

(6)poll方法返回时会返回一个描述读写操做是否就绪的mask掩码,根据这个mask掩码给fd_set赋值。

(7)若是遍历完全部的fd,尚未返回一个可读写的mask掩码,则会调用schedule_timeout是调用select的进程(也就是current)进入睡眠。当设备驱动发生自身资源可读写后,会唤醒其等待队列上睡眠的进程。若是超过必定的超时时间(schedule_timeout指定),仍是没人唤醒,则调用select的进程会从新被唤醒得到CPU,进而从新遍历fd,判断有没有就绪的fd。

(8)把fd_set从内核空间拷贝到用户空间。

总结:

select的几大缺点:

(1)每次调用select,都须要把fd集合从用户态拷贝到内核态,这个开销在fd不少时会很大

(2)同时每次调用select都须要在内核遍历传递进来的全部fd,这个开销在fd不少时也很大

(3)select支持的文件描述符数量过小了,默认是1024

epoll既然是对select和poll的改进,就应该能避免上述的三个缺点。那epoll都是怎么解决的呢?在此以前,咱们先看一下epoll和select和poll的调用接口上的不一样,select和poll都只提供了一个函数——select或者poll函数。而epoll提供了三个函数,epoll_create,epoll_ctl和epoll_wait,epoll_create是建立一个epoll句柄;epoll_ctl是注册要监听的事件类型;epoll_wait则是等待事件的产生。

  对于第一个缺点,epoll的解决方案在epoll_ctl函数中。每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把全部的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每一个fd在整个过程当中只会拷贝一次。

  对于第二个缺点,epoll的解决方案不像select或poll同样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每一个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工做实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是相似的)。

  对于第三个缺点,epoll没有这个限制,它所支持的FD上限是最大能够打开文件的数目,这个数字通常远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目能够cat /proc/sys/fs/file-max察看,通常来讲这个数目和系统内存关系很大。

 

主要来源:

https://segmentfault.com/a/1190000003063859

http://www.cnblogs.com/Anker/p/3265058.html

相关文章
相关标签/搜索