I/O-多路复用

多路复用是解决的什么问题

解决的最根本的问题是: 我们怎么让我们的服务器能并发处理更多的数量的请求

最经典的问题就是C10K问题: 服务器怎么并发处理1w个请求

解决这个问题我们就需要考虑到, 连接占用的资源有哪些

  • 文件描述符: Socket实际上是一个虚拟的文件, 也就对应着有相应的文件描述符, 在Linux中一个进程能打开的文件描述符的数量是有限的, 一般来说是1024(默认值)
  • 系统内存: 每个TCP连接在内核中都有对应的数据结构, 也就是每个连接都占用了一定的内存

在这些基础上, 我们该怎么实现并发处理1w个请求呢?

多进程模型?

我们每成功建立一个连接就创建一个进程, 这个时候因为fork()创建的子进程中的文件描述符也是被继承过去, 让子进程来通过已连接Socket来提供服务

但是这种方式很明显是不能解决C10K问题的, 没有哪个系统扛得住创建1W个进程, 并且进程间切换的成本很高, 性能很差

多线程模型

为了解决多进程模型中, 进程的体量很大并且切换成本高的问题, 我们换成多线程模型

当服务器与客户端 TCP 完成连接后,通过 pthread_create() 函数创建线程,然后将「已连接 Socket」的文件描述符传递给线程函数,接着在线程里和客户端进行通信,从而达到并发处理的目的。

使用线程池避免频繁地创建和销毁线程, 维护一个已连接Socket队列, 每建立一个连接, 就将已连接Socket添加到队列中, 然后子线程负责从队列中取出来已连接Socket进行处理

但是同样是没有哪个操作系统能同时维护1w个线程, 也是不可行的

I/O 多路复用

为每个请求分配一个进程/线程不合适, 我们就只能使用一个进程来维护多个Socket, 这个就是I/O多路复用技术

这就像一个线程调度算法一样, 我们只有一个CPU, 但是我们通过线程调度, 提供了一种我们并发执行多个程序的视图

我们将处理每个请求的事件耗时控制在0.1ms内, 我们1s就能同时处理1w个请求, 拉长时间来看, 就是多个请求复用了一个进程, 也被称为时分多路复用

select/poll/epoll 内核提供给用户态的多路复用系统调用, 一个进程可以通过一个系统调用函数从内核中获取多个事件, 当其中任何一个文件描述符准备好IO操作的时候, 程序会被通知

它们是怎么获取网络事件的呢? 处理事件的时候, 先将所有的连接 (文件描述符) 传给内核, 再由内核返回产生了事件的连接, 然后在用户态处理这些连接对应的请求

select/poll

参考
小林coding-I/O 多路复用