IO多路复用

Catalogue   

发展史

开始的时候,为了实现一个服务器可以支持多个客户端连接,人们想出了fork/thread等办法,当一个连接来到的时候,就fork/thread一个进程/线程去接收并且处理请求。

到了80年代,计算机网络开始成型,越来越多的用户进行网络连接,但是之前的fork/thread模型在高并发场景快不行了。1983年,发明了select。

对应的编程模型就是:一个连接来了,就必须遍历所有已经注册的文件描述符,来找到那个需要处理信息的文件描述符,如果已经注册了几万个文件描述符,那会因为遍历这些
已经注册的文件描述符,导致cpu爆炸。

到2002年,epoll出现了,于Linux 2.5.44首度登场。

基本概念

用户空间/内核空间

操作系统的核心是内核,为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操作系统将虚拟空间划分为两部分。

针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节
(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

同步与异步

  • 同步:同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回。
  • 异步:异步就是发一个调用后,立刻得到被调用者的回应表示已接收到请求,但是被调用者并没有返回结果,此时可以处理其他的请求,被调用者通常依靠事件、
    回调等机制来通知调用者其返回结果。

阻塞和非阻塞

  • 阻塞:阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续。
  • 非阻塞:非阻塞就是发起一个请求,调用者不用一直等着结果返回,可以先去干其他的事情。

进程切换

挂起正在CPU上运行的进程,并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。

从一个进程的运行转到另一个进程上运行,这个过程中经过下面这些变化:

  1. 保存处理机上下文,包括程序计数器和其他寄存器。
  2. 更新PCB信息。
  3. 把进程的PCB移入相应的队列,如就绪、在某事件阻塞等队列。
  4. 选择另一个进程执行,并更新其PCB。
  5. 更新内存管理的数据结构。
  6. 恢复处理机上下文。

文件描述符

File descriptor:表述指向文件的引用的抽象化概念。在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。
当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。

在实际编程中,不管是进行文件操作还是Socket编程,都是操作文件描述符。

缓存I/O

缓存I/O又称为标准I/O,大多数文件系统的默认I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存中,
即数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

具体实现

select

它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,
对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。

缺点

  • 单个进程所打开的FD是有限制的,通过 FD_SETSIZE 设置,默认1024 ;
  • 每次调用 select,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大;
  • 对 socket 扫描时是线性扫描,采用轮询的方法,效率较低(高并发)

poll

poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的。

缺点

它没有最大连接数的限制,但是同样有缺点:

  • 每次调用 poll ,都需要把 fd 集合从用户态拷贝到内核态,这个开销在 fd 很多时会很大;
  • 对 socket 扫描是线性扫描,采用轮询的方法,效率较低(高并发时)

epoll

epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上
事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))

参考