Lazy loaded image
操作系统丨当鼠标点击时,操作系统背后到底发生了什么?
字数 7669阅读时长 20 分钟
2025-12-21
type
Post
status
Published
date
Dec 21, 2025
slug
os1
summary
tags
技术探索
category
icon
password
我们每天都在用电脑。
点击鼠标、打开浏览器、播放视频、运行程序,这些操作看起来很自然。
但如果继续往下追问:
鼠标点下去之后,计算机内部到底发生了什么?
CPU 是怎么执行程序的?
操作系统为什么能同时运行多个程序?
普通程序为什么不能随便修改系统内存?
用户程序想读文件、访问网络,为什么必须经过操作系统?
这些问题背后,其实就是操作系统的核心工作。
要理解操作系统,不能一上来就背“进程、线程、内存管理、文件系统”。那样很容易变成概念堆砌。
更好的方式是从最底层开始看:
计算机是怎么执行一条指令的?

一、先从 CPU 说起:计算机到底是怎么“算”的?

打开一台计算机的机箱,可以看到主板、CPU、内存、硬盘等部件。
其中最核心的是 CPU。
CPU 内部有很多复杂结构,但如果先抓最关键的一点,可以把它理解成:
CPU 的核心工作,就是不断读取指令、执行指令、处理数据。
在 CPU 里,真正负责计算的是 算术逻辑单元,也就是 ALU。
ALU 可以做加法、减法、逻辑运算、移位等操作。它的工作方式可以简单理解成:
给它两个输入数据,再给它一个操作码,它就输出一个计算结果。
比如要计算:
6 + 7
CPU 可以把 6 和 7 送到 ALU 的两个输入端,再给 ALU 一个“加法”的操作码,ALU 经过电路计算后输出 13。
如果要计算:
8 - 3
那就把 8 和 3 送进去,同时给一个“减法”的操作码,ALU 输出 5。
不过计算机内部不会直接保存十进制的 6、7、13,而是使用二进制。
可以把二进制理解成开关:
通电表示 1,断电表示 0。
早期计算机甚至真的需要工程师手动插线,让不同的电路连接起来完成不同的计算。
这就带来一个问题:
ALU 计算很快,但如果每次计算都要人手动插线,计算机的性能根本发挥不出来。
所以,计算机需要一种办法,把人工插线变成自动执行。
这就是“程序”的起点。

二、程序是什么?本质上就是一组可以自动执行的指令

最早可以用纸带来实现自动输入。
纸带上有孔和没有孔两种状态:
有孔的地方可以让电路接通,表示 1;
没孔的地方不接通,表示 0。
只要把纸带上的不同位置连接到 ALU 的输入端和操作码端,纸带上的一行内容就可以变成一次计算任务。
纸带向前移动一行,计算机就执行下一条指令。
这就是程序执行最直观的样子:
纸带的一行,就是一条指令。
多条指令组合起来,就是一个程序。
后来,纸带变成了内存,机械推动变成了程序计数器,但核心思想没有变:
CPU 按顺序读取指令,然后一条一条执行。
每执行完一条指令,程序计数器就指向下一条指令。
如果遇到跳转指令,程序计数器就不再简单加一,而是跳到指定位置继续执行。
于是,程序就有了顺序、分支和循环。
比如:
这就是一个简单的循环。
为了方便人类阅读,二进制指令后来被转换成更容易理解的形式。
比如:
这就是汇编语言。
汇编语言本质上还是非常接近机器码,只不过把难读的二进制换成了人能看懂的助记符。

图组一:程序是怎么执行的?

notion image
这张图可以理解为程序执行的核心链路:
程序和数据先从磁盘加载到内存;
CPU 根据程序计数器找到当前指令;
指令告诉 CPU 要做什么操作;
数据从内存进入寄存器,再送到 ALU 计算;
计算结果写回寄存器或内存;
程序计数器继续指向下一条指令,或者根据跳转指令改变执行位置。
这就是程序执行的底层模型。

三、寄存器、内存和变量:程序的数据放在哪里?

ALU 可以计算,但计算结果总要保存起来。
这就需要存储数据的地方。
计算机里常见的存储位置有三个:
磁盘:容量大,但速度慢,用来长期保存程序和数据。
内存:速度比磁盘快,程序运行时会被加载到内存。
寄存器:在 CPU 内部,速度最快,用来临时保存 CPU 正在处理的数据。
程序执行时,先把程序和数据从磁盘加载到内存,再从内存读到寄存器,最后交给 ALU 计算。
比如一段简单的计算过程:
可以理解成:
先把数字 8 放到内存第 7 个位置;
再把内存第 7 个位置的数据复制到 AX 寄存器;
然后让 AX 里的值加上 5;
最后把结果写回内存第 7 个位置。
如果用高级语言来看,这其实就是:
所以,变量的本质并不神秘。
变量就是内存里的某个存储位置。
CPU 通过指令把数据读出来,计算完以后再写回去。
更重要的是,在冯诺依曼结构里:
程序指令和程序数据,本质上都存放在内存中。
CPU 既可以从内存中读取指令,也可以从内存中读取数据。
这也是现代计算机程序执行的基础。

四、分支和函数:高级语言底层是怎么实现的?

有了顺序执行还不够,程序还需要判断。
比如高级语言里的:
底层怎么实现?
CPU 里有一个 标志寄存器,用来保存 ALU 计算后的状态。
比如有一个常见的标志位叫 ZF,也就是零标志位。
如果两个数相减,结果是 0,那么 ZF 就会被设置为 1。
于是,判断逻辑就可以这样实现:
其中:
CMP 可以理解成一次比较,底层类似减法。
如果 x - 13 的结果是 0,ZF 就会变成 1。
JZ 表示“如果 ZF 为 1,就跳转”。
所以,if 的底层本质就是:
先比较,再根据标志位决定是否跳转。
函数调用也是类似的。
高级语言里写:
底层可能对应一次 CALL 指令。
CALL 和普通跳转很像,都会跳到函数所在的位置执行。
但它比普通跳转多做了一件事:
CALL 会记录函数执行完之后应该回到哪里。
函数执行结束后,再通过 RETURN 指令跳回原来的位置继续执行。
这样,程序就拥有了函数调用能力。
到这里,变量、计算、分支、循环、函数这些能力就都具备了。
也就是说:
只要有指令、内存、寄存器、ALU、跳转和保存现场的能力,就可以构造出我们熟悉的大多数程序逻辑。

五、为什么需要操作系统?

现在我们已经知道,一个程序可以被加载到内存,然后由 CPU 一条指令一条指令地执行。
但早期计算机还有一个很大的问题:
程序的加载、启动、停止,很多时候需要人工操作。
比如一个程序执行完了,再人工加载下一个程序。
这很浪费时间。
于是出现了 监控程序
监控程序负责把一批任务组织起来:
自动加载程序;
自动运行程序;
程序执行完后自动加载下一个程序;
输出执行结果。
这就是单道批处理系统。
所谓“单道”,意思是:
同一时间只有一个程序在运行。
单道批处理比人工操作进步了很多,但它还有一个严重问题:
如果当前程序在等待 IO,CPU 只能闲着。
比如一个程序需要等待打印机、磁盘、网络、水龙头这样的外部资源。
程序在等 IO 的时候,CPU 明明没事做,却不能去执行其他程序。
这就造成了资源浪费。
所以,操作系统继续演进,出现了多道批处理系统。
多道批处理的思想是:
内存里同时放多个程序。
如果一个程序等待 IO,就让出 CPU,切换到另一个程序继续执行。
这样 CPU 的利用率就提高了。
但是多道批处理还有一个问题:
如果一个程序一直占用 CPU,不主动等待 IO,其他程序就很难获得执行机会。
比如一个程序需要持续计算 5 个小时,它就可能一直霸占 CPU。
对于交互式系统来说,这是不可接受的。
用户点击鼠标、敲键盘、打开终端,都希望系统及时响应。
于是,分时操作系统出现了。
分时系统给每个任务分配一个很短的时间片。
时间片到了,不管当前任务有没有执行完,都要暂停,切换到下一个任务。
只要时间片足够短,人就会感觉多个程序好像在同时运行。
这就是并发的基本感觉。

图组二:操作系统为什么出现?

这张图可以总结操作系统调度的演化逻辑:
单道批处理解决了人工加载程序的问题;
多道批处理解决了 IO 等待时 CPU 空闲的问题;
分时系统解决了某个程序长期霸占 CPU 的问题。
所以,操作系统本质上是在解决一个问题:
如何更高效、更公平、更可控地管理 CPU、内存和外部设备。

六、任务切换是怎么做到的?

现在还有一个关键问题:
CPU 正在执行程序 A,怎么突然切换到程序 B?
程序 A 以后怎么从原来的地方继续执行?
答案是:保存现场,恢复现场。
程序运行时,CPU 里有很多重要状态。
比如:
当前执行到哪一条指令;
各个寄存器里保存了什么值;
标志寄存器的状态是什么;
当前使用的是哪段内存、哪个栈。
如果操作系统要暂停程序 A,就必须先把这些状态保存起来。
这个过程叫:
保护现场。
保存完以后,操作系统再加载程序 B 之前保存的状态。
这个过程叫:
恢复现场。
恢复完成后,CPU 就像从来没有离开过程序 B 一样,可以继续从程序 B 上次暂停的位置往下执行。
这个过程就是上下文切换。
上下文切换通常由中断、系统调用、IO 等待、时间片到期等事件触发。
比如:
程序 A 正在运行;
时间片到了;
CPU 触发时钟中断;
操作系统保存程序 A 的上下文;
调度器选择程序 B;
操作系统恢复程序 B 的上下文;
CPU 开始执行程序 B。
这就是多个程序能够“轮流使用 CPU”的根本原因。

图组三:任务是如何切换的?

上下文切换的核心是:
保存当前程序在 CPU 上的运行状态;
再恢复另一个程序之前保存的运行状态。
这就是为什么多个程序可以共享一个 CPU。
并发和并行也可以在这里顺便区分。
并发是:
一个 CPU 核心快速切换多个任务。
微观上是轮流执行,宏观上看起来像同时执行。
并行是:
多个 CPU 核心同时执行多个任务。
微观上也是真正同时执行。
所以:
并发强调任务调度。
并行强调硬件上真的有多个执行单元。

七、普通操作系统和实时操作系统有什么区别?

分时系统适合大多数个人计算机操作系统。
比如 Windows、macOS、Linux 桌面系统、Android 等。
它们追求的是:
用户体验好;
系统吞吐量高;
多个程序都能得到响应。
但它们不保证每个任务一定在某个严格时间内完成。
比如一个页面刷新本来 30 毫秒完成,有时候 100 毫秒完成,用户可能只是觉得卡了一下,但不会造成灾难。
然而在某些场景里,时间不确定是不可接受的。
比如汽车安全气囊。
如果事故发生后,安全气囊本应在几十毫秒内弹出,但因为系统正在调度其他任务,导致一分钟后才弹出,那就完全失去了意义。
这种场景需要实时操作系统。
实时操作系统追求的是:
任务执行时间可预测;
调度行为可控;
关键任务必须在规定时间内完成。
实时系统又可以分成两类:
类型
特点
例子
硬实时系统
错过截止时间会造成严重后果
汽车安全气囊、航空航天、医疗设备
软实时系统
偶尔超时可以接受,但体验会下降
视频播放、音频处理、部分交易系统
所以,不同操作系统的目标并不完全一样。
普通操作系统更关注吞吐量和交互体验。
实时操作系统更关注确定性和时间约束。

八、操作系统本身也是程序,为什么不会被普通程序破坏?

前面说过,程序运行时会被加载到内存。
但操作系统本身也是一个程序,它也在内存里。
这就带来一个危险问题:
如果用户程序可以随便访问内存,那它是不是可以直接修改操作系统的代码和数据?
如果可以,那系统就完全不安全了。
所以,操作系统必须保护自己。
要理解保护机制,需要先理解内存寻址。
内存可以看作一张很大的表。
每个位置都有一个地址,每个地址上可以保存数据。
CPU 想读取某个内存位置,就需要先给出地址。
早期 CPU 的地址寄存器位数有限,比如 16 位,只能表示:
如果每个地址保存 1 字节,那就只能访问 64KB 内存。
后来为了访问更大的内存,引入了段寄存器。
简单理解就是:
比如:
这种方式扩大了 CPU 可以访问的地址范围。
但是,仅仅有段式寻址还不够。
因为用户程序仍然可能访问操作系统所在的内存段。
所以,计算机又引入了保护模式。
保护模式的核心思想是:
给不同代码和内存区域设置不同权限。
低权限程序不能随便访问高权限区域。
现代操作系统一般主要使用两种权限:
内核态:权限高,操作系统内核运行在这里。
用户态:权限低,普通应用程序运行在这里。
普通用户程序不能直接执行特权指令,也不能直接访问内核内存。
比如:
修改中断表;
开关中断;
直接操作 IO 设备;
修改内核代码段。
这些操作只能由内核态完成。
如果用户态程序强行执行特权指令,CPU 会触发异常,交给操作系统处理。

图组四:操作系统如何保护自己?

notion image
这张图是全文的核心闭环。
用户程序不能直接操作系统资源。
如果它想读取文件、访问网络、操作设备,必须通过系统调用或者中断进入内核态。
进入内核态以后,由操作系统代替用户程序完成真正的资源访问。
这就是为什么普通程序不能随便破坏操作系统。
因为硬件和操作系统共同建立了一套保护机制:
用户态不能直接访问内核态资源;
用户态不能直接执行特权指令;
访问受保护资源必须通过系统调用;
系统调用入口也需要经过权限检查;
真正操作硬件和内核数据的是操作系统。

九、从鼠标点击回到操作系统

现在再回到最开始的问题:
当鼠标点击时,操作系统背后发生了什么?
如图所示:
notion image
所以,一个简单的鼠标点击背后,其实串起了很多操作系统机制:
硬件中断;
CPU 执行指令;
用户态和内核态切换;
操作系统调度;
内存保护;
应用程序事件处理。
我们平时看到的是一次点击。
但在计算机内部,它是一整套硬件和操作系统协作的结果。

十、总结:操作系统到底在做什么?

如果把整篇文章压缩成一句话:
操作系统的核心作用,就是在硬件之上管理程序运行,并通过调度、隔离和保护机制,让多个程序安全、高效地共享 CPU、内存、磁盘、网络和各种外部设备。
更具体地说,操作系统主要解决了四类问题。
第一,程序怎么运行。
程序从磁盘加载到内存;
CPU 读取指令;
寄存器保存临时数据;
ALU 负责计算;
程序计数器控制执行位置;
跳转指令实现分支、循环和函数调用。
第二,多个程序怎么共享 CPU。
单道批处理一次只运行一个程序;
多道批处理在 IO 等待时切换任务;
分时系统通过时间片避免程序长期霸占 CPU;
并发让多个程序看起来像同时运行。
第三,任务切换怎么实现。
操作系统保存当前任务的寄存器、PC、标志位、栈等上下文;
调度器选择另一个任务;
再恢复另一个任务的上下文;
CPU 从它上次暂停的位置继续执行。
第四,操作系统怎么保护自己。
普通程序运行在用户态;
操作系统内核运行在内核态;
用户态不能直接访问内核资源;
特权指令只能在内核态执行;
用户程序需要通过系统调用进入内核态,由操作系统代理访问文件、网络和设备。
所以,操作系统并不是一个抽象的黑盒。
它本质上就是一套管理机制:
管 CPU,让程序轮流执行;
管内存,让程序彼此隔离;
管设备,让用户程序不能乱操作硬件;
管权限,让系统本身不会被普通程序破坏。
理解了这些,再去学习进程、线程、虚拟内存、系统调用、文件系统、中断、调度算法,就不会只是背概念,而是能知道它们分别是在解决什么问题。

附:从鼠标点击到屏幕变化:一次点击背后的完整链路

现在再回到文章最开始的问题:
当鼠标点击时,操作系统背后到底发生了什么?
如果只用一句话概括,可以说:
一次鼠标点击,本质上是一次外部设备输入事件,经过硬件、中断、驱动、操作系统输入子系统、窗口系统、应用程序事件循环,再到图形系统渲染和屏幕显示的一整条链路。
把这个过程展开,大致会经历下面这些步骤。

1. 鼠标按键被按下,设备状态发生变化

当你按下鼠标左键时,最先发生变化的是鼠标这个硬件设备本身。
鼠标内部会检测到按键状态从“未按下”变成“已按下”,并把这个状态变化整理成一份输入数据。
如果是 USB 鼠标,这份数据通常会通过 USB 协议发送给计算机上的 USB 控制器。
也就是说,最开始并不是操作系统“知道你点了鼠标”,而是:
鼠标这个外设先感知到了状态变化,并把变化上报给设备控制器。

2. 设备控制器通过中断通知 CPU

USB 控制器接收到鼠标发来的数据后,会通知 CPU:
有新的输入事件到了,需要处理。
这个通知方式通常就是中断。
中断可以理解成:
CPU 本来正在执行别的程序,突然收到一个“外部设备有重要事情需要处理”的信号,于是要先暂停当前手头的工作,转去处理这个更紧急的事件。
所以这一步的核心是:
鼠标点击不是靠应用程序不停轮询出来的,而是硬件通过中断主动通知 CPU。

3. CPU 响应中断,切换到内核态执行中断处理程序

CPU 收到中断后,会先暂停当前正在执行的任务,保存现场,然后根据中断号找到对应的中断处理入口。
这时,CPU 会从用户态切换到内核态,开始执行操作系统内核中的中断处理程序。
到这里,前面文章里讲的几件事就串起来了:
  • 为什么要有中断机制
  • 为什么要保存现场、恢复现场
  • 为什么设备访问通常发生在内核态
  • 为什么用户程序不能直接操作硬件
因为真正处理硬件事件的,是操作系统内核,而不是普通应用程序。

4. 鼠标驱动读取设备数据,并解析成输入事件

进入内核态以后,真正和鼠标设备打交道的,通常不是“操作系统所有代码一起上”,而是专门负责鼠标的驱动程序。
驱动程序的作用可以理解成:
把具体硬件设备的原始数据,翻译成操作系统能理解的统一事件。
比如鼠标驱动会读取这次上报的数据,解析出:
  • 是左键还是右键
  • 是按下还是松开
  • 鼠标当前位置是多少
  • 有没有移动位移
于是,一份原始设备数据,就会被转换成更高层的输入事件,比如:
左键按下
鼠标移动到某个坐标
左键松开

5. 操作系统输入子系统把事件放入事件队列

驱动把数据解析出来后,并不会直接跑去通知某个具体应用程序。
更常见的做法是:
先把这次输入事件交给操作系统的输入子系统,再由输入子系统统一管理。
输入子系统可以把事件放进事件队列里,等待后续分发。
这里你可以把它理解成一个“统一收件处”:
不管是鼠标、键盘、触摸板,还是别的输入设备,都会先把事件交到这里,再由系统继续分发。
这样做的好处是,操作系统可以统一处理各种输入事件,而不是让每个应用程序直接面对硬件。

6. 窗口系统判断这次点击应该交给谁

有了输入事件之后,接下来要解决一个问题:
屏幕上可能同时开着很多窗口,这次鼠标点击到底点在了谁的身上?
这通常由窗口系统、桌面系统或者 GUI 框架来判断。
比如它会结合当前鼠标坐标、窗口层级、焦点状态等信息,判断:
  • 这次点击落在了哪个窗口上
  • 是点中了按钮、菜单、输入框,还是空白区域
  • 哪个应用程序应该接收这次点击事件
所以鼠标点击是:
操作系统或窗口系统先做命中测试,再把事件定向分发给目标窗口和目标控件。

7. 目标应用程序从事件循环中取到点击事件

确定目标之后,对应的应用程序就会在自己的事件循环里收到这个点击事件。
很多图形界面程序本质上都在做类似的事情:
不断从事件队列中取出事件 → 判断事件类型 → 执行对应逻辑。
比如如果点中的是一个按钮,应用程序可能会执行:
  • 打开菜单
  • 提交表单
  • 切换页面
  • 弹出对话框
  • 触发网络请求
这一步开始,才真正进入“应用程序逻辑”。
也就是说:
前面几步主要是硬件、CPU、操作系统和窗口系统在协作;
到这里,应用程序才正式接棒。

8. 应用程序请求重绘,图形系统把新画面显示到屏幕上

如果点击事件导致界面状态发生变化,比如按钮变色、菜单展开、页面切换,那么应用程序就需要请求重绘界面。
接下来,图形系统会根据新的界面状态重新组织绘制内容。
在现代系统里,这个过程往往还会涉及 GPU:
应用程序生成新的界面内容
→ 图形系统组织绘制
→ GPU 或显示子系统完成渲染
→ 最终把新的画面送到屏幕上显示出来
于是你就看到了:
鼠标点下去之后,按钮高亮了、菜单弹出来了,或者页面切换了。
这就是“界面发生变化”的真正来源。
回到首页