现在位置: 首页 > 计算机组成原理 > 正文

I/O 的三种方式 -- 轮询、中断、DMA

上一讲我们了解了 CPU 和内存之间如何通过总线通信。但计算机不只是 CPU 加内存——它还需要和键盘、鼠标、硬盘、网卡等各种外部设备(I/O 设备)交换数据。

CPU 与外部设备交换数据有三种基本方式:轮询(Polling)中断(Interrupt)DMA(直接存储器访问)。三者的效率天差地别——选择哪种方式,往往决定了整个系统的性能表现。

本讲用三种生活场景帮你建立直觉,再用 Python 代码直观对比三种方式的效率差异。


三个生活场景类比

场景一:轮询——每隔 5 秒去看看水开了没

你在厨房烧水,但没有水壶哨子。你每隔 5 秒就从客厅跑到厨房看一眼:「水开了吗?水开了吗?水开了吗?」

在此期间,你完全没法安心看电视,因为每隔几秒就要跑一趟。你几乎把所有时间都花在了「检查状态」上。

这就是轮询:CPU 不断循环检查设备是否就绪,把时间都浪费在无意义的查询上。

场景二:中断——水壶哨子响了再去关火

这回水壶带哨子。你把水放上炉子后,回到客厅安心看电视。水烧开了,哨子尖锐地响起来。你暂停看电视,去厨房关火,然后继续看电视。

绝大多数时间都在做有意义的事情(看电视),只在关键时刻被打断一小会儿。

这就是中断:CPU 正常执行程序,设备就绪时主动发信号通知 CPU。CPU 暂停手头工作,处理设备数据,然后恢复之前的工作。

场景三:DMA——直接找搬家公司,你根本不用动手

你要搬家,有一整车家具要从旧房子运到新房子。如果一件一件自己搬(中断方式),你得来回跑几十趟。

你雇了一家搬家公司(DMA 控制器),告诉他们:「旧房子地址是 X,新房子地址是 Y,一共有 Z 件家具。」然后你继续忙自己的事。搬家公司搞定一切后,给你打个电话说「搬完了」。

这就是DMA:专门的 DMA 控制器直接管理内存和外设之间的数据传输,CPU 完全不用参与。传输完成后,DMA 控制器才给 CPU 发一个中断通知。


三种方式的本质区别

特性轮询 (Polling)中断 (Interrupt)DMA
CPU 参与度全程忙碌,不停检查传输时短暂参与几乎不参与,只收完成通知
数据谁搬CPU 亲自搬每一个字节CPU 亲自搬每一个字节DMA 控制器搬,CPU 不管
CPU 空闲时间几乎没有大部分时间空闲几乎全部时间空闲
实现复杂度最简单需要中断控制器硬件需要 DMA 控制器硬件
响应延迟取决于轮询间隔微秒级取决于 DMA 传输总时长
适用数据量极少量中小量(KB 级)大量(MB/GB 级)
典型场景简单嵌入式、极短等待键盘输入、鼠标移动、网络包到达磁盘读写、显卡帧缓冲、网卡大包

你每次敲键盘触发的是中断,你拷贝大文件时用的是 DMA。现代操作系统会根据数据量和场景,自动在三种方式之间选择最优方案。


时间轴可视化:CPU 忙碌与空闲的对比

假设需要从硬盘读取 4 个数据块到内存。我们用 ASCII 时间轴来直观对比三种方式下 CPU 的状态:

任务: 从硬盘读取 4 个数据块到内存

【轮询 - CPU 几乎全程忙碌】
时间: 0----1----2----3----4----5----6----7----8----9----10--->
CPU:  [查][查][搬][查][查][搬][查][查][搬][查][查][搬]
       忙  忙  忙  忙  忙  忙  忙  忙  忙  忙  忙  忙
                                   ↑ 几乎没有任何空闲

【中断 - CPU 大部分时间空闲】
时间: 0----1----2----3----4----5----6----7----8----9----10--->
CPU:  [工作][等待...][搬][工作]  [等待...][搬][工作]
       忙    闲       忙  忙      闲       忙  忙
                    ↑ 中断触发    ↑ 中断触发

【DMA - CPU 几乎全程空闲】
时间: 0----1----2----3----4----5----6----7----8----9----10--->
CPU:  [设置DMA][工作][工作][工作]      [被通知完成]
       忙       忙    忙    忙              忙
DMA:         [搬][搬][搬][搬]                  ↑ DMA 完成后中断
             忙  忙  忙  忙
                                  ↑ CPU 完全不管传输过程

一目了然:轮询下 CPU 被绑死,中断下 CPU 只被短暂打断,DMA 下 CPU 几乎完全解放。


代码演示:Python 模拟三种 I/O 方式并打印时间轴

下面这段代码分别用轮询、中断、DMA 三种方式模拟同一批数据的传输过程,统计 CPU 忙碌/空闲的时间占比,并用字符画时间轴直观展示差异。

实例

"""
I/O 三种方式对比:轮询 vs 中断 vs DMA(runoob 演示)

本程序模拟 CPU 从「外部设备」传输 10 个数据块到内存的场景,
分别用轮询、中断、DMA 三种方式完成,打印时间轴直观对比 CPU 状态。

核心指标: CPU 忙碌时间占比。占比越高,说明 CPU 被 I/O 拖累越严重。
"""


import time
import random


class IODevice:
    """模拟外部 I/O 设备(如硬盘、键盘等)"""

    def __init__(self, name="硬盘", response_time=4):
        """
        name: 设备名称
        response_time: 设备响应所需的「时间单位」数
        """

        self.name = name
        self.response_time = response_time  # 每个数据块的设备准备时间
        self.data_buffer = []               # 设备内部数据缓冲区
        self.ready = False                  # 设备是否就绪

    def prepare_data(self, block_id):
        """
        设备准备数据——模拟设备内部处理过程。
        实际硬件中这一步是硬盘寻道、网卡收包等操作。
        """

        self.ready = False
        data = f"DATA_BLOCK_{block_id:02d}"
        self.data_buffer.append(data)
        # 模拟设备在不同时间单位后准备好
        return data

    def is_ready(self):
        """CPU 轮询时检查此状态"""
        return self.ready

    def mark_ready(self):
        """设备完成准备,发送中断信号"""
        self.ready = True

    def get_data(self):
        """从设备缓冲区取走一个数据块"""
        if self.data_buffer:
            return self.data_buffer.pop(0)
        return None


class Timeline:
    """
    时间轴记录器——记录每个时间单位 CPU 和 DMA 的状态
    用于最终绘制 ASCII 时间轴
    """


    def __init__(self):
        self.cpu_states = []     # 每个时间单位 CPU 的状态: 'BUSY' 或 'IDLE'
        self.dma_states = []     # 每个时间单位 DMA 的状态
        self.events = []         # 事件日志: (时间, 描述)

    def record_cpu(self, state):
        self.cpu_states.append(state)

    def record_dma(self, state):
        self.dma_states.append(state)

    def log_event(self, time_unit, desc):
        self.events.append((time_unit, desc))

    def draw(self, title="时间轴"):
        """绘制 ASCII 时间轴"""
        total = len(self.cpu_states)
        if total == 0:
            print("  (无时间数据)")
            return

        print(f"\n  ╔══ {title} ══╗")
        print(f"  ║ 总时间单位: {total}")

        # CPU 状态行
        cpu_line = "CPU:  "
        for s in self.cpu_states:
            cpu_line += "[" + ("忙" if s == "BUSY" else "闲") + "]"
        print(f"  ║ {cpu_line}")

        # DMA 状态行(如果有 DMA 活动)
        if any(s == "BUSY" for s in self.dma_states):
            dma_line = "DMA:  "
            for s in self.dma_states:
                dma_line += "[" + ("忙" if s == "BUSY" else "闲") + "]"
            print(f"  ║ {dma_line}")

        # 事件日志
        if self.events:
            print(f"  ╠══ 事件日志 ══╣")
            for t, desc in self.events:
                print(f"  ║  T={t}: {desc}")

        # 统计汇总
        busy_count = sum(1 for s in self.cpu_states if s == "BUSY")
        idle_count = total - busy_count
        busy_pct = busy_count / total * 100 if total > 0 else 0
        idle_pct = idle_count / total * 100 if total > 0 else 0
        print(f"  ╠══ 统计 ══╣")
        print(f"  ║  CPU 忙碌: {busy_count}/{total} ({busy_pct:.0f}%)")
        print(f"  ║  CPU 空闲: {idle_count}/{total} ({idle_pct:.0f}%)")
        print(f"  ╚{'═' * 20}╝")

        return busy_pct


class Memory:
    """模拟主内存"""
    def __init__(self):
        self.data = {}

    def write(self, addr, data):
        self.data[addr] = data

    def __repr__(self):
        return f"Memory({len(self.data)} 条数据)"


class Controller:
    """CPU 控制器——执行不同 I/O 方式的模拟"""

    def __init__(self):
        self.memory = Memory()
        self.total_transferred = 0

    def simulate_polling(self, device, block_count):
        """
        轮询方式模拟:
        CPU 在每个时间单位都去检查设备是否就绪。
        如果就绪,搬一个数据块;否则继续检查。
        """

        print(f"\n{'#'*55}")
        print(f"# 方式一: 轮询 (Polling)")
        print(f"# 设备: {device.name} | 数据块数: {block_count}")
        print(f"# 策略: CPU 每个时间单位都检查设备状态")
        print(f"{'#'*55}")

        timeline = Timeline()
        time_unit = 0
        blocks_done = 0
        device_ready_at = 0  # 设备在哪个时间单位准备好下一块

        # 初始化: 设备开始准备第一块数据
        device.prepare_data(0)
        device_ready_at = time_unit + device.response_time

        while blocks_done < block_count:
            # CPU 检查设备状态(轮询)
            timeline.record_cpu("BUSY")
            timeline.record_dma("IDLE")

            if time_unit >= device_ready_at and device.is_ready():
                # 设备就绪 -> CPU 搬运数据
                data = device.get_data()
                self.memory.write(blocks_done, data)
                timeline.log_event(time_unit, f"CPU 搬运数据块 {blocks_done}: {data}")
                blocks_done += 1
                self.total_transferred += 1

                # 准备下一块数据(如果还有)
                if blocks_done < block_count:
                    device.prepare_data(blocks_done)
                    device_ready_at = time_unit + device.response_time
            else:
                # 设备未就绪 -> CPU 白跑一趟
                timeline.log_event(time_unit,
                    f"CPU 检查设备... {'就绪' if device.is_ready() else '未就绪'}")

            # 设备后台工作(如果是准备阶段)
            if time_unit == device_ready_at:
                device.mark_ready()
                timeline.log_event(time_unit, "设备准备完成,数据就绪")

            time_unit += 1

        print(f"\n  传输完成: {self.total_transferred} 块数据")
        busy_pct = timeline.draw("轮询方式时间轴")
        return busy_pct

    def simulate_interrupt(self, device, block_count):
        """
        中断方式模拟:
        CPU 大部分时间在执行其他任务(记为 IDLE),
        只在收到中断信号后暂停当前工作,处理数据传输,然后继续。
        """

        print(f"\n{'#'*55}")
        print(f"# 方式二: 中断 (Interrupt)")
        print(f"# 设备: {device.name} | 数据块数: {block_count}")
        print(f"# 策略: CPU 正常执行任务,设备就绪时发中断通知")
        print(f"{'#'*55}")

        timeline = Timeline()
        time_unit = 0
        blocks_done = 0
        transferred_in_interrupt = 0
        device_ready_at = 0

        # 初始化
        device.prepare_data(0)
        device_ready_at = time_unit + device.response_time

        while blocks_done < block_count:
            # 默认:CPU 在执行其他任务(空闲)
            if time_unit == device_ready_at:
                # 设备就绪,发中断信号
                device.mark_ready()
                timeline.log_event(time_unit, "中断触发!设备数据就绪")

                # CPU 响应中断,搬运数据(这段时间 CPU 忙碌)
                data = device.get_data()
                for substep in range(2):  # 中断处理需要 2 个时间单位
                    if substep == 0:
                        timeline.record_cpu("BUSY")  # 保存上下文,进入中断处理
                        timeline.record_dma("IDLE")
                        timeline.log_event(time_unit + substep, f"CPU 进入中断处理")
                    else:
                        timeline.record_cpu("BUSY")  # 搬运数据
                        timeline.record_dma("IDLE")
                        self.memory.write(blocks_done, data)
                        transferred_in_interrupt += 1
                        timeline.log_event(time_unit + substep, f"CPU 搬运数据块 {blocks_done}: {data}")
                        blocks_done += 1

                # 中断处理完成,CPU 恢复之前的工作
                timeline.record_cpu("BUSY")
                timeline.record_dma("IDLE")
                timeline.log_event(time_unit + 2, "CPU 恢复之前的工作")
                time_unit += 3  # 跳过中断处理占用的时间

                # 准备下一块
                if blocks_done < block_count:
                    device.prepare_data(blocks_done)
                    device_ready_at = time_unit + device.response_time
                continue

            # 正常执行阶段——CPU 处理自己的任务
            timeline.record_cpu("IDLE")
            timeline.record_dma("IDLE")
            timeline.log_event(time_unit, "CPU 执行其他任务 (空闲/用户程序)")

            time_unit += 1

        print(f"\n  传输完成: {self.total_transferred} 块数据")
        busy_pct = timeline.draw("中断方式时间轴")
        return busy_pct

    def simulate_dma(self, device, block_count):
        """
        DMA 方式模拟:
        CPU 仅在一开始配置 DMA 控制器,随后完全不管数据传输。
        DMA 控制器自主搬运所有数据块。
        搬运完成后,DMA 发中断通知 CPU。
        """

        print(f"\n{'#'*55}")
        print(f"# 方式三: DMA (直接存储器访问)")
        print(f"# 设备: {device.name} | 数据块数: {block_count}")
        print(f"# 策略: CPU 配置 DMA 后继续工作,DMA 控制器自动搬运数据")
        print(f"{'#'*55}")

        timeline = Timeline()
        time_unit = 0
        dma_busy_until = 0
        setup_done = False
        blocks_to_transfer = list(range(block_count))
        dma_buffer = []

        # 准备所有数据块
        for i in range(block_count):
            device.prepare_data(i)

        while blocks_to_transfer or dma_buffer:
            # 第一阶段: CPU 配置 DMA 控制器(仅一次,占用 2 个时间单位)
            if not setup_done:
                for substep in range(2):
                    timeline.record_cpu("BUSY")
                    timeline.record_dma("IDLE")
                    if substep == 0:
                        timeline.log_event(time_unit,
                            "CPU 配置 DMA: 设置源地址、目标地址、传输字节数")
                    else:
                        timeline.log_event(time_unit,
                            "CPU 启动 DMA 传输,然后继续自己的工作")
                    time_unit += 1
                setup_done = True
                dma_busy_until = time_unit + block_count * device.response_time
                # 标记设备就绪
                device.mark_ready()
                # 预填充 DMA 缓冲区
                while device.data_buffer:
                    dma_buffer.append(device.data_buffer.pop(0))
                continue

            # 第二阶段: DMA 控制器自主搬运数据,CPU 空闲
            if time_unit < dma_busy_until:
                timeline.record_cpu("IDLE")
                timeline.record_dma("BUSY")
                if dma_buffer:
                    data = dma_buffer.pop(0)
                    blocks_to_transfer.pop(0)
                    mem_addr = block_count - len(blocks_to_transfer) - 1
                    self.memory.write(mem_addr, data)
                    self.total_transferred += 1
                    timeline.log_event(time_unit, f"DMA 搬运数据块 {mem_addr}: {data}")
                else:
                    timeline.log_event(time_unit, "DMA 控制器工作中...")
            else:
                # 第三阶段: DMA 完成,发中断通知 CPU
                timeline.record_cpu("BUSY")
                timeline.record_dma("BUSY")
                timeline.log_event(time_unit, "DMA 传输完成,中断通知 CPU")
                timeline.record_cpu("BUSY")
                timeline.record_dma("IDLE")
                timeline.log_event(time_unit + 1, "CPU 确认传输完成")
                time_unit += 2
                break

            time_unit += 1

        print(f"\n  传输完成: {self.total_transferred} 块数据")
        busy_pct = timeline.draw("DMA 方式时间轴")
        return busy_pct


# ===== 主程序: 三种方式对比 =====
if __name__ == "__main__":
    print("=" * 60)
    print("    I/O 三种方式效率对比 —— RUNOOB 计算机组成原理演示")
    print("=" * 60)
    print()
    print("场景: 从外部设备传输 5 个数据块到内存")
    print("设备响应时间(每块数据准备所需时间单位): 4")
    print()

    BLOCK_COUNT = 5
    RESPONSE_TIME = 4

    results = {}

    # ---- 方式一: 轮询 ----
    dev1 = IODevice("硬盘", RESPONSE_TIME)
    ctrl1 = Controller()
    pct1 = ctrl1.simulate_polling(dev1, BLOCK_COUNT)
    results["轮询"] = pct1

    # ---- 方式二: 中断 ----
    dev2 = IODevice("硬盘", RESPONSE_TIME)
    ctrl2 = Controller()
    pct2 = ctrl2.simulate_interrupt(dev2, BLOCK_COUNT)
    results["中断"] = pct2

    # ---- 方式三: DMA ----
    dev3 = IODevice("硬盘", RESPONSE_TIME)
    ctrl3 = Controller()
    pct3 = ctrl3.simulate_dma(dev3, BLOCK_COUNT)
    results["DMA"] = pct3

    # ---- 最终对比 ----
    print("\n" + "=" * 60)
    print("           三种方式 CPU 忙碌时间占比对比")
    print("=" * 60)
    print(f"  轮询 (Polling):  CPU 忙碌 {results['轮询']:.0f}% 的时间")
    print(f"  中断 (Interrupt): CPU 忙碌 {results['中断']:.0f}% 的时间")
    print(f"  DMA:             CPU 忙碌 {results['DMA']:.0f}% 的时间")
    print()

    best = min(results, key=results.get)
    worst = max(results, key=results.get)
    print(f"  最优方式: {best} (CPU 占用最少)")
    print(f"  最差方式: {worst} (CPU 占用最多)")
    print()
    print(f"  结论: 对于大量数据传输,DMA 相比轮询的 CPU 占用降低了")
    print(f"        约 {results['轮询'] - results['DMA']:.0f} 个百分点。")
    print(f"        这就是为什么现代计算机传输大文件时使用 DMA。")
    print("=" * 60)

现实世界的实际应用场景

键盘输入——中断的典型例子

你每按下一个键,键盘控制器就向 CPU 发一个中断信号。CPU 暂停当前程序,读取按键码,然后恢复。这个过程快到让你觉察不到任何延迟。

如果键盘用轮询方式——CPU 每秒钟检查 1000 次「用户按键了吗?」——你的电脑就会卡死,什么也干不了。

大文件复制——DMA 的典型例子

你从 C 盘复制一个 2GB 的电影到 D 盘。硬盘控制器通过 DMA 直接向内存搬运数据,整个过程中 CPU 几乎不参与。你还可以同时刷网页、写代码,完全不受影响。

如果没有 DMA——CPU 要亲自搬运 2GB 的数据——在此期间 CPU 基本无法处理任何其他任务。

为什么轮询没有被淘汰?

轮询虽然效率最低,但它在极简单的嵌入式系统中仍然有用:

  • 硬件成本最低——不需要中断控制器和 DMA 控制器。
  • 对于「设备几乎瞬间就绪」的场景,轮询的开销与中断相当,但实现简单得多。
  • 某些实时系统中,轮询的确定性(精确知道何时检查)比中断的随机性更有优势。

小结与检验

一句话总结:轮询是 CPU 不停地问「好了没」,中断是设备主动喊「我好了」,DMA 是找个帮手替你干活——数据量越大,DMA 的优势越明显。

自测题

  1. 你正在用电脑写文档,同时后台在复制一个大文件。复制文件用的是哪种 I/O 方式?你敲键盘用的又是哪种?
  2. 如果一个设备的数据准备时间极短(比如只有 1 微秒),用轮询还会不会浪费 CPU?为什么?
  3. DMA 控制器完成数据传输后,怎样通知 CPU?它用的是什么机制?

参考答案:1. 文件复制用 DMA,键盘输入用中断。2. 仍然会浪费——因为轮询期间 CPU 无法做其他事,即使每次轮询很快,被占用的时间片也无法用于执行用户程序。但如果设备确实极快,轮询的实现简单性可能更有优势。3. DMA 完成后通过中断通知 CPU——这是 DMA 唯一需要 CPU 参与的时刻。