你的程序为何卡顿?从LINUX I/O三大模式寻找答案

I/O交互流程

在LINUX中,内核空间和用户空间都位于虚拟内存中。LINUX采用两级保护机制:0级供内核使用,3级供用户程序使用。每个进程都有独立的用户空间(0~3G),对其他进程不可见,而最高的1G虚拟内核空间则由所有进程和内核共享。

操作系统和驱动程序运行在内核空间,应用程序运行在用户空间。由于LINUX使用虚拟内存机制,两者之间不能直接通过指针传递数据。用户空间必须通过系统调用请求内核协助完成I/O操作。内核会为每个I/O设备维护缓冲区,而用户空间的数据可能被换出,因此内核无法直接使用用户空间的指针。

对于一个输入操作,进程发起I/O系统调用后,内核会先检查缓冲区是否有缓存数据。如果没有,则从设备读取数据;如果有,则直接将数据复制到用户空间。因此,网络输入操作通常分为两个阶段:

1)内核空间阶段:内核通过协议栈和设备驱动程序接收数据,并将其存储在内核缓冲区;

2)用户空间阶段:数据从内核缓冲区复制到用户进程的缓冲区后,用户进程即可处理这些数据。

I/O操作方式

在操作系统中,通常有三种主要的I/O操作方式,每种方式都有其独特的特性和适用场景。

阻塞I/O

阻塞I/O(Blocking I/O)是最简单的I/O模型。当进程发起I/O操作(如read或write)时,当前线程会被阻塞,直到I/O操作完成。这种模型是标准的同步I/O实现,例如POSIX标准中的默认read和write系统调用。

阻塞I/O的优点是实现简单,适合低并发的场景,因为内核已经对这些系统调用进行了高度优化。然而,在并发场景下,阻塞I/O的性能瓶颈会显现出来:每个I/O操作都会阻塞一个线程,导致内核需要频繁地进行线程切换,这会增加上下文切换的开销,降低处理器缓存的利用率,并可能使依赖线程本地存储(Thread-Local Storage, TLS)的代码性能下降。

cpp 复制代码
// 伪代码: 阻塞I/O
socket = accept(); // 阻塞,直到新连接到达
data = read(socket); // 阻塞,直到数据被读取
process(data);

非阻塞I/O

非阻塞I/O(Non-Blocking I/O)允许I/O操作在没有数据可用时立即返回,而不会阻塞执行线程。在非阻塞I/O模式下,如果数据未准备好,系统通常会返回一个错误码(如EAGAIN 或 EWOULDBLOCK),指示操作需要稍后重试。进程可以通过轮询监控多个文件描述符的就绪状态。

非阻塞I/O的优点是提高程序的并发性,因为它允许线程在等待I/O操作完成的同时,执行其他任务。然而,这种模式也带来了更高的编程复杂度,程序需要不断检查文件描述符的状态,以确保在数据可用时及时处理。这种轮询机制不仅增加了代码的复杂性,还可能导致处理器资源的浪费。

以下伪代码,展示了非阻塞I/O的执行过程。

cpp 复制代码
// 伪代码: 非阻塞I/O
while (true) {
    data = read(socket);
    if (data != EAGAIN) {
        process(data);
        break;
    }
    // do other things...
}

异步 I/O

异步I/O (Asynchronous I/O)是一种真正的异步模型,进程在发起 I/O 操作后立即返回,并通过回调函数或事件通知机制在操作完成后得到通知。典型的实现包括Windows的OVERLAPPED和I/O完成端口(IOCP),以及LINUX的原生异步I/O(AIO)。需要注意的是,LINUX的原生AIO 仅对文件I/O有效,对网络I/O的支持有限。

异步I/O的优点是能够最大限度地提高并发性能,同时减少线程阻塞和上下文切换的开销。然而,异步I/O的实现和调试复杂度较高,且在某些平台上的支持不够完善。

cpp 复制代码
// 伪代码: 异步I/O
// 定义一个I/O操作完成后的回调函数
void on_read_complete(data, error) {
    if (error) {
        handle_error(error);
    } else {
        process(data);
    }
    // 可以在回调中发起下一次异步读
    aio_read(socket, buffer, on_read_complete);
}

// 1. 主程序发起异步读操作,并注册回调函数
// aio_read会立即返回,不会阻塞
aio_read(socket1, buffer1, on_read_complete);
aio_read(socket2, buffer2, on_read_complete);

// 2. 主线程可以继续执行其他任务,或进入一个等待退出的循环
do_other_work();
event_loop_wait_for_shutdown(); // 例如,等待信号

未完待续

很高兴与你相遇!如果你喜欢本文内容,记得关注哦

相关推荐
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush43 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5203 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩3 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言