操作系统I/O - 多路复用
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操作的时候, 程序会被通知
它们是怎么获取网络事件的呢? 处理事件的时候, 先将所有的连接 (文件描述符) 传给内核, 再由内核返回产生了事件的连接, 然后在用户态处理这些连接对应的请求