当前位置:首页>南京>南京大学操作系统原理2026 - 22输入输出设备和驱动程序

南京大学操作系统原理2026 - 22输入输出设备和驱动程序

  • 2026-06-22 10:06:24
南京大学操作系统原理2026 - 22输入输出设备和驱动程序

课程引入:从系统调用到 IO 设备

前面我们已经讲完了 Virtualization 和 Concurrency 两个部分。我们以经典的 UNIX 进程世界为例,从最底层的 System Call 开始一层一层往上构建,先有 libc,再在系统调用之上构建出整个 Linux 应用世界。讲完这个基本的 UNIX 世界之后,我们往外做了一点扩展,介绍了线程的 Spawn 和 Join 这组新的 API。结果这一讲反而打开了一个"魔鬼的盒子":从入门到放弃,我们后续讲了各种并发编程模型。虽然这些模型都可以用"创建一个线程"的方式去理解,但在并行计算、JavaScript 的事件循环计算,以及 GPU 的计算里,编程方式其实都不太一样——有的是为了性能,有的是为了照顾程序员的使用习惯,有的则是为了硬件实现的高效,所以才衍生出不同的编程模型。

讲完这些内容之后,我终于可以补上操作系统课里最后一块拼图:操作系统中的"对象"。前面讲系统调用的时候提到过,操作系统里有许多对象,我们用 open 打开它们,得到一个文件描述符。文件描述符是一个非常重要的概念,它是指向操作系统对象的一个 handle、一个把柄。在 UNIX 和 Linux 的设计里,"Everything is a file",所以各种各样的对象都可以用同一套 API 访问。比如我们有 procfs,如果想知道进程长什么样,可以去 procfs 里遍历目录;如果想知道一个进程的地址空间,可以去看 /proc/<pid>/maps 这个文件,把它读出来就行——也就是说,通过文件系统这一套 API,就可以访问几乎一切。

我们当时告诉大家:操作系统里有 TTY(字符终端)、有 Disk(磁盘),还有各种各样的对象,都可以直接拿来用。今天我要把这个黑盒子打开,展开讲一讲计算机系统里这些 IO 设备到底是怎么实现的,它们是怎么被抽象的,又是怎么进一步变成今天所说的文件系统的。所以今天正式开始 Persistence(持久化)部分的内容——严格来说,这一课和"持久化数据存储"本身关系不大,但我觉得有必要先讲一讲输入输出设备。

这里其实很有意思。你们第一次接触电脑,可能是上小学计算机课,或者家里有一台电脑的时候,看到的是键盘、屏幕,还有主机机箱,会让你不由自主地认为"这就是电脑"。但实际上,你和计算机打交道的时候,并不是直接在和 CPU、内存打交道:我们花了很多时间上数字逻辑电路、讲逻辑门,又花了很多时间讲计算机系统基础里的 CPU,但这些东西在你使用电脑的时候是看不见的。你能看见的所有东西,都是 IO 设备——你用键盘和鼠标操纵,你看到的是屏幕上显示的内容。我们这个专业学的是底下看不见的那部分东西,而看得见的部分——摄像头、鼠标、打印机这些——才是大家口语中"和电脑相关"的东西,它们统一有一个名字,叫输入输出设备。

为什么要叫输入输出设备?你们在《计算机系统基础》课上其实学过中断和 IO 那部分内容,IO 就是 Input and Output。所谓输入输出设备,是整个计算机(你看不见的那个部分)和物理世界之间的一座桥梁。一个 IO 设备的模型简单到不能再简单:它就是一个能与 CPU 交换数据的接口,或者说一个控制器。也就是说,任何一个你能想象到的东西,不管它多简单或多复杂,只要它能和 CPU 交换数据,它就可以成为一个 IO 设备。

既然叫 Input and Output,站在 CPU 的角度看,"输入"是往机器内送数据,"输出"是往机器外送数据;但站在机器外面的角度看,输入和输出又会反过来。所以这套术语是按照 CPU 的视角来定义的:这些输入输出设备,使得 CPU 能够获取来自系统外部的数据,也能把 CPU 算出来的数据推送到系统外部。换句话说,Everything is a state machine——我们写 NEMU 实验的时候有一个 CPU state,这个 CPU state 本身是不能干涉物理世界的,它只是一些寄存器和内存里的 0 和 1。

这有点像我们的大脑:大脑被封在颅骨里,如果我们想感受这个世界,需要用眼睛和耳朵;如果我们想改变这个世界,需要用手和脚。同样地,IO 设备就是计算机的手和脚。这也解释了为什么你们第一次用电脑时,实际打交道的都是各种 IO 设备。

IO 设备的本质模型

IO 设备的模型也可以很简单,基本上可以理解为一个 canonical device model(标准设备模型)。CPU 里面本身就有计算能力,你需要做算术、需要 load/store、需要往内存里读字或写字,你同样可以把 IO 设备看成一台"小计算机",它以某种总线的方式连接到你的计算机里。比如在 x86 里,你们学过 in 和 out 指令:你需要一个额外的地址,每个设备都有一个编号——1 号、2 号、3 号、4 号,你可以从 1 号设备读数据,往 2 号设备写数据。

你也可以用一个物理电路,在处理器执行 load/store 时做一个判断:如果这个内存地址落在某个范围(比如 0x123456),就去对应设备的"小计算机"里把数据取过来——这本质上就是一个多路选择器就能实现的事情。所以从 CPU 的角度看,无论这个设备有多复杂——可能复杂到是一块真正的显卡,也可能简单到是一个小小的 LED 灯——你都可以这样理解:它有好几个寄存器,CPU 可以访问这些寄存器。这个接口把设备本身的复杂性屏蔽掉了:一台打印机内部可能有很多机械部件,但 CPU 完全不需要看到这些,它看到的只是状态、命令和数据。

举个例子,一个设备至少需要三类寄存器,当然也可以有更多,但这三类基本就够用了:你可以从设备读一个 status 寄存器,知道它是不是 ready、是否在正常工作;你可以给设备发一个 command,比如"开始打印"或者"结束当前任务";你还可以读写 data 寄存器。道理就是这么简单。

当然,真正的 IO 设备往往还会向 CPU 发送中断。CPU 有一根中断线,比如时钟中断——时钟中断可能是 CPU 自己产生的,它能保证操作系统始终处于"霸权地位",不会被一个死循环的程序卡死。IO 设备在数据到达之后,也可以向 CPU 发一个中断,这些事情都是由操作系统来处理的。

你可以想象:CPU(比如我们 NEMU 模拟的那个)连了一根线到设备,设备上面有一些寄存器;CPU 也有一根中断线,设备会以某种方式接上这根中断线,比如给它一个低电平触发,CPU 就会响应中断。CPU 是怎么处理中断的呢?可以这样理解:操作系统本身也可以创建线程,比如创建一个叫 T-device 的线程,这个线程会 P 一个信号量(也就是 condition wait),等待中断数据到来。

中断处理程序在中断到来时会被强制跳转执行——如果处理器没有关中断,中断一来,处理器就会立刻跳转到一个预先配置好的地址,这个地址上是操作系统的代码。操作系统执行这段代码,相当于执行一次信号量的 V 操作,把 T-device 线程唤醒;中断返回之后,操作系统可以重新选择一个线程执行,这时候就可能选到刚被唤醒的 T-device 线程。原理大概是这样,实际的操作系统实现会稍微复杂一点,但思路上和你们写用户态多线程程序时,在中断里通知另一个线程"中断已到来"是类似的。

这就是 IO 设备的全部原理了——当然也没有讲完,我会从简单到复杂,带大家了解这样一个很薄的接口,是怎么变成今天各种各样的输入输出设备的。

案例一:LED 灯

第一个最简单的设备,是一个 LED 灯。LED 灯甚至只需要一个可写的寄存器就够了:如果 CPU 想控制一个灯,根本不需要中断线,只需要一个内存地址,这个地址映射到一个寄存器,寄存器直接连接一个发光二极管。给这个寄存器写 0 或 1,这个发光二极管就会熄灭或点亮,甚至可以让这个寄存器直接连一个电阻,再接到发光二极管上。这种事情在 PC 上比较难实现,但在树莓派上就容易得多,所以今天特意带了一些有趣的设备来演示(现场插上了一个 UVC 摄像头和一个放大型显微镜,并观察了电源指示灯)。

我写了一段控制 LED 灯的普通 C 代码,对应的文件系统路径是 /sys/class/leds/.../power。这在用户态看是一个文件,但实际上在内核里,它是一个内存映射的地址。我可以通过 write 系统调用,往这个文件对应的文件描述符里写一个字节,LED 灯就会闪烁——运行程序,灯确实开始闪了;关掉程序,灯就停止闪烁;调整闪烁频率(比如改成 1000),闪烁速度也会随之变化。

这就是一个非常典型的 IO 设备,它在底层实现上完美符合刚才讲的那个模型:就是一个寄存器。理论上,只要能实现这样的 IO 设备,就可以实现很多"有用"的东西,比如一个核弹发射器:在电脑上做一次密码验证,验证通过后给一个高电平,这个电平可以触发别的东西。理论上,你能驱动一个 LED 灯,就能驱动一个继电器,就能驱动任何一个马达或别的装置,因为你能控制 GPIO 上的电压是 0 还是 VCC(一般是 3.3 伏)。只要能控制一个小电压,就有办法用电路驱动物理世界中任何一个状态的变化——可以发射一个导弹,也可以打开一扇门,理论上这都和驱动树莓派上的一个 LED 灯没有本质区别。在这样一个简单的 canonical model 之上,我们可以做出越来越复杂的设备。

案例二:串口(UART)

比如串口,也就是 /dev/tty。我们前面讲终端的时候花了很大篇幅讲过,这个设备来自于 teletypewriter(电传打字机)的传统,我们可以在终端里发送 ANSI escape code,终端还支持很多有趣的功能,比如 Ctrl+C、job control,这些都和操作系统是联动的。

你们的电脑上其实都有终端,比如 PC 上有一个叫 COM1 的东西,它是一个 Universal Asynchronous Receiver Transmitter(UART)。在我的树莓派上也有一个 /dev/ttyS0(或者叫 /dev/ttyAMA0),这个串口是绑定到 GPIO 接口上的,就像绑在一块面包板上,接在两根细针脚上。

在 PC 上,理论上也是用类似的方式访问。x86 和 ARM、RISC-V 不一样,它有一个独立的 IO 地址空间:在 ARM 上,所有寄存器都直接映射到一个内存地址,直接读写那个地址就能访问寄存器;而 x86 有一个专门的 IO 地址空间,需要用 IN 和 OUT 指令去访问。COM1 的基础端口号是 0x3F8,要完成 UART 的配置,需要往 COM1+2、COM1+3、COM1+0 这些不同的寄存器里写入正确的值——就像前面说的,有 status、有 command、有 data。

配置完成之后,如果想写一个字符,实际上就是往 COM1 写一个 data,这个字符就会被送出去,送到终端;这和刚才把 0 或 1(高电平或低电平)写到 LED 寄存器是一回事。同样地,要从串口读数据,可以不断地从 COM1+5 这个寄存器里读,看是否有数据到来:如果有,就从 COM1 里读一个字节;如果没有,说明现在没有数据。理论上,你只要能实现一个 1 比特的寄存器,就能实现一个传输数据的设备。

有了这样的 UART,计算机就变得可用了——你可以用电路实现一个终端,电脑就可以真正用起来。其实你们在数字电路课和计算机系统基础课上做的那些小东西,可能觉得很简单、甚至有点"过度简化",但它和真正的计算机已经非常接近了:基本上,只要给它足够多的外部设备,它就是一台真正的计算机。

案例三:键盘与鼠标(PS/2)

你们还常用的设备是键盘和鼠标。现在的键盘鼠标可能更复杂一点,比如蓝牙无线鼠标这门课可能不会覆盖;这里的键盘是 USB 接口的,USB 有一根线。

在更早的 IBM PC 时代,还没有 USB 总线的时候,键盘控制器用的是另一种接口——一种六根线的接口,拆过老机器或者翻出过老式鼠标的同学可能见过。USB 接口里面其实也有引脚,应该是八根线;而 PS/2 接口一共六根线:Data(数据)、Clock、VCC、Ground(高低电平),还有两根预留、没有任何固定功能的线,可以自己定义。

PS/2 的 Data 线传输数据,会映射到两个 IO 地址空间端口:60 和 64(这是第一个 PS/2 接口),60 是 Data,64 是 Status 和 Command。比如可以写一些特殊字符给 Command 端口,来配置键盘和鼠标。

以前,键盘的行为和 TTY 比较像:按下一个键(比如 A)之后,它会每隔一小段时间不断发送这个按键(也就是按键重复),而且这个行为是可以配置的——按键重复的速率是写在键盘控制器里的。这样做可以减轻 CPU 的负担,因为那时候 CPU 速度很慢,大家希望尽可能把功能放到设备端实现。于是产生了一些特殊字符串或字节,用来配置键盘,比如让 Caps Lock 灯亮或灭,或者调整按键重复的延迟。

但现在,更多时候我们是直接把"按下"和"松开"两个事件发给软件,由软件决定要发送多少次重复的按键序列,这样操作系统就可以自己调整按键重复延迟——但以前确实是硬件实现这套逻辑的。所以你看,IO 设备其实没什么了不起,本质就是寄存器:只要有寄存器,就可以和另一个用电路实现的东西交换数据,从而实现 input 和 output。

案例四:磁盘控制器(ATA)

除了键盘以外,类似的还有 ATA 控制器(Advanced Technology Attachment),它连接到 IDE 磁盘接口——一根很粗的数据线,如果你们装过电脑,可能见过那种很胖的 40-pin 数据线。它映射到 x86 里的 0x1F0–0x1F7(或者 0x170–0x177)这组端口。

如果想从磁盘读数据,先要等磁盘 ready——同样是从 status 寄存器里读取这个状态;ready 之后,往正确的寄存器里写上正确的值,比如要读多少个 sector、扇区编号是多少,最后把读命令写到一个寄存器里,就可以开始读数据了,四个字节、四个字节地读出来。

这其实已经构成了一个经典"寄存器世界"的全部:键盘、鼠标、磁盘、终端,这些都是。当然还可以有一个显示器,这个我们一会儿再讲。

案例五:打印机——从绘图仪到 PostScript / PDF

这确实是一个完整的"寄存器体验",但如果想要真正和物理世界更丰富地交互,还会想要更多有趣的东西,比如打印机。有没有同学想过打印机本质上是什么?按照刚才的说法,任何 IO 设备在 CPU 眼里都是一堆寄存器,所以打印机也应该是一组寄存器——那么打印机收发的到底是什么数据呢?这个模型一定还是适用的。

网上有很多老的打印机文档和手册,非常值得花时间读一读,尤其是可以让 AI 帮你读。早期的打印机和今天的打印机很不一样——它不是基于点阵的打印方式。我们现在的打印机,不管是激光还是喷墨,都是用一个一个小点的方式把油墨打到纸上,干了之后就形成图形,但最早的打印机做不到这么精细,所以早期的打印机本质上就是一支笔,甚至可以叫"绘图仪"。

如果要在二维平面上画图,需要的就是几个自由度:一个打印头(或者一支笔),可以提起、放下,可以向左、向右、向上、向下移动,甚至同时按某个方向斜着移动——理论上这样就可以画出任何图形。回到 1950 年代设计一台打印机,大概也会得到这样的设计:用一个卷纸筒提供 Y 轴自由度(可以往上卷、往下卷),用一个机械臂提供 X 轴自由度。打印机只需要知道笔现在应该放下还是提起;如果放下,沿 X 方向以什么速度移动多少、沿 Y 方向以什么速度移动多少——这样就可以画出任意直线(任何直线都能分解成 X、Y 方向的速度分量),从而画出线段;再配合提笔、放笔,理论上就能画出任何图形。

这个模型真的可以画出任何图形,因为只要能画线,再内置一些字模(比如 A、B、C、D、E 这些字符的轮廓),本质上一台打印机就是一个"程序的解释器"。你需要在 CPU 上生成一个程序:这个程序可以由一些非常底层、近似机器代码的指令组成(比如"提笔""放笔""沿某方向移动"),也可以有更高级一点的指令,比如 line(x1, y1, x2, y2),或者 string(x1, ...)——这就是高级语言层面的程序。这个高级语言程序可以被编译成更低级的程序,也就是真正的提笔、放笔、X/Y 轴移动指令序列。

这套系统就和计算机联系起来了:计算机上有一个编译器,把高级语言转换成低级语言,再通过 data 寄存器把这个低级语言程序送给打印机。打印机里有一个缓存,它本身也是一台小计算机,它收到这个低级程序之后,执行这一串指令序列,就可以把图形真正打印出来。理论上,任何图形都可以这样打印出来——这是一个非常简单的想法,但非常 powerful。

随着 CPU 越来越强,打印机能执行的程序也越来越复杂,这就需要一种更强大的编程语言来描述要打印的内容,于是自然就有了像 PCL、PostScript 这样的语言。如果你们装过打印机,比如实验室的打印机,可能是一台兼容 PCL6 的打印机。在 PCL6 之前(比如 PCL5),打印命令大致是这样的:每次往 data 寄存器里送一段程序,程序由 ESC(escape)加一个命令构成,打印机支持各种命令,比如设置打印分辨率、设置打印模式、设置打印宽度等各种配置;也可以把一个 binary 的图像数据送过去,打印机收到后直接打印。

为了方便交换这种"可打印的文件",后来又有了 PostScript——它和今天的 SVG 很像。[已删去疑似串台内容:关于使用 AI 撰写论文、生成多选项文本风格的讨论]

PostScript 本身就是一个编程语言,甚至是图灵完备的:它可以循环,有一个叫 gstack 的状态机。它的本质,是用图形的状态机去构造路径——路径不一定是直线,也可以是二次曲线,这些曲线可以描述路径,甚至可以对路径做填充,这些都是该语言内置支持的功能,还可以设置字体、打印文字。

(老师当场用 DeepSeek 生成了一份 PostScript 文件,通过 ps2pdf 转成 PDF 打开演示,内容包含文字和图形。)这份 PostScript 可以精确描述一个矢量图形,可以无限放大依然保持精确,因此可以用来做极高质量的印刷。比如打印文字时,可以指定字体、设置字号(比如 28 号),设置字体后把打印头移到某个位置,用这个字体打印一个标题。

PostScript 在很长一段时间里都是打印行业的事实标准,直到 PDF 成为它的继任者——PDF 兼容性更好,而且去掉了 PostScript 里图灵完备的那部分,变成一个更安全的容器格式,但保留了很多 PostScript 描述图形的能力。现在有很多编译器,比如 LaTeX、Markdown,都可以通过编译器把内容编译成 PDF,再送到打印机里,现在很多打印机也支持直接打印 PDF 文件。

在更早的时候,甚至在还没有图形界面的年代,UNIX 系统里就已经有 lp 或 lpr 命令,可以直接把文件送到打印机。一切都是用编程语言完成的——写 LaTeX 的时候也没有图形界面,但通过一层一层的编译,最终编译成底层打印机能理解的语言,打印机就能帮你输出高质量的稿件。这其实是一件非常神奇的事情:一个命令,就可以把文字打印出来。

案例六:摄像头(UVC)

除了打印机,还有一个比较有意思的设备是摄像头。今天买到的所有 USB 摄像头(包括今天演示用的这个显微镜摄像头),基本上都遵循一个标准,叫 UVC(USB Video Class)。这个标准化协议规定了系统里的这类 USB 视频设备该如何工作。

在 Linux 里,你会看到 /dev/video0/dev/video1 这样的设备节点,摄像头会和操作系统(或应用程序)协商抓取图像的格式、分辨率、帧率、压缩方式等参数。(老师提到刚才演示时图像出现颜色异常,可能是因为这个摄像头用了非标准的山寨实现,没有完全遵循 UVC 标准协议导致的。)

协商完成后,传输就可以开始,UVC 提供逐帧读取数据的功能——一帧一帧、一张一张图片地读取,可以是未压缩的图像,也可以是压缩格式(比如 Motion JPEG 或 H.264)。但无论哪种格式,本质上仍然是字节流的交换;虽然经过了 USB,但数据交换的模式依然和"寄存器模式"是一致的。

今天看到的行车记录仪、显微镜、内窥镜、平板扫描仪,甚至有些"眼球鼠标"之类的设备,本质上都是 UVC 设备。这样设计的好处是,开发者只要买一个兼容 UVC 的摄像头,在 Windows 上开发软件就可以直接使用。打印机和摄像头算是相对有趣的两类设备。

让设备访问内存:DMA

当然,"有趣"是不够的。如果想借助 IO 机制完成更复杂的任务,本质上 IO 设备是 CPU 的手和脚,是 CPU 的延伸,要做一些 CPU 完成不了的事情。除了和另一个设备按协议交换数据之外,还可以做一件非常有趣的事情:让设备和 CPU 共享内存——也就是说,设备能不能和 CPU 接在同一个内存上?这当然可以,这就变成了"协处理器"。

当然,协处理器未必一定以共享内存的方式接入。比如早期的 CPU(比如 80386)是没有浮点指令的,它配了一个 80387 浮点协处理器,这个协处理器上有一些寄存器,CPU 通过寄存器去访问它。今天绝大部分协处理器会更复杂一些,以共享内存的方式接入内存。

一个早期的典型例子就是 DMA(Direct Memory Access)。可以把 DMA 理解成一个"CPU",但这个 CPU 不像我们熟悉的 CPU 那样能执行任意指令——它的指令集非常受限,甚至可以说没有指令集,只能做一件事。比如英特尔的 8237 DMA 芯片,一共有四个通道,它会不断轮询每个通道:如果某个通道还有待拷贝的字节数大于零,它就拷贝一个字节。

因为 DMA 和 CPU 共享内存,甚至可以共享 IO 端口地址空间,所以 DMA 本质上就是一个专门执行"内存拷贝"的小 CPU——可以是内存到内存、内存到端口、或者端口到内存的拷贝。这样就把 CPU 从繁琐、耗时的 IO 操作里解放出来了。比如前面提到的 ATA 磁盘读取:磁盘准备好数据后会放到自己的缓冲区里,CPU 原本需要四个字节、四个字节地从端口里读出来——如果要读 1MB 的数据,CPU 就要执行一个很大的循环;如果能把 CPU 从这个"搬运"任务里解放出来,只需要一个更小的"CPU"专门执行内存拷贝,CPU 的算力就释放出来了。

DMA 本身仍然表现为一组寄存器:CPU 可以读写这些寄存器,用来配置内存拷贝的地址、端口、拷贝的字节数,也可以通过这些寄存器控制 DMA 任务的开始和结束(比如取消一个正在进行的任务)。一旦启动,DMA 就会开始在内存与内存、或者内存与 IO 设备之间搬运数据,CPU 的算力就被释放出来了。所以,一旦发现一个 IO 设备不仅是一组寄存器协议,还能访问内存,就可以实现很多更复杂的功能。

GPU 与 NPU 作为协处理器

比如 IO 加速器、GPU 和 NPU。前面一次课讲过 GPU 编程:写一个 .cu 文件,可以理解成一个 C 程序,里面有一些 Kernel 运行在 GPU 上,然后用某个调用启动这个 Kernel,还有像 CUDA Memory Copy 这样的命令。

这时候 CPU 和 GPU 的关系就变得有意思了:GPU 自己有显存,它觉得 CPU 的内存太慢,不太想用,但实际上是可以共享内存的——可以在 CUDA host 端做一次内存分配,分配出和 GPU 共享的内存,但速度会慢一些。大多数时候,GPU 会认为自己的显存"比金子还贵",更快,所以它更想用自己的显存。

那这会变成什么样的流程呢?CPU 通过 GPU 的一些寄存器和它通信,告诉它要做的配置;然后把编译好的 GPU Kernel 代码,以及 CUDA Memory Copy 要传输的数据,都通过 DMA 搬运——因为这些数据一开始本来就在内存里,通过 DMA 送到显存里。等全部拷贝完成,CPU 再告诉 GPU"请运行显存里的这个程序",有点像启动一个程序一样。GPU 本身也是一台计算机,只不过它需要受 CPU 控制才能启动 Kernel 的执行,然后很多"T-Worker 线程"就启动起来了。

其他的加速器,比如 NPU——手机里高通处理器上就有一个 NPU 引擎——也是类似的逻辑。高通的 NPU 是共享内存的,不像 GPU 有独立显存:只需要告诉 NPU 要执行什么样的程序,它就会以共享内存的方式开始执行。

总线:PCIe 与 USB

讲了这么多设备,大家可能也意识到一个问题:每个设备都暴露了一组寄存器接口。在 IBM PC 时代,每个端口都是写死的——比如 0x3F8 是什么、0x60/0x64 是什么,每个端口都绑定到一个具体的设备。这是因为那个年代要做标准化,而且还没有那么多设备出现,没有人能预见到未来会有各式各样的设备。

那如果想兼容无限多种设备,有没有可能?也就是说,每个设备都是一组寄存器,能不能像虚拟地址空间一样,做一个统一的地址空间——不再是写死的 IO 端口,而是有一个地址空间 0、1、2、3、4、5……可以动态映射到一组寄存器,动态映射到进程的地址空间里?比如插入一个设备(打印机),它占用地址 2、3、4;再插入另一个设备,占用 6、7、8。

这就是总线。总线其实也是一种特殊的 IO 设备:它也是一组寄存器,但寄存器数量会比一个具体设备(比如摄像头)多一些,而且这些寄存器是可以动态分配的。总线有一组协议:CPU 只需要和总线对话,可以询问总线上有哪些设备(轮询总线),总线上设备的变化也可以触发中断通知 CPU;总线甚至自带 DMA——只要配置好,CPU 告诉总线的 DMA"从哪个端口开始,把哪些数据搬到内存的哪个位置",就可以完成搬运。所以总线就是这样一个"IO 设备的管理器",只有建立起这样的生态,硬件厂商才会持续生产兼容设备,这个生态才会越滚越大。

目前最知名的总线之一是 PCIe。CPU 上会有一些 PCIe lane,这就是 PCIe 总线的插槽;把显卡插到底,就完成了显卡上的寄存器和总线上寄存器的对齐,插好之后,总线会和设备进行几轮通信,把设备管理好。之后,CPU 就可以用内存映射 IO(memory-mapped IO)的方式访问这个设备了——不管插的是显卡还是网卡,都是构建出这样一条 IO 数据的寄存器通路,这就是总线的功能:总线把 CPU 这一侧的数据转发到相应设备上。实际上,前面提到的 Port IO(比如 ATA 磁盘访问、PS/2 接口的 IO)本身也算一种总线,总线是计算机里非常重要的一个设计。

大家用得更多的是 USB 总线。把 USB 设备插好之后,同样完成了上述这个过程。CPU 可以询问总线上有什么设备,比如可以用 lspci 命令列出 PCI 设备(在树莓派上 topology 会和 x86 系统不太一样);在 x86 系统里,USB 总线是挂在 PCI 总线上的——PCI 总线接了一个 USB 总线控制器,USB 还可以再往后接一层一层的设备。(老师现场用 lsusb -t 展示了一个树状的 USB 拓扑,插入并打开摄像头后,看到对应的端口、class=videodriver=uvcvideo,以及 480 Mbps 的传输速率。)总线支持设备的发现,也支持给设备命名——比如扫描 PCIe 总线时,可以从设备上得到一个设备号,通过设备号知道是哪个厂商生产的,从而加载相应的驱动,让设备正确运行起来。

总线其实是一个非常复杂的东西,历史上也有过不少"名场面"——比如比尔·盖茨(Bill Gates)曾在一次产品发布会现场演示插入 USB 设备时系统直接蓝屏崩溃,他当场打了一个圆场,说这就是为什么产品还没有正式发布。这是因为插入设备后,操作系统会收到一个中断,然后轮询总线,找出哪个设备是新插入的,再自动加载相应的驱动程序——而驱动程序里很可能存在 bug,或者存在时序上的问题,由于驱动是内核代码,一旦出错,整个系统就会崩溃。这种情况一直延续到 Windows 7 时代,微软才把整个 driver 子系统做了大规模重构。

如今,各种总线(PCI、USB)中,PCIe 因为速度快,在 PC 和其他设备(比如 AI 盒子)上都受到特殊优待。CPU 的 specification 会标明 CPU 上有多少条 PCIe lane(比如 40 条),会分成几部分——比如 16 条给显卡,4 条给 NVMe 等,这样每个高速设备都能享受到足够的带宽。总线和内存的连接由 CPU 负责,这里涉及到内存的一致性问题:CPU 有自己的 cache hierarchy,也会访问内存;PCIe 总线也可以直接访问内存,因为总线是可以 DMA 的、可以和内存直连的。为了保持整体一致性,主流做法是直接把 PCIe lane 放在 CPU 内部(有些系统则不太一样,内存和 PCIe 都挂在一个高速总线上,由这个总线统一完成 coherence),这些细节比较深入,目前不需要掌握。

现在的 PCIe 6.0 x16 带宽可以到 128 GB/s,也就是说可以得到一块 800 Gbps 的网卡——这是一个相当离谱的速度。总线自带 DMA,可以高速地搬运大量数据,比如游戏加载时那条很长的进度条,其实并不是 GPU 在花大量时间计算初始场景,而是需要把高清贴图等数据从硬盘读出来,再通过 DMA 传给游戏;如果有一块高速 SSD,加载速度会快很多,因为这中间确实需要在显存和内存之间搬运较多的数据(当然也会有一些计算耗时,但磁盘 IO 通常占用更多时间)。PCIe 还支持供电,默认支持 75 瓦的供电,如果显卡功率更高,就需要额外的 6-pin 或 8-pin 供电接口。现在像 FPGA、显卡、NVMe 这些真正高速的设备,都连在 PCIe 总线上。

CXL:更激进的内存共享

基于 PCIe 总线的物理层,还诞生了一种更"夸张"的协议,叫 CXL(Compute Express Link),可以认为它是数据中心里常用的下一代总线。它包含三种 protocol:IO、Cache、Memory——可以看出它的"野心":不再局限于像显卡、打印机这样的 canonical device model(设备本质上是一个相对慢的、和物理世界交互的东西,通过寄存器和 host/CPU 交换数据),而是希望总线可以直接和远端共享内存。它不仅可以在本地和显卡共享一块高带宽内存,最夸张的特性是可以共享另一台计算机上的内存,通过高速网络互联实现数据传送。

这就可以实现所谓的 disaggregated data center(解耦合数据中心):本地服务器的 CPU 有本地内存(因为只有本地内存访问才够快,所以本地不需要太大),但远端可能有 16 TB 的共享大内存——这块内存对本地 CPU 来说,每一段都可以像本地内存一样寻址访问。比如可以 allocate 4 TB 的内存,纳入自己的地址空间;如果命中本地 memory hierarchy 就走本地通路,如果是远端内存,可能会有一次网络的 round trip,带来几百纳秒级的延迟。只要程序写得足够好,就可以利用这种数据中心级别的 disaggregated memory,这也是相当"离谱"的事情。

正如上节课说的:真正走进系统内部之后,会发现教科书上学的很多东西"都错了",所以真正能留下来的,是分析这些问题的基本原理——它们是什么、应该怎么面对、出问题之后应该怎么诊断。这就是总线。我们讲了一些有意思的设备,它们本质上都没有区别,都是在交换数据;但只要能交换数据,就可以实现很多有趣的东西,比如打印机、摄像头,以及总线本身——这些都是真正让计算机系统世界繁荣起来的各种 IO 设备。

设备的文件抽象与设备驱动

以上都是底层视角:站在 CPU 的角度看,IO 设备就是一堆寄存器,可以读、可以写。但应用程序不应该直接访问这些寄存器——想一想,如果一个程序能访问某个设备的寄存器,另一个程序大概也想访问,比如 GPU 显然不是某一个程序独享的,每个程序都可能想显示图形。如果让每个程序自己直接访问这些寄存器(不管是 GPU、终端还是别的设备),那么这些资源就是共享的,程序之间就必须同步,不能"打架"。

这是因为这些设备暴露的接口非常底层:打印机的接口就是"给我一个程序,我就把它执行了""你可以问我现在是忙还是不忙"。如果两个程序同时想打印,它们就必须先后排队,因为打印机一次真的只能打印一个作业——这两个程序之间就需要同步;如果忘记同步,出现 data race,你写的数据会被我覆盖,我写的数据会被你覆盖,打出来的就是一堆乱码。你们也知道,如果把这种同步完全交给应用程序自己去做,程序员一定会写出 bug,甚至是会"要命"的并发 bug。

所以我们不应该让应用程序直接做这件事,而应该做一层抽象:用一组统一的 API 来访问这些对象。这组 API 大家已经很熟悉了——既然 everything is a file,那么设备也被抽象成了 file。这是大家非常熟悉的对象:在 /dev 目录下有很多设备,每一个设备都是一个 file,可以用文件的那一套 API:open、read、write;如果要互斥访问,可以用 flock 这样的机制给它上锁。

因为 everything is a file,只要实现了文件的操作,设备就可以被接入文件系统。所谓的"设备驱动程序",就是让硬件能够正常工作所依赖的那段代码。比如显卡没有驱动的时候,会有一个默认驱动,这个默认驱动的图形性能很差——它可能能正确显示图形界面,但鼠标移动会感觉到延迟,显示复杂图形时也会有明显的滞后,这是因为没有设备驱动时,CPU 不知道应该怎样正确地把这个设备的能力转换成统一的 API。

显卡这类设备非常重要,如果完全没有驱动就拒绝提供服务,那未安装驱动时屏幕会直接黑屏,什么都干不了。所以显卡通常会有一个"假装自己是一台老旧的标准 VGA 显示器"的兼容模式——这样不管是 N 卡、A 卡,还是某个国产第三方显卡,都可以工作在一种可能从 8086 时代就存在的标准模式下,比如把自己假装成一个大的数组——一个 frame buffer,每个像素对应一个颜色(比如 24 位真彩色,或者 256 色)。如果显卡默认不做任何配置就处于这个状态,那么所有操作系统都能识别这种显卡,至少可以显示一些基本图形——虽然性能会差一些,没有 DMA、没有三维显示,也不能像 GPGPU 一样加载 CUDA Kernel。直到加载了设备驱动之后,才能创建一个代表这块显卡真正能力的新设备,显示出高性能的图形。

所以本质上,因为 IO 设备的功能就是从里面读数据或者往里面写数据,把它抽象成文件是一件很自然的事情。当然,struct file_operations 这个结构本身定义了设备完成虚拟化之后的接口,内容非常庞大;对操作系统来说,围绕这套接口还有很多"二次开发",让设备变得更好用。比如在 Persistence 部分,会讲到怎么把磁盘抽象成一个文件——磁盘确实可以抽象成一个字节序列来访问(比如系统里的 MMC block,也就是 SD 卡;你们用的可能是更高速的 NVMe 磁盘)。但因为要在磁盘上构造文件系统才能更好地实现共享,所以对于比较复杂的设备,一般都会做这样的二次开发。

设备驱动所做的事情,就是把系统调用(比如对一个已经打开的设备文件做 read 或 write)翻译成设备能听懂的语言。比如某个设备要接收或读出数据,必须先等到 status 寄存器里有数据,或者设备 ready;设备驱动程序就会把这个等待翻译成一次中断等待,或者一次轮询(不断查询寄存器是否 ready),一旦 ready,就从里面取出数据返回给用户态。所以设备驱动程序本质上就是一个"翻译官":把文件系统层面的系统调用,翻译成设备能听懂的数据和动作,它就是一段非常普通的内核代码。

举例来说,Linux 源码里的 /dev/null 实现了 read 和 write:它的 read 直接返回 0,因为从这个空设备里读不出任何数据;它的 write 会直接返回"写入成功"的字节数(也就是系统调用里传入的 count),但不执行任何实际的数据处理——给它多少数据,它就告诉你写了多少,但这些数据全部消失,这就实现了一个"数据黑洞"式的设备。

案例:一个"核弹发射器"驱动

我们也可以自己实现一个设备驱动程序。这里给大家实现了之前承诺过的一个"核弹发射器"驱动——它会假装发射一枚核弹,也就是说,日志里能看到"好像核弹发射了"的记录。但理论上,这段代码真正能执行的效果,和一开始演示的点亮一个 GPIO 灯、打印一个字符是没有本质区别的,因为在内核里能访问所有内存映射的寄存器,这其中也包括那个对应 LED 灯的寄存器:只要能给寄存器一个电平,就可以把对物理世界的 side effect 不断传递下去。

这个驱动做了什么事呢?它有一个 launcher 的 read,还有一个 launcher 的 write,可以从这个名为"launcher"的设备里读数据,也可以往里面写数据。驱动里有一些初始化逻辑:在 init 时,会在 /dev 下注册这个设备;在 exit 时,做一些清理工作。

launcher_read 的实现很简单:每次读,都返回字符串 "this is dangerous"。因为 everything is a file,这个核弹发射器也是一个 file,从这个 file 里读数据,理论上可以送出任何东西——比如可以从核弹发射井的传感器里读取真实数据再返回,这里只是演示性地把字符串拷贝到用户态。

launcher_write 的实现会做一次密码校验:核弹不能像 LED 灯那样,写一个 1 就直接发射出去,所以需要先写入密码,由驱动做校验。(现实中除了软件校验,往往还会有物理上的双钥匙同时转动机制,以及内部的冗余逻辑,确保某些部件误短路时也不会错误触发发射,需要做很多检查。)这里的实现是一次简单的 memcmp:如果写入的密码正确,就触发发射逻辑。

以前写驱动是一件挺麻烦的事,但现在可以直接"vibe coding"——老代码在新内核上编译不过,直接让 AI 帮忙修复,很快就修好了。修好之后,用 insmod 加载这个驱动,没有任何问题;加载完后,/dev 下会多出对应的设备文件,用 ls 可以看到这是一个字符设备(character device)。

可以直接用 cat 命令打印这个设备,因为 cat 本质上执行的是 open(打开文件得到文件描述符)再 read——Linux 会把这次 read 转发到设备驱动程序的 launcher_read,于是终端里就打印出了 "this is dangerous"。如果往这个设备里写入一个错误的密码,内核日志会记录类似 incorrect secret, cannot launch 的信息(如果真的做一个核弹发射器,这里可以点亮一个红灯表示密码错误)。如果在用户态执行一个写入正确密码(硬编码在驱动里)的程序,执行一次 open + write,再看日志,就会看到刚才那次"核弹发射"确实被打印出来了——也就是密码正确时,"核弹"就发射了。

ioctl:隐藏在水面下的复杂性

这就是设备驱动程序的本质。所有设备驱动本质上都是这样:打印机本质上也是接收各种各样的命令,理论上可以封装成同样的接口。但你可能还是会觉得有点奇怪——struct file_operations 这套接口(read、write、mmap、flock 等)的基本假设,是这个设备本质上是一个字节流或字节序列。对终端这样的字符设备来说,它确实是字节流;对磁盘来说,它确实是字节序列,可以 mmap。但打印机、显卡这些设备,不仅仅是数据的载体——它们除了数据之外,还有一个很重要的功能是"控制"。

回到 canonical device 模型,设备有三类寄存器:status、command、data。"everything is a file" 的抽象,对应的主要是 data 这一类——比如打印机确实需要把一段程序从 data 寄存器送进去,GPU 确实需要把贴图和 Kernel 都送进去,所以 data 肯定是最核心的抽象。但"控制"怎么实现?比如想让打印机停下来,难道还是写数据吗?这就有点奇怪了——你总不能像 ANSI escape code 那样,硬编码一些特殊的控制字符串发给它。想让打印机停止打印,该怎么办?想让它亮一个灯,又该怎么办?

这就是 UNIX 世界里比较"dirty"的地方:有一个系统调用叫 ioctl(IO Control)。如果你查它的手册,会看到它的描述大意是"操控特殊文件底层的设备参数"——也就是说,这个 API 可以用来给设备发送一条设备相关的指令。手册还提到,字符设备(比如终端)的许多操作特性都可以通过 ioctl 请求来控制——比如终端想改变窗口大小、改变模式,都可以用 ioctl 实现。

这意味着,所有非数据类的设备功能,几乎全部都是用 ioctl 实现的。仅仅这一个 API,就承载了整个设备驱动世界里巨量的复杂性。ioctl 的参数是以"不加解释"的方式直接送给设备驱动的——应用程序可以构造一个奇怪的结构体,而总线和接口的设计者在设计的时候,甚至不知道未来会接入什么样的设备(这次可能是一块 GPU,下次可能是自己发明的 GPU,或者一个 GPU 和 FPGA 的联合体,又或者是一个核弹发射器)。

具体怎么和某个设备"对话",只有应用程序和对应的设备驱动知道。比如硬件厂商会发布一个配套的应用程序——支持灯效编程的鼠标往往会附带一个配置软件,配置完之后,这个软件会再和设备驱动通信,把配置真正落地生效。而所有这一切,都是用 ioctl 实现的:打印机的卡纸状态、清洁、自动装订;键盘的跑马灯(如果自己实现了一个 TTY 设备,本质上它还是要支持 read/write,但如果想加一个跑马灯功能,就必须用 ioctl,加一个特殊字段,当这个字段等于某个特殊值时,触发跑马灯的控制);磁盘的健康状况查询、缓存控制——这些配置全部都是用 ioctl 实现的。这是水面之下的一座冰山。

比如刚才展示的核弹发射器驱动是用 read/write 实现密码校验的,但完全也可以用 ioctl 来实现:用 ioctl 写入一个特定字符串向内核查询发射器状态,也可以用 ioctl 写入密码来触发发射。实际上,对于这种"控制类"功能,用 ioctl 才是更正确的方式;刚才那个驱动只是为了演示如何 override struct file_operations 里的 read/write,展示一种"可读写设备"的实现方式。

所以,ioctl 就是一个堆叠出来的"冰山",因为设备的复杂性是无法被降低的——比如显卡每一代新产品都有额外的新功能,每个功能在某种意义上都需要一个开关来控制,于是就需要在应用程序这一侧发布一个控制程序(比如允许打开或关闭某项画质增强功能),各种 feature 都可以这样打开和关闭。正因为系统有这么多功能,这其实是 UNIX 设计里一个很大的负担:ioctl 隐藏着非常复杂的、未公开的规范(hidden specification)。

你可能会觉得"everything is a file"是一个非常干净的抽象,但回头想想 procfs 里那成千上万个文件——一开始你可能觉得"Linux 系统调用总共也就 300 多个,做一个 Linux 兼容的操作系统应该不难"。这正是微软曾经想做 WSL(Windows Subsystem for Linux)的初衷:可以在 Windows 里直接启动一个 Windows Terminal 跑本地的 Linux。最早的时候,每一个跑在 WSL 里的 Linux 进程,其实都是作为一个 Windows 进程运行的:每次它调用 Linux 系统调用,Windows 都会悄悄把这个调用拦截下来,模拟执行,再把结果返回给它——这是一个非常天才的实现思路。

但正因为 UNIX 世界里(比如 ioctl、procfs)藏着太多隐藏细节,想做到 100% 兼容,在工程上几乎是不可能的。所以微软最后还是放弃了这个纯模拟的方案,转而在虚拟机里跑一个真正的 Linux 内核,才实现了完整的功能兼容。

像网卡、GPU 这类设备,是高度依赖 ioctl 系统调用的——它们绝大部分功能都是通过 ioctl 完成的。比如告诉内核"这段内存已经准备好了,可以做一次 CUDA Memory Copy 了",就是发一条 ioctl,把当前需要拷贝的 buffer 地址传给内核,内核里的驱动程序会据此启动 DMA,把这段 buffer 拷贝到 GPU 上——因为只有内核里的驱动才知道该怎么和 GPU 对话,该用什么样的命令、和哪些控制寄存器通信来完成这次内存拷贝。

案例:KVM 虚拟化

这里还有另一个例子(不过这个例子在"vibe coding"的时候还没来得及把所有 bug 改完,以前在 x86 上是可以跑的)。Linux 系统(以及 x86 系统)里有一个非常特别的设备,叫 /dev/kvm

还记得开学时实现的那个 "crazy-os" 吗?它的做法是:有一个进程,模拟一个 RV32IMA 的 step——这个进程每次往前执行一步,如果是 system call 就处理它,这是纯软件模拟的方式。而 /dev/kvm 是一个非常神奇的工具,它本身就是一个虚拟机。

使用方式大致是:先 open("/dev/kvm"),如果打开成功就拿到了一个文件描述符(打开失败说明系统不支持 KVM)。这个接口看起来很简单,但 ioctl 默默承担了操作系统里所有的复杂性。通过对这个 kvm 文件描述符执行 ioctl,可以调用 KVM_CREATE_VM 命令,内核会悄悄在内核里创建一个虚拟机。

创建好之后,还可以分配内存——比如在当前进程的地址空间里用 mmap 分配一块内存(比如 128 MB),然后通过 ioctl 调用 KVM_SET_USER_MEMORY_REGION,把这块内存交给虚拟机使用。接下来还可以创建一个(或多个)vCPU,做一大堆配置;可以把一段代码(code/data)拷贝到刚才那块进程内存里(这有点像前面 GPU 例子里把数据送进显存的过程),还可以初始化这个 vCPU 的寄存器状态。

真正神奇的地方在于:这个虚拟机是在操作系统内核里创建的,用户态程序不能直接访问或控制它,只能通过 ioctl 来控制——比如修改它的寄存器;进程里分配的那段内存,本身就属于这个虚拟机。当对 vCPU 的文件描述符执行 KVM_RUN 这个 ioctl 时,它执行的效果就好像直接在跑这台虚拟出来的 CPU:可以执行特权指令(比如关中断这种操作),理论上都可以执行,系统不会去干涉——执行"关中断",虚拟机内部的状态就确实是"已关闭中断",然后继续往前执行。

也就是说,一旦执行了 ioctl(vcpu_fd, KVM_RUN),当前进程就"变成"了这台虚拟机,开始连续执行指令。如果虚拟机内部执行到一些特殊情况——不是系统调用,而是比如要执行一次 IO 操作、或者触发 hypercall、或者 VM_EXIT、shutdown 等——由于这是一台虚拟机,没有真实的 IO 设备,无法真正执行 IO,这时就会触发一次 VM exit,流程会跳回到这个用户态进程继续执行,可以在代码里看到对应的 exit reason。如果是 MMIO(memory-mapped IO)相关的退出,就可以在用户态模拟一个虚拟 IO 设备,把对应的数据送回去。

所有这一切,只用了 ioctl 这一个 API,由此可以看出 ioctl 有多么复杂——整个虚拟化子系统,都是基于这一个 API 实现的。可以说,ioctl 承载了访问操作系统内部对象和子系统的全部复杂性,某种程度上,几乎所有"非标准化"的设备访问,都可以通过 ioctl 来完成。

案例:UVC 摄像头抓图与 strace 分析

讲到这里,理解了操作系统里的"对象":设备驱动程序实现的是 struct file_operations,最终应用程序通过设备驱动程序去访问实际设备的数据。这里还有一个演示例子,还是用那个 UVC 摄像头。(老师演示了一个抓图程序:运行 snapshot,从摄像头取一帧,保存为 capture.jpg。)这张抓到的图里,能看到键盘上 Caps Lock 锁定灯旁边的那个字母 "A",说明确实从摄像头里抓取到了一帧图像。这个程序基本是可以工作的,虽然图像本身因为这是个非标准 UVC 设备,色彩上有点问题。

这段代码是当天上午用五分钟"vibe coding"出来的,事先并不了解 UVC 协议细节,完全靠 AI 完成。做完之后,不要止步于"作业交了就行"——大家应该养成一个习惯:操作系统课上学到的很多工具,这时候就能用上了,把这个程序的系统调用序列用 strace 跟踪一遍,再让 AI 帮忙解释,就能搞清楚它到底是怎么工作的。

跟踪结果里能看到一个时序图:首先是 open("/dev/video0", ...),这确实印证了它就是 /dev/video0;接下来有一长串与摄像头交互的 ioctl 调用,这部分相对来说是 UVC 驱动给我们提供的能力,因为这个设备懂 UVC 协议(理论上,如果发送一个特定的结构体,设备就会返回一个对应的结构体,每个字段有特定含义)。比如可以查询设备支持哪些分辨率(query cap)、支持哪些帧率、支持哪些视频格式(s_fmt 等)。

接下来程序会用 mmap 分配一个缓冲区,再用 ioctl 告诉 UVC 驱动"缓冲区已经准备好了,如果有数据,请放到这个内存地址"。然后程序就等待数据流(stream)。这里用了 4 块缓冲区:有时候处理程序比较慢,有时候比较快;摄像头的数据节奏也会变化——这些都是由整个摄像头子系统统一调度处理的。在 stream on 之后,就可以从 buffer 里取数据,等待一帧准备好(同样通过 ioctl 实现),最后用一次 write 把 capture.jpg 写出来。

所以,Linux 的摄像头子系统(V4L2)主要不是靠 read/write 实现的,而是靠 ioctl 完成绝大部分"控制"层面的接口,数据接口部分则配合 mmap 和缓冲区机制完成,这是一个典型的生产者-消费者模型。类似的例子还有 GPU:GPU 是一个有自己内存的协处理器,它同样用 mmap,把内存交给设备驱动(甚至可以把进程自己的指针直接送给设备驱动),再通过 ioctl 提交命令(比如一个 doorbell 信号),让内核从这块内存里启动 GPU 的执行。本质上 GPU 也是一台处理器,空闲时什么也不做,必须接受 CPU 的指令才会加载并执行一个程序;在更上层,有 Direct Rendering Manager(DRM)、libdrm(用户态库,封装这些 ioctl),以及 Mesa(封装 OpenGL/Vulkan 实现)等,层层封装之后,再去提交真正的渲染命令——本质上,它和刚才 UVC 摄像头的例子是比较类似的。

这就是关于 IO 设备的全部内容。我们的每一个系统调用,不管是 read、write 还是别的,在操作系统里转一大圈,最终都会变成总线上连着的某个设备上的一个寄存器——写进去或者读出来,这就是计算机的本质,没有什么神秘的。你们可以从最简单的"寄存器"开始,一点一点往后想,就能做出很多有趣的东西。

延伸:USB OTG 与"假装成设备"

再举一个例子:树莓派的 USB 接口。它可以做普通的 USB 接口用,比如接一个 U 盘、一个鼠标,当鼠标键盘用;同时,这个 USB 接口还可以把自己"假装"成一个 USB 设备(USB OTG 模式)——可以用 CPU 控制,通过一根线接到另一台电脑上,把自己模拟成一个键盘或鼠标,假装这是一个 USB 键盘,把按键信息发送给那台电脑。

也就是说,即使树莓派本身没有接键盘,也可以写一个程序,不停地模拟按下 "AAA",通过 USB OTG 模式把这些按键数据按照 USB 键盘的协议格式发送出去——因为说到底,USB 设备本身也只是一组寄存器,只要"伪装"成对应的协议,数据就可以伪装成一个设备发给计算机。

这里面有很多可以玩的东西——比如,完全可以借此实现一个游戏外挂。

最新文章

随机文章

基本 文件 流程 错误 SQL 调试
  1. 请求信息 : 2026-06-24 15:43:49 HTTP/2.0 GET : https://b.460.net.cn/a/589542.html
  2. 运行时间 : 0.089518s [ 吞吐率:11.17req/s ] 内存消耗:4,565.18kb 文件加载:140
  3. 缓存信息 : 0 reads,0 writes
  4. 会话信息 : SESSION_ID=33d9089b107fb2b5cd2d60d0a9a93df8
  1. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/public/index.php ( 0.79 KB )
  2. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/autoload.php ( 0.17 KB )
  3. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/composer/autoload_real.php ( 2.49 KB )
  4. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/composer/platform_check.php ( 0.90 KB )
  5. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/composer/ClassLoader.php ( 14.03 KB )
  6. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/composer/autoload_static.php ( 4.90 KB )
  7. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-helper/src/helper.php ( 8.34 KB )
  8. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-validate/src/helper.php ( 2.19 KB )
  9. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/helper.php ( 1.47 KB )
  10. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/stubs/load_stubs.php ( 0.16 KB )
  11. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Exception.php ( 1.69 KB )
  12. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-container/src/Facade.php ( 2.71 KB )
  13. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/symfony/deprecation-contracts/function.php ( 0.99 KB )
  14. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/symfony/polyfill-mbstring/bootstrap.php ( 8.26 KB )
  15. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/symfony/polyfill-mbstring/bootstrap80.php ( 9.78 KB )
  16. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/symfony/var-dumper/Resources/functions/dump.php ( 1.49 KB )
  17. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-dumper/src/helper.php ( 0.18 KB )
  18. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/symfony/var-dumper/VarDumper.php ( 4.30 KB )
  19. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/App.php ( 15.30 KB )
  20. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-container/src/Container.php ( 15.76 KB )
  21. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/psr/container/src/ContainerInterface.php ( 1.02 KB )
  22. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/provider.php ( 0.19 KB )
  23. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Http.php ( 6.04 KB )
  24. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-helper/src/helper/Str.php ( 7.29 KB )
  25. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Env.php ( 4.68 KB )
  26. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/common.php ( 0.03 KB )
  27. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/helper.php ( 18.78 KB )
  28. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Config.php ( 5.54 KB )
  29. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/app.php ( 0.95 KB )
  30. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/cache.php ( 0.78 KB )
  31. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/console.php ( 0.23 KB )
  32. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/cookie.php ( 0.56 KB )
  33. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/database.php ( 2.47 KB )
  34. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/facade/Env.php ( 1.67 KB )
  35. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/filesystem.php ( 0.61 KB )
  36. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/lang.php ( 0.91 KB )
  37. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/log.php ( 1.35 KB )
  38. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/middleware.php ( 0.19 KB )
  39. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/route.php ( 1.89 KB )
  40. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/session.php ( 0.57 KB )
  41. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/trace.php ( 0.34 KB )
  42. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/config/view.php ( 0.82 KB )
  43. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/event.php ( 0.25 KB )
  44. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Event.php ( 7.67 KB )
  45. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/service.php ( 0.13 KB )
  46. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/AppService.php ( 0.26 KB )
  47. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Service.php ( 1.64 KB )
  48. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Lang.php ( 7.35 KB )
  49. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/lang/zh-cn.php ( 13.70 KB )
  50. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/initializer/Error.php ( 3.31 KB )
  51. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/initializer/RegisterService.php ( 1.33 KB )
  52. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/services.php ( 0.14 KB )
  53. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/service/PaginatorService.php ( 1.52 KB )
  54. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/service/ValidateService.php ( 0.99 KB )
  55. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/service/ModelService.php ( 2.04 KB )
  56. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-trace/src/Service.php ( 0.77 KB )
  57. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Middleware.php ( 6.72 KB )
  58. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/initializer/BootService.php ( 0.77 KB )
  59. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/Paginator.php ( 11.86 KB )
  60. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-validate/src/Validate.php ( 63.20 KB )
  61. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/Model.php ( 23.55 KB )
  62. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/model/concern/Attribute.php ( 21.05 KB )
  63. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/model/concern/AutoWriteData.php ( 4.21 KB )
  64. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/model/concern/Conversion.php ( 6.44 KB )
  65. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/model/concern/DbConnect.php ( 5.16 KB )
  66. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/model/concern/ModelEvent.php ( 2.33 KB )
  67. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/model/concern/RelationShip.php ( 28.29 KB )
  68. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-helper/src/contract/Arrayable.php ( 0.09 KB )
  69. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-helper/src/contract/Jsonable.php ( 0.13 KB )
  70. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/model/contract/Modelable.php ( 0.09 KB )
  71. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Db.php ( 2.88 KB )
  72. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/DbManager.php ( 8.52 KB )
  73. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Log.php ( 6.28 KB )
  74. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Manager.php ( 3.92 KB )
  75. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/psr/log/src/LoggerTrait.php ( 2.69 KB )
  76. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/psr/log/src/LoggerInterface.php ( 2.71 KB )
  77. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Cache.php ( 4.92 KB )
  78. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/psr/simple-cache/src/CacheInterface.php ( 4.71 KB )
  79. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-helper/src/helper/Arr.php ( 16.63 KB )
  80. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/cache/driver/File.php ( 7.84 KB )
  81. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/cache/Driver.php ( 9.03 KB )
  82. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/contract/CacheHandlerInterface.php ( 1.99 KB )
  83. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/Request.php ( 0.09 KB )
  84. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Request.php ( 55.78 KB )
  85. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/middleware.php ( 0.25 KB )
  86. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Pipeline.php ( 2.61 KB )
  87. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-trace/src/TraceDebug.php ( 3.40 KB )
  88. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/middleware/SessionInit.php ( 1.94 KB )
  89. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Session.php ( 1.80 KB )
  90. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/session/driver/File.php ( 6.27 KB )
  91. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/contract/SessionHandlerInterface.php ( 0.87 KB )
  92. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/session/Store.php ( 7.12 KB )
  93. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Route.php ( 23.73 KB )
  94. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/route/RuleName.php ( 5.75 KB )
  95. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/route/Domain.php ( 2.53 KB )
  96. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/route/RuleGroup.php ( 22.43 KB )
  97. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/route/Rule.php ( 26.95 KB )
  98. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/route/RuleItem.php ( 9.78 KB )
  99. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/route/app.php ( 1.72 KB )
  100. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/facade/Route.php ( 4.70 KB )
  101. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/route/dispatch/Controller.php ( 4.74 KB )
  102. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/route/Dispatch.php ( 10.44 KB )
  103. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/controller/Index.php ( 4.81 KB )
  104. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/app/BaseController.php ( 2.05 KB )
  105. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/facade/Db.php ( 0.93 KB )
  106. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/connector/Mysql.php ( 5.44 KB )
  107. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/PDOConnection.php ( 52.47 KB )
  108. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/Connection.php ( 8.39 KB )
  109. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/ConnectionInterface.php ( 4.57 KB )
  110. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/builder/Mysql.php ( 16.58 KB )
  111. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/Builder.php ( 24.06 KB )
  112. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/BaseBuilder.php ( 27.50 KB )
  113. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/Query.php ( 15.71 KB )
  114. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/BaseQuery.php ( 45.13 KB )
  115. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/TimeFieldQuery.php ( 7.43 KB )
  116. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/AggregateQuery.php ( 3.26 KB )
  117. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/ModelRelationQuery.php ( 20.07 KB )
  118. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/ParamsBind.php ( 3.66 KB )
  119. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/ResultOperation.php ( 7.01 KB )
  120. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/WhereQuery.php ( 19.37 KB )
  121. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/JoinAndViewQuery.php ( 7.11 KB )
  122. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/TableFieldInfo.php ( 2.63 KB )
  123. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-orm/src/db/concern/Transaction.php ( 2.77 KB )
  124. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/log/driver/File.php ( 5.96 KB )
  125. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/contract/LogHandlerInterface.php ( 0.86 KB )
  126. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/log/Channel.php ( 3.89 KB )
  127. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/event/LogRecord.php ( 1.02 KB )
  128. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-helper/src/Collection.php ( 16.47 KB )
  129. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/facade/View.php ( 1.70 KB )
  130. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/View.php ( 4.39 KB )
  131. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Response.php ( 8.81 KB )
  132. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/response/View.php ( 3.29 KB )
  133. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/Cookie.php ( 6.06 KB )
  134. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-view/src/Think.php ( 8.38 KB )
  135. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/framework/src/think/contract/TemplateHandlerInterface.php ( 1.60 KB )
  136. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-template/src/Template.php ( 46.61 KB )
  137. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-template/src/template/driver/File.php ( 2.41 KB )
  138. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-template/src/template/contract/DriverInterface.php ( 0.86 KB )
  139. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/runtime/temp/b35eef690f41e64ad9e1c098cfc7d3bc.php ( 11.98 KB )
  140. /yingpanguazai/ssd/ssd1/www/b.460.net.cn/vendor/topthink/think-trace/src/Html.php ( 4.42 KB )
  1. CONNECT:[ UseTime:0.000341s ] mysql:host=127.0.0.1;port=3306;dbname=b460;charset=utf8mb4
  2. SHOW FULL COLUMNS FROM `fenlei` [ RunTime:0.000680s ]
  3. SELECT * FROM `fenlei` WHERE `fid` = 0 [ RunTime:0.000299s ]
  4. SELECT * FROM `fenlei` WHERE `fid` = 63 [ RunTime:0.000980s ]
  5. SHOW FULL COLUMNS FROM `set` [ RunTime:0.000583s ]
  6. SELECT * FROM `set` [ RunTime:0.000973s ]
  7. SHOW FULL COLUMNS FROM `article` [ RunTime:0.000742s ]
  8. SELECT * FROM `article` WHERE `id` = 589542 LIMIT 1 [ RunTime:0.003224s ]
  9. UPDATE `article` SET `lasttime` = 1782287029 WHERE `id` = 589542 [ RunTime:0.000868s ]
  10. SELECT * FROM `fenlei` WHERE `id` = 65 LIMIT 1 [ RunTime:0.000272s ]
  11. SELECT * FROM `article` WHERE `id` < 589542 ORDER BY `id` DESC LIMIT 1 [ RunTime:0.001492s ]
  12. SELECT * FROM `article` WHERE `id` > 589542 ORDER BY `id` ASC LIMIT 1 [ RunTime:0.000471s ]
  13. SELECT * FROM `article` WHERE `id` < 589542 ORDER BY `id` DESC LIMIT 10 [ RunTime:0.001021s ]
  14. SELECT * FROM `article` WHERE `id` < 589542 ORDER BY `id` DESC LIMIT 10,10 [ RunTime:0.001356s ]
  15. SELECT * FROM `article` WHERE `id` < 589542 ORDER BY `id` DESC LIMIT 20,10 [ RunTime:0.003664s ]
0.091126s