目录
4689 字
23 分钟
打印机:程序员最持久的敌人

一、能调分布式系统的人,为什么搞不定一台打印机#

面试的时候,我们可以聊 CAP、聊共识算法、聊跨机房容灾。回到工位,行政在群里问:「谁有空看一下打印机?又打不出来了。」整个技术部瞬间安静。

这件事的荒诞之处在于:打印机明明是最没有技术含量的设备。它不联网也能复印,插上电就能自检,结构上就是一个滚轮、一个激光器、一堆塑料齿轮。可一旦把它接进电脑,它立刻变成一个状态不可见、错误码不解释、还坚持要你走到它面前按一下绿色按钮的分布式系统。

电影《办公空间》(Office Space)里有一个流传很广的梗:主角砸打印机之前,屏幕上反复出现一行提示——PC LOAD LETTER。字面意思是「纸盒装 Letter 纸」,但没有任何一个用户能从这五个词里推断出它想干什么。这个梗之所以成立,是因为它精准命中了打印机交互的核心问题:设备知道自己在等什么,但它不打算告诉你。

下面我们不修打印机,只做一次链路考古。把「Ctrl+P 到出纸」之间发生的事拆开,你会发现大部分「玄学」都有明确的原因。

二、一张纸的旅程:六层,每一层都能坏#

从你按下打印,到纸上出现墨粉,中间大致要经过六层。不同操作系统的叫法不一样,但分层是相通的。

层Linux / CUPSWindows这一层坏了的表现
应用浏览器、LibreOffice、lpWord、浏览器、记事本预览正常,打印错版
渲染与驱动filter、PPD、驱动打印驱动、打印处理器字体变了、排版跑偏
假脱机cupsd 队列spoolsv.exe、spool\PRINTERS任务卡住、队列不动
传输backend(socket、ipp、usb)端口监视器(Standard TCP/IP Port)完全没反应、时好时坏
控制器打印机固件打印机固件报错页、半页、乱码
引擎走纸、转印、定影同左卡纸、全白、字一抹就掉

关键点在于:应用并不直接把纸送进打印机,它只生成一份「页面描述」。这份描述要经过驱动翻译成打印机认识的页面描述语言(PDL),再由假脱机程序排队、落盘,最后由传输层按某种协议送到设备。打印机收到之后,还要自己解释、光栅化,才轮到机械结构去执行。

六层里任何一层出问题,用户看到的往往都是同一句话:「打印失败」。这就是玄学的来源——失败的位置在第六层,报错的位置在第二层,而你能看到的只有第一层的对话框。

在 Linux 上,这个链路由 CUPS(Common UNIX Printing System)串起来。cupsd 是常驻的调度进程,监听 631 端口;每个任务会依次经过过滤器(filter)和后端(backend)。过滤器负责格式转换,后端负责把字节送出去——socket 后端走 9100,ipp 后端走 631,usb 后端走 USB。日志默认在 /var/log/cups/error_log,把日志级别调到 debug 之后,能看到每一层具体在等什么。

在 Windows 上,对应的是 Print Spooler 服务(spoolsv.exe)。应用通过 GDI 或 XPS 生成打印数据,打印处理器(print processor)决定是原样输出还是先渲染,端口监视器负责发送。任务文件会落在 C:\Windows\System32\spool\PRINTERS,扩展名通常是 .SPL 和 .SHD。很多人「清空打印队列」就是删这个目录里的文件——先停服务再删,顺序反了会把队列弄坏。

三、驱动不是「装个软件」,真正的契约是 PDL#

我们习惯把驱动理解成「让系统认识硬件的程序」。在打印这件事上,这个理解只对了一半。驱动真正做的事,是把应用的页面描述翻译成打印机固件能执行的页面描述语言。翻译成什么,取决于打印机支持什么。

主流的页面描述语言有这么几种:

  • PostScript:Adobe 在 1985 年推出的页面描述语言,图灵完备,打印机端解释执行。它把「排版」变成了一门编程语言,好处是设备无关、精度高,坏处是解释器复杂、出错时只能吐一张错误页,上面写着 %%[ Error: undefined; OffendingCommand: ... ]%%,让用户自己猜。
  • PCL:惠普的 Printer Command Language,从 PCL 5 到 PCL 6(也叫 PCL XL),指令流更紧凑,在办公设备里覆盖面很广。
  • PDF:现代中高端打印机可以直接解释 PDF,省掉一次转换。PDF 直打听起来很美,但不同固件对透明度、字体子集、颜色空间的支持参差不齐,遇到不认识的特性,有的设备会降级,有的会直接拒绝。
  • PWG-Raster / Apple Raster:位图格式,驱动在主机端把页面渲染成一大张图再发过去。兼容性好,代价是数据量大、主机 CPU 出力。

还有一类设备干脆没有 PDL,业内叫 host-based printer,俗称「WinPrinter」。它把渲染全部交给主机,打印机只负责收位图、控制激光器。这类设备便宜,但在 Linux、macOS 上常常没有可用驱动,或者在非 Windows 系统上只能以很低的分辨率工作。买打印机时只看「支持什么系统」,往往就是在这里踩坑。

驱动之上还有一层描述文件。CUPS 2.x 依赖 PPD(PostScript Printer Description)来声明设备能力:支持哪些纸张、有没有双面器、能不能装订。PPD 本质上是给 PostScript 打印机写的「能力清单」,用它来描述一台只支持 PCL 的设备,就会出现「选项里有的功能,打出来没有」的错位。近年的 CUPS 在往无 PPD 的 IPP 模型迁移,方向是让设备自己通过 IPP 汇报能力,而不是靠一份静态文件猜。

无驱动打印:把「驱动矩阵」换成「标准协议」#

传统打印最麻烦的地方是驱动矩阵:操作系统 × 机型 × 版本,组合爆炸。于是有了 driverless printing 这一路线:

  • IPP Everywhere:PWG 推动的标准,用标准 IPP 属性描述能力,用 PDF 或 PWG-Raster 传数据;
  • AirPrint:苹果在 2010 年随 iOS 推出的方案,本质上是 IPP + PDF 的一套约定;
  • Mopria:Android 阵营的对应方案;
  • WSD:微软的 Web Services on Devices,用 WS-Discovery 发现设备,再用 SOAP over HTTP 通信。

无驱动打印解决的是「装不上驱动」这个最大的痛点,但它没有解决「高级功能覆盖不全」。双面、装订、指定纸盒这些能力,标准里定义了,厂商实现得却未必完整。于是你会看到一种熟悉的场景:系统自带驱动能打,但打不了双面;厂商驱动能双面,但每隔两个系统大版本就失联。

四、Spooler:队列是状态机,不是一个文件夹#

假脱机(spooling)的本意,是让慢速设备不阻塞快速的主机。应用把任务交给队列就可以退出,由后台慢慢送。这个设计很合理,但它引入了一个用户很难理解的东西:任务状态。

一个打印任务在 CUPS 里会经历 pending、processing、completed、canceled、aborted 等状态;在 Windows 里则表现为「正在打印」「已暂停」「脱机」「错误」。问题在于,这些状态并不总是来自设备。Windows 的「脱机」很多时候是客户端自己的判断:系统通过 SNMP 轮询打印机状态,如果打印机关了 SNMP,或者 community string 对不上,Windows 就可能把一台明明在线的打印机标成「脱机」。你取消「脱机使用打印机」的勾选,它立刻就好了——不是打印机恢复了,是客户端停止了瞎猜。

队列的另一个特性是 FIFO 与队头阻塞。一个任务卡住,后面的任务全部排队等着。用户看到的现象是「我的文档一直显示正在打印」,实际上它前面压着一个三天前没打完的坏任务。处理办法不是反复点打印,而是先把队头清掉:

Terminal window
# 看所有队列和任务状态
lpstat -t
# 取消某台打印机上的全部任务
cancel -a PrinterName
# 或者只取消某个任务
cancel 12345

Windows 上可以用 PowerShell:

Terminal window
Get-PrintJob -PrinterName "Office"
Remove-PrintJob -PrinterName "Office" -ID 42
Restart-Service -Name Spooler

如果队列已经坏到 GUI 打不开,标准流程是:停止 Spooler 服务,清空 C:\Windows\System32\spool\PRINTERS,再启动服务。顺序很重要——服务运行时删除 spool 文件,可能留下一个永远「正在删除」的幽灵任务。

Spooler 还是一个安全问题。2021 年的 PrintNightmare(CVE-2021-34527)利用 Windows Print Spooler 的远程代码执行漏洞,影响范围大到很多企业直接把域控制器上的 Spooler 服务禁用。代价是这些服务器上所有依赖打印的功能一起失效。安全与可用性在打印机这里是正面冲突的:你要打印,就得让一个高权限服务解析外部数据;你不要风险,就得接受「这台机器不能打印」。

五、端口与发现:ping 得通,不代表能打印#

网络打印的排障,最容易从 ping 开始,也最容易在 ping 结束。ICMP 通只能说明 IP 层可达,和「631 端口上的 IPP 服务是否活着」是两件事。防火墙完全可以让 ICMP 通过、把 TCP 9100 和 631 拒掉。

常见的打印端口有三个,它们承诺的东西完全不同:

端口 / 协议名称能做什么不能告诉你什么
TCP 9100raw / JetDirect / AppSocket把字节流原样灌给打印机,最快任务是否收到、是否打完、错在哪;没有认证和加密
TCP 515LPD老式行式打印协议,带队列名现代作业属性、状态回传都很有限
TCP 631IPP查询能力、提交任务、读取任务状态;可套 TLS(ipps)需要设备实现完整;部分老设备只实现了子集
UDP 5353mDNS / DNS-SD局域网内自动发现 _ipp._tcp、_ipps._tcp、_pdl-datastream._tcp默认不跨 VLAN,跨网段就「找不到打印机」
UDP 3702WS-DiscoveryWindows 的 WSD 发现,组播 + SOAP防火墙、IPv6 配置、固件实现都会影响

9100 是「能打就行」的典型:它不返回任务状态,所以很多打印服务器用起来很顺,但用户永远不知道任务到底打没打。631 是「能问清楚」的典型:IPP 有属性、有作业状态、有错误原因,代价是设备要完整实现协议。515 则是历史遗留,今天还留着主要是为了兼容老设备。

自动发现失败,是另一个高频问题。mDNS 用的是组播,默认只在同一个二层广播域内传播。办公室按楼层或部门划了 VLAN,打印机在 VLAN 10,你的电脑在 VLAN 20,系统就「看不到」它——但手动输入 IP 又能加。这不是打印机坏了,是发现协议被网络边界挡住了。同理,把打印机配成 DHCP 动态地址、队列里却写死了旧 IP,租约一变,任务就发给了一台不相干的新设备,或者干脆发进黑洞。要么做 DHCP 保留,要么用主机名 / mDNS 名称,别在队列里硬编码会变的地址。

想绕过上层直接验证设备,可以往 9100 灌一个已知能打的文件:

Terminal window
# 直接发送 PostScript 文件,绕过 spooler 和驱动
nc -w 5 printer.local 9100 < test.ps
# 检查 IPP 的 HTTP 端点是否响应
curl -s -o /dev/null -w '%{http_code}\n' http://printer.local:631/

第一条命令如果出纸,说明设备、网络和 PDL 都没问题,故障在主机侧的队列或驱动;如果不出纸,问题在设备或传输层。第二条命令返回 200、401 或 404 都说明 HTTP 服务活着,连不上则要查端口和防火墙。用 nc 直接发文件时注意,打印机不会给你任何回执,「没有输出」既可能是成功,也可能是失败——这正是 raw 协议的特点。

六、故障树:从「哪张纸能打」开始分叉#

打印机排障最有效的工具不是驱动安装包,而是一张能逐层排除的故障树。核心是找到第一个「能正常工作」的环节,故障就在它和上一个环节之间。

现象先验证常见原因
打印机自检页能打,电脑打不了主机到设备的链路队列、端口、驱动、防火墙
测试页能打,某个文档打不了应用与 PDL字体缺失、纸张大小、PDF 特性不兼容
一台电脑能打,另一台不能客户端差异驱动版本、权限、系统防火墙
ping 通,打印没反应TCP 端口9100/631 被拦、IPP 服务崩溃、VLAN 隔离
打一半停住设备状态缺纸、卡纸、双面器故障、耗材耗尽
任务显示完成,纸没出来队列指向发到了另一台同名设备、旧 IP、错误纸盒
文字发虚、一抹就掉引擎定影单元温度不足、纸张受潮、省墨模式

这张表里最有价值的一行是「打印机自检页」。自检页由设备固件直接生成,完全绕过主机、驱动、spooler 和网络。它能打,说明设备本身和耗材没问题,排查范围立刻缩小到主机侧;它不能打,说明再重装十遍驱动也没用。

同理,「打印到文件」是保留现场的好办法。把文档输出成 .ps 或 .pdf,你就拿到了驱动真正发给打印机的字节。之后可以用 nc 直接灌给设备,也可以换一台电脑用同样的文件测试。能复现的故障,才有资格被修;不能复现的故障,只能被重启。

七、那些「玄学」,其实都有解释#

「重启一下就好了。」 重启清掉的是 spooler 里卡住的状态、断掉的 TCP 连接、失效的 mDNS 缓存。它修不了驱动错误,也修不了 PDL 不兼容,所以「重启有用」和「重启没用」的案例会同时存在。它有效,是因为故障在状态层;它无效,是因为故障在翻译层。

「重装驱动就好了。」 重装驱动通常会顺带删除并重建打印队列、端口和 PPD。真正修好问题的,往往是「重建队列」这一步,而不是驱动文件本身。所以会出现「重装同一个版本也好了」的现象——不是驱动变了,是配置被重置了。

「明明显示打印成功,纸就是没出来。」 先看任务到底发给了哪台设备。同名队列、旧 IP 被 DHCP 分给了别的机器、纸盒选错、双面器把纸翻进了另一条通路,都会让「成功」停留在主机侧。打印机的「成功」通常只代表「字节发出去了」,不代表「纸出来了」。

「Word 能打,PDF 不能打。」 两者走的是不同的渲染路径,生成的 PDL 也不同。某些 PDF 的透明度、图层、字体子集会让老固件解析失败;把 PDF 用「打印为图片」再打,往往能绕过。这不是 PDF 格式的错,是设备对 PDF 的支持只覆盖了一个子集。

「换根网线 / 换个 USB 口就好了。」 链路协商、供电、USB 选择性挂起都会影响结果。USB 打印机在系统休眠后「消失」,很多时候是端口供电策略导致的,不是打印机坏了。

PC LOAD LETTER。 这是 PCL 的状态消息,意思是「纸盒需要装 Letter 尺寸的纸」。它难懂,不是因为技术复杂,而是因为把「缺 Letter 纸」压缩成了五个大写单词。更荒诞的是,纸盒里明明有纸,只是纸型设置成了 A4,它也会这么报。

八、如果轮到你维护打印系统#

如果你不幸要为一个团队、一栋楼或一个产品维护打印能力,下面几条比「多装几个驱动」有用:

  1. 把状态暴露出来。 不要只返回「打印失败」。任务 ID、当前状态、设备返回的原因、最后发送的字节数,至少留一个。打印是典型的「失败在远端、报错在近端」的系统,可观测性比功能更重要。
  2. 队列要隔离。 一台打印机一个队列,一个租户一组队列。一个坏任务不该阻塞整层楼。重试要有上限,重复提交要有去重键,否则用户点三次「打印」,出来三份。
  3. 保留原始 PDL。 存下真正发给设备的字节,而不是只存源文档。源文档 + 驱动 + 系统版本三者任一变化,故障就可能无法复现。
  4. 优先无驱动方案。 IPP Everywhere / AirPrint 能把「驱动矩阵」压缩成「协议兼容性」,长期维护成本低得多。但要逐项验证双面、纸盒、装订这些高级能力。
  5. 把打印机当网络设备管。 单独划分 VLAN,关闭不需要的 9100 raw 和 515 LPD,IPP 尽量走 TLS,固件及时更新。打印机的固件漏洞不会上热搜,但它和一台无人值守的 Linux 主机没有本质区别。
  6. 监控耗材与队列深度。 通过 IPP 属性或 SNMP 读纸张、墨粉、错误码。等到行政在群里喊,说明监控已经晚了。

九、结语#

打印机之所以让人崩溃,不是因为它落后,而是因为它把软件世界最擅长的事情——抽象、重试、回滚——全部还原成了物理动作。软件可以重试一次请求,纸不能重试一张已经卡在定影器里的 A4。软件可以把失败封装成一个异常,打印机只能让一个红色指示灯闪两下。

所以下次它又卡纸的时候,不必怀疑自己的技术能力。你面对的是一条六层链路:应用生成了页面描述,驱动翻译成了 PDL,spooler 排了队,端口把字节送了出去,固件解释了它们,引擎把墨粉压进了纸纤维。这六层各有各的状态机,谁也不和谁共享日志。能修好,说明你排障有功;修不好,说明你至少知道了该从哪一层开始怀疑。

云原生解决了很多问题,但至今没有解决「走到打印机前按一下绿色按钮」。

打印机:程序员最持久的敌人
https://www.hehonglei.cn/posts/printer-the-programmers-persistent-enemy/
作者
Honglei He
发布于
2026-11-08
许可协议
CC BY-NC-SA 4.0