浅谈 I/O 与 I/O 多路复用
浅谈 I/O 与 I/O 多路复用
I/O操作是计算机系统中最基本的操作之一,特别是在网络编程中,I/O模型的选择直接影响着程序的性能和效率。本文将深入探讨I/O的基础概念、各种I/O模式以及I/O多路复用技术,帮助读者更好地理解这一核心概念。
1.基础知识
在讨论I/O模型之前,我们需要先回顾一些基础概念。
1.1 用户空间和内核空间
现代操作系统都采用虚拟寻址机制。在Linux环境下,32位系统的虚拟存储空间为4G,其中最高1G字节(从虚拟地址0xC0000000到0xFFFFFFFF)供内核使用,称为内核空间;较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF)供各个进程使用,称为用户空间。
1.2 进程上下文切换
进程切换是指操作系统挂起正在运行的进程并恢复以前挂起的进程的过程。这个过程包括保存当前进程的上下文、切换页全局目录以及恢复新进程的上下文。进程切换是一个资源密集型操作。
1.3 进程的阻塞
进程的阻塞是一种主动行为,当进程期待的某些事件未发生时,它会调用阻塞原语(Block)使自己进入阻塞状态。只有处于运行态的进程才能被阻塞,阻塞状态的进程不占用CPU资源。
1.4 文件描述符
文件描述符是一个非负整数,用于表示指向文件的引用。在Linux系统中,每个进程都有一个打开文件的记录表,通过文件描述符来访问这些文件。
1.5 直接I/O 和 缓存I/O
缓存I/O(标准I/O)在Linux中默认使用,数据会先被拷贝到进程的缓冲区,再拷贝到操作系统内核的缓冲区,最后写入存储设备。而直接I/O则省去了拷贝到进程缓冲区这一步。
2.I/O模式
对于一次I/O访问(以read为例),数据会经历两个阶段:等待数据准备和将数据从内核拷贝到进程。基于这两个阶段,Linux系统产生了五种网络模式的方案:
- 阻塞I/O(blocking IO)
- 非阻塞I/O(nonblocking IO)
- I/O多路复用(IO multiplexing)
- 信号驱动I/O(signal driven IO)
- 异步I/O(asynchronous IO)
由于signal driven IO在实际中并不常用,这里只讨论前四种I/O模型。
2.1 block I/O模型(阻塞I/O)
阻塞I/O模型中,进程发起read操作后会一直等待,直到数据从内核拷贝到用户空间。
2.2 non-block(非阻塞I/O模型)
非阻塞I/O模型中,如果数据没有准备好,系统会立即返回一个错误,进程可以再次发送read操作。这种模式下,用户进程需要不断主动询问数据是否准备好。
2.3 I/O多路复用
I/O多路复用使用select、poll、epoll监听多个io对象,当io对象有变化时通知用户进程。这种方式允许单个进程处理多个Socket。
2.4 asynchronous I/O(异步I/O)
真正的异步I/O中,进程发起read操作后立即返回,kernel在数据准备完成后将数据拷贝到用户内存,并通过信号通知进程。
2.5 小结
- blocking IO和non-blocking IO的区别在于是否在数据准备阶段阻塞进程。
- synchronous IO和asynchronous IO的区别在于是否在真正的IO操作(如recvfrom)中阻塞进程。
- non-blocking IO和asynchronous IO的主要区别在于数据拷贝是否需要进程主动操作。
3.I/O多路复用
I/O多路复用通过select、poll、epoll监听多个io对象,当io对象有变化时通知用户进程。
3.1 select
select是一个系统调用函数,可以监控一组文件描述符。其主要缺点包括需要每次传入fd数组、内核通过轮询检查就绪状态、返回就绪描述符个数需要用户遍历。
3.2 poll
poll与select的主要区别是去掉了1024个文件描述符的限制。
3.3 epoll
epoll解决了select和poll的一些问题:
- 内核保存一份文件描述符集合,无需用户每次都重新传入
- 内核通过异步IO事件唤醒,不再轮询
- 仅返回有IO事件的文件描述符,用户无需遍历整个集合
3.4 使用场景
I/O多路复用适用于以下场合:
- 处理多个描述字(交互式输入和网络套接口)
- 同时处理多个套接口
- 同时处理监听套接口和已连接套接口
- 同时处理TCP和UDP
- 处理多个服务或多个协议
与多进程和多线程技术相比,I/O多路复用的最大优势是系统开销小,不需要创建和维护额外的进程/线程。