select poll epoll之间的区别

前导

首先在分析他们之前的区别前,需要明确几个概念。

  • 同步阻塞、
  • 同步非阻塞
  • 异步非阻塞

同步和异步的概念:

  • 同步是指用户线程发起IO请求后,需要等待或者轮询内核IO操作完成后才能继续执行;

  • 异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后会通知用户线程或者调用用户线程注册的回调函数。

阻塞和非阻塞的概念:

  • 阻塞是指IO操作需要彻底完成后才返回到用户空间;

  • 非阻塞是指IO操作被调用后立即返回给用户一个状态值,无需等到IO操作彻底完成。

I/O 同步和异步的区别在于:将数据从内核复制到用户空间时,用户进程是否会阻塞。

I/O 阻塞和非阻塞的区别在于:进程发起系统调用后,是会被挂起直到收到数据后在返回、还是立即返回成功或错误。

一般来讲一个IO分为两个阶段:

  • 等待数据到达;
  • 把数据从内核空间拷贝到用户空间(数据的拷贝过程)

现在假设一个进程/线程A,试图进行一次IO操作。

A发出IO请求,两种情况:

  • 立即返回;
  • 由于数据未准备好,需要等待,让出CPU给别的线程,自己睡眠挂起(sleep)

第一种情况就是非阻塞,A为了知道数据是否准备好,需要不停的询问,而在轮询的空歇期,理论上是可以干点别的活
第二种情况就是阻塞,A除了等待就不能做任何事情。

数据终于准备好了,A现在要把数据取回去,有几种做法:

  • A自己把数据从内核空间拷贝到用户空间。
  • A创建一个新线程(或者直接使用内核线程),这个新线程把数据从内核空间拷贝到用户空间。

第一种情况,所有的事情都是同一个线程做,叫做同步,有同步阻塞(BIO)同步非阻塞(NIO)
第二种情况,叫做异步,只有异步非阻塞(AIO)

文件描述符:系统为每一个进程维护了一个文件描述符表,表示该进程打开文件的记录表,而文件描述符实际上就是这张表的索引(下文代称fd)。

多路复用:为了实现一个服务器可以支持多个客户端连接。

socket 创建连接过程,并把tcp/udp包含地址、类型和通信协议等信息对底层的封装进行屏蔽,返回一个连接或者fd。socket 通信,实际上就是通过文件描述符 fd 读写文件,这也符合 Unix“一切皆文件”的哲学。

select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符fd,一旦某个描述符就绪(一般是读就绪或者写就绪),系统会通知有I/O事件发生了(不能定位是哪一个)。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。

区别与联系

select、poll和epoll这三个函数是Linux系统中I/O复用的系统调用函数。I/O复用使得这三个函数可以同时监听多个文件描述符(File Descriptor, FD),因为每个文件描述符相当于一个需要 I/O的“文件”,在socket中共用一个端口。

select

工作原理
select poll epoll之间的区别_第1张图片Select可以是非阻塞模型,非阻塞并不一定是异步模型,但异步模型一定是非阻塞的。利用 select 函数来判断某Socket上是否有数据可读,或者能否向一个套接字写入数据,防止程序在Socket处于阻塞模式中时,在一次 I/O 调用(如send或recv、accept等)过程中,被迫进入“锁定”状态;

优点:

  • select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。

缺点:

  • select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024(32位1024,64位2048),可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
  • 第二个缺点,对socket进行扫描时是线性扫描,即采用轮询的方法,效率较低
  • 第三个缺点,select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理,因此需要维护一个用来存放大量fd的数据结构(fd_set)。fd_set简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD,如果是1,那么表示这个FD有需要处理的I/O事件,否则没有,其是连续存储的。每次select查询都要遍历整个事件列表。这样会使得用户空间和内核空间在传递该结构时复制开销大。

poll

可以认为poll是一个增强版本的select,因为select的比特位操作决定了一次性最多处理的读或者写事件(文件描述符数量)只有1024个,而poll使用一个新的方式优化了这个模型。
poll底层操作的数据结构pollfd:

struct pollfd {
    int fd;          // 需要监视的文件描述符
    short events;    // 需要内核监视的事件
    short revents;   // 实际发生的事件
};

优点:

  • 在使用该结构的时候,不用进行比特位的操作,而是对事件本身进行操作就行。同时还可以自定义事件的类型。这样的好处是在内存中存放就不需要连续的内存地址,很像是list队列结构,读或者写事件数量(文件描述符数量)理论上是无限的,取决于内存的大小。
  • 它没有最大连接数的限制,原因是它是基于链表来存储的。

缺点:

  • 内核需要将消息传递到用户空间,都需要内核拷贝动作。需要维护一个用来存放大量fd的数据结构,使得用户空间和内核空间在传递该结构时复制开销大。大量的fd被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
  • poll还有一个特点是“水平触发”,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。

epoll

epoll给出了一个新的模式,直接申请一个epollfd的文件,对这些进行统一的管理,初步具有了面向对象的思维模式。可理解为event poll,epoll会把哪个流发生哪种I/O事件通知我们。所以epoll是事件驱动(每个事件关联fd)的,此时我们对这些流的操作都是有意义的。复杂度也降低到了O(1)。

epoll通过在Linux内核中申请一个简易的文件系统。把原先的select/poll调用分成了3个部分:

  • 调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源);
  • 调用epoll_ctl向epoll对象中添加这些连接的套接字;
  • 调用epoll_wait收集发生的事件的连接.

如此一来只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这些连接的句柄数据内核也不需要去遍历全部的连接

总结

select poll epoll之间的区别_第2张图片
select、poll、epoll 区别总结:

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

  • select:单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是3232,同理64位机器上FD_SETSIZE为3264),当然我们可以对进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。

  • poll:poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的。

  • epoll:虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。

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

  • select:因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。

  • poll:同上

  • epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。

3、 消息传递方式

  • select:内核需要将消息传递到用户空间,都需要内核拷贝动作

  • poll:同上

  • epoll:epoll通过内核和用户空间共享一块内存来实现的。

你可能感兴趣的:(I/O,poll,select,epoll)