【IO多路复用】IO机制、select、poll、epoll、边缘触发、水平触发等重点知识汇总

目录

多路复用

Linux有哪些IO机制

select、poll、epoll 区别

select、poll、epoll 各自的优缺点

边缘触发ET和水平触发LT


多路复用

IO多路复用是一种处理多个IO流的技术。

它允许单个进程同时监视多个文件描述符,当一个或多个文件描述符准备好读或写时,它就可以立即响应。这种技术可以提高系统的并发性和响应能力,减少系统资源的浪费。

在Linux中,epoll、select、poll都是IO多路复用的实现方式,他们都可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。

select/poll/epoll 是如何获取网络事件的呢?在获取事件时,先把所有连接(文件描述符)传给内核,再由内核返回产生了事件的连接,然后在用户态中再处理这些连接对应的请求即可。

IO多路复用的优点包括:

  • 节省资源:不需要为每个连接创建一个线程或进程,因此节省了大量的系统资源,避免了线程或进程的开销。
  • 高性能:允许同时处理大量的IO操作,从而提高了系统的并发处理能力和性能。

Linux有哪些IO机制

阻塞IO:应用程序在进行IO操作时,会一直阻塞等待IO完成,期间无法进行其他操作。

非阻塞IO:应用程序在进行IO操作时,会立即返回,无论IO操作是否完成,应用程序都可以进行其他操作。需要通过轮询的方式来判断IO是否完成,因此效率较低。

IO多路复用:通过select、poll、epoll等系统调用,在一个进程中可以同时监控多个文件描述符,当有任何一个文件描述符就绪时,就可以进行IO操作。

信号驱动IO:应用程序在进行IO操作时,向内核注册一个信号处理函数,内核在IO完成时会向应用程序发送一个信号,应用程序收到信号后再进行数据处理。

异步IO:应用程序进行IO操作时,可以立即返回,内核负责将数据读取到指定的缓冲区中,并在完成后通知应用程序,应用程序可以继续进行其他操作。异步IO需要操作系统和硬件的支持,目前主要应用于高性能IO场景。

select、poll、epoll 区别

最基础的 TCP 的 Socket 编程,它是阻塞 I/O 模型,基本上只能一对一通信,那为了服务更多的客户端,我们需要改进网络 I/O 模型。

比较传统的方式是使用多进程/线程模型,每来一个客户端连接,就分配一个进程/线程,然后后续的读写都在对应的进程/线程,这种方式处理 100 个客户端没问题,但是当客户端增大到 10000 个时,10000 个进程/线程的调度、上下文切换以及它们占用的内存,都会成为瓶颈。

为了解决上面这个问题,就出现了 I/O 的多路复用,可以只在一个进程里处理多个文件的 I/O,Linux 下有三种提供 I/O 多路复用的 API,分别是:select、poll、epoll

select 和 poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的 Socket 集合。

select通常受到文件描述符数量的限制,由FD_SETSIZE(通常是1024)定义。

poll没有文件描述符数量的限制,可以处理比select更多的文件描述符。

在多个平台上都可用,具有较好的可移植性。

在使用的时候,首先需要把关注的 Socket 集合通过 select/poll 系统调用从用户态拷贝到内核态,然后由内核检测事件,当有网络事件产生时,内核需要遍历进程关注 Socket 集合,找到对应的 Socket,并设置其状态为可读/可写,然后把整个 Socket 集合从内核态拷贝到用户态,用户态还要继续遍历整个 Socket 集合找到可读/可写的 Socket,然后对其处理。

很明显发现,select 和 poll 的缺陷在于,当客户端越多,也就是 Socket 集合越大,Socket 集合的遍历和拷贝会带来很大的开销,因此也很难应对 C10K。

epoll 是解决 C10K 问题的利器,通过两个方面解决了 select/poll 的问题。

  • epoll 在内核里使用「红黑树」来关注进程所有待检测的 Socket,红黑树是个高效的数据结构,增删改一般时间复杂度是 O(logn),通过对这棵黑红树的管理,不需要像 select/poll 在每次操作时都传入整个 Socket 集合,减少了内核和用户空间大量的数据拷贝和内存分配。
  • epoll 使用事件驱动的机制,内核里维护了一个「链表」来记录就绪事件,只将有事件发生的 Socket 集合传递给应用程序,不需要像 select/poll 那样轮询扫描整个集合(包含有和无事件的 Socket ),大大提高了检测的效率。

仅在Linux系统上可用,不具有可移植性,但在Linux上性能卓越。适用于高并发的网络服务器

而且,epoll 支持边缘触发和水平触发的方式,而 select/poll 只支持水平触发,一般而言,边缘触发的方式会比水平触发的效率高。

那如果服务器的内存只有 2 GB,网卡是千兆的,能支持并发 1 万请求吗?

并发 1 万请求,也就是经典的 C10K 问题 ,C 是 Client 单词首字母缩写,C10K 就是单机同时处理 1 万个请求的问题。

select、poll、epoll 各自的优缺点

select优缺点

优点:

  • 在多个平台上都可用,具有较好的可移植性。
  • 适用于小规模连接,对于文件描述符数量不太多的情况性能还可以接受。
  • 简单易用,不需要特殊的数据结构。

缺点:

  • select() 检测数量有限制,最大值通常为 1024(bit),每一个比特位对应一个监听的文件描述符,如果您尝试监听超过这个数量的文件描述符,可能会导致未定义的行为或错误。
  • 一旦select调用返回,fd_set中的位将反映哪些文件描述符发生了IO事件,已经被内核修改。因此,您需要在每次调用select之前重新设置fd_set,以重新指定要监听的文件描述符。
  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大(时间复杂度是O(n))

poll优缺点

优点:

  • 类似于select,也具有一定的可移植性。
  • 没有文件描述符数量的限制,理论上可以处理更多的文件描述符。
  • 不需要像select一样重新设置文件描述符集合,因为它在每次调用时都使用数组。

缺点:

  • 每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
  • 不能直接返回哪些文件描述符发生了事件,需要在返回后遍历数组来查找。
  • 每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大(时间复杂度是O(n))
  • 随着文件描述符数量的增加,性能可能下降,因为每次调用poll仍然需要遍历整个文件描述符数组。
  • 不能直接返回哪些文件描述符发生了事件,需要在返回后遍历数组来查找。
  • 也可能受到高并发连接的限制。

epoll优缺点

优点:

  • 仅在Linux系统上可用,不具有可移植性,但在Linux上性能卓越。
  • 没有文件描述符数量的限制,可以处理大规模的并发连接。
  • 使用一个事件表来存储已注册的文件描述符和相关的IO事件,不需要在每次调用时重新设置监听。
  • 可以以水平触发(LT,Level-Triggered)或边缘触发(ET,Edge-Triggered)两种工作模式运行。
  • 返回只有发生IO事件的文件描述符,避免了不必要的遍历。
  • epoll底层数据结构很好,红黑树增删改综合效率高
  • 就绪的描述符的链表。当有的连接就绪的时候,内核会把就绪的连接放到 rdllist 链表里。这样应用进程只需要判断链表就能找出就绪进程,而不用去遍历整棵树。

缺点:

  • 不具备跨平台可移植性,仅适用于Linux系统。
  • 有时需要维护较复杂的数据结构来管理事件表。

边缘触发ET和水平触发LT

epoll 支持两种事件触发模式,分别是边缘触发(edge-triggered,ET)和水平触发(level-triggered,LT

  • 使用边缘触发模式时,当被监控的 Socket 描述符上有可读事件发生时,服务器端只会从 epoll_wait 中苏醒一次,即使进程没有调用 read 函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完;
  • 使用水平触发模式时,当被监控的 Socket 上有可读事件发生时,服务器端不断地从 epoll_wait 中苏醒,直到内核缓冲区数据被 read 函数读完才结束,目的是告诉我们有数据需要读取;

举个例子,你的快递被放到了一个快递箱里,如果快递箱只会通过短信通知你一次,即使你一直没有去取,它也不会再发送第二条短信提醒你,这个方式就是边缘触发;如果快递箱发现你的快递没有被取出,它就会不停地发短信通知你,直到你取出了快递,它才消停,这个就是水平触发的方式。

这就是两者的区别,

水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;

边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。

如果使用水平触发模式,当内核通知文件描述符可读写时,接下来还可以继续去检测它的状态,看它是否依然可读或可写。所以在收到通知后,没必要一次执行尽可能多的读写操作。

如果使用边缘触发模式,I/O 事件发生时只会通知一次,而且我们不知道到底能读写多少数据,所以在收到通知后应尽可能地读写数据,以免错失读写的机会。因此,我们会循环从文件描述符读写数据,那么如果文件描述符是阻塞的,没有数据可读写时,进程会阻塞在读写函数那里,程序就没办法继续往下执行。所以,边缘触发模式一般和非阻塞 I/O 搭配使用,程序会一直执行 I/O 操作,直到系统调用(如 read 和 write)返回错误,错误类型为EAGAINEWOULDBLOCK

一般来说,边缘触发的效率比水平触发的效率要高,因为边缘触发可以减少 epoll_wait 的系统调用次数,系统调用也是有一定的开销的的,毕竟也存在上下文的切换。

select/poll 只有水平触发模式,epoll 默认的触发模式是水平触发,但是可以根据应用场景设置为边缘触发模式。

另外,使用 I/O 多路复用时,最好搭配非阻塞 I/O 一起使用,Linux 手册关于 select 的内容中有如下说明:

在Linux下,select() 可能会将一个 socket 文件描述符报告为 "准备读取",而后续的读取块却没有。例如,当数据已经到达,但经检查后发现有错误的校验和而被丢弃时,就会发生这种情况。也有可能在其他情况下,文件描述符被错误地报告为就绪。因此,在不应该阻塞的 socket 上使用 O_NONBLOCK 可能更安全。

简单点理解,就是多路复用 API 返回的事件并不一定可读写的,如果使用阻塞 I/O, 那么在调用 read/write 时则会发生程序阻塞,因此最好搭配非阻塞 I/O,以便应对极少数的特殊情况。

你可能感兴趣的:(php,服务器,开发语言)