select、poll、epoll等

POSIX概述

    同步IO、异步IO、阻塞IO、非阻塞IO,这几个词常见于各种各样的与网络相关的文章之中,往往不同上下文中它们的意思是不一样的,以致于我在很长一段时间对此感到困惑,所以想写一篇文章整理一下。

    POSIX(可移植操作系统接口)把同步IO操作定义为导致进程阻塞直到IO完成的操作,反之则是异步IO。

    按POSIX的描述似乎把同步和阻塞划等号,异步和非阻塞划等号,但是为什么有的人说同步IO不等于阻塞IO呢?

 

用户空间与内核空间

  现在操作系统都是采用虚拟存储器,那么对32位操作系统而言,它的虚拟地址空间为4G(一个进程能够访问的地址最大是4G)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为两个部分,一个部分为内核空间,一部分为用户空间。

  如何分配这两个空间的大小也是有讲究的,如windows 32位操作系统,默认的用户空间:内核空间的比例是1:1;而在32位Linux系统中的默认比例是3:1(3G用户空间,1G内核空间)。

进程切换

  为了控制进程的执行,内核必须要有能力挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为成为进程的切换。任何进程都是在操作系统内核的支持下运行的,是与内核紧密相关的。进程切换的过程,会经过下面这些变化:

    1、保存处理机上下文,包括程序计数器和其他寄存器。

    2、更新PCB信息。

    3、将进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。

    4、选择另外一个进程执行,并更新PCB

    5、更新内存管理的数据结构。

    6、恢复处理机上下文

 

    缓存IO又称称为标准IO,大多数文件系统的默认IO操作都是缓存IO。在Linux的缓存IO机制中,操作系统会将IO的数据缓存在文件系统的页缓存(page cache)。也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓存区拷贝到应用程序的地址空间中。这种做法的缺点就是,需要在应用程序地址空间和内核进行多次拷贝,这些拷贝动作所带来的CPU以及内存开销是非常大的

    至于为什么不能直接让磁盘控制器把数据送到应用程序的地址空间中呢?最简单的一个原因就是应用程序不能直接操作底层硬件。

 

    总的来说,IO分两阶段:

    1)数据准备阶段

    2)内核空间复制回用户进程缓冲区阶段。如下图:

 

 

 

    基于这两个过程,接下来本文将分析不同的IO模型。

常见的IO模型

    这里统一使用Linux下的系统调用recv作为例子,它用于从套接字上接收一个消息,因为是一个系统调用,所以调用时会从用户进程空间切换到内核空间运行一段时间再切换回来。默认情况下recv会等到网络数据到达并且复制到用户进程空间或者发生错误时返回,而第4个参数flags可以让它马上返回。

       具体的分类如下:

同步IO模型

阻塞IO模型

    网络模型:在这个模型中,应用程序为了执行这个recv操作,会调用相应的一个system call,将系统控制权交给内核,然后就进行等待,内核开始执行这个system call,执行完毕后会向应用程序返回响应,应用程序得到响应后,就不再阻塞,并进行后面的工作。

    优点:能够及时返回数据,无延迟。

    缺点:对用户来说处于等待就要付出性能代价。

 

    以recv函数为例,使用recv的默认参数一直等数据直到拷贝到用户空间,这段时间内进程始终阻塞。比如A同学用杯子装水,打开水龙头装满水然后离开。这一过程就可以看成是使用了阻塞IO模型,因为如果水龙头没有水,他也要等到有水并装满杯子才能离开去做别的事情。很显然,这种IO模型是同步的。

 

 

 

非阻塞IO模型

    网络模型:当用户进程发出recv操作时,调用相应的system call,这个system call会立即从内核中返回。但是在返回的这个时间点,内核中的数据可能还没有准备好,也就是说内核只是很快就返回了system call,只有这样才不会阻塞用户进程,对于应用程序,虽然这个IO操作很快就返回了,但是它并不知道这个IO操作是否真的成功了,为了知道IO操作是否成功,应用程序需要主动的循环去问内核。

    优点:能够在等待的时间里去做其他的事情。

    缺点:任务完成的响应延迟增大了,因为每过一段时间去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成,这对导致整体数据吞吐量的降低。

 

    以recv函数为例,让recv不管有没有获取到数据都返回,如果没有数据那么一段时间后再调用recv看看,如此循环。B同学也用杯子装水,打开水龙头后发现没有水,它离开了,过一会他又拿着杯子来看看……在中间离开的这些时间里,B同学离开了装水现场(回到用户进程空间),可以做他自己的事情。这就是非阻塞IO模型。但是它只有是检查无数据的时候是非阻塞的,在数据到达的时候依然要等待复制数据到用户空间(等着水将水杯装满),因此它还是同步IO。

 

IO复用模型

    网络模型:和第二种一样,调用system call后,并不等待内核的返回结果而是立即返回。虽然返回结果的调用函数是异步的方式,但应用程序会被像select、poll和epoll等具有多个文件描述符的函数阻塞住,一直等到这个system call有结果返回,再通知应用程序。

    IO复用模型使用select函数等可以为多个文件描述符提供通知。举个例子:例如有一万个并发的read请求,但是网络上仍然没有数据,此时这一万个read会同时各自阻塞,现在用select、poll、epoll这样的函数来专门负责阻塞同时监听这一万个请求的状态,一旦有数据到达了就负责通知,这样就将一万个等待和阻塞转化为一个专门的函数来负责与管理。

    异步与同步的区别在于:同步是需要应用程序主动地循环去询问是否有数据,而异步是通过像select等IO多路复用函数来同时检测多个事件句柄来告知应用程序是否有数据。

  高并发的程序一般使用同步非阻塞模式,而不是多线程+同步阻塞模式。要理解这点,先弄明白并发和并行的区别:比如去某部门办事需要依次去几个窗口,办事大厅的人数就是并发数,而窗口的个数就是并行度。就是说并发是同时进行的任务数(如同时服务的http请求),而并行数就是可以同时工作的物理资源数量(如cpu核数)。通过合理调度任务的不同阶段,并发数可以远远大于并行度。这就是区区几个CPU可以支撑上万个用户并发请求的原因。在高并发的情况下,为每个用户请求创建一个进程或者线程的开销非常大。而同步非阻塞方式可以把多个IO请求丢到后台去,这样一个CPU就可以服务大量的并发IO请求。

    IO多路复用在阻塞到select阶段时,用户进程是主动等待并调用select函数来获取就绪状态消息,并且其进程状态为阻塞。所以IO多路复用是同步阻塞模式

 

    以recv函数为例,这里在调用recv前先调用select或者poll,这2个系统调用都可以在内核准备好数据(网络数据到达内核)时告知用户进程,这个时候再调用recv一定是有数据的。因此这一过程中它是阻塞于select或poll,而没有阻塞于recv,有人将非阻塞IO定义成在读写操作时没有阻塞于系统调用的IO操作(不包括数据从内核复制到用户空间的阻塞,因为这相对于网络IO来说确实很短暂),如果按这样理解,这种IO模型也能称之为非阻塞IO模型,但是按POSIX来看,它也是同步IO,那么也和楼上一样称之为同步非阻塞IO吧。

 

    这种IO模型比较特别,分个段。因为它能同时监听多个文件描述符(fd)。这个时候C同学来装水,发现有一排水龙头,舍管阿姨告诉他这些水龙头都还没有水,等有水了告诉他。于是等啊等(select调用中),过了一会阿姨告诉他有水了,但不知道是哪个水龙头有水,自己看吧。于是C同学一个个打开,往杯子里装水(recv)。这里再顺便说说鼎鼎大名的epoll(高性能的代名词啊),epoll也属于IO复用模型,主要区别在于舍管阿姨会告诉C同学哪几个水龙头有水了,不需要一个个打开看(当然还有其它区别)。

 

信号驱动IO模型

    应用程序提交read请求,调用system call,然后内核开始处理相应的IO操作,而同时,应用程序并不等内核返回响应,就会开始执行其他的处理操作,当内核执行完毕,返回read响应,就会产生一个信号或执行一个基于线程的回调函数来完成这次IO处理过程。在这里IO的读写操作是在IO事件发生之后由应用程序来完成。

    通过调用sigaction注册信号函数,等内核数据准备好的时候系统中断当前程序,执行信号函数(在这里面调用recv)。D同学让舍管阿姨等有水的时候通知他(注册信号函数),没多久D同学得知有水了,跑去装水。是不是很像异步IO?很遗憾,它还是同步IO(省不了装水的时间啊)。

 

 

异步IO模型

    异步IO与上面的异步概念是一样的, 当一个异步过程调用发出后,调用者不能立刻得到结果,实际处理这个调用的函数在完成后,通过状态、通知和回调来通知调用者的输入输出操作。异步IO的工作机制是:告知内核启动某个操作,并让内核在整个操作完成后通知我们,这种模型与信号驱动的IO区别在于,信号驱动IO是由内核通知我们何时可以启动一个IO操作,这个IO操作由用户自定义的信号函数来实现,而异步IO模型是由内核告知我们IO操作何时完成。

    比如调用aio_read,让内核等数据准备好,并且复制到用户进程空间后执行事先指定好的函数。E同学让舍管阿姨将杯子装满水后通知他。整个过程E同学都可以做别的事情(没有recv),这才是真正的异步IO。

 

总结

       IO分两阶段:

    1.数据准备阶段

    2.内核空间复制回用户进程缓冲区阶段

    一般来讲:阻塞IO模型、非阻塞IO模型、IO复用模型(select/poll/epoll)、信号驱动IO模型都属于同步IO,因为阶段2是阻塞的(尽管时间很短)。只有异步IO模型是符合POSIX异步IO操作含义的,不管在阶段1还是阶段2都可以干别的事。

 

IO多路复用原理详解

    目前支持I/O多路复用的系统调用有 selectpselectpollepoll与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。

使用场景

    IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合:

  1)当客户处理多个描述符时(一般是交互式输入和网络套接口),必须使用I/O复用。

  2)当一个客户同时处理多个套接口时,这种情况是可能的,但很少出现。

  3)如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。

  4)如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。

       5)如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

select、poll、epoll简介

  epoll跟select都能提供多路I/O复用的解决方案。在现在的Linux内核里有都能够支持,其中epollLinux所特有,而select则应该是POSIX所规定,一般操作系统均有实现。

select基本原理

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

  select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。

    select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。这样所带来的缺点是:

    1、select最大的缺陷就是单个进程所打开的FD是有一定限制的,它由FD_SETSIZE设置,默认值是1024

    一般来说这个数目和系统内存关系很大,具体数目可以cat /proc/sys/fs/file-max察看。32位机默认是1024个。64位机默认是2048.

    2、对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低

当套接字比较多的时候,每次select()都要通过遍历FD_SETSIZE个Socket来完成调度,不管哪个Socket是活跃的,都遍历一遍。这会浪费很多CPU时间。如果能给套接字注册某个回调函数,当他们活跃时,自动完成相关操作,那就避免了轮询,这正是epoll与kqueue做的。

    3、需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大

poll基本原理

    基本原理:poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有fd后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd。这个过程经历了多次无谓的遍历。

 

    它没有最大连接数的限制,原因是它是基于链表来存储的,但是同样有一个缺点:

    1)大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。

    2)poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

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

 

epoll基本原理

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

    基本原理:epoll支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。

 

    epoll的优点:

    1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。

    2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降

  只有活跃可用的FD才会调用callback函数;Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。

    3、内存拷贝,利用mmap()文件映射内存加速与内核空间的消息传递;epoll使用mmap减少复制开销

 

    epoll对文件描述符的操作有两种模式LTlevel trigger)和ETedge trigger。LT模式是默认模式,LT模式与ET模式的区别如下:

    1)LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。

    2)ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。

 

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

  ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fdIO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)

    ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。

 

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

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

select、poll、epoll区别

    1、支持一个进程所能打开的最大连接数

    2、FD剧增后带来的IO效率问题

 

    3、消息传递方式

 

      

 

    综上,选择select,poll,epoll时要根据具体的使用场合以及这三种方式自身特点:

    1、表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,selectpoll的性能可能比epoll,毕竟epoll的通知机制需要很多函数回调。

    2、select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。

 

 

select、poll、epoll简单示例

select示例

    接口函数

    select函数决定一个或者多个套接字的状态,如果需要的话,等待执行异步I/O。

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

    maxfd参数:需要监视的最大的文件描述符值+1;

    readnfds参数: 指向检查可读性的套接字集合的可选的指针。   

    writefds参数: 指向检查可写性的套接字集合的可选的指针。

    exceptfds参数: 指向检查错误的套接字集合的可选的指针。

    三个参数 readfds、writefds 和 exceptfds 指定我们要让内核监测读、写和异常条件的描述字。如果不需要使用某一个的条件,就可以把它设为NULL 。

 

    timeout参数: select函数需要等待的最长时间,需要以TIMEVAL结构体格式提供此参数,对于阻塞操作,此参数为null。

    参数timeout一共三种情况:

    1)永远等待下去:timeout 设置为空指针 NULL,且没有一个描述符准备好。

    2)等待固定时间:timeout 设置为某个固定时间,在有一个描述符准备好时返回,如果时间到了,就算没有文件描述符准备就绪,这个函数也会返回 0

    3)不等待(不阻塞):检查描述字后立即返回,这称为轮询。为此,struct timeval变量的时间值指定为 0 0 微秒,文件描述符属性无变化返回 0,有变化返回准备好的描述符数量。

 

    返回值一共三种情况:

    1) 成功:就绪描述符的数目(同时修改readfdswritefds exceptfds 三个参数)

    2) 超时返回 0

    3) 出错:-1

 

    select常见的宏定义函数有如下四个:

    FD_CLR(s, *set) :从set集合中移除描述符s

    FD_ISSET(s, *set):如果s在set中,返回非0,否则返回0

    FD_SET(s, *set):增加描述符s到set中

    FD_ZERO(*set):初始化set集合为null集合

服务器端

#include <cstdio>

#include <sys/select.h>

#include <unistd.h>

#include <stdlib.h>

#include <cstring>

#include <cassert>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>



const int BUFFER_SIZE = 4096;

const int SERVER_PORT = 2222;



inline int max(int a, int b){ return (a > b ? : a, b);}



int main()

{

       int server_socket;

       char buff1[BUFFER_SIZE];

       char buff2[BUFFER_SIZE];

       fd_set rfds;

       struct timeval tv;

       int ret;

       int n;



       server_socket = socket(AF_INET, SOCK_STREAM, 0);

       assert(server_socket != -1);



       struct sockaddr_in server_addr;

       memset(&server_addr, 0, sizeof(server_addr));

       server_addr.sin_family = AF_INET;

       server_addr.sin_port = htons(SERVER_PORT);

       server_addr.sin_addr.s_addr = htonl(INADDR_ANY);



       assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

       assert(listen(server_socket, 5) != -1);

      

       struct sockaddr_in client_addr1, client_addr2;

       socklen_t client_addr_len = sizeof(struct sockaddr_in);

      

       printf("waiting...\n");



       //此处先建立两个 TCP 连接

       int connfd1 = accept(server_socket, (struct sockaddr*)&client_addr1, &client_addr_len);

       assert(connfd1 != -1);

       printf("connect from %s:%d\n", inet_ntoa(client_addr1.sin_addr), ntohs(client_addr1.sin_port));

       int connfd2 = accept(server_socket, (struct sockaddr*)&client_addr2, &client_addr_len);

       assert(connfd2 != -1);

       printf("connect from %s:%d\n", inet_ntoa(client_addr2.sin_addr), ntohs(client_addr2.sin_port));



       while(1)

       {

              FD_ZERO(&rfds);

              FD_SET(connfd1, &rfds);

              FD_SET(connfd2, &rfds);



              tv.tv_sec = 10;

              tv.tv_usec = 0;

             

              printf("select...\n");

              ret = select(max(connfd1, connfd2) + 1, &rfds, NULL, NULL, NULL);

              //ret = select(max(connfd1, connfd2) + 1, &rfds, NULL, NULL, &tv);

             

              if(ret == -1)

                     perror("select()");

              else if(ret > 0)

              {

                     if(FD_ISSET(connfd1, &rfds))

                     {     

                            n = recv(connfd1, buff1, BUFFER_SIZE, 0);

                            buff1[n] = '\0';                                   //注意手动添加字符串结束符

                            printf("connfd1: %s\n", buff1);

                     }

                     if(FD_ISSET(connfd2, &rfds))

                     {

                            n = recv(connfd2, buff2, BUFFER_SIZE, 0);

                            buff2[n] = '\0';                                   //注意手动添加字符串结束符

                            printf("connfd2: %s\n", buff2);

                     }            

              }

              else

                     printf("time out\n");

       }



       return 0;

}



客户端

客户端1:

#include <cstdio>

#include <unistd.h>

#include <stdlib.h>

#include <cstring>

#include <cassert>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>



const int BUFFER_SIZE = 4096;

const int SERVER_PORT = 2222;



int main()

{

       int client_socket;

       const char *server_ip = "127.0.0.1";

       char buffSend[BUFFER_SIZE] = "I'm from d.cpp";



       client_socket = socket(AF_INET, SOCK_STREAM, 0);

       assert(client_socket != -1);



       struct sockaddr_in server_addr;

       memset(&server_addr, 0, sizeof(server_addr));

       server_addr.sin_family = AF_INET;

       server_addr.sin_port = htons(SERVER_PORT);

       server_addr.sin_addr.s_addr = inet_addr(server_ip);



       assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

      

       while(1)

       {

              assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1);

              sleep(5);

       }

       close(client_socket);



       return 0;

}

 

客户端2:

#include <cstdio>

#include <unistd.h>

#include <stdlib.h>

#include <cstring>

#include <cassert>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>



const int BUFFER_SIZE = 4096;

const int SERVER_PORT = 2222;



int main()

{

       int client_socket;

       const char *server_ip = "127.0.0.1";

       char buffSend[BUFFER_SIZE];



       client_socket = socket(AF_INET, SOCK_STREAM, 0);

       assert(client_socket != -1);



       struct sockaddr_in server_addr;

       memset(&server_addr, 0, sizeof(server_addr));

       server_addr.sin_family = AF_INET;

       server_addr.sin_port = htons(SERVER_PORT);

       server_addr.sin_addr.s_addr = inet_addr(server_ip);



       assert(connect(client_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

      

       while(1)

       {

              fgets(buffSend, BUFFER_SIZE, stdin);

              assert(send(client_socket, buffSend, strlen(buffSend), 0) != -1);

       }

       close(client_socket);



       return 0;

}

 

poll示例

接口函数

   select() 和 poll() 系统调用的本质一样,前者在 BSD UNIX 中引入的,后者在 System V 中引入的。poll() 的机制与 select() 类似,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll() 没有最大文件描述符数量的限制。函数原型如下:

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

   fds参数:与 select() 使用三个 fd_set 的方式不同,poll() 使用一个 pollfd 的指针实现。一个 pollfd 结构体数组,其中包括了你想监视的文件描述符和事件, 事件由结构中事件域 events 来确定,调用后实际发生的事件将被填写在结构体的 revents 域。

   每一个 pollfd 结构体指定了一个被监视的文件描述符,可以传递多个结构体,指示 poll() 监视多个文件描述符。

   每个结构体的 events 域是监视该文件描述符的事件掩码,由用户来设置这个域。

   revents 域是文件描述符的操作结果事件掩码,内核在调用返回时设置这个域。events 域中请求的任何事件都可能在 revents 域中返回。

https://i-blog.csdnimg.cn/blog_migrate/6533885ca6d4bb12ca4f19aa22c0a562.png

   POLLIN | POLLPRI 等价于 select() 的读事件,POLLOUT | POLLWRBAND 等价于 select() 的写事件。POLLIN 等价于 POLLRDNORM | POLLRDBAND,而 POLLOUT 则等价于 POLLWRNORM 。例如,要同时监视一个文件描述符是否可读和可写,我们可以设置 events 为 POLLIN | POLLOUT。

 

   nfds参数:用来指定第一个参数数组元素个数。

   timeout参数:指定等待的毫秒数,无论 I/O 是否准备好,poll() 都会返回。

https://i-blog.csdnimg.cn/blog_migrate/b87ed9c234d65da1435c68f2bde85b66.png

 

   函数返回值一共三种情况:

   1)成功时,poll() 返回结构体中 revents 域不为 0 的文件描述符个数

   2)如果在超时前没有任何事件发生,poll()返回 0;

   3)失败时,poll() 返回 -1。

服务器端

#include <cstdio>

#include <poll.h>

#include <unistd.h>

#include <stdlib.h>

#include <cstring>

#include <cassert>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>



const int BUFFER_SIZE = 4096;

const int SERVER_PORT = 2222;



int main()

{

       int server_socket;

       char buff1[BUFFER_SIZE];

       char buff2[BUFFER_SIZE];

       struct timeval tv;

       int ret;

       int n;



       server_socket = socket(AF_INET, SOCK_STREAM, 0);

       assert(server_socket != -1);



       struct sockaddr_in server_addr;

       memset(&server_addr, 0, sizeof(server_addr));

       server_addr.sin_family = AF_INET;

       server_addr.sin_port = htons(SERVER_PORT);

       server_addr.sin_addr.s_addr = htonl(INADDR_ANY);



       assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

       assert(listen(server_socket, 5) != -1);

      

       struct sockaddr_in client_addr1, client_addr2;

       socklen_t client_addr_len = sizeof(struct sockaddr_in);

      

       printf("waiting...\n");



       int connfd1 = accept(server_socket, (struct sockaddr*)&client_addr1, &client_addr_len);

       assert(connfd1 != -1);

       printf("connect from %s:%d\n", inet_ntoa(client_addr1.sin_addr), ntohs(client_addr1.sin_port));

       int connfd2 = accept(server_socket, (struct sockaddr*)&client_addr2, &client_addr_len);

       assert(connfd2 != -1);

       printf("connect from %s:%d\n", inet_ntoa(client_addr2.sin_addr), ntohs(client_addr2.sin_port));



       struct pollfd rfds[2];

       rfds[0].fd = connfd1;

       rfds[0].events = POLLIN;

       rfds[1].fd = connfd2;

       rfds[1].events = POLLIN;

       tv.tv_sec = 10;

       tv.tv_usec = 0;

      

       while(1)

       {

              printf("poll...\n");

              ret = poll(rfds, 2, -1);

             

              if(ret == -1)

                     perror("poll()");

              else if(ret > 0)

              {     

                     if((rfds[0].revents & POLLIN) == POLLIN)

                     {     

                            n = recv(connfd1, buff1, BUFFER_SIZE, 0);

                            buff1[n] = '\0';

                            printf("connfd1: %s\n", buff1);

                     }

                     if((rfds[1].revents & POLLIN) == POLLIN)

                     {

                            n = recv(connfd2, buff2, BUFFER_SIZE, 0);

                            buff2[n] = '\0';

                            printf("connfd2: %s\n", buff2);

                     }     

              }

              else

                     printf("time out\n");

       }



       return 0;

}

客户端

       代码参考select的客户端代码

epoll示例

接口函数

       epoll 是在内核 2.6 中提出的,是之前的 select() 和 poll() 的增强版本。相对于 select() 和 poll() 来说,epoll 更加灵活,没有描述符限制。epoll 使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次。主要有如下的接口函数:

 

   int epoll_create(int size); 

   功能:该函数生成一个 epoll 专用的文件描述符。

   参数size: 用来告诉内核这个监听的数目一共有多大,参数 size 并不是限制了 epoll 所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。自从 linux 2.6.8 之后,size 参数是被忽略的,也就是说可以填只有大于 0 的任意值。

   返回值:如果成功,返回poll 专用的文件描述符,否者失败,返回-1。

 

   int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

   功能:epoll 的事件注册函数,它不同于 select() 是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

   参数epfd:  epoll 专用的文件描述符,epoll_create()的返回值

   参数op:  表示动作,用三个宏来表示:

      EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;

      EPOLL_CTL_MOD:修改已经注册的fd的监听事件;

      EPOLL_CTL_DEL:从 epfd 中删除一个 fd;

   参数fd:  需要监听的文件描述符

   参数event:  告诉内核要监听什么事件,struct epoll_event 结构如:

      

   events 可以是以下几个宏的集合:

      EPOLLIN :表示对应的文件描述符可以读(包括对端 SOCKET 正常关闭);

      EPOLLOUT:表示对应的文件描述符可以写;

      EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);

      EPOLLERR:表示对应的文件描述符发生错误;

      EPOLLHUP:表示对应的文件描述符被挂断;

      EPOLLET :将 EPOLL 设为边缘触发(Edge Trigger)模式,这是相对于水平触发(Level Trigger)来说的。

      EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里

   返回值为0表示成功,为-1表示失败。

 

   int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); 

   功能:等待事件的产生,收集在 epoll 监控的事件中已经发送的事件,类似于 select() 调用。

   参数epfd:  epoll 专用的文件描述符,epoll_create()的返回值

   参数events:  分配好的 epoll_event 结构体数组,epoll 将会把发生的事件赋值到events 数组中(events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。

   参数maxevents:  maxevents 告之内核这个 events 有多少个 。

   参数timeout:  超时时间,单位为毫秒,为 -1 时,函数为阻塞。

   返回值:

      1)如果成功,表示返回需要处理的事件数目

      2)如果返回0,表示已超时

      3)如果返回-1,表示失败

      

   在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描。而 epoll() 事先通过 epoll_ctl() 来注册一个文件描述符,一旦某个文件描述符就绪时,内核会采用类似 callback 的回调机制(软件中断 ),迅速激活这个文件描述符,当进程调用 epoll_wait() 时便得到通知。

 

服务器端

#include <cstdio>

#include <sys/epoll.h>

#include <unistd.h>

#include <stdlib.h>

#include <cstring>

#include <cassert>

#include <sys/types.h>

#include <sys/socket.h>

#include <netinet/in.h>

#include <arpa/inet.h>



const int BUFFER_SIZE = 4096;

const int SERVER_PORT = 2222;



int main()

{

       int server_socket;

       char buff1[BUFFER_SIZE];

       char buff2[BUFFER_SIZE];

       struct timeval tv;

       int ret;

       int n, i;



       server_socket = socket(AF_INET, SOCK_STREAM, 0);

       assert(server_socket != -1);



       struct sockaddr_in server_addr;

       memset(&server_addr, 0, sizeof(server_addr));

       server_addr.sin_family = AF_INET;

       server_addr.sin_port = htons(SERVER_PORT);

       server_addr.sin_addr.s_addr = htonl(INADDR_ANY);



       assert(bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1);

       assert(listen(server_socket, 5) != -1);

      

       struct sockaddr_in client_addr1, client_addr2;

       socklen_t client_addr_len = sizeof(struct sockaddr_in);

      

       printf("waiting...\n");



       int connfd1 = accept(server_socket, (struct sockaddr*)&client_addr1, &client_addr_len);

       assert(connfd1 != -1);

       printf("connect from %s:%d\n", inet_ntoa(client_addr1.sin_addr), ntohs(client_addr1.sin_port));

       int connfd2 = accept(server_socket, (struct sockaddr*)&client_addr2, &client_addr_len);

       assert(connfd2 != -1);

       printf("connect from %s:%d\n", inet_ntoa(client_addr2.sin_addr), ntohs(client_addr2.sin_port));



       tv.tv_sec = 10;

       tv.tv_usec = 0;

      

       struct epoll_event event;

       struct epoll_event wait_event[2];

      

       int epfd = epoll_create(10);

       assert(epfd != -1);

      

       event.data.fd = connfd1;

       event.events = EPOLLIN;

       assert(epoll_ctl(epfd, EPOLL_CTL_ADD, connfd1, &event) != -1);

       event.data.fd = connfd2;

       event.events = EPOLLIN;

       assert(epoll_ctl(epfd, EPOLL_CTL_ADD, connfd2, &event) != -1);



      

       while(1)

       {

              printf("epoll...\n");

              ret = epoll_wait(epfd, wait_event, 2, -1);

             

              if(ret == -1)

                     perror("epoll()");

              else if(ret > 0)

              {     

                     for(i = 0; i < ret; ++i)

                     {

                            if(wait_event[i].data.fd == connfd1 && (wait_event[i].events & EPOLLIN) == EPOLLIN)

                            {     

                                   n = recv(connfd1, buff1, BUFFER_SIZE, 0);

                                   buff1[n] = '\0';

                                   printf("connfd1: %s\n", buff1);

                            }

                            else if(wait_event[i].data.fd == connfd2 && (wait_event[i].events & EPOLLIN) == EPOLLIN)

                            {

                                   n = recv(connfd2, buff2, BUFFER_SIZE, 0);

                                   buff2[n] = '\0';

                                   printf("connfd2: %s\n", buff2);

                            }     

                     }

              }

              else

                     printf("time out\n");

       }



       return 0;

}

客户端

       参考select的客户端代码。

 

   select() 和 poll() 函数返回后, 处理就绪 fd 的方法还是轮询,如下:

      

  而 epoll() 只需要从就绪链表中处理就绪的 fd:

      

   epoll没有最大文件数量限制,它所支持的FD上限是最大可以打开文件的数目,这个和系统限制有关,linux里面可以用ulimit查看文件打开数限制。

   但是epoll是 linux 特有的,而 select 和 poll 是在 POSIX 中规定的,跨平台支持更好。

 

    综上:select 、poll、epoll 的使用要根据具体的使用场合,并不是 epoll 的性能就一定好,因为回调函数也是有消耗的,当 socket 连接较少时或者是即使 socket 连接很多,但是连接基本都是活跃的情况下,select / poll 的性能与 epoll 是差不多的。即如果没有大量的 idle-connection 或者 dead-connection,epoll 的效率并不会比 select/poll 高很多,但是当遇到大量的 idle-connection,就会发现epoll 的效率大大高于 select/poll。

 

 

  参考博客:https://blog.csdn.net/woxiaohahaa/article/details/51498951

 

1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REAdMe.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看REAdMe.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看READme.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值