1. 阻塞 I/O(Blocking I/O)
在阻塞 I/O 模型中,当应用程序发起 I/O 操作时,整个进程会被阻塞,直到操作完成。在这个过程中,应用程序无法执行其他任务,必须等待 I/O 操作的完成。
特点:
- 简单性:编程简单,逻辑清晰,容易理解和实现。
- 低效性:在高并发场景下,由于每个 I/O 操作都会阻塞整个进程,资源利用率较低。
2. 非阻塞 I/O(Non-blocking I/O)
非阻塞 I/O 模型允许应用程序在发起 I/O 操作时立即返回,即使数据尚未准备好。应用程序可以在等待 I/O 完成的同时执行其他任务,需通过轮询(多次尝试)来检查 I/O 是否完成。
特点:
- 并发性:在等待 I/O 完成时,应用程序可以继续处理其他任务。
- 轮询开销:需要频繁检查 I/O 状态,增加了 CPU 的负担。
3. I/O 多路复用(I/O Multiplexing)
I/O 多路复用(如 select
、poll
、epoll
)允许应用程序同时监听多个 I/O 事件,并在任何一个 I/O 操作准备好时被通知。应用程序可以集中处理多个 I/O 操作,从而避免轮询带来的开销。
特点:
- 高效性:适用于需要同时处理多个 I/O 连接的场景,尤其是高并发服务器。
- 复杂性:编程复杂度高,需要仔细管理多个 I/O 描述符和事件。
4. 信号驱动 I/O(Signal-driven I/O)
信号驱动 I/O 模型中,应用程序发起 I/O 操作并继续执行其他任务,当数据准备好时,内核会通过信号通知应用程序。这种方式允许应用程序避免轮询和阻塞,且能够异步处理 I/O 事件。
特点:
- 异步性:内核通过信号通知应用程序,无需轮询。
- 复杂性:信号处理逻辑复杂,容易出错。
5. 异步 I/O(Asynchronous I/O)
在异步 I/O 模型中,应用程序发起 I/O 操作并立即返回,I/O 操作由内核完成,操作完成后内核通过回调机制通知应用程序。应用程序无需等待 I/O 完成,也无需轮询或处理信号。
特点:
- 最高效:真正的异步模型,应用程序可以充分利用 CPU 时间。
- 复杂性:编程难度较大,需要处理异步回调和并发问题。
1. 多线程并发服务器
在多线程模型中,服务器为每个客户端连接创建一个独立的线程。每个线程处理客户端的请求,并将处理结果返回给客户端。由于线程是在同一进程内执行的,因此它们共享内存空间和其他资源。
工作流程:
- 主线程监听:服务器在指定端口上监听客户端连接请求。
- 接受连接:当有新的客户端连接时,服务器接受该连接,并为其创建一个新的线程。
- 线程处理:新线程负责处理该客户端的所有请求,直到客户端断开连接。线程可以读取客户端发送的数据、进行处理,并将结果发送回客户端。
- 线程终止:在处理完毕后,线程可以选择继续等待新的请求(长连接)或终止(短连接)。
优点:
- 资源共享:线程间共享同一进程的资源(如内存、文件描述符),使得在不同线程之间共享数据变得容易。
- 响应速度快:创建线程的开销相对较低,线程切换也比进程切换快,适合需要快速响应的场景。
缺点:
- 同步问题:由于线程共享同一地址空间,因此在访问共享资源时,容易出现数据竞争问题,需要使用同步机制(如互斥锁)来避免竞争条件,这会增加代码复杂度。
- 稳定性:一个线程崩溃可能会影响整个进程,因为所有线程共享同一进程空间。
使用场景:
- 高并发应用:适合需要处理大量并发连接的场景,如聊天室、实时通信系统。
- 轻量级任务:当每个请求的处理时间较短时,多线程模型能够有效提高处理效率。
2. 多进程并发服务器
在多进程模型中,服务器为每个客户端连接创建一个独立的进程。每个进程在自己的内存空间中运行,处理来自客户端的请求并返回结果。由于进程是独立的,数据不会在进程之间共享。
工作流程:
- 主进程监听:服务器在指定端口上监听客户端连接请求。
- 接受连接:当有新的客户端连接时,服务器接受该连接,并为其派生一个新的子进程。
- 进程处理:子进程独立运行,处理客户端的请求,直到客户端断开连接。子进程在处理过程中可以读取数据、进行处理,并返回结果。
- 进程终止:子进程处理完毕后终止,释放相关资源。
优点:
- 独立性强:每个进程都有独立的内存空间和资源,因此一个进程崩溃不会影响其他进程的运行,服务器整体的稳定性较高。
- 安全性高:由于进程间的数据不共享,因此天然避免了线程间的竞争条件和同步问题。
缺点:
- 资源开销大:创建和销毁进程的开销比线程大得多,尤其是在高并发场景下,进程的频繁创建和销毁可能会耗尽系统资源。
- 进程通信复杂:如果进程之间需要通信,必须使用 IPC 机制(如管道、消息队列、共享内存等),这增加了开发的复杂度。
使用场景:
- 高安全性应用:适合对安全性要求较高的场景,如需要严格隔离不同客户端的应用。
- 长时间任务:适合处理时间较长、复杂度较高的任务,因为进程之间相互独立,不会因为某个进程的长时间运行影响到其他任务的处理。
- 多线程并发服务器:适合轻量级、高并发的应用场景,能够快速响应请求,但需要注意线程同步问题和稳定性。
- 多进程并发服务器:适合高安全性、高稳定性的场景,尤其是需要隔离不同任务的应用,但进程开销较大,进程间通信较为复杂。
fcntl()
原型:
int fcntl(int fd, int cmd, ... /* arg */ );
用法:
- 首先,使用
fcntl()
函数获取文件描述符的当前标志。- 可以通过传递
F_GETFL
作为cmd
参数来实现。
- 可以通过传递
- 然后,将非阻塞标志
O_NONBLOCK
添加到当前标志中。 - 最后,再次使用
fcntl()
函数将新的标志设置回文件描述符。- 可以通过传递
F_SETFL
作为cmd
参数来实现
- 可以通过传递
- 首先,使用
实现信号驱动 I/O 主要依赖以下函数:
1. 设置文件描述符为信号驱动模式
要将文件描述符设置为信号驱动模式,可以使用 fcntl()
函数。
fcntl()
原型:
int fcntl(int fd, int cmd, ... /* arg */ );
用法:
- 首先,使用
fcntl()
函数获取文件描述符的当前标志。- 通过传递
F_GETFL
作为cmd
参数来获取当前标志。
- 通过传递
- 然后,将
O_ASYNC
标志添加到文件描述符的当前标志中。- 通过传递
F_SETFL
作为cmd
参数,并将O_ASYNC
与现有标志结合来实现。
- 通过传递
- 最后,使用
fcntl()
设置信号接收进程或进程组:- 通过传递
F_SETOWN
作为cmd
参数,并传递要接收信号的进程 ID 或进程组 ID。
- 通过传递
这样,当文件描述符有 I/O 事件发生时,系统将向指定进程发送
SIGIO
信号。- 首先,使用
2. 处理信号
在信号驱动 I/O 中,当文件描述符准备好时,会触发 SIGIO
信号。应用程序需要设置信号处理程序来处理该信号。
sigaction()
原型:
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
用法:
- 使用
sigaction()
函数为SIGIO
信号设置一个处理函数。 - 在信号处理函数中,应用程序可以执行相应的 I/O 操作(如读取或写入数据)。
通过
sigaction()
配置SIGIO
信号的处理程序后,应用程序在文件描述符准备好进行 I/O 操作时会自动收到通知并调用处理程序。- 使用